PHPの標準入力・標準出力について

採用はこちら

PHPで標準入力・標準出力を扱う際は、基本的な使い方だけでなく、PHPのバージョン差や関数の戻り値、実行環境にも注意が必要です。

特に重要なのは、次の点です。

  • PHPには ferror() 関数が存在しない
  • PHP 8.4以降では、fgetcsv()escape 引数を明示した方がよい
  • fgets() の戻り値は厳密比較する
  • trim() は改行以外の空白も削除する
  • STDINSTDOUTSTDERR は主にCLI環境で使用する
  • 標準出力と標準エラー出力は用途に応じて分ける
  • 大量データは入力全体を読み込まず、逐次処理する
目次

ferror(STDIN) は使用できない

PHPには、標準の ferror() 関数は存在しません。

そのため、次のコードは正しくありません。

if (ferror(STDIN)) {
    fwrite(STDERR, "標準入力の読み込み中にエラーが発生しました。\n");
    exit(1);
}

実行すると、次のようなエラーが発生します。

Fatal error: Uncaught Error: Call to undefined function ferror()

標準入力を1行ずつ読み込む基本形

標準入力を入力終了まで1行ずつ読み込む場合は、次のように記述します。

<?php

declare(strict_types=1);

while (($line = fgets(STDIN)) !== false) {
    $line = rtrim($line, "\r\n");

    if ($line === '') {
        continue;
    }

    fwrite(
        STDOUT,
        mb_strtoupper($line, 'UTF-8') . "\n"
    );
}

exit(0);

通常のファイル入力やパイプ入力では、fgets()false を返した時点で、入力終了として扱うことが一般的です。

ただし、false が返った理由が、必ず正常な入力終了であるとは限りません。

読み取り不能や、ストリームの状態によっても false が返る可能性があります。

fgets() の戻り値は厳密比較する

fgets() は、標準入力から1行分の文字列を読み取ります。

$line = fgets(STDIN);

読み取りに成功した場合は文字列を返し、入力終端や読み取り不能の場合は false を返します。

if ($line === false) {
    // 入力終端または読み取り不能
}

if (!$line) を避ける理由

次のような判定は避けた方が安全です。

if (!$line) {
    // 入力がないと判定
}

PHPでは、文字列の "0" も偽として評価されるためです。

たとえば、ユーザーが次の値を入力したとします。

0

曖昧な比較を使用すると、正しい入力である "0" を、入力がない状態として誤判定する可能性があります。

そのため、必ず次のように厳密比較を使用します。

if ($line === false) {
    // 読み取れなかった場合のみ処理する
}

feof() で入力終端を確認する

feof() を使用すると、ストリームが入力終端に到達したかを補助的に確認できます。

$line = fgets(STDIN);

if ($line === false) {
    if (feof(STDIN)) {
        fwrite(STDERR, "入力が終了しました。\n");
    } else {
        fwrite(STDERR, "入力を読み取れませんでした。\n");
    }
}

ただし、ノンブロッキングストリームや特殊なストリームでは挙動が異なる場合があります。

一般的なCLIスクリプトで、ファイルやパイプから入力を受け取る場合は、次の形が基本です。

while (($line = fgets(STDIN)) !== false) {
    // 1行ずつ処理する
}

PHP 8.4以降の fgetcsv() に注意する

CSVを標準入力から読み取る場合は、fgetcsv() を使用できます。

$row = fgetcsv(STDIN);

ただし、PHP 8.4以降では、第5引数の escape を省略してデフォルト値に依存する書き方が非推奨になっています。

escape 引数を明示する

位置引数を使う場合は、次のように記述します。

while (($row = fgetcsv(
    STDIN,
    null,
    ',',
    '"',
    ''
)) !== false) {
    print_r($row);
}

名前付き引数を使うと、各引数の役割が分かりやすくなります。

while (($row = fgetcsv(
    stream: STDIN,
    length: null,
    separator: ',',
    enclosure: '"',
    escape: ''
)) !== false) {
    print_r($row);
}

CSVのヘッダーを利用する例

<?php

declare(strict_types=1);

