zsh で pipe の前段の失敗時に early return する
結論
set -o pipefail でパイプ中のエラーを検出する(エラー箇所から先に進まず終了する)
逆に言えば、これを使わないとエラーが発生しても最後までコマンドを実行してしまう
説明
shell に詳しくない人は直感に反するかもしれないが、たとえば以下の shell は最後まで実行される。
sl || return $? | echo
echo 'success'
($? は直前のコマンドの exit code を示す)
これはなぜかというと、 bash や zsh の実装では pipe の各コマンドは subshell で実行されているからである。(POSIX で定められているわけではないようである)
ただし zsh をはじめとした一部の shell では、最後のコマンドのみ読み出し元のセッションで実行される。
sl | echo || return $?
echo 'success'
しかしこのようにすると、最後の || は echo に対してかかっている。echo は
sl の stdout を受け取って成功しているので、 return $? は通らない。
(return $? 時点での exit code は echo のものである)
今回のケースでは sl が失敗した時点で pipe がエラーとなっていないことが原因であるが、これは set -o pipefail を有効にすることで、エラーとする挙動となる。
これを有効にしてあると、 return の exit code も sl のものとなる。
なお、 bash ではこの手段での解決はできない。
背景
mise のセットアップスクリプトの eval "$(mise activate zsh)" の subshell の終了を拾いたかった。上述の pipe の話と同様に、 mise activate zsh は失敗するが、 stdout の "" を受け取り、 eval "" は成功してしまうので、外形からは成功としてしか見ることができない。
そこで上述の手段を用い、以下のように書き換えた。
mise activate zsh | eval "$(cat)" || return $?
これで mise が見つからない (PATH が通っていない、入っていないなど) 場合にエラーとして終了できるようになった。
return $? する必要があるのは遅延実行用の関数に分離したかったからで、詳しくは以下の通り。
Discussion