skip to content

CLI Scripts

A PHP command-line script reads $argv and getopt options, writes to STDOUT and STDERR, and returns an exit code. Interviewers check you can build a script that behaves well in a pipeline.

part ofPHPoverview, primer and where to startread it →
on this pageshow

explore

questions

6

In a PHP CLI script, what do $argv and $argc contain, and why can a function not see $argv directly?

level: juniorimportance: must knowfreq 50%

answer

  1. index 0 is the script name
  2. $argc counts the script name too
  3. every value is a string
  4. globals, not superglobals
  5. $_SERVER['argv'] works anywhere

basics

~20 s

$argv is an array of the command-line arguments with the script name at index 0, and $argc is count($argv). Both are ordinary global variables, not superglobals, so inside a function use $_SERVER['argv'] or pass the array in.

solid answer

~40 s

When you run `php import.php users.csv 100`, `$argv` is `['import.php', 'users.csv', '100']` and `$argc` is `3` — the count includes the script name. Every element is a `string`, so `'100'` needs converting and validating before use as a number. `$argv` and `$argc` are **global variables**, not superglobals: inside a function or method they are undefined unless you write `global $argv;`, so real code reads `$_SERVER['argv']` or, better, passes the array into the entry point. Arguments that start with `-` can be taken by the PHP binary itself; put `--` before them so they reach the script. To test whether a script runs from the command line, check `PHP_SAPI === 'cli'` or `php_sapi_name()`, not whether `$argv` is set.

code

php · 19 lines
php
<?php
declare(strict_types=1);

/** @param list<string> $argv */
function main(array $argv): int
{
    if (count($argv) < 2) {
        fwrite(STDERR, "usage: {$argv[0]} <file.csv>\n");
        return 2;
    }
    $file = $argv[1];
    // ... import $file
    return 0;
}

if (PHP_SAPI !== 'cli') {
    exit(1);
}
exit(main($argv));

go deeper

for a junior

Recall that $argv[0] is the script name, that $argc counts it, and that every argument is a string.

for a middle

Explain why functions cannot see $argv, the options $_SERVER['argv'] and dependency passing, and how -- keeps dash-prefixed arguments away from the PHP binary.

for a senior

Structure commands so the entry point receives argv as a parameter, validates every value, and refuses to run outside the CLI SAPI.

for a principal

Decide when hand-rolled argv handling stops being acceptable and a shared console library should own argument parsing across the codebase.

## What the CLI gives a script When a PHP script runs from a terminal, cron or a CI job, the words after the script name are its **arguments**. PHP's command-line SAPI exposes them in two variables: | Variable | Content for `php import.php users.csv 100` | |---|---| | `$argv` | `[0 => 'import.php', 1 => 'users.csv', 2 => '100']` | | `$argc` | `3` | | `$_SERVER['argv']` | the same array as `$argv` | | `$_SERVER['argc']` | the same number as `$argc` | Key points: - `$argv[0]` is always the name used to run the script, exactly as typed (`import.php`, `./bin/import`, or a full path). - `$argc` is the number of elements in `$argv`, so it is **one more** than the number of real arguments. - Every element is a **string**. `'100'` must be converted and validated — for example with `filter_var($argv[2], FILTER_VALIDATE_INT)` — before being used as a limit. With inline code (`php -r '...'`) or code piped on standard input, `$argv[0]` is the string `"Standard input code"` rather than a file name. ## Why functions do not see `$argv` `$argv` and `$argc` are registered as ordinary **global variables** in the script's top-level scope. PHP functions have their own local scope and do not see globals unless told to. Only a fixed set of **superglobals** (`$_SERVER`, `$_GET`, `$GLOBALS` and the like) are visible everywhere. So: ```php <?php function main(): int { var_dump($argv); // Warning: Undefined variable $argv, then NULL return 0; } exit(main()); ``` Three fixes, from worst to best: 1. `global $argv;` inside the function — works, but hides the dependency. 2. `$_SERVER['argv']` — a superglobal, so visible anywhere. 3. **Pass it in**: `exit(main($argv));` with `function main(array $argv): int`. The function becomes testable with any array, which is how command classes in frameworks receive input. ## When `$argv` is missing The variables exist only when `register_argc_argv` is enabled, which the CLI SAPI always does. Under a web SAPI they are normally absent. That is why the manual recommends testing the SAPI rather than the variable: - `PHP_SAPI === 'cli'` or `php_sapi_name() === 'cli'` answers "am I on the command line?"; - `isset($argv)` can be fooled by configuration and says nothing reliable. A command meant only for the terminal often starts with `if (PHP_SAPI !== 'cli') { exit(1); }` so it can never be triggered through a web request. ## Arguments that start with a dash The `php` binary parses its own options (`-r`, `-d`, `-n` and so on) before starting the script. An argument placed after the code or file that looks like a PHP option can be taken by the binary instead of the script. The argument separator `--` ends PHP's own option parsing: - `php -r 'var_dump($argv);' -h` prints PHP's usage text; - `php -r 'var_dump($argv);' -- -h` passes `-h` to the code. With a script file, arguments after the file name normally go to the script, but `--` is a harmless habit in wrapper scripts. ## Beyond positional arguments Positional access (`$argv[1]`, `$argv[2]`) is fine for one or two required values. As soon as a command grows flags such as `--dry-run` or `--limit=100`, parse them with `getopt()` or a console library instead of scanning `$argv` by hand — manual parsing breaks on `--limit 100` versus `--limit=100`, repeated flags, and flags placed in unexpected positions.

  • What is $argc for php import.php with no arguments?
    `1`. `$argc` counts the elements of `$argv`, and `$argv[0]` is always the script name, so a run with no arguments still has one element. A check for "no arguments" is therefore `$argc < 2`, not `$argc === 0`.
  • Why does the manual recommend php_sapi_name() over isset($argv) to detect the CLI?
    `$argv` exists only when `register_argc_argv` is on, which is a configuration detail rather than a statement about the SAPI. `php_sapi_name()` (or the `PHP_SAPI` constant) returns `'cli'` exactly when the command-line SAPI is running, so it is the reliable test.

saying these in an interview costs you the question

  • $argv[0] holds the first argument after the script name
  • $argc is the number of arguments excluding the script name
  • $argv is a superglobal, visible inside every function
  • Numeric arguments arrive in $argv as int
  • isset($argv) is the reliable way to detect the CLI
open as a page

What do the PHP CLI options -r, -l and -a do, and how does a shebang line make a PHP script runnable as ./import?

level: juniorimportance: should knowfreq 35%

basics

~10 s

php -r runs inline code without <?php tags; php -l only checks syntax; php -a opens an interactive shell that needs readline. A first line #!/usr/bin/env php plus chmod +x makes ./import runnable.

open as a page

How would you parse --dry-run and --limit=100 in a PHP CLI import script with getopt(), and what does getopt() return for each?

level: middleimportance: should knowfreq 38%

basics

~10 s

Call getopt('', ['dry-run', 'limit:'], $rest). A given --dry-run appears as key 'dry-run' with value false, --limit=100 as 'limit' => '100', absent options are missing keys, and $rest is the index where positional arguments begin.

open as a page

In a PHP CLI script, what are the STDIN, STDOUT and STDERR constants, and what should each carry in a shell pipeline?

level: middleimportance: should knowfreq 35%

basics

~20 s

They are already-open stream resources the CLI SAPI defines for standard input, output and error. Read data from STDIN, write results to STDOUT, and send progress, warnings and errors to STDERR, so the next program in the pipeline receives only data.

open as a page

A PHP import script run by cron ends with exit('Import failed') on errors, yet the scheduler records success; why, and how should it report failure?

level: seniorimportance: should knowfreq 32%

basics

~20 s

exit() with a string prints it and ends with status 0, meaning success. Write the message to STDERR and exit() with an integer: 0 for success, 1-254 for failures; PHP itself uses 255 for fatal errors and uncaught exceptions.

open as a page

How do you prompt for confirmation in a PHP CLI script with readline(), and what must change when input is piped instead of typed?

level: middleimportance: nice to knowfreq 15%

basics

~20 s

readline('Proceed? [y/N] ') shows a prompt and returns the typed line without its newline, or false when input ends. With piped input nobody can answer, so check stream_isatty(STDIN) first and require an explicit flag instead of prompting.

open as a page