skip to content

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%

answer

  1. short string, long array
  2. colon required, two colons optional
  3. flags come back as false
  4. stops at the first non-option
  5. rest_index finds the positional arguments

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.

solid answer

~40 s

`getopt(string $short_options, array $long_options = [], &$rest_index = null): array|false` parses the script's arguments. In the long-options array, a bare name is a flag, `name:` requires a value and `name::` takes an optional value that must be attached with `=`. For `php import.php --dry-run --limit=100 users.csv`, `getopt('', ['dry-run', 'limit:'], $rest)` returns `['dry-run' => false, 'limit' => '100']`: a flag's value is `false`, so test it with `isset()` or `array_key_exists()`, never truthiness. Values are strings; a repeated option becomes an array. Unknown options are skipped silently, and parsing stops at the first non-option argument, so options written after `users.csv` are ignored. `$rest` receives the index of the first positional argument for `array_slice($argv, $rest)`.

code

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

$opts = getopt('', ['dry-run', 'limit:'], $rest);
$files = array_slice($argv, $rest);

$dryRun = array_key_exists('dry-run', $opts);   // value is false when present
$limit = null;
if (isset($opts['limit'])) {
    $limit = is_string($opts['limit'])
        ? filter_var($opts['limit'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]])
        : false; // repeated --limit gives an array
    if ($limit === false) {
        fwrite(STDERR, "--limit must be a positive integer\n");
        exit(2);
    }
}
if ($files === []) {
    fwrite(STDERR, "usage: import.php [--dry-run] [--limit=N] <file.csv>...\n");
    exit(2);
}

go deeper

for a junior

Recall the declaration syntax — bare name for a flag, one colon for a required value — and that a present flag comes back as false, so test with isset().

for a middle

Explain the parsing rules: strings only, arrays for repeats, silent skipping of unknown options, stop at the first non-option, and rest_index for positional arguments.

for a senior

Build an entry point that validates every option, fails loudly on typos and bad values with a usage message and non-zero exit, and hands typed values to the import code.

for a principal

Decide when getopt() is enough and when a shared console library should standardise options, help text and error handling across all of the team's commands.

## The function `getopt()` is the standard library's command-line option parser. Its signature in `ext/standard/basic_functions.stub.php` is: `getopt(string $short_options, array $long_options = [], &$rest_index = null): array|false` - `$short_options` is a string of single characters, each one an option like `-v`. Only `a-z`, `A-Z` and `0-9` are allowed. - `$long_options` is an array of names, each one an option like `--dry-run`. - `$rest_index`, passed by reference, receives the index in `$argv` where option parsing stopped. It reads the arguments from `$_SERVER['argv']`, so it needs no argument array of its own, and returns `false` if it cannot find them. ## Declaring options | Declaration | Meaning | Accepted forms | |---|---|---| | `'dry-run'` | flag, no value | `--dry-run` | | `'limit:'` | value required | `--limit=100`, `--limit 100` | | `'limit::'` | value optional | `--limit=100` or `--limit`; **not** `--limit 100` | | `'n:'` in the short string | short option with required value | `-n100`, `-n 100`, `-n=100` | | `'v'` in the short string | short flag | `-v`, and `-vvv` counts three times | ## What comes back For `php import.php --dry-run --limit=100 users.csv`: ```php $opts = getopt('', ['dry-run', 'limit:'], $rest); // ['dry-run' => false, 'limit' => '100'], $rest === 3 ``` The rules behind that result: 1. **A flag's value is `false`.** Presence is signalled by the key existing, so `isset($opts['dry-run'])` or `array_key_exists('dry-run', $opts)` is the test. `if ($opts['dry-run'])` is always false — the most common getopt bug. 2. **Values are strings.** `'100'` must be validated and converted, for example `filter_var($opts['limit'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]])`, which returns `false` for bad input. 3. **Absent options are absent keys.** Supply defaults with `??`. 4. **Repeated options become arrays.** `--limit=5 --limit=10` yields `'limit' => ['5', '10']`, so a strict script checks `is_string()` before using the value. ## The parsing traps - **Unknown options are ignored silently.** A typo like `--dryrun` produces no error and no key — the import runs for real. Compare the given options with the declared ones yourself if typos must fail. - **Parsing stops at the first non-option.** In `php import.php users.csv --dry-run`, `users.csv` ends option parsing and `--dry-run` is never seen. Put options first in usage text, or scan the remainder yourself. - **A required value that is missing drops the option.** `--limit` as the last word, with no value, is not returned at all. - **Optional values need `=`.** With `'limit::'`, `--limit 100` treats `100` as a positional argument. - **`--` ends options.** Everything after a bare `--` is positional. ## `rest_index` and positional arguments Positional arguments (the CSV path here) are what remains after the options. `$rest_index` gives their start: ```php $files = array_slice($argv, $rest); ``` Without `rest_index` you would have to reconstruct which `$argv` entries were consumed as option values, which is error-prone once `--limit 100` (two words) is allowed. ## A complete import entry point - parse with `getopt('hn:', ['help', 'dry-run', 'limit:'], $rest)`; - print usage to `STDERR` and exit with a non-zero code when `$files` is empty or `--limit` fails validation; - set `$dryRun = isset($opts['dry-run']);` and pass it through, so the import reports what it would do without writing; - keep the parsing in the entry point and hand typed values (`bool $dryRun`, `?int $limit`, `list<string> $files`) to the code that does the work. For larger tools, console libraries add typed options, validation, help generation and errors for unknown options, but `getopt()` is enough for a small import script and has no dependency.

  • Why does if ($opts['dry-run']) never enter the branch even when --dry-run is passed?
    `getopt()` stores `false` as the value of an option that takes no value; presence is expressed by the key existing. `false` is falsy, so the branch never runs. Use `isset($opts['dry-run'])` or `array_key_exists('dry-run', $opts)`.
  • A user types --limt=10. What does getopt() do, and how would you catch it?
    It skips the unknown option silently, so the key is simply absent and the import runs without a limit. To catch typos, scan the arguments before `$rest` for anything starting with `--` whose name is not in your declared list, and fail with a usage message.
  • What changes if you declare 'limit::' instead of 'limit:'?
    The value becomes optional, and optional values must be attached: `--limit=100` works, while in `--limit 100` the `100` is not taken as the value and `limit` comes back as `false`. Use `::` only when a bare flag form is meaningful.

saying these in an interview costs you the question

  • A flag given on the command line comes back as true
  • getopt() reports an error for unknown options
  • Options are found wherever they appear in the argument list
  • getopt() converts --limit=100 to the integer 100
  • An optional value can be passed as --limit 100 with a space