Start Here

Linux Shell Basics for PHP Developers

PHP developers do not need to become full-time system administrators, but they do need enough shell skill to run projects, inspect logs, debug paths, check permissions, and understand automation.

The shell is where many PHP tasks happen: Composer commands, test runners, syntax checks, queue workers, deployment checks, cron jobs, Docker commands, and production diagnostics.

Start with the commands that help you understand where you are and what files exist.

pwd
ls
ls -la
cd /path/to/project
find . -maxdepth 2 -type f -name "*.php"

Useful meanings:

  • pwd prints the current directory
  • ls -la shows hidden files and permissions
  • cd changes directory
  • find locates files by name, type, or path

Before running a PHP command, confirm you are in the project directory you think you are in.

Inspecting Files

Common file-reading commands:

cat composer.json
less storage/logs/app.log
tail -n 50 storage/logs/app.log
tail -f storage/logs/app.log

tail -f follows a log as new lines are written. It is useful while reproducing a web request or running a queue worker.

Searching Code And Logs

Use rg when available because it is fast and respects many ignore files by default.

rg "DB_HOST"
rg "function handle" app
rg "RuntimeException" storage/logs

If rg is not installed, grep is common:

grep -R "DB_HOST" .
grep -R "RuntimeException" storage/logs

Search before guessing. Many configuration, route, service, and error questions can be answered by finding the exact string in the project.

PATH And Active Commands

When you type a command, the shell finds it through PATH.

echo $PATH
which php
which -a php
php -v

For PHP projects, always be ready to prove which php, composer, and framework command you are running.

which composer
composer --version

If the shell runs the wrong binary, application changes will not fix it.

Environment Variables

Environment variables are part of the process environment.

printenv
printenv APP_ENV
APP_ENV=local php -r 'echo getenv("APP_ENV") . PHP_EOL;'

That last command sets APP_ENV only for that single PHP process.

Inside PHP:

PHP example
<?php

declare(strict_types=1);

echo getenv('APP_ENV') ?: 'not set';
echo PHP_EOL;

// Example:
// APP_ENV=local php env.php
//
// Prints:
// local

Shell environment, systemd environment, Docker environment, and web-server environment can all differ.

Pipes And Redirection

Pipes pass output from one command into another.

php -m | sort
php -m | grep mbstring
tail -n 200 storage/logs/app.log | grep ERROR

Redirection writes output to files.

php script.php > output.txt
php script.php 2> errors.txt
php script.php > output.txt 2> errors.txt

Normal output is stdout. Error output is stderr. Good CLI tools keep those separate.

Reading Piped Input In PHP

PHP can read piped input from STDIN.

PHP example
<?php

declare(strict_types=1);

$input = stream_get_contents(STDIN);
$lines = array_filter(explode("\n", trim($input)));

echo 'Lines: ' . count($lines) . PHP_EOL;

// Example:
// printf "one\ntwo\n" | php count-lines.php
//
// Prints:
// Lines: 2

This is useful for small maintenance tools that work with command output.

Permissions And Ownership

Permission issues are common in PHP projects because web servers, queue workers, and shell users may be different users.

whoami
id
ls -la storage
ps aux | grep php-fpm

If PHP cannot write cache, sessions, logs, or uploads, check ownership and permissions before changing application logic.

Avoid blindly running chmod -R 777. It hides the problem and creates security risk.

Processes And Services

You may need to know whether PHP-FPM, a queue worker, or a local server is running.

ps aux | grep php
systemctl status php-fpm
systemctl list-units "php*"

For a local built-in server:

php -S 127.0.0.1:8000 -t public

Stop a foreground process with Ctrl+C.

Exit Codes

The shell tracks whether the last command succeeded.

php -l public/index.php
echo $?

0 means success. Non-zero means failure.

CI jobs, deploy scripts, cron, and shell pipelines depend on exit codes. A PHP script used in automation should exit non-zero when it fails.

Quote Arguments Deliberately

The shell parses a command line before PHP receives any arguments. Spaces, wildcard characters, dollar signs, quotes, pipes, and redirection symbols can therefore change meaning.

Quote paths containing spaces:

cd "/path/to/PHP Projects/store"
php "scripts/import orders.php"

Single quotes normally preserve text literally in POSIX shells. Double quotes allow substitutions such as $HOME while keeping the resulting value as one argument:

printf '%s\n' '$HOME'
printf '%s\n' "$HOME"

The first prints the literal characters $HOME; the second prints the variable value. Do not place untrusted text into a shell command by string concatenation. In PHP, prefer structured process APIs and argument escaping appropriate to the operating system. Shell injection is covered in the security track.

Use -- when a command supports it to mark the end of options. This prevents a filename beginning with - from being interpreted as a flag:

rm -- -unexpected-name.txt

Inspect the command's help before assuming support.

Understand Relative And Absolute Paths

