⚙️

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 $? する必要があるのは遅延実行用の関数に分離したかったからで、詳しくは以下の通り。

https://zenn.dev/euxn23/articles/8cf91b768b46b3

Discussion