27
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

シェルスクリプト&PowerShellAdvent Calendar 2023

Day 10

【POSIX準拠】set -o pipefailを使おう!ただしdash、テメーはダメだ

Last updated at Posted at 2023-12-10

はじめに

set -o pipefail は POSIX で標準化されているシェルオプションです。パイプラインにおけるエラーを確実に検出するために、シェルスクリプトでは基本的に使うようにしましょう。

dashで2024-04-05にmasterにマージされたようです。
おそらくv0.5.13に入るのではないかと思います。

https://git.kernel.org/pub/scm/utils/dash/dash.git/commit/?id=6347b9fc52d742f36a0276cdea06cd9ad1f02c77


某コメントより

“set -o pipefail は標準化されました” っていってここ何年かの標準化を無邪気に正当化できるのいいなと思う(目の前のターミナルを見ながら)

どのシェルを今使っているのか聞きたいですね。商用 Unix を含む主流の環境で、すでに何年(十数年、数十年)も前から set -o pipefail は実装済みなんですが? おそらくシェルの事をよく知らないで言ってるのでしょう。私は標準化の有無は関係なく実際のシェルのことを調べ尽くして言ってるわけで無邪気に正当化とか失礼な話です。標準化とか気にしてるから何年(十数年、数十年)も前に実装された便利な機能が使えないんですよ。自業自得です。

「set -o pipefail」ってどんな機能?

詳細は別の記事やシェルのドキュメントを参照してもらうとして、簡単に言えばパイプライン全体の終了ステータスをデフォルトのパイプラインに書いたコマンドのうち「最後のコマンドの終了ステータス」から、「エラーになった最後のコマンドの終了ステータス」(エラーになったコマンドがなければ 0)に変更するためのシェルのオプションです。

デフォルトでは cmd1 や cmd2 でエラーが発生してもcmd3の終了ステータスになる
cmd1 | cmd2 | cmd3
echo $? # cmd3 の終了ステータス(cmd1 や cmd2 がエラーになったことを検出できない)
pipefailを有効にするとエラーになった最後のコマンドの終了ステータスになる
set -o pipefail
cmd1 | cmd2 | cmd3
echo $? # cmd1、cmd2、cmd3 でエラーになった最後のコマンドの終了ステータス

記事公開時は「最初にエラーになったコマンドの終了ステータス」と書いていましたが「エラーになった最後のコマンドの終了ステータス」の間違いでした。

「エラーになった最後のコマンド」というのは、(時間的な意味で)最後に実行が完了したコマンドではなくパイプラインの記述の中で最後に書いたコマンドという意味です。

set -o pipefail
(sleep 3; exit 10) | (exit 11) | (exit 12) | (exit 0)
echo $? # => 12

注意点として pipefail を有効にしたとしても cmd1 や cmd2 がエラーになった時点ですぐにシェルスクリプトが止まるわけではありません。処理自体は pipefail を有効にしてもしなくても同じようにパイプライン全体が実行され、違いがあるのはパイプライン全体の終了ステータスだけです。

この動作は set -e (errexit) を有効にしている場合でも同じです。errexit はコマンドの実行でエラーが発生した時(かつ if&&|| の条件の一部でない場合)に自動的にシェルスクリプトを中断するシェルのオプションですが、中断処理が行われるのはパイプラインに含まれるすべてのコマンドの実行が完了した後です。

え?「set -o pipefail」はbash拡張だろ!?

まず、なんでもかんでも bash が勝手に拡張してるなんて考えるのはやめるようにしてください。bash の拡張機能の多くは元は ksh93 の拡張機能です。ksh93 は Unix を開発していた AT&T で開発され、Bourne シェルの後継シェルとして、現在の商用 Unix で広く使われている POSIX 準拠シェルです。(注意: Bourne シェル自体は POSIX に準拠していません)

「set -o pipefail」が拡張機能だったのは、もはや古い常識です。POSIX.1-202x (Issue 8) (来年には完成するはず・・・)で set -o pipefail は標準化されました。対応済みの sh は次のとおりです。これら以外の拡張機能を実装している POSIX 準拠シェル(zsh、mksh、yash など)も、もちろん対応済みです。

  • sh として使われている ksh - ksh93g (1998-04) 以降
    • Solaris、AIX など
  • sh として使われている bash - bash 3.00 (2004-08) 以降
    • Red Hat 系 Linux や macOS など
  • BusyBox ash - BusyBox 1.16.0 (2010-01) 以降
  • FreeBSD sh - FreeBSD 11 (2016-10) 以降
    • FreeBSD 10 系は 2018-10 にサポート終了済み
  • NetBSD sh - NetBSD 9.0 (2020-02) 以降
    • NetBSD 8.x は 2024 年の初め頃にサポート終了の見込み
  • OpenBSD sh - OpenBSD 6.8 (2020-10) 以降
    • OpenBSD 6.7 は 2021-05 にサポート終了済み

残念ながら Debian / Ubuntu で /bin/sh として使われている dash は 0.5.12 時点ではまだ非対応です(参考)。ただしパッチは提供されているので POSIX.1-202x が完成するころにはマージされるでしょう。マージされればこの記事のハジケたタイトルは(忘れなければ)修正する予定です。

もうね、シェルの対応進んじゃって、多くの環境ですでに使えるんですよ。set -o pipefail は。使えない環境のほとんどはすでにサポート終了済みです。

POSIX 標準化までの流れについて

POSIX で「set -o pipefail」の標準化が提案されたのは 2013-11-08 です(0000789: Add set -o pipefail)。タイミングから言って ksh、bash、zsh、mksh の拡張機能が取り込まれた形になります。それから議論が重ねられ 2019-10-03 に既存のシェルの動作を正確に定義できたとして受け入れられました。POSIX の標準化というのは既存の実装を文書化することであり、今までに実装されてない新機能を POSIX が考案したり標準化することはありません。

つまりシェルに拡張機能を実装してはいけないなんてことはなく、むしろ逆にシェルに拡張機能が実装されて初めて POSIX はそれを標準化できるので、bash などの拡張機能はシェルの未来にとって必要で重要なものです。拡張機能を悪者にしないようにしてください。これからの POSIX の改定でも bash などの先進的な拡張機能が標準化されていくでしょう。POSIX シェルの最新機能は bash、ksh、zsh で最初に実装され、POSIX で標準化されていない拡張機能の実装を最小限にする方針の dash、FreeBSD sh、NetBSD sh、OpenBSD sh は基本的に後追いです。

標準化された以降に追加したシェルは別として、各シェルで拡張機能に互換性があるのは POSIX が標準化した結果ではなく、シェル開発者がシェルスクリプトの移植性を高めようと他のシェルと互換性がある形で拡張機能を実装したからです。シェルスクリプトの移植性を本当の意味で向上させているのは、POSIX ではなくシェル開発者の努力によるものです。POSIX がやっているのは移植性があることのお墨み付きです。

PIPESTATUS配列には非対応

これらの sh シェルが set -o pipefail に対応したと言っても PIPESTATUS 配列には非対応です。配列なので実装するにしてもまず配列の機能を sh に追加する必要があります。実現されるとしても時間が掛かるでしょう。

しかし通常は PIPESTATUS 配列は不要です。なぜなら set -o pipefail はパイプラインの中でエラーになったコマンドの終了ステータスを返すものだからです。各コマンド毎の終了ステータスを見たい場合には PIPESTATUS 配列を使わなければなりませんが、単にエラーが発生したかどうかを調べるだけなら PIPESTATUS 配列を参照する必要はありません

PIPESTATUS配列を使わないとSIGPIPEがー

はい、あなたの言いたいことはわかります。途中のコマンドで無視したいエラー(例えば SIGPIPE)が発生した場合はどうするかという話でしょう? 以下のコードは SIGPIPE のエラーを無視するための参考コードです。

#!/bin/sh

set -o pipefail

ignore_sigpipe() {
  "$@" && return 0
  set -- $?
  # SIGPIPE の終了ステータスが 141 とは限らないのでシグナル名に変換する
  case $(kill -l "$1" 2>/dev/null) in
    PIPE) set -- 0 ;;
  esac
  return "$1"
}

ignore_sigpipe seq -f "%09g" 10000 | head -n 1
echo $?

このような感じで無視したいエラーがあるなら、ピンポイントでそのエラーだけを無視すれば良いだけので PIPESTATUS 配列は不要なのです。その他の方法として dash などでも動く pipefail 相当の実装方法もあります(個人的にはもう少し見直したいと思っています)。こちらは PIPESTATUS 相当の値を取得できるようにしています。

さいごに

ということで、人によっては今すぐ使えないかもしれませんが、set -o pipefail は POSIX に取り入れられるのでこれからは使っていきましょうという話です。この話からの教訓は POSIX で標準化されていない機能は使わないという縛りプレイをしても後で標準化されれば縛りプレイしたことは無駄だったという話になってしまう可能性があるということです。目的のない POSIX 縛りはやめましょう。あなたが最新の POSIX 標準機能が使えないのは、あなたが使わないでやろうとした結果です。私は理由がないなら bash をインストールするだけで新しいシェルの機能が使えると何度も伝えています。パッケージをインストールすることは難しいことでしょうか?

最新の POSIX 規格にすでに準拠している bash はオススメです。bash は POSIX に準拠して(C 言語で)作られているどの環境でも(インストールすれば)動く移植性が高いシェルです。どこぞの商用 UNIX とは違いオープンなライセンスでソースコードが公開されているので、将来に渡って自由に使うことが出来ます。bash に乗り換えれば dash の対応を待たずに今すぐ最新の POSIX シェルの機能をどの環境でも使うことが出来ます。縛りプレイが好きなら bash を使いながら縛りプレイをすれば良いですね。どちらにしろ Red Hat 系 Linux や macOS は /bin/sh は bash なので bash 使いながら縛りプレイするしかありません。標準化されてすべてのシェルで実装されるまで使うのを待つということは、bash などのインストールの手間を省ける代わりに環境ごとのシェルの違いに悩まされ、数年、十数年、下手すれば永遠にシェルの便利な機能が使えずにシェルスクリプトを書く手間がかかるということです。そうすることで実際にメリットがあったことはありますか?

27
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?