An absolute path begins from the filesystem root. A relative path is resolved from the process's current working directory.

pwd
realpath composer.json

A script invoked from two directories can resolve ./config.php differently. PHP exposes __DIR__ so source-relative paths can be constructed from the file's own directory rather than the caller's current directory.

Do not fix path confusion by scattering cd commands through automation. Establish the intended working directory explicitly and use stable paths for inputs and outputs.

Combine Commands According To Intent

Shell operators have different control-flow meanings:

php -l app.php && php app.php
php migrate.php || printf '%s\n' 'Migration failed' >&2
php first.php ; php second.php

&& runs the next command only after success. || runs it only after failure. ; runs the next command regardless of the previous exit status.

For validation followed by execution, && is safer than ;: a failed syntax check should prevent the script from running. In deployment and CI scripts, accidental unconditional continuation can hide the first failure and create partial changes.

Shell scripts should normally use a deliberate error policy such as set -eu, with pipeline behaviour considered separately. Do not copy strict-mode snippets without understanding commands that legitimately return non-zero.

Treat Pipelines As Multiple Processes

In:

php generate.php | grep ERROR

PHP and grep are separate processes connected by a pipe. PHP's stdout becomes grep's stdin. PHP's stderr remains separate unless redirected.

A pipeline's reported exit status can depend on shell options and may otherwise represent only the final command. Automation that must fail when any stage fails should use the shell's supported pipeline-failure behaviour and test it explicitly.

Avoid filtering away the evidence you need. command | grep pattern can hide surrounding context or produce a non-zero exit merely because no line matched. Save or inspect complete logs when diagnosing an unfamiliar failure.

Redirect Without Destroying Evidence

> creates or truncates a file. >> appends:

php report.php > report.txt
php report.php >> report-history.txt

Check the destination before using >, especially with generated reports, exports, or logs. Shell redirection occurs before the command runs, so an existing file can be emptied even when PHP immediately fails.

Combine streams only when that is useful:

php script.php > combined.log 2>&1

Keeping stdout and stderr separate is often better for automation. stdout can contain machine-readable results while stderr explains failure.

Use tee when you need to see output and write it to a file:

php report.php | tee report.txt

Remember that tee participates in a pipeline, so preserve the producing command's failure status where correctness depends on it.

Inspect Exit Status At The Right Time

$? contains the status of the most recently completed foreground pipeline. Read it immediately:

php -l app.php
echo "$?"

Any intervening command replaces that value. A clearer control flow is often:

if php -l app.php; then
    printf '%s\n' 'Syntax is valid'
else
    printf '%s\n' 'Syntax check failed' >&2
fi

Document meaningful exit codes for PHP CLI tools. Use 0 for success and non-zero for failure. Avoid returning a successful status after printing an error, because CI and schedulers rely on status rather than human interpretation of text.

Manage Foreground And Background Processes

A foreground process owns the terminal until it exits or is interrupted. Ctrl+C sends an interrupt signal; it is the normal way to stop PHP's built-in server or a local worker.

Appending & starts a background job in many POSIX shells:

php worker.php &
jobs

Backgrounding a process is not production supervision. It can lose logs, outlive the terminal unpredictably, or stop without restart. Use the project's process manager, container orchestrator, systemd unit, or queue-worker tooling for long-running services.

Inspect listeners before assuming a port is free:

ss -ltnp

On systems without ss, use the supported equivalent. Do not kill an unidentified process merely because it uses the desired port; identify its owner and purpose first.

Use Destructive Commands With A Verification Step

Commands such as rm, mv, database imports, cache clears, and recursive permission changes can alter or remove data. Before a destructive command:

  1. print the current directory;
  2. inspect the exact expanded path;
  3. list matching files;
  4. confirm the environment and project;
  5. preserve required data;
  6. use the narrowest command.

Quote variables and reject empty paths in scripts. A command intended for $PROJECT_CACHE becomes dangerous if the variable is empty or points somewhere unexpected.

Do not learn shell operation by pasting commands you cannot explain, especially commands using sudo, recursive deletion, downloaded scripts, or broad permission changes.

Read Documentation From The Installed Tool

Use local help because command versions and options differ:

php --help
composer help install
rg --help
man find

A short command copied from another operating system may use incompatible flags. Confirm the tool, version, and option meaning in the environment where it will run.

Preserve A Reproducible Command

When a command fixes or diagnoses a recurring project problem, move it from shell history into the appropriate project documentation, Composer script, Make target, task runner, or checked-in shell script. Include its required working directory, environment, inputs, outputs, and failure behaviour.

Do not turn every one-off inspection into automation. Record commands that teammates and CI should run consistently; leave personal aliases and exploratory searches local.

What You Should Be Able To Do

After this lesson, you should be able to navigate a project, inspect files, search logs, prove active binaries, read environment variables, use pipes and redirection, understand basic permissions, and check PHP-related processes.

For junior PHP work, this matters because many real bugs are found from the shell first: wrong PHP version, missing extension, bad permissions, failing worker, or an error line in a log.

Practice

Practice: Build A Read-Only Shell Diagnosis

Create an ordered, non-destructive checklist for investigating a PHP project that behaves differently from expected.

Requirements

Include commands that establish:

  • current directory and resolved project path;
  • visible files and permissions;
  • Git worktree status;
  • PHP and Composer command sources and versions;
  • loaded PHP configuration;
  • locations of relevant PHP files and configuration names;
  • recent application log output;
  • PHP-related processes and listening ports.

Use quoting for paths that may contain spaces. For every command, state what evidence it contributes. End with the condition under which you would move from inspection to a mutating command.

Show solution
pwd
realpath .
ls -la
git status --short
command -v php
type -a php
php -v
php --ini
command -v composer
composer --version
find . -maxdepth 3 -type f -name '*.php'
rg 'APP_ENV|DB_HOST' .
tail -n 100 storage/logs/app.log
ps -ef | grep '[p]hp'
ss -ltnp

Use a quoted path such as tail -n 100 "storage/logs/app log.log" when spaces are present. Replace paths and search terms with ones established by the repository.

The sequence proves location before interpreting files, confirms tracked changes before modifying anything, identifies the actual tools, discovers configuration and source, then inspects recent runtime evidence and processes. None of these commands intentionally changes the project.

Move to a mutating command only after naming the failing boundary, intended change, affected data or process, rollback path, and verification. For example, do not restart FPM until logs and configuration identify the correct service and its configuration has been validated.

Task: Build A Pipeline-Friendly PHP Filter

Create count-lines.php, a CLI tool that reads standard input and counts non-empty lines.

Requirements

  • Detect whether stdin contains usable input.
  • Treat lines containing only whitespace as empty.
  • Print only the numeric count to stdout on success.
  • Write diagnostics to stderr.
  • Exit 0 on success and non-zero when no usable input is supplied.
  • Demonstrate successful piping, separate stdout/stderr redirection, and immediate exit-code inspection.
  • Explain how a later pipeline command can hide the PHP process's failure unless pipeline status is handled deliberately.
Show solution
PHP example
<?php

declare(strict_types=1);

$input = stream_get_contents(STDIN);

if ($input === false) {
    fwrite(STDERR, "Unable to read standard input.\n");
    exit(2);
}

$lines = preg_split('/\R/', $input);

if ($lines === false) {
    fwrite(STDERR, "Unable to split input into lines.\n");
    exit(2);
}

$nonEmptyLines = array_filter(
    $lines,
    static fn (string $line): bool => trim($line) !== '',
);

if ($nonEmptyLines === []) {
    fwrite(STDERR, "No non-empty input lines were provided.\n");
    exit(1);
}

echo count($nonEmptyLines), PHP_EOL;
exit(0);

Successful use:

printf 'alpha\nbeta\n\n' | php count-lines.php

It prints 2 to stdout. Separate streams and inspect status immediately:

printf '' | php count-lines.php > count.txt 2> error.txt
echo "$?"

The status is non-zero, count.txt remains empty, and error.txt contains the diagnostic.

In php count-lines.php | another-command, the shell may report only the final command's status unless pipeline-failure handling is enabled. Automation must use and test the shell's supported pipeline status policy so a downstream success cannot conceal PHP's failure.

Task: Make A Safe Command Sequence
php -l public/index.php ; rm -rf "$CACHE_DIR"/* ; php public/index.php

Review and replace it with a safe diagnostic and execution sequence.

Requirements

  • Explain the control-flow difference between ; and &&.
  • Account for an empty, unset, or incorrect CACHE_DIR.
  • Verify the working directory, Git status, target path, and matching files before any deletion.
  • Keep the initial diagnosis read-only.
  • Do not perform cache deletion unless the project documents it and recovery is understood.
  • Show how to run the PHP file only after syntax validation succeeds.
Show solution

The original command continues after a failed syntax check because ; is unconditional. Its recursive deletion is dangerous because CACHE_DIR has not been validated, and wildcard expansion can affect an unintended location.

Start read-only:

pwd
realpath .
git status --short
printf 'CACHE_DIR=%s\n' "${CACHE_DIR-<unset>}"
php -l public/index.php

If the cache path is documented, inspect it without deleting:

realpath -- "$CACHE_DIR"
find "$CACHE_DIR" -mindepth 1 -maxdepth 1 -print

Do not run these path commands with an unset or empty variable. A checked shell script should reject that state explicitly and verify the resolved path is the project's intended cache directory.

Run the file only after successful validation:

php -l public/index.php && php public/index.php

Cache clearing should use the framework or project's documented command where available. Perform it only after confirming the environment, effect, required backup or regeneration, and rollback. Replacing uncertainty with a broader rm -rf command is not a fix.