The CLI Foundation Automation Scripts Build On
Recall, in one place, the small set of CLI primitives -- arguments, exit codes, the three standard streams, and environment variables -- that every console application and cron job in the rest of this section is built on top of.
Before building structured console applications (next lesson) or scheduling scripts with cron (the lesson after that), it's worth pausing on one fact: none of that is new machinery -- it's all the same handful of CLI primitives PHP has always exposed, just used deliberately and combined into something more robust. If any of $argc/$argv, STDIN/STDOUT/STDERR, exit codes, or getenv() feel unfamiliar, stop here and work through the dedicated "PHP CLI" lesson (Building Command-Line Applications with PHP) first -- it covers each of these in full depth with examples. This lesson is deliberately short: a fast recap of exactly the parts that matter for what comes next, framed around why automation and scheduled jobs depend on getting them right.
The Four Primitives, Recapped
Arguments in, exit code out: a CLI script receives its input through $argv (an indexed array of every token typed after php, with $argv[0] always the script's own filename) and $argc (its count), and reports how things went by returning a small integer exit code via exit($code) -- 0 for success, non-zero for failure. Every subcommand dispatcher and every cron job in the next two lessons leans on exactly this contract: read $argv to decide what to do, exit() with a code that tells the caller -- a human, a shell script, or the cron daemon -- whether it worked.
Three streams, three jobs: STDOUT carries the program's real output (what a pipeline like php report.php > out.csv wants to capture), STDERR carries diagnostics, warnings and errors (so they stay visible even when STDOUT is redirected to a file), and STDIN is where interactive input is read from with fgets(STDIN). A console application's --help text, progress messages, and log lines all go to the right stream on purpose -- getting this wrong is the single most common CLI bug, and it's the first thing the next lesson builds structure around.
Environment variables carry configuration: getenv('NAME') reads values the calling context set -- a shell export, a cron user's crontab, a systemd unit's Environment= line, a Docker container's env block -- and is how a script picks up database URLs, API keys, and, critically for scheduled jobs, an explicit TZ so a script's idea of "now" doesn't silently depend on whatever timezone the machine happens to be in.
• $argv / $argc -- how a script receives arguments; see the PHP CLI lesson for full parsing patterns. • exit($code) -- 0 succeeded, non-zero failed; the contract every dispatcher and every cron entry relies on. • STDOUT vs STDERR vs STDIN -- output, diagnostics, and input each have their own stream, and mixing them up breaks pipelines. • getenv('NAME') -- configuration and secrets from the calling environment, never hardcoded.
Why This Matters More Once Scripts Are Unattended
A script you run yourself, by hand, in a terminal, tolerates sloppiness: you can see everything it prints, you notice if it hangs, and you know whether it "worked" just by reading the screen. The moment a script becomes a console application invoked by teammates, or a cron job invoked by a daemon at 3 a.m. with nobody watching, that tolerance disappears -- the exit code is the only thing anything downstream can check, STDERR (or a log file) is the only record of what went wrong, and there is no human present to notice a hang or a silent failure. Sections 17.2 and 17.3 are really about building the scaffolding -- subcommands, help text, logging, locking -- around these same four primitives so a script keeps behaving correctly with nobody watching it run.
Concretely: getting exit codes right is what lets a deploy pipeline stop instead of continuing after a migration script actually failed; getting STDOUT/STDERR right is what lets a cron job's crontab MAILTO setting mail a human only the error output, not the whole normal report; and reading configuration from the environment rather than hardcoding it is what lets the exact same script run correctly in a developer's terminal, a CI runner, and a production cron entry without editing the file three different ways.
• Interactive use tolerates mistakes a human catches by eye; unattended use does not. • Exit codes are what automation actually branches on -- get them right from the first line of the script. • See the "PHP CLI" lesson for argument-parsing patterns, input validation, and CLI-specific php.ini/testing details not repeated here.
Code Examples
Four lines, four primitives: $argv for input, getenv() as a fallback source of configuration, fwrite(STDERR, ...) for the usage diagnostic, fwrite(STDOUT, ...) for the real result, and exit() codes that mean something to whatever calls this script. Expected result: $ php greet.php Usage: php greet.php <name> (or set DEFAULT_NAME) $ echo $? 1 $ php greet.php Priya Hello, Priya! $ echo $? 0 Run the failing case in a pipeline -- php greet.php > out.txt -- and note that out.txt stays empty while the usage message still appears on the terminal, because it went to STDERR, not STDOUT.
fwrite(STDERR, "Usage: ...\n"); exit(1);
echo "Error: missing name\n"; exit(1); // using echo for an error fwrite(STDERR, "Error: missing name\n"); exit(1);
echo always writes to STDOUT. An error written with echo lands inside redirected output (php script.php > report.csv), silently corrupting the file the caller expected to contain only clean data -- exactly the mistake the full PHP CLI lesson calls out, and the one every console app and cron job below must avoid.
A cron job's script writes its progress messages with plain echo and its output is redirected to a CSV file by cron's job definition. What goes wrong? A. Nothing -- echo and fwrite(STDOUT, ...) are interchangeable B. The progress messages get mixed into the CSV file because echo writes to STDOUT, corrupting the data a downstream process expects to be clean C. The script fails to run at all because cron doesn't support echo D. The progress messages disappear entirely
B. The progress messages get mixed into the CSV file because echo writes to STDOUT, corrupting the data a downstream process expects to be clean — echo always targets STDOUT. If STDOUT is redirected to a file that's supposed to hold only real output (like a CSV), any diagnostic/progress text written with echo instead of fwrite(STDERR, ...) ends up mixed into that file.
#!/usr/bin/env php
<?php
declare(strict_types=1);
// $argv[0] is the script name; real arguments start at $argv[1].
$name = $argv[1] ?? getenv('DEFAULT_NAME') ?: null;
if ($name === null) {
// Diagnostics go to STDERR, not STDOUT.
fwrite(STDERR, "Usage: php greet.php <name> (or set DEFAULT_NAME)\n");
exit(1); // non-zero: a shell script or CI step can detect this failure
}
// Real output goes to STDOUT -- safe to pipe or redirect.
fwrite(STDOUT, "Hello, {$name}!\n");
exit(0); // success