CODE Polyglot / 備忘録

コマンドの出力を | tail に流すと、失敗しても終了コードが 0 になる

パイプの終了コードは、最後のコマンド(tail)のものです。前のコマンドが失敗しても 0 になるので、bash は set -o pipefail、zsh は ${pipestatus[1]} で前のコマンドの終了コードを見ます。

起きたこと

長いデプロイの手順書を、表示を短くするために bash deploy.sh 2>&1 | tail -25 として裏で動かしました。手順書は1行目で止まっていたのに、終了コードは 0 で、成功したように見えました。

再現の手順

$ bash -c 'false | tail -1; echo "pipefail なし: $?"; set -o pipefail; false | tail -1; echo "pipefail あり: $?"'
pipefail なし: 0
pipefail あり: 1
$ zsh -c 'false | tail -1; echo "zsh なし: $?"; false | tail -1; echo "zsh pipestatus: ${pipestatus[1]}"'
zsh なし: 0
zsh pipestatus: 1

原因

パイプ全体の終了コードは、いちばん最後のコマンドの終了コードです。tail は入力を読み終われば成功するので、前のコマンドが失敗しても 0 になります。

直し方

  • bash: set -o pipefail を先に書く(どれか1つが失敗すれば、パイプ全体が失敗になる)
  • zsh: ${pipestatus[1]} で1つ目のコマンドの終了コードを見る(bash では ${PIPESTATUS[0]}。zsh は1から数える)
  • そもそもパイプにしない。出力はファイルに書いて、終了コードを取ってから、ファイルの後ろを見る
bash deploy.sh > deploy.log 2>&1; echo "exit=$?"; tail -20 deploy.log

確かめたこと

上の再現の手順を macOS 26.6 の bash 5.2.37 と zsh 5.9 で動かし、出力のとおりになりました。直したあとは、手順書の失敗が exit=1 として見えるようになりました。