標準入出力・パイプ・終了コード
出力データと終了判定を別々に扱い、処理のつなぎ方を確かめます。
説明
コマンドには標準入力・標準出力・標準エラーの三つの基本的な入出力先があります。出力の内容とは別に、終了コードで成功や失敗を後続処理へ伝えます。
データを出しながら未完了の終了コードを返すスクリプトを使います。データと警告を分離して集計し、パイプの途中の失敗、条件付き実行、リダイレクトの順序、上書きと追記を同じファイルで確認します。
1標準出力と標準エラーを持つ処理
producer.shは標準出力へ3行、標準エラーへ警告を1行出し、終了コード3を返します。先にソースを読みます。
cat producer.sh出力先と終了値を確認通常のprintfは標準出力へ書き、>&2付きは標準エラーへ書きます。exit 3はこのスクリプトで未完了を示すために決めた値です。0が成功、0以外が失敗を示す慣習はありますが、各数値の意味はコマンドごとに確認します。
2二つの出力を別々に保存
標準出力をdata.txt、標準エラーをerror.txtに保存し、直後の終了コードを表示します。
sh producer.sh > data.txt標準出力を保存 2> error.txt標準エラーを保存; printf 'status=%s\n' "$?"直前の終了コード; cat data.txt; cat error.txt保存内容を読む終了コードは3です。データ3行と警告1行は別ファイルに残ります。$?は直前のコマンドの終了コードで、別のコマンドを実行すると値が更新されます。結果が出力されていても、処理が成功したとは限りません。
3標準入力からデータを受け取る
data.txtを標準入力へ接続し、重複する行の出現回数を数えます。
sort < data.txtファイルを標準入力へ | uniq -c隣り合う同一行を数えるsortが行を並べ替え、その標準出力をuniqの標準入力へ渡します。uniqは隣接する同じ行をまとめるため、先に並べ替えています。パイプを流れるのはファイル名ではなくデータのバイト列です。
4最後の成功で上流の失敗が隠れる
保存を挟まずproducerから直接パイプでつなぎます。Bashのpipefailは無効にして、既定の終了コードを調べます。
set +o pipefail既定の終了判定; sh producer.sh 2> error.txt | sort | uniq -c警告を分離して集計; printf 'pipeline=%s\n' "$?"パイプ全体の終了コード集計は表示され、パイプラインの終了コードは0です。既定では最後のuniqの終了コードが使われるため、producerの3は全体の終了コードに現れません。
5各処理の終了コードを取り出す
同じパイプをもう一度実行し、BashのPIPESTATUSを直後に表示します。
sh producer.sh 2> error.txt | sort | uniq -c同じ処理を実行; printf 'stages=%s\n' "${PIPESTATUS[*]}"各段階の終了コード3 0 0の順で、producer、sort、uniqの終了コードが並びます。PIPESTATUSも次のコマンドで更新されるため、必要なら直後に変数へ保存します。どの段階が失敗したかを個別に確認できます。
6pipefailで上流の失敗を反映
pipefailを有効にして再実行します。
set -o pipefail途中の失敗も反映; sh producer.sh 2> error.txt | sort | uniq -c同じパイプ; printf 'pipeline=%s\n' "$?"変更後の終了判定今度は3です。pipefailでは右側から見て最後に現れる非0の終了コードを使い、すべて0なら0になります。出力が同じでも、終了判定を変えると後続処理を実行する条件が変わります。
7失敗したときだけ後続処理を実行
終了コードで保存完了の扱いを変えます。producerが失敗した場合には、成功メッセージを出さず再確認のメッセージを出します。
sh producer.sh > data.txt 2> error.txt出力と終了コードを分離 && printf 'accepted\n'成功時のみ || printf 'needs review\n'非0のときneeds reviewだけが表示されます。&&の右側は直前が0の場合、||の右側は直前が非0の場合に実行されます。一般にA && B || Cはif文と完全に同じではなく、Aが成功してもBが失敗すればCが実行されます。
8リダイレクトの指定順
標準出力をファイルにしてから標準エラーを同じ先へ複製します。その後、順序を逆にして比較します。
sh producer.sh > both.txt 2>&1両方をファイルへ; cat both.txt4行を確認; sh producer.sh 2>&1 > only-data.txt複製後に1番だけ変更; cat only-data.txtデータだけ確認最初は4行ともboth.txtへ入ります。後半は警告が直接表示され、only-data.txtにはデータ3行だけが入ります。2>&1はその時点の1番の行き先を2番へ複製する指定です。後から1番の行き先を変更しても、2番が自動追従するわけではありません。
9上書きと追記の違い
集計結果をファイルへ出し、追記を一回行った後、上書きでもう一度出します。
sort data.txt | uniq -c > summary.txt集計を書き込む; sort data.txt | uniq -c >> summary.txt末尾に追加; cat summary.txt二組を確認; sort data.txt | uniq -c > summary.txt内容を置き換える; cat summary.txt一組を確認追記後は同じ集計が二組、上書き後は一組です。>はコマンドが動く前に既存ファイルを切り詰めます。そのため入力と出力に同じファイルを指定すると、元の内容を読む前に失う場合があります。更新時は別ファイルに書き、成功を確認して置き換えます。
まとめ
パイプは標準出力を次の標準入力へ接続し、標準エラーは別に扱います。
パイプの出力と成功判定は別です。pipefailとPIPESTATUSで途中の失敗を確認できます。
リダイレクトの順序と上書き・追記の指定によって、残る内容が変わります。
操作履歴と解説を振り返れます。
Bash / シェル展開と入出力
cat producer.sh#!/bin/sh printf 'apple\nbanana\napple\n' printf 'warning: sample is incomplete\n' >&2 exit 3
sh producer.sh > data.txt 2> error.txt; printf 'status=%s\n' "$?"; cat data.txt; cat error.txtstatus=3 apple banana apple warning: sample is incomplete
sort < data.txt | uniq -c2 apple 1 banana
set +o pipefail; sh producer.sh 2> error.txt | sort | uniq -c; printf 'pipeline=%s\n' "$?"2 apple 1 banana pipeline=0
sh producer.sh 2> error.txt | sort | uniq -c; printf 'stages=%s\n' "${PIPESTATUS[*]}"2 apple 1 banana stages=3 0 0
set -o pipefail; sh producer.sh 2> error.txt | sort | uniq -c; printf 'pipeline=%s\n' "$?"2 apple 1 banana pipeline=3
sh producer.sh > data.txt 2> error.txt && printf 'accepted\n' || printf 'needs review\n'needs review
sh producer.sh > both.txt 2>&1; cat both.txt; sh producer.sh 2>&1 > only-data.txt; cat only-data.txtapple banana apple warning: sample is incomplete warning: sample is incomplete apple banana apple
sort data.txt | uniq -c > summary.txt; sort data.txt | uniq -c >> summary.txt; cat summary.txt; sort data.txt | uniq -c > summary.txt; cat summary.txt2 apple 1 banana 2 apple 1 banana 2 apple 1 banana
演習完了