yamadamn’s blog

IT関連技術で経験したこと・気になったことをたまに書きます

パイプを使いつつ、簡単に終了コードを取得したかったが・・

command 2>&1 | tee command.log

とかやって、command の標準出力を画面上でも確認しながら、ログにも出力したい、というケースは割とあると思います。
ただ、command の実行が成功したかどうかも知りたい。単純にパイプでつなげてしまうと、一番最後のコマンド(この場合は tee)の終了コードしか取れないんだよね。
まあ、でも簡単に取得できるよね、と思ったら、意外と試行錯誤してしまったという話。

前提知識

コマンドをグルーピングするためには、丸括弧「()」と中括弧「{}」が利用できます。

  • 丸括弧でグルーピングしたものは、サブシェル(子プロセス)で実行される
  • 中括弧でグルーピングしたものは、カレントシェル(自プロセス)で実行される

という違いがあります。例えば、

hoge=0
(hoge=1)
echo $hoge
# ->「0」が出力される。(子プロセスで変更しても自プロセスには影響を与えない)
hoge=0
{ hoge=1; }
echo $hoge
# ->「1」が出力される。(同一プロセスで実行しているので変更される)

ということになります。

試した方法

カレントシェルで実行して、終了コードを変数に保存すればいいだけじゃん、と思って対話シェルで軽く試してみる。

{ command; status=$?; }; echo $status

動作確認OKだったので、スクリプトファイルに以下を記述。

{ command; status=$?; } 2>&1 | tee command.log
if [ $status -eq 0 ]; then
  # 何かコマンド
fi

実行すると、testコマンド([ $status -eq 0 ]の部分)の引数が足りてないよ、ってエラーが。
「sh -x <スクリプトファイル>」で実行すると、確かにstatus変数が空になっているみたい。*1

再度、対話シェルでリダイレクトやパイプも追加し、試してみる。

{ command; status=$?; } 2>&1 | tee command.log
echo $status

あれ?ちゃんと取れるじゃん!Windowsコマンドプロンプトみたいに、対話シェルと実行時で動きが違うんだっけ??


ちょっと混乱。・・・気づきました。対話シェルはbashで実行してて、スクリプトファイルの実行は古きよき /bin/sh(Bourne Shell)です。そういえば、Bourne Shellでは中括弧「{}」で実行しても、リダイレクトを利用するとサブシェルで実行されてしまうんだっけ。*2
bash(など最近のシェル)では、この動きが改善されており、リダイレクトを使ってもカレントシェルで動作するようになっているようです。bashが利用できればよかったんだけど、今回は色んな環境で動作させたかったのでBourne Shellの利用が前提です。無念。


念のため

{ command 2>&1; status=$?; } | tee command.log
echo $status

標準エラー出力のリダイレクトを中に入れてみたが、パイプはリダイレクションと同様に扱われるので、やはりstatusは取得できず。そんな甘くはなかったようです。

パイプを使いつつ、(多少手間をかけて)終了コードを取得する方法

  • ファイルディスクリプタを活用する (適当に書いたので間違っている可能性高し)
status=`exec 3>&1; { command 2>&1 3>&-; echo $? 1>&3; } | tee command.log 1>&2 3>&-`
echo $status

知っている人が見れば分かるのでしょうが、トリッキーでメンテナンス性はお世辞にもよいとは言えない。

  • ファイルに終了コードを書きこんでしまう*3
status_file=/tmp/status.$$
# 中括弧じゃなくて丸括弧でサブプロセスで動作させても全然OK。
(command; echo $? >$status_file) | tee command.log
status=`cat $status_file`
rm -f $status_file

そんな複雑でもない(むしろ簡単と言える)が、わざわざファイルを介すのが多少冗長かなと。ただ、汎用性が高く、リモートシェルを使っている場合にも応用できるテクニックです。
やはり、これを使うのが現実解かもしれませんが、なんか、他に(Bourne Shellでもっと簡単にできる)いい方法はないもんですかねぇ。

*1:この場合、status変数に確実に値が入っていることが前提なので、エラーを回避するために"$status"のようにクォートしても意味がない。というか[ "" -eq 0 ] は、true(0)とみなされてしまうので逆効果。

*2:この手でよくある話は、while xxx do 〜 done >yyy.log などのように、制御構造とリダイレクトを組み合わせた場合に、ループ内部で変更した変数がループの外では利用できない、なんて現象として現れます。

*3:この方法は今回、試行錯誤する前から知っていたけど、もっと簡単な方法があるはず、と思ったんだよね。