パイプを使いつつ、簡単に終了コードを取得したかったが・・
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でもっと簡単にできる)いい方法はないもんですかねぇ。