shell scriptの標準入力は短い一行でも扱えますが、入力の種類や版を確認しないまま本番へ使うと誤判定を招きます。この記事の結論は「標準入力はfile descriptor 0として扱い、line入力はIFS= read -r、file入力はredirectionを基本にします。pipeline内のsubshellと終了codeを確認します。」。Bashでfile descriptor 0を読み、terminal、file、pipe、here-documentから入力が届く場合を対象に、確認結果から次の行動を選べる形で解説します。 確認ポイント:read -rを使い、末尾newlineのない最終行も捨てないloop条件にします。
stdinがterminalかredirectかを判定する
手順書の「対象」をwildcardのままにせず、実際の一覧へ展開してreviewします。read -r、redirection、pipelineがshell builtin、cmdlet、外部programのどれかも確認し、別実装のoptionを混在させません。
- stdinがterminal、file、pipeのどれか確認する
- line、byte、structured dataの単位を決める
- 末尾newlineなしとbackslashをtestする
- producerとconsumer双方の終了codeを記録する
実体や対象が確定しない間は変更commandを組み立てません。test用copyで構文と件数だけを検証します。
read・redirect・pipelineを用途で選ぶ
一行をそのまま読む
IFS= read -r line
printf '%s\n' "$line"
IFS=と-rで前後空白とbackslashを保持します。
fileをloopへredirect
while IFS= read -r line || [ -n "$line" ]; do
printf '%s\n' "$line"
done < ./input.txt
末尾newlineがない最終lineも扱います。
command出力をpipelineで受ける
producer | while IFS= read -r line; do
printf '%s\n' "$line"
done
loopがsubshellになるshellでは、loop内variableを後で使わない設計にします。
stdinがterminalか判定
if [ -t 0 ]; then printf '%s\n' 'stdin is a terminal'; else printf '%s\n' 'stdin is redirected'; fi
interactive promptを出すかどうかの判断に使えます。
固定test入力を渡す
./check-input.sh <<'EOF'
alpha
beta with spaces
EOF
quoted delimiterなら本文中のparameter展開を抑止できます。
末尾newlineとsubshellの影響を切り分ける
- readで-rを省いてbackslashを失う
- for word in $(cat file)で空白を壊す
- pipeline loop内のvariableを親shellで期待する
- 末尾newlineなしの最終lineを落とす
- binaryやJSONをline単位で誤parseする
似た名前のcommandやoptionを入れ替えず、現在の実体のhelpへ戻ります。
readのstatusと取得文字列を分ける
stdinは単なる文字列ではなくopen file descriptionへつながるdescriptorです。readはdelimiterまでを一件として読み、NUL、binary、巨大一行、structured formatには専用parserが適します。pipelineの各commandは別processになり得るため、variable scopeとpipefailの有無を明示します。
値の単位、累積か瞬間か、localかUTCかを記録します。表示formatをdata objectと混同しません。
stdin処理と出力先変更を分離する
外部入力は信頼せず、evalやunquoted expansionへ渡しません。passwordやtokenをstdinへ渡してもprocess側のlogやtraceへ残る場合があります。入力上限、timeout、encoding error時の停止を決め、原本への書き戻しは別段階にします。
元データと作業copyを別pathに置き、生成途中を正本へ上書きしません。不合格なら候補だけを破棄します。
入力件数と最終行を照合する
空入力、空line、前後空白、backslash、末尾newlineなし、非常に長い一行、producer failureをsample化します。行数、byte数、終了code、親shellのvariable状態を照合します。
testと本番の証跡を分け、どの確認を通過して反映したかを追跡できる形にします。
秘密値を除いたstdin fixtureを保存する
実例のうち「一行をそのまま読む」はbaselineを得る用途、「fileをloopへredirect」は対象をさらに具体化する用途として使い分けます。両方の出力を同じ形式へ無理に整形せず、元の型と件数を保持したまま比較します。期待値は画面の見た目ではなく、対象ID、path、時刻など再照合できる列で定義します。
標準入力の検証では、入力元コマンドの終了値、末尾改行の有無、空入力を別々に試します。パイプの途中が失敗しても末尾コマンドだけ成功する構成があるため、Bashでは必要に応じて pipefail を検証用サブシェル内で使います。端末入力かパイプ入力かを分けるなら test -t 0 の判定結果も記録し、同じ入力を再現できる小さなfixtureで確認します。
固定入力を渡す検証には、実装差の出やすい echo より printf '%s\n' を使うと、改行とescapeの扱いを明示できます。複数行は引用したhere-documentで作り、変数展開を許すかどうかをdelimiterの引用で決めます。実データや秘密値をfixtureへ複製せず、空入力、通常入力、不正入力を再現できる最小の値だけを用意します。
再試行の前に、read へ -r を付けずbackslashを失っていないか、for word in $(cat file) で空白と空行を壊していないか確認します。pipeline内のwhile loopがsubshellになるshellでは、loop内の代入を親へ持ち帰れると仮定せず、最小fixtureでscopeを確かめます。
標準入力にtokenやpasswordを渡す場合、set -x、process一覧、診断ログへ値が露出しない設計が必要です。検証には架空値を使い、stdinの原本を別の公開ファイルへ複製しません。binary、JSON、NUL区切りの入力を行単位の read で扱えると仮定せず、形式に対応したparserを選びます。
再現記録には、実行shellとversion、producerとconsumerのcommand、入力byte数と末尾改行、pipelineの終了値を含めます。Bashで pipefail を使った場合はその有無も残します。読み取りだけの例ではrollbackは不要で、出力ファイルを作った場合だけ、その新規pathと削除判断を記録します。
完了条件は、空入力、空白を含む行、backslash、末尾改行なしの最終行が期待どおり処理されることです。producer失敗とconsumer失敗を区別し、端末入力とpipe入力の分岐も確認します。処理件数が期待値と一致しない場合は、本番入力へ進まずfixtureへ戻ります。
shell scriptの標準入力の開始記録には「stdinがterminal、file、pipeのどれか確認する」を最初に置きます。続けて「line、byte、structured dataの単位を決める」を確認すると、対象違いと環境違いを作業前に分けられます。
対象範囲を確定する段階では「末尾newlineなしとbackslashをtestする」が判断材料になります。また「producerとconsumer双方の終了codeを記録する」を満たさない場合は、技術的に実行できても運用上の準備不足です。

コメント