skip to content

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%

answer

  1. prompt string in, line out
  2. false when input ends
  3. history is added by hand
  4. extension may be missing
  5. stream_isatty(STDIN) before prompting

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.

solid answer

~40 s

`readline(?string $prompt = null): string|false` from the readline extension prints the prompt, lets the user edit a line, and returns it without the trailing newline; it returns `false` when there is no more input, for example after Ctrl-D. Lines are not added to history automatically — call `readline_add_history()` if you want that. Treat anything other than an explicit `y` as "no", and treat `false` as "no" too. Before prompting, check `stream_isatty(STDIN)`: in cron or with `cat users.csv | php import.php`, standard input is not a terminal, so a prompt would either consume data or get end-of-input. In that case skip the prompt and require a flag such as `--yes`. Because readline is an optional extension, fall back to `fgets(STDIN)` when `function_exists('readline')` is false.

code

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

function confirm(string $question, bool $assumeYes): bool
{
    if (!stream_isatty(STDIN)) {
        return $assumeYes; // no person to ask: only an explicit --yes proceeds
    }
    if (function_exists('readline')) {
        $answer = readline($question . ' [y/N] ');
    } else {
        fwrite(STDERR, $question . ' [y/N] ');
        $answer = fgets(STDIN);
    }
    if ($answer === false) {
        return false; // end of input is never consent
    }
    return in_array(strtolower(trim($answer)), ['y', 'yes'], true);
}

$opts = getopt('', ['yes'], $rest);
if (!confirm('Import 1240 rows?', isset($opts['yes']))) {
    fwrite(STDERR, "aborted\n");
    exit(2);
}

go deeper

for a junior

Recall that readline() shows a prompt and returns the typed line without its newline, or false when input ends.

for a middle

Explain the default-no confirmation pattern, adding history by hand, and the fgets(STDIN) fallback when the readline extension is missing.

for a senior

Make commands safe for both people and automation: stream_isatty() decides whether to prompt, non-interactive runs require an explicit flag, and prompts never mix into data output.

for a principal

Set a rule for destructive commands — dry-run first, explicit confirmation or flag, documented non-interactive behaviour — so automation and humans follow the same safety path.

## Why prompt at all A data-import command that writes to a production database is a good candidate for a last check: after a `--dry-run` shows what would change, the real run asks **"Import 1,240 rows? [y/N]"** before touching anything. The question is how to ask safely in PHP, and what to do when no person is there to answer. ## `readline()` The **readline** extension wraps the GNU Readline or libedit library. Its main function: `readline(?string $prompt = null): string|false` - prints the prompt and lets the user type with line editing (arrow keys, backspace); - returns the line **without** its trailing newline; - returns `false` when there is no more data, for example when the user presses Ctrl-D or input is closed. Related functions: | Function | Purpose | |---|---| | `readline_add_history(string $prompt): true` | add a line to the in-memory history (not done automatically) | | `readline_read_history()` / `readline_write_history()` | load and save history from a file | | `readline_completion_function(callable $callback): bool` | register tab completion | For a yes/no confirmation you need only `readline()` itself. ## Reading the answer safely 1. Normalise: `strtolower(trim($answer))`. 2. Accept only explicit consent: `'y'` or `'yes'`. 3. Treat everything else — an empty line, `'n'`, a typo, and `false` — as **no**. This "default no" rule matters because `false` from end-of-input must never be read as permission. A loose check like `if ($answer !== 'n')` would import on Ctrl-D. ## When input is not a terminal Commands also run from cron, CI and pipelines. Then: - in `cat users.csv | php import.php`, standard input carries the CSV, so a prompt would consume the first data line as the "answer"; - under cron, standard input is usually empty or closed, so `readline()` returns `false` immediately; - either way, no person will ever see the question. So decide **before** prompting: - `stream_isatty(STDIN)` returns `true` only when standard input is a terminal; - if it is not, do not prompt: proceed only when an explicit flag such as `--yes` was given, otherwise exit with a usage error and a message on `STDERR`; - if it is, prompt as usual. This mirrors the non-interactive options of mature command-line tools: automation states its intent through flags, people answer prompts. ## When readline is missing readline is an **optional extension**; it has to be compiled in or installed, and slim images often omit it. The manual also notes that some readline functions exist only with certain underlying libraries. A portable prompt helper therefore: - uses `readline()` when `function_exists('readline')` is true; - otherwise writes the prompt to `STDERR` (so it never pollutes data on standard output) and reads a line with `fgets(STDIN)`, trimming the newline and treating `false` as end of input. ## Putting it together for the import command - `--dry-run` never prompts: it only reports what would happen; - a real run on a terminal asks for confirmation with a default of "no"; - a real run without a terminal requires `--yes`, or stops with exit code 2 and a message explaining the flag; - the prompt itself goes to the terminal, never into the data written on `STDOUT`.

  • What does readline() return when the user presses Ctrl-D at the prompt, and how should the script treat it?
    It returns `false`, meaning there is no more input. A confirmation helper must treat `false` as "no" — never as an empty answer that falls through to a default of yes — so closing input can never trigger the import.
  • Why is the fallback prompt written to STDERR rather than echoed?
    Standard output may carry the command's data or be redirected to a file. Writing the question to `STDERR` keeps it on the terminal and out of that data, the same way progress and error messages are kept out of it.

saying these in an interview costs you the question

  • readline() returns the line including its trailing newline
  • readline() adds every line to the history automatically
  • readline() is always available because it is part of the core
  • An empty or false answer can safely be treated as yes
  • Prompting works the same in cron as in a terminal