$header = fgetcsv(
    stream: STDIN,
    length: null,
    separator: ',',
    enclosure: '"',
    escape: ''
);

if ($header === false) {
    fwrite(
        STDERR,
        "CSVヘッダーを読み取れませんでした。\n"
    );
    exit(1);
}

while (($row = fgetcsv(
    stream: STDIN,
    length: null,
    separator: ',',
    enclosure: '"',
    escape: ''
)) !== false) {
    if ($row === [null]) {
        continue;
    }

    if (count($header) !== count($row)) {
        fwrite(
            STDERR,
            "ヘッダーとデータの列数が一致しません。\n"
        );
        continue;
    }

    $record = array_combine($header, $row);

    if ($record === false) {
        fwrite(
            STDERR,
            "CSVデータの変換に失敗しました。\n"
        );
        continue;
    }

    print_r($record);
}

CSVの空行に注意する

CSV内に空行がある場合、fgetcsv() が次の配列を返すことがあります。

[null]

空行を無視したい場合は、次の判定を追加します。

if ($row === [null]) {
    continue;
}

STDINSTDOUTSTDERR の役割

PHPのCLI環境では、標準入力・標準出力・標準エラー出力を表す定数が用意されています。

STDIN
STDOUT
STDERR

それぞれの役割は次のとおりです。

定数役割
STDIN標準入力を受け取る
STDOUT正常な処理結果を出力する
STDERRエラー、警告、ログを出力する

標準入力を受け取る

$line = fgets(STDIN);

標準出力へ書き込む

fwrite(STDOUT, "処理結果\n");

簡潔に書く場合は、echo も使用できます。

echo "処理結果\n";

標準エラー出力へ書き込む

fwrite(
    STDERR,
    "エラーが発生しました。\n"
);

STDINSTDOUTSTDERR は主にCLIで使用する

STDINSTDOUTSTDERR は、主にコマンドラインからPHPを実行するCLI環境で使用します。

CLI専用のスクリプトであることを確認する場合は、PHP_SAPI を使用します。

<?php

declare(strict_types=1);

if (PHP_SAPI !== 'cli') {
    exit('このプログラムはCLI専用です。');
}

CLI系SAPIを複数許可する場合

PHPには、通常のCLI以外に phpdbg などのコマンドライン系SAPIもあります。

複数のSAPIを許可する場合は、次のように判定できます。

if (!in_array(
    PHP_SAPI,
    ['cli', 'phpdbg'],
    true
)) {
    exit(
        'このプログラムはコマンドライン専用です。'
    );
}

一般的なバッチ処理では、cli だけを許可することが多いため、用途に応じて判断します。

php://stdinSTDIN の違い

STDIN は、PHPがあらかじめ用意している標準入力のストリームリソースです。

$line = fgets(STDIN);

一方、php://stdin は、標準入力を表すストリームURLです。

$handle = fopen('php://stdin', 'r');

if ($handle === false) {
    throw new RuntimeException(
        '標準入力を開けませんでした。'
    );
}

$line = fgets($handle);

基本的には STDIN を使用する

単純なCLIスクリプトでは、STDIN を直接使う方が簡潔です。

$line = fgets(STDIN);

入力全体を一度に読み取る場合は、次のように記述できます。

$input = stream_get_contents(STDIN);

または、次の書き方も可能です。

$input = file_get_contents('php://stdin');

php://inputphp://stdin の違い

php://stdinphp://input は用途が異なります。

ストリーム主な用途
php://stdinCLIの標準入力
php://inputHTTPリクエストの生のボディ
php://stdoutPHPプロセスの標準出力
php://stderrPHPプロセスの標準エラー出力
php://outputPHPの出力バッファへの出力

php://input の使用例

JSON形式のHTTPリクエストボディを受け取る場合は、次のように記述します。

<?php

declare(strict_types=1);

$body = file_get_contents('php://input');

if ($body === false) {
    http_response_code(400);
    exit(
        'リクエストボディを読み取れませんでした。'
    );
}

try {
    $data = json_decode(
        $body,
        true,
        512,
        JSON_THROW_ON_ERROR
    );
} catch (JsonException $e) {
    http_response_code(400);
    exit('JSON形式が不正です。');
}

multipart/form-data では制約がある

ファイルアップロードなどで使用される multipart/form-data では、php://input の利用に制約があります。

アップロードされたファイルは、通常は $_FILES から取得します。

$uploadedFile = $_FILES['file'] ?? null;

通常のフォームデータは、$_POST から取得します。

$name = $_POST['name'] ?? '';

trim()rtrim() を使い分ける

fgets(STDIN) で取得した文字列には、通常、行末の改行文字が含まれます。

$line = fgets(STDIN);

改行を取り除くために、trim() が使われることがあります。

$line = trim(fgets(STDIN));

ただし、trim() は改行だけでなく、次の文字も削除します。

  • 半角スペース
  • タブ
  • 改行
  • キャリッジリターン
  • NULL文字
  • 垂直タブ

行末の改行だけを削除する

入力値の先頭や末尾にあるスペースを残したい場合は、rtrim() の削除対象を明示します。

$line = rtrim(
    fgets(STDIN),
    "\r\n"
);

この書き方なら、行末のCRとLFだけを削除できます。

入力全体に trim() を使う場合の注意

次のコードは、入力全体の先頭と末尾にある空白文字をすべて削除します。

$input = trim(
    stream_get_contents(STDIN)
);

入力データが次の内容だった場合、

  hello world

先頭のスペースは削除されます。

ファイル変換やテキストフィルターなど、空白もデータとして保持する必要がある場合は、trim() を使用しない方が安全です。

$input = stream_get_contents(STDIN);

空白区切りの入力を処理する

1行に複数の値が空白区切りで入力される場合は、explode() または preg_split() を使用できます。

explode() の注意点

次のコードは、半角スペース1文字を区切りとして分割します。

$values = explode(
    ' ',
    trim($line)
);

入力内に複数の連続スペースがあると、空文字列が配列に含まれます。

入力例は次のとおりです。

10   20

分割結果は次のようになります。

[
    '10',
    '',
    '',
    '20',
]

preg_split() を使用する

連続するスペースやタブにも対応する場合は、preg_split() を使用します。

$values = preg_split(
    '/\s+/',
    trim($line)
);

整数へ変換する場合は、次のように記述できます。

$values = preg_split(
    '/\s+/',
    trim($line)
);

if ($values === false) {
    fwrite(
        STDERR,
        "入力の分割に失敗しました。\n"
    );
    exit(1);
}

$numbers = array_map(
    'intval',
    $values
);

空入力を先に確認する

入力が空の場合は、分割処理の前に確認します。

$line = trim($line);

if ($line === '') {
    fwrite(
        STDERR,
        "値を入力してください。\n"
    );
    exit(1);
}

$values = preg_split(
    '/\s+/',
    $line
);

整数入力はキャストだけで検証しない

次のコードは、入力値を整数へ変換します。

$value = (int) trim(
    fgets(STDIN)
);

ただし、PHPでは不正な文字列も 0 に変換されます。

(int) 'abc'; // 0

そのため、正しい入力値の 0 と、不正入力の abc を区別できません。

filter_var() で整数を検証する

$line = fgets(STDIN);

if ($line === false) {
    fwrite(
        STDERR,
        "入力がありません。\n"
    );
    exit(1);
}

$input = trim($line);

$value = filter_var(
    $input,
    FILTER_VALIDATE_INT
);

if ($value === false) {
    fwrite(
        STDERR,
        "整数を入力してください。\n"
    );
    exit(1);
}

0 は正しい整数として返されるため、判定には必ず厳密比較を使用します。

if ($value === false) {
    // 不正な入力
}

整数の範囲を制限する

$value = filter_var(
    $input,
    FILTER_VALIDATE_INT,
    [
        'options' => [
            'min_range' => 1,
            'max_range' => 100,
        ],
    ]
);

この例では、1以上100以下の整数だけを受け付けます。

標準出力と標準エラー出力を分ける

正常な処理結果は標準出力へ、エラーやログは標準エラー出力へ出力します。

fwrite(
    STDOUT,
    "処理結果\n"
);

fwrite(
    STDERR,
    "エラーが発生しました。\n"
);

JSON出力にログを混ぜない

機械処理用のJSONを標準出力へ出す場合、案内文やログを混ぜると、JSONとして読み取れなくなります。

不適切な例は次のとおりです。

echo "処理を開始します\n";
echo json_encode($result) . "\n";

出力は次のようになります。

処理を開始します
{"status":"ok"}

この全体は、有効なJSONではありません。

ログは標準エラー出力へ分けます。

fwrite(
    STDERR,
    "処理を開始します\n"
);

echo json_encode(
    $result,
    JSON_UNESCAPED_UNICODE
    | JSON_THROW_ON_ERROR
) . "\n";

これにより、標準出力にはJSONだけが出力されます。

echoprintfwrite() の違い

PHPで標準出力へ文字列を出す代表的な方法は、次の3つです。

echo "Hello\n";
print "Hello\n";
fwrite(STDOUT, "Hello\n");

echo

echo "Hello\n";

最も簡潔で、一般的によく使用されます。

複数の値をカンマ区切りで渡すこともできます。

echo "Hello", " ", "PHP", "\n";

print

$result = print "Hello\n";

print は、常に整数の 1 を返します。

ただし、戻り値を利用する場面は多くありません。

fwrite()

$writtenBytes = fwrite(
    STDOUT,
    "Hello\n"
);

fwrite() は、書き込んだバイト数を返します。

書き込みに失敗した場合は false を返します。

$result = fwrite(
    STDOUT,
    "Hello\n"
);

if ($result === false) {
    exit(1);
}

入力全体を一度に読み取る

標準入力全体をまとめて取得する場合は、stream_get_contents() を使用します。

$input = stream_get_contents(STDIN);

失敗時には false を返す可能性があるため、確認します。

$input = stream_get_contents(STDIN);

if ($input === false) {
    fwrite(
        STDERR,
        "標準入力の読み込みに失敗しました。\n"
    );
    exit(1);
}

次の書き方も可能です。

$input = file_get_contents(
    'php://stdin'
);

標準入力は繰り返し読めない場合がある

標準入力全体を一度読み取ると、通常は入力終端に到達します。

$input1 = stream_get_contents(STDIN);
$input2 = stream_get_contents(STDIN);

この場合、$input2 から同じ内容を取得できるとは限りません。

入力を複数回利用する場合は、最初に変数へ保存します。

$input = stream_get_contents(STDIN);

if ($input === false) {
    exit(1);
}

// $inputを複数回利用する

大量データは1行ずつ処理する

stream_get_contents(STDIN) は、入力全体をメモリへ読み込みます。

$input = stream_get_contents(STDIN);

数GBのログやCSVを処理する場合は、メモリ不足になる可能性があります。

大量データは、1行ずつ逐次処理します。

while (($line = fgets(STDIN)) !== false) {
    $line = rtrim(
        $line,
        "\r\n"
    );

    // 1行ずつ処理する
}

1行自体が巨大な場合

1行ずつ処理しても、1行自体が非常に大きい場合は、多くのメモリを使用します。

たとえば、次のようなデータです。

  • 改行のない巨大なJSON
  • 非常に長いCSVレコード
  • 改行のないテキストデータ

固定サイズで処理できる場合は、fread() を使用します。

while (!feof(STDIN)) {
    $chunk = fread(
        STDIN,
        8192
    );

    if ($chunk === false) {
        fwrite(
            STDERR,
            "読み込みに失敗しました。\n"
        );
        exit(1);
    }

    if ($chunk === '') {
        break;
    }

    // 8192バイト単位で処理する
}

標準入力が端末かどうかを判定する

CLIツールでは、標準入力がキーボードから来ているのか、パイプやリダイレクトなどの端末以外から来ているのかを判定したい場合があります。

その場合は、stream_isatty() を使用します。

if (stream_isatty(STDIN)) {
    fwrite(
        STDOUT,
        "標準入力は端末に接続されています。\n"
    );
} else {
    fwrite(
        STDOUT,
        "標準入力は端末に接続されていません。\n"
    );
}

TTYではない入力の例

stream_isatty(STDIN)false の場合、入力元には次のようなものが考えられます。

  • パイプ
  • ファイルリダイレクト
  • ソケット
  • CI環境の入力ストリーム
  • IDEが提供するストリーム
  • 特殊なデバイス

そのため、false を必ずパイプ入力と断定するのは正確ではありません。

対話入力のときだけ案内を表示する

if (stream_isatty(STDIN)) {
    fwrite(
        STDOUT,
        "文字列を入力してください: "
    );
    fflush(STDOUT);
}

$input = stream_get_contents(STDIN);

if ($input === false) {
    fwrite(
        STDERR,
        "入力を読み取れませんでした。\n"
    );
    exit(1);
}

出力バッファリングに注意する

標準出力へ書き込んだ内容が、必ず即座に画面へ表示されるとは限りません。

PHP、OS、ターミナル、パイプ先のプログラムなどが、出力を一時的にバッファへ保持する場合があります。

プロンプトを即時表示する

fwrite(
    STDOUT,
    "名前を入力してください: "
);

fflush(STDOUT);

$name = fgets(STDIN);

fflush() は、指定したストリームの出力バッファを書き出します。

ただし、環境によっては、fflush() を呼んでも即時表示が完全には保証されません。

改行コードを使い分ける

PHPで改行を出力する場合は、"\n" または PHP_EOL を使用します。

echo "Hello\n";
echo "Hello" . PHP_EOL;

PHP_EOL

PHP_EOL は、実行しているOSの一般的な改行コードを表します。

  • Linux、macOS系:LF
  • Windows:CRLF

"\n"

外部仕様やデータフォーマットでLFが指定されている場合は、"\n" を使用します。

echo $result . "\n";

競技プログラミング、JSON Lines、Linux向けCLIツールなどでは、"\n" が使われることが一般的です。

コマンドライン引数と標準入力を区別する

次のように実行した場合、

php main.php apple banana

applebanana は標準入力ではなく、コマンドライン引数です。

print_r($argv);

出力は次のようになります。

Array
(
    [0] => main.php
    [1] => apple
    [2] => banana
)

一方、次のデータは標準入力です。

echo "apple banana" | php main.php

PHPでは次のように取得します。

$input = stream_get_contents(STDIN);

コマンドライン引数と標準入力を組み合わせる

cat access.log | php analyze.php --format=json

この場合、役割は次のように分かれます。

  • access.log の内容:標準入力
  • --format=json:コマンドライン引数

リダイレクトとパイプ

標準入力・標準出力・標準エラー出力は、シェルのリダイレクトやパイプと組み合わせて使用できます。

ファイルを標準入力として渡す

php main.php < input.txt

標準出力をファイルに保存する

php main.php > output.txt

標準出力をファイルへ追記する

php main.php >> output.txt

標準エラー出力をファイルへ保存する

php main.php 2> error.log

標準出力と標準エラー出力を分ける

php main.php > output.txt 2> error.log

標準出力と標準エラー出力を同じファイルへ保存する

php main.php > all.log 2>&1

別のコマンドへ渡す

php main.php | sort

これらの構文は、主にBashやZshなどのUnix系シェルを前提としています。

Windowsでは、コマンドプロンプト、PowerShell、Git Bash、WSLなど、使用するシェルによって細かな挙動が異なります。

終了コードを適切に返す

CLIプログラムでは、処理結果を終了コードで通知します。

正常終了

exit(0);

異常終了

exit(1);

一般的には、次のルールが使用されます。

  • 0:成功
  • 0以外:失敗

終了コードを用途別に分ける

const EXIT_SUCCESS = 0;
const EXIT_FAILURE = 1;
const EXIT_INVALID_INPUT = 2;
const EXIT_PROCESSING_ERROR = 3;
if ($input === '') {
    fwrite(
        STDERR,
        "入力が空です。\n"
    );
    exit(EXIT_INVALID_INPUT);
}

終了コードを分けることで、シェルスクリプトやバッチ処理側から失敗理由を判定しやすくなります。

JSONを標準入力から受け取る

JSONを標準入力から受け取り、解析して出力する例です。

<?php

declare(strict_types=1);

$input = stream_get_contents(STDIN);

if ($input === false) {
    fwrite(
        STDERR,
        "標準入力の読み込みに失敗しました。\n"
    );
    exit(1);
}

try {
    $data = json_decode(
        $input,
        true,
        512,
        JSON_THROW_ON_ERROR
    );
} catch (JsonException $e) {
    fwrite(
        STDERR,
        "JSONの解析に失敗しました: "
        . $e->getMessage()
        . "\n"
    );
    exit(1);
}

try {
    $json = json_encode(
        $data,
        JSON_UNESCAPED_UNICODE
        | JSON_PRETTY_PRINT
        | JSON_THROW_ON_ERROR
    );
} catch (JsonException $e) {
    fwrite(
        STDERR,
        "JSONの生成に失敗しました: "
        . $e->getMessage()
        . "\n"
    );
    exit(1);
}

fwrite(
    STDOUT,
    $json . "\n"
);

exit(0);

実務向けの基本テンプレート

標準入力全体を読み取り、処理結果を標準出力へ返すCLIスクリプトの例です。

<?php

declare(strict_types=1);

const EXIT_SUCCESS = 0;
const EXIT_FAILURE = 1;
const EXIT_INVALID_INPUT = 2;

if (PHP_SAPI !== 'cli') {
    exit(
        'このプログラムはCLI専用です。'
    );
}

try {
    $input = stream_get_contents(STDIN);

    if ($input === false) {
        throw new RuntimeException(
            '標準入力の読み込みに失敗しました。'
        );
    }

    $value = rtrim(
        $input,
        "\r\n"
    );

    if ($value === '') {
        fwrite(
            STDERR,
            "入力が空です。\n"
        );
        exit(EXIT_INVALID_INPUT);
    }

    $result = mb_strtoupper(
        $value,
        'UTF-8'
    );

    $writtenBytes = fwrite(
        STDOUT,
        $result . "\n"
    );

    if ($writtenBytes === false) {
        throw new RuntimeException(
            '標準出力への書き込みに失敗しました。'
        );
    }

    exit(EXIT_SUCCESS);
} catch (Throwable $e) {
    fwrite(
        STDERR,
        $e->getMessage() . "\n"
    );

    exit(EXIT_FAILURE);
}

大量データ向けの逐次処理テンプレート

大量のテキストを1行ずつ処理する例です。

<?php

declare(strict_types=1);

const EXIT_SUCCESS = 0;
const EXIT_FAILURE = 1;

if (PHP_SAPI !== 'cli') {
    exit(
        'このプログラムはCLI専用です。'
    );
}

while (($line = fgets(STDIN)) !== false) {
    $line = rtrim(
        $line,
        "\r\n"
    );

    $converted = mb_strtoupper(
        $line,
        'UTF-8'
    );

    $writtenBytes = fwrite(
        STDOUT,
        $converted . "\n"
    );

    if ($writtenBytes === false) {
        fwrite(
            STDERR,
            "標準出力への書き込みに失敗しました。\n"
        );

        exit(EXIT_FAILURE);
    }
}

exit(EXIT_SUCCESS);

PHPの標準入力・標準出力の要点

PHPで標準入力を1行読み取る基本形は、次のとおりです。

$line = fgets(STDIN);

入力全体を読み取る場合は、次のように記述します。

$input = stream_get_contents(STDIN);

正常な処理結果は、標準出力へ書き込みます。

fwrite(
    STDOUT,
    "処理結果\n"
);

エラーやログは、標準エラー出力へ書き込みます。

fwrite(
    STDERR,
    "エラーが発生しました。\n"
);

実務では、次の点を意識することが重要です。

  • fgets() の戻り値は === false で判定する
  • PHPには ferror() 関数がない
  • trim() は改行以外の空白も削除する
  • PHP 8.4以降では fgetcsv()escape 引数を明示する
  • 大量データは1行ずつ処理する
  • JSONやCSVなどの機械処理用データにログを混ぜない
  • 正常な結果は STDOUT、エラーやログは STDERR へ出す
  • 正常終了は exit(0)、異常終了は0以外を返す
  • CLI専用処理では PHP_SAPI を確認する

以上、PHPの標準入力・標準出力についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次