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.
Navigation And Files
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:
pwdprints the current directoryls -lashows hidden files and permissionscdchanges directoryfindlocates 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
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
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:
- print the current directory;
- inspect the exact expanded path;
- list matching files;
- confirm the environment and project;
- preserve required data;
- 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
0on 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
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.