Why not login to Qiita and try out its useful features?

We'll deliver articles that match you.

You can read useful information later.

167
157

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

祝🎉 POSIX.1-2024 (Issue 8) 改定!16年ぶりの大幅改定でシェルスクリプトはどう新しくなるのか?

167
Last updated at Posted at 2024-06-14

はじめに

自称日本一の「POSIX おじさん(シェルスクリプト限定)」が最新の POSIX 準拠シェルスクリプトの世界をご案内いたします。POSIX の大幅な改定である POSIX.1-2024 (IEEE 1003.1-2024, Issue 8) は、2024年3月21日に The Open Group によって承認され、5月21日に IEEE で承認され、6月14日に改定されました。そうです。つい先程一時間ほど前です。最速で最新情報をお届けいたしております。前回の大幅な改定が POSIX.1-2008 なので実に 16 年ぶりの大幅改定です(POSIX.1-2017は小規模な修正)。なお ISO/IEC での投票の締め切りは6月28日なので ISO/IEC の承認はまだですが、承認されてないと言っても手続き上の都合によるもので、POSIX と呼ばれている標準規格は 3 つの標準化団体(The Open Group、IEEE、ISO/IEC)が同じ標準規格を採用することになってるので内容に関して異議が出ることはないはずです。

今回の改定では標準規格の間違い曖昧な点の修正のほか、これまで bashism と言われていたシェルの拡張機能や、GNU コマンドや BSD 系 Unix の独自コマンドや拡張されたオプションなどが追加・標準化され、わずかながら変更になった機能や廃止された機能もあります。ちまたにある POSIX 準拠シェルスクリプトの情報のいくつかは古くなってしまいました。この記事では今回の改定でシェルやコマンドがどう変わるのかについて解説します。重要な点はおおむね書いていると思いますが、漏れがあったら後で追加します。また「将来の方向性」として今回の改定には含まれていないが将来変更するかもしれない情報と、新しく標準化された機能にまだ対応していない実装のための「回避策」もいくつか解説しています。

「将来の方向性」(FUTURE DIRECTIONS) は将来のPOSIXで標準化されると決まったことではありません。POSIX がそのようにしたいと考えていることであり、実現するかどうかは OS の実装者が同意してその内容に対応することが前提です。

POSIX.1-2024 の改定では POSIX が参照する C 言語が C99 から C17 に変更になりました。ただし本記事には C 言語についての話はありません。だって C 言語に詳しい人なら何人もいるでしょう? 私が太刀打ちできる世界ではないので他の人に任せます。とは言うもののそれだけではちょっと寂しいので、新しく追加・削除された関数へのリンクだけ紹介する予定です。

新しく追加・削除された POSIX インターフェース

POSIX から削除されたインターフェースは、POSIXが「実装してはいけない」「実装から削除しろ」と命令しているわけではなく、「移植性がない」(から使用する場合は注意が必要)という意味です。現実の実装は「拡張機能」としてこれからも実装して構いませんし、移植性に注意するのであれば使用しても構いません。それは「拡張機能を使用する POSIX 準拠アプリケーション」と呼ばれます。

POSIX が改定されたと言っても後方互換性を破壊するような変更が要求されているわけではないので安心してください。ただしここには重大な事実があります。それは今現在、各 OS 間で互換性がない動作が POSIX の改定で同じ動作に統一されるものもないということです。なぜならどの OS でも同じ動作に統一するということは、どこかの OS では動作を変更せねばならず、後方互換性が破壊されてしまうからです。安心してください。POSIX がどれだけ改定されようが、後方互換性を保つために移植性(異なる OS 間の互換性)は犠牲になっており、移植性がない部分はこれからもずっと移植性はないままなので、別のシステムに乗り換えるときは注意が必要ですが、今まで作ったシェルスクリプトは OS 開発者が後方互換性を保とうとする限り、同じ OS とその後継バージョンでなら同じように動作するはずです。

この記事の修正履歴(大きなもののみ)

  • 2024-06-23: dash で $'...' に対応する修正が行われていることを追記
  • 2024-06-29: compress / uncompress / zcat コマンドについて加筆
  • 2024-06-30: bash で cd コマンドに空文字を指定したときの挙動について追記
  • 2024-06-30: 「発明をしない」の意味について補足

あなたは誰?

たまには自己紹介と言うか、私がプライベートでやっていることの紹介をしたいと思います。宣伝なので興味がない方はお先へどうぞ。

私はシェルスクリプトの作成を楽にするためのライブラリやツールを開発しています。もっとも有名な代表作は ShellSpec です。最近 GitHub のスターが 1K を超え、ChatGPT さんが 100 回に 1 回ぐらいの確率で私のことを認知してくれるようになりました。どこかの Unix/Linux の(準)公式パッケージにも含まれているとかいないとか(誰かが勝手にやってるのでリアルに把握してない)。ShellSpec はシェルスクリプト用の高機能なテストフレームワークで、bash だけではなくすべての POSIX シェルとそれが動く環境に対応しているのが特徴です。どの環境でも動くようにするために、わずかな POSIX コマンドだけに依存し、ほぼすべてをシェルスクリプトの言語(シェル言語)で実装し、環境依存をなくすための高度なシェルプログラミング技術が使われています。しばらく開発を停止していたのですが(まだ本調子ではありませんが)再始動しました。最近の修正では AIX 7.2 / 7.3 上の bash と ksh93 への対応が行われています(/bin/sh = ksh88f への対応は ksh88f のバグの影響範囲が大きく保留中。Solaris 10 の ksh88i では動作するが ksh88 は基本的に POSIX 準拠ではない。POSIX に完全に準拠してない古すぎる ksh88f が悪い)。当然ながら ShellSpec は(一部の環境を除いて)CI による継続的なテストが行われています。

シェルスクリプトの大きな問題点の一つは移植性の低さです。これは一般的なシェルスクリプトが環境依存のコマンド、すなわち Unix/Linux に標準で含まれている OS 標準コマンド(POSIX コマンドを含む)に大きく依存しているのが原因です。一般的に実装の数が多ければ多いほど実装間の互換性は低くなります。OS標準コマンドは複数の Unix ベンダーや団体がそれぞれ同じようなコマンドを独立して作っているのだから互換性が低くなるのは当たり前と言えます。POSIX は互換性問題を解決しようとする団体ではなく、ウェブの標準規格を作っている WHATWG とは全く方針が違う標準化団体です。このような現状で「どの環境でも動くシェルスクリプト」を開発するのは至難の業で誰でも簡単にできるようなことではありません。どの環境でも動くシェルスクリプト用のテストフレームワークが(ShellSpec 誕生以前は)なかったことも、移植性が高いシェルスクリプトを開発するのが難しかった理由の一つでしょう。ShellSpec ではテストに独自の DSL (シェルスクリプト互換)を使用しますが、DSL を使用している限りはテストコードに移植性があり単一のテストコードで全てのシェルのテストを行うことが可能です。

ShellSpec は Linux、macOS、BSD 系 Unix、System V 系 Unix(Solaris と AIX)、Windows (WSL 上の Linux だけではなくCygwin や BusyBox などを含む) など、どの環境でも動きます。どの環境でも動くというのは、どの OS でも動くという意味だけではなく、世界中どの場所でも動くということです。最近行った AIX 対応の内容の一つは AIX 環境の time コマンドが原因で発生していたロケール依存をなくすための修正で、(C ロケールに変更するのではなく)現在のロケールを尊重してどのロケール環境でも動くように修正しています。この修正で ShellSpec が対応する環境は更に増えましたが、この例からもわかるように環境ごとにコマンドの動作が違うためどの環境でも動くシェルスクリプトを書くのは難しいのです。すべての環境の動作を把握してそれにあらかじめ対応して書ける人はまずいません。問題は後から見つかります。後から修正をする必要があります。そしたら全ての再テストが必要です。

人はだれでもミスをします。最初から完璧に正しく動くコードを書けることはありません。シェルスクリプトが環境に依存する存在である以上、環境依存の問題を完璧に把握している人以外は必ずバグを含めてしまいます。それに対処するには多数の環境ごとにテストを行うしかありません。あなたは最初から完璧なコードを書ける? たとえそうだとしても普通は一人で開発するわけではないので、あなただけができても意味はありません。経験の浅い人が完璧なコードを書けたとしたら驚くべきことです。そうです。テストは必ず必要なのです。そしてそのテストは自動化する必要があります。もしテスト自動化の仕組みがなければ手作業でテストを行い目視で結果を確認するしかなくなってしまいます。これの何が悪いのかは言うまでもありませんよね? 環境毎に行う手作業で目視によるテストは時間がかかりミスも発生します。どのようなテストをしたのという記録がなく、テスト作業を問題なく行ったのかわからず、修正したときに全体を再テストするのが不可能だからです。そのようなテストのやり方が許されたのは 20~30 年前までです。シェルスクリプトは作業を自動化するために作るものだと言うのに、そのシェルスクリプトのテストを手作業に頼るようでは皮肉以外の何物でもありません。

念の為ですがどのようなシェルスクリプトでもテストをしろという話ではありません。使い捨てで間違うことがないような数行程度の小さなシェルスクリプトであればテストは不要です。テストが必要なのは多数のシェルスクリプトの組み合わせで動く大きくて複雑なシステムや高い信頼性や移植性が必要なものだけです。だから大抵の人には必要ありません。しかしそれでも必要な場合もあります。テストがなければ他の環境で動くか調べることができませんし、安心して OS のバージョンをアップデートすることもできなくなってしまいます。POSIX に準拠して書いていれば「きっと動くかもしれません」ではなく、動くという保証が必要なのです。環境を変えたりコードを修正したときに問題なく動くという保証ができなければ、長く使えるシェルスクリプトにはなりません。

難しいとはいえ ShellSpec の実現によりどの環境でも動くシェルスクリプトを作ることが可能であることは証明されました。私がやっている活動はシェルスクリプトの環境依存の問題をなくして、普通にシェルスクリプトを書くだけで、どの環境でも動くシェルスクリプトを簡単に作れるような基盤を作ることです。POSIX に準拠してシェルスクリプトを書くというのはとても非効率な作業です。POSIX に準拠する作業はシェルスクリプトを書く上での本当の目的ではありませんし、移植性を高めたとしても他の環境に移植すること自体がほとんどなくなってしまった現代では移植性に力をそそいでもその努力は報われません。しかしながら私は OS には多様性があるべきだと考えているので Linux 以外の BSD 系 Unix や System V 系 Unix を無視したくはありません。POSIX 準拠や移植性の実現という努力をせずとも「勝手に POSIX に準拠して高い移植性が実現されている」ようにするのが最終目標です。他の言語では何年も前から実現できていることなので、この点でシェルスクリプトは大きく遅れています。もちろん目標は移植性の実現だけではなく、バグのないシェルスクリプトを誰でも簡単に書けるようにすることも目標の一つです。「その書き方には問題があるんだぜ」という無駄知識(バッドノウハウ)をひけらかしたりありがたがるのではなく、なくしてしまうのが目標です。とは言えそのような世界をすぐに作ることはできないのでこのような記事を書いて最新情報を広めています。

その他のシェルスクリプトに関する私の研究の成果はこちらで公開しています。すべて(改定前の)POSIX で標準化されているシェルとコマンドの機能のみを使って実装しています。これらを使えばシェルスクリプトを書くのは幾分楽になるでしょう。

  • https://github.com/ko1nksm-shlab
    • sh-string - 文字列操作ライブラリ(置換や trim 処理など)
    • sh-path - パス名操作ライブラリ(拡張子の除去やパスの正規化など)
    • sh-datetime - 日付ライブラリ(UNIX時間との変換や曜日の取得)
    • seq.awk - awk による seq コマンド相当機能の実装
    • sh-maketemp - 一時ファイルやディレクトリ作成ライブラリ
    • sh-pipestatus - pipefail と PIPESTATUS 機能の実装
      • おまけ: SIGPIPE によるエラーを無視するための関数
    • sh-findcmd - which コマンドの相当機能の実装
    • sh-xorshift - Xorshift による疑似乱数生成ライブラリ
    • sh-fnv1 - FNV1ハッシュ値計算ライブラリ
    • sh-base64 - Base64 エンコーダー・デコーダー
    • sh-hmac-sha1 - HMAC-SHA1 の実装

一部のツールは個人のメインプロジェクトにおいています。

  • getoptions - シェルスクリプト用のオプションパーサー
  • shdotenv - シェル用のdotenv(環境変数の設定)ツール
  • readlinkf - POSIX コマンドのみを使った readlink -f の互換実装
  • url - URL エンコード・デコードを簡単に行うツール
  • sh-i18n - 国際化(翻訳)ライブラリ

前提知識

POSIX などに関する前提知識の話です。興味がない人は飛ばしてください。

POSIX.1-2024の新機能はすでに使えます!

記事のタイトルでシェルスクリプトはどう変わるのか?と書きましたが(環境によっては)何年も前から新しい機能が使えています。なぜ標準規格ができる前から新しい機能が使えていたのかと言うと、POSIX が行っている標準化作業とは、新しい機能を作ることではなく既存の実装の動作を明文化することだからです。こちらで「invention」というキーワードで検索してみるとわかりますが、POSIX では「発明を行わない」ことは共通認識です。つまり新しい機能が「発明」されたのではなく、先進的な POSIX シェルである bash や ksh や、GNU コマンドや BSD コマンドなどに実装されていたいわゆる「拡張機能」から移植性が高く重要だと思われた機能が標準規格に組み込まれました。拡張機能という言葉は少々誤解を招きやすい言葉です。POSIX にとっては標準化されていない機能はすべて拡張機能にあたりますが、一部の機能は POSIX が誕生するよりも前からシェルやコマンドが持っていた機能です。(POSIX スレッドなどの例外はありますが)基本的に POSIX は後からやってきて移植性の観点から標準化すべき OS の機能を選別しているだけであり、シェルやコマンドを含む OS の機能を作るのは、昔から各 Unix/Linux の開発者の役目です。

POSIX は発明を行わないため、標準化された機能というのはすでに何年も前から使えた機能です。したがって POSIX にこだわっていない人にとってはすでに使ってる「あの機能」が標準化されたというだけです。それでは POSIX.1-2024 の改定にはどういう意味があるのでしょうか? それは、私のような 「POSIX 準拠(シェルスクリプト)にうるさい人が、アップデートしなければいけない知識」というだけです。今まで POSIX で規定されてないから使わない(使えない)と言っていた「あの機能」の使用が標準規格上は解禁となり、うるさく言う必要がなくなったということです。一般的に今使っているシステムを古い OS や互換性のないシステムに移行することはないでしょうから、POSIX で標準化された機能は、現在使えるのであれば今後も安心して使えるはずです。

2024-06-30 補足 「発明を行わない」というのは「新しい機能」を追加しないという意味です。すでに実装されている機能とほぼ同じ機能を標準化のために少し調整したりして、似たような新しいインターフェースを追加することはあります。また何事にも例外はあるので標準化の過程でどうしても必要と判断された場合には発明が行われることもあるでしょう。

各環境の対応状況は自分で調べるしかありません

POSIX が改定され新しい機能が一部の環境が使えると言っても、環境によってはそのままでは使えないかもしれないんでしょ? と言われたらそのとおりです。それなら各環境が POSIX.1-2024 に準拠したと発表するまで使えないじゃんとなりますが、ちょっと待ってください。いつまで古い POSIX 標準規格を参照してシェルスクリプトを書く気ですか? 多くの環境は POSIX に準拠していると明言していませんし、POSIX の古いバージョンにしか準拠していない場合があります。いくら待っても各環境がどの POSIX のバージョンに対応しているかなんて、(一部を除いて)公表されることはありません。POSIX 標準規格は POSIX が標準規格としてまとめたただの文書でしかなく、現実の Unix/Linux の動作は書いてありません。POSIX は(特定のバージョンの)POSIX に準拠している環境と(同じバージョンの)POSIX に準拠しているアプリケーションの組み合わせであれば、別の環境との間に移植性が実現できるはずだと主張しているだけで、各 Unix/Linux 間に互換性があることを保証していません。じゃあ一体どの環境がどの POSIX のバージョンに準拠しているんだ?という話になりますが、正式に公表されているのは次の環境だけです。

OS 準拠バージョン 補足
macOS version 14.0 POSIX.1-2001 (SUSv3)
IBM AIX 5, 6, 7.1 POSIX.1-2001 (SUSv3) サポート終了OS
Solaris 10, 11 POSIX.1-2001 (SUSv3) UNIX認証の有効期限切れ
HP-UX 11i V3 B.11.31 POSIX.1-2001 (SUSv3)
INTEGRITY POSIX.1-2003 POSIX.1-2001の修正版

IBM AIX 7.2 以上 POSIX.1-2008 (SUSv4)
Solaris 11.4 POSIX.1-2008 (SUSv4) UNIX認証の有効期限切れ

まだなし POSIX.1-2024 (SUSv5)

各 Linux 明言なし
各 BSD系Unix 明言なし

POSIX.1-2024 は出たばかりだから、しばらくは一つ前の POSIX.1-2008 を参照しようと考えるかもしれません。しかし macOS などはさらに前の POSIX.1-2001 までの対応しか公表していません。Solaris は以前は UNIX の認証を受けていたので一応 POSIX に準拠していると分類していますが、実際には UNIX 認証の更新を行っておらず有効期限切れとなっています。つまりこれまでの最新である POSIX.1-2008 に準拠していると認められているのは事実上 IBM AIX 7.2 以降しかないということです。終息間近の HP-UX が更新するとは考えにくいですし、macOS が UNIX の認証を受けた経緯を読む限り訴訟対策でしかないため、macOS は 20 年以上前の古い POSIX 準拠のままで、公式には最新の POSIX のバージョンには追従しないのではないかと私は考えています。

macOS が POSIX.1-2001 までしか対応していないというのは、あくまで Open Group が行っている UNIX の認証での話です。実際には POSIX.1-2008 に(ほぼ)準拠していたとしても不完全であるとかお金がかかる認証テストを行っていないだけという場合もあります。POSIX.1-2024 に準拠していないというのは、POSIX.1-2024 で標準化された機能がいっさい使えないという意味ではありません。ほとんどの機能が使えたとしても認証テストに合格しなければ準拠していないとして扱われます。macOS の多くのコマンドは FreeBSD 版を導入しており、FreeBSD は新しい標準規格への対応を継続して行っています。したがって macOS がバージョンアップするたびに新しい機能は増えています。ただし macOS の sh は FreeBSD sh ではなく古い bash なので POSIX.1-2024 に準拠するのであれば何かしらの対応が必要です。古いとはいえ高機能な bash から低機能な FreeBSD sh に変更すると互換性が保たれないため単純に変更することはまず考えられません。多くのコマンドはすでに POSIX.1-2024 に対応していても sh が非対応なら POSIX.1-2024 に準拠と公表されることはありません。macOS の sh が今後どうなるのかは現時点では誰にもわかりませんし、各 Linux ディストリビューションやその他の BSD 系 Unix も POSIX にほぼ準拠していると言われていますが、しかしどこまで準拠しているかは不明です。

実は getconf コマンドを使用して各 Unix/Linux が対応している POSIX のバージョンを調べることができます。

OS _POSIX_VERSION
macOS 13.4 200112
FreeBSD 14.0 200112
NetBSD 10.0 200112
OpenBSD 7.5 200809
Ubuntu 22.04 200809

FreeBSD では 2024-05-31 に 200112 から 200809 への変更がようやく行われました(一度間違えて 200808 と書いてしまっていますが)。

macOS は FreeBSD のユーザーランドのコマンドを使用しているため、そのせいで 200112 のままだった可能性も考えられますが、シェルやカーネルは FreeBSD のものではないため、FreeBSD が変更になったからと言って macOS が更新されるとは限らないでしょう。Solaris 10 と 11 ではディレクトリごとに準拠バージョンが異なるバイナリが配置されており以下のようになります。Solaris では適切な POSIX のバージョンに準拠する環境を、環境変数 PATH を設定して作る必要があります。デフォルトの /usr/bin/getconf PATH では一つ前のバージョンを参照していることに注意してください。

  • Solaris 10
    • /usr/bin/getconf
      • _POSIX_VERSION: 199506
      • PATH: /usr/xpg4/bin:/usr/ccs/bin:/usr/bin:…
    • /usr/xpg4/bin/getconf
      • _POSIX_VERSION: 199506
      • PATH: /usr/xpg4/bin:/usr/ccs/bin:/usr/bin:…
    • /usr/xpg6/bin/getconf
      • _POSIX_VERSION: 200112
      • PATH: /usr/xpg6/bin:/usr/xpg4/bin:/usr/ccs/bin:/usr/bin:…
  • Solaris 11
    • /usr/bin/getconf
      • _POSIX_VERSION: 200809
      • PATH: /usr/xpg6/bin:/usr/xpg4/bin:/usr/bin:…
    • /usr/xpg4/bin/getconf
      • _POSIX_VERSION: 198808
      • PATH: /usr/xpg4/bin:/usr/bin:…
    • /usr/xpg6/bin/getconf
      • _POSIX_VERSION: 200112
      • PATH: /usr/xpg6/bin:/usr/xpg4/bin:/usr/bin:…
    • /usr/xpg7/bin/getconf
      • _POSIX_VERSION: 200809
      • PATH: /usr/xpg7/bin:/usr/xpg6/bin:/usr/xpg4/bin:/usr/bin:…

このように対応している POSIX のバージョンは調べることができますが、あくまでシステム全体の話であり、個々のコマンドの対応状況はわかりませんし、各 Unix/Linux で 特定の POSIX のバージョンに準拠した環境を作る方法はさまざまです。ここからの結論は、それぞれの環境でどの機能が使えるかなんて、昔から自分で調べないとわからないことだということです。POSIX という標準規格があったとしても、現実の実装は最新の POSIX に準拠していない場合やバグで正しく動かない場合もあります。そもそも POSIX に準拠している実装の間でも動作が異なるため(POSIX は一部の仕様で動作の違いを許容している)、POSIX という標準規格があったからと言って、現実の実装で 100% の互換性が保証されているわけではありません。100% の互換性を保証するのであれば、それはアプリケーション(シェルスクリプト)を開発するプログラマの役目です。

ね? POSIX に準拠してどの環境でも動くシェルスクリプトを開発するのは大変でしょ?

安易に POSIX に準拠してシェルスクリプトを書けとか、そのコマンドやオプションは POSIX で規定されてないからうんぬんかんぬんという人がいますが、POISX を参照して本当に移植性があるシェルスクリプトを書いた人なんてほとんどいなくて、大抵の場合は問題になる書き方がたまたま含まれていなくて、たまたま動いている(ように見える)だけです。本気でどの環境でも動くシェルスクリプトを開発するのは、他の言語に比べるととても大変なんです。テストを自動化するテストフレームワークなしにやれるようなものではありません。(だから私は ShellSpec を作りましたし、普通の人はこんなことやらなくていいですよ)

POSIXの目的

POSIX とは Portable Operating System Interface の略でアプリケーションをさまざまな OS に移植しやすくするために OS のインターフェースを標準化したものです。しばしば POSIX は Unix を作るための仕様だとか勘違いされていますが、POSIX の目的は Unix 以外を含むさまざまな OS へのアプリケーションを移植できるようにすることです。POSIX の目的は以下に記されています。

  • アプリケーション指向
  • 実装ではなくインターフェース(ライブラリ関数とシステムコールは区別なし)
  • C言語(仕様は ISO C 標準で指定されている標準 C 言語の「用語」で記述される)
  • スーパーユーザーなし、システム管理なし
  • 最小限のインターフェース、最小限の定義
  • 幅広く実装可能
  • 過去の実装に対する最小限の変更
  • 既存のアプリケーションコードに対する最小限の変更

この標準の開発者は、以下を含む既存のシステムや潜在的なシステムを含む幅広い範囲で、指定されたすべての機能を実装できるように努めた:

  • オリジナルの UNIX システムコードから派生した現在の主要システムすべて
  • オリジナルの UNIX システムコードに由来しない互換システム
  • まったく異なるオペレーティングシステム上でホストされるエミュレーション
  • ネットワーク化されたシステム
  • 分散システム
  • 幅広いハードウェア上で動作するシステム

POSIX は「まったく異なるオペレーティングシステム上でホストされるエミュレーション」で実装できるよう標準規格が作られています。つまりこれは Windows 上の Cygwin や WSL として実装しても構わないということです。POSIX が求めているのは Unix が持っていた OS のインターフェースを実装して「Unix 用のアプリケーションを移植しやすくすること」であり、Unix としての性能や信頼性を実現した OS の開発を求めているわけではありません。もし仮に一から OS を作るとして POSIX に完全に準拠させて開発したとしても、皆が想像するような Unix のような OS を作ることはできません。なぜなら Unix に備わっているはずのシステム管理(ハードウェア管理やユーザー管理など)、簡単に言えば root 権限が必要なものは POSIX で標準化の対象範囲ではないからです。Windows で sudo コマンドがようやく利用できるようになったと話題になりましたが、sudo コマンドなどは明らかに POSIX で標準化されるわけがないコマンドです。

ウインドウシステムも POSIX に含まれていないので、知っての通り Unix であるはずの macOS の GUI は他の Unix とは全く異なる独自仕様です。GUI は Unix 的ではないというのが macOS が優れている理由の一つです。もし OS を作るために必要なものすべてが POSIX の対象範囲に含まれていたとしたら、それは Unix を作るための標準規格になってしまい Unix 以外の OS(例えば Windows)は OS を Unix に作り変えない限り POSIX に準拠させられなくなってしまいます。OS にも多様性があったほうが良いでしょう? Unix 系 OS の間でしか移植性がないのであれば、それは移植性がある OS のインターフェースではなく、単なる Unix のインターフェースです。POSIX は Unix の実装を参考に作られましたが、作られた OS のインターフェースは Unix に限定したものではないのです。ただし POSIX の拡張規格である SUS (Single Unix Specification) は、認定テストに合格すると「Unix を名乗る」権利が与えられるという標準規格です。ある意味 Unix を作るための標準規格と言えますが、その内部実装は Unix のような OS でなくても構いません。理屈の上では認定テストに合格さえすれば、現在の Windows の姿のまま Unix を名乗ることだってできます。POSIX には何かをカーネルで実装しなければならないなどとは定義されていないので、Unix を名乗るために Windows のカーネルを Linux や Unix に変更する必要はありません。そもそも POSIX はカーネルのシステムコールとライブラリ関数の区別を意図的にしていません。

POSIX 標準規格はすでに存在する Unix/Linux を元に作られています。POSIX 標準規格を元に Unix/Linux が作られているわけではありません。例えば今回の標準規格で追加された timeout コマンドの仕様は OpenBSD (FreeBSD) のマニュアル (man ページ) から取ってきたものであることが明記されています。(参考

Add the timeout utility.
Here is the current manual page as present on OpenBSD, taken from
FreeBSD, under 2-clause BSD license:

このように POSIX 標準規格は実在する Unix/Linux の man ページや実際の動作を元に作られています。POSIX が行っている標準化というのは、仕様を作ることではなく実在する Unix/Linux の仕様をアプリケーションの移植性の観点から役に立つようにまとめることです。POSIX に書かれているのは、どの環境でも全く同じ動きをすることが保証されたコマンドとオプションの一覧ではありません。POSIX に書かれたコマンドとオプションだけを使えばそれだけで移植性が実現できるという単純な話ではなく、プログラマは移植作業を行う必要があります。POSIX にはアプリケーション(シェルスクリプトを含む)を他の OS に移植するときに、各 OS の互換性がない部分などの何に気をつけなければならないかが書かれており、POSIX を読んで実際の環境の動作を調べて 「どの環境でも動くように作る作業を行う」のはプログラマの仕事です。どの環境でも動く移植性が高いアプリケーションを作るときに、プログラマが読んで役に立つものが POSIX という標準規格書なのです。

POSIX 標準規格で特に役立つのは「未指定」や「実装定義」と書いてある部分です。「未指定」や「実装定義」と書いてある部分は、各 Unix/Linux で動作が違うところが判明しているという意味なので、実際の環境での動作を調べなければいけない部分として役に立ちます。例えば今回の改定では、od コマンドで -A n を指定したときにデータの最終行の次にアドレスだけの行が出力されるかされないかが明確に書かれていないという指摘があり修正されました(参考)。改定によって「『出力されるかされないかは未指定である』と明確になった」ので、POSIX 標準規格を読んだプログラマはその事に気づいて、出力されてもされなくても問題ないように両方の動作に対応するシェルスクリプトを書くためのきっかけになります。POSIX 標準規格がアプリケーションを移植するときに役に立つというのは、こういう話なのです。

各 Unix/Linux の動作は細かい動きが違いますが、POSIX が未指定や実装定義と決めたから各 Unix/Linux がバラバラの動作で実装してしまったのではありません。各 Unix/Linux で動作がバラバラだったから、POSIX は互換性を保つために仕様を変更せず「未指定や実装定義という仕様」で標準化してプログラマに対応を促すしかなかったのです。POSIX は各 Unix/Linux 間で 100% の互換性を保証する標準規格ではなく、100% の環境で動くアプリケーションを作りたいのであればプログラマが頑張るしかありません。そのような面倒さをなくすために、他の言語では 100% の互換性を実現していない POSIX 層の上に、言語やライブラリによる環境の違いを吸収する層を作ることで、ほぼ 100% の互換性を20年も前から実現しています。この仕組みによって一般のプログラマの開発の負担を減らしながら、POSIX 環境以外を含むすべての環境でアプリケーションを動かすことを実現しています。シェルスクリプトを含むアプリケーションの移植性を高めるためには環境依存が高い Unix/Linux の基本機能(POSIX インターフェースや POSIX コマンドなどを含む)を「直接」使わないことが重要な考え方です。私が目指しているのは POSIX を意識することなく、他の言語並みの移植性をシェルスクリプトの世界にもたらすことです。

POSIXの成り立ち

POSIX は IEEE 1003 標準規格に対するわかりやすい呼び名で、GNU プロジェクトで有名な Richard Stallman が名前を考案しました。それまで(標準規格の策定中)は誰かが名付けた IEEEIX と呼ばれており、Portable Operating System というサブタイトルが付いていました。このサブタイトルの略である POS に IEEEIX と同じ IX を組み合わせて作られた用語が POSIX です。おそらく IX は当時の Unix(Microsoft が開発した世界初の商用 Unix である Xenix など)が末尾に IX をつけた名前をよく使っていたことから来ていると思われます。現在では I は Interface の略として POSI となり、最後の X には特に意味はないとされています。

標準化は 1970 年代後半から 1980 年代前半にかけて多数のコンピュータメーカーが独自に開発していた Unix 間で互換性が低くなり、それぞれの Unix 用にアプリケーションを開発するのが大変になったために始まりました。「独自に開発していた Unix」というのは、AT&T から Unix のコードとライセンスを購入するか、事実上のオープンソースである BSD Unix を元に、自分たちが販売するコンピュータ用に Unix を移植するという意味です。各 Unix 間の互換性が低くなった理由は Unix を搭載したコンピュータを販売するコンピュータメーカーは Unix を移植するだけではなく、他社よりもより優れた Unix として改良を加えると同時に API を顧客を囲い込むためのベンダーロックインとして利用したからです。当時の Unix の多くは OS 単体で配布されるものではなくコンピュータの付属品でありソースコードは非公開でした。そのため各 Unix ベンダーの独自の修正を他社が取り込むことはできず、似たような機能を実装するのが精一杯で、完全に同じ仕様で実装できないため互換性上の問題が発生しました。今のようにオープンソースが当たり前の世界になるのは 1990 年代の後半以降です。

標準化はコンピュータメーカーではなくアプリケーション開発者によって推進されました。POSIX の前身となる標準化団体がユーザーグループの /usr/group です。/usr/group は 1981 年頃に標準化委員会が設立され、1984 年に /usr/group 標準規格が策定されました。しかしさまざまな理由でアメリカ政府が /usr/group を公式の標準化団体とみなさなかったため、IEEE のスポンサーのもとで再始動し 1988 年に策定されたのが POSIX です。この経緯から推測するにコンピュータメーカーは移植性の向上にはあまり乗り気ではなかったと思われます。だってアプリケーションがどの Unix でも動くのなら、他社製コンピュータの Unix に乗り換えやすくなるでしょう? Unix ベンダーは当時のアメリカ政府が納入条件として POSIX 準拠(ただし C 言語インターフェースの範囲でシェルやコマンドは含まない)を指定したために POSIX に準拠せざるを得なかったのだと思われます。POSIX 準拠はそれはそれで Unix 搭載コンピュータを売るときのアピールポイントにはなります。アメリカ政府は本当は Unix を指定したかったようなのですが特定のベンダーに依存しない標準規格は POSIX だけでした。その結果、Windows NT は Unix ではないにも関わらず POSIX 準拠として開発され納入条件を満たすことになります。結局のところ Windows NT は POSIX 準拠でしたが Unix ではないため Unix の代替にはなりませんでした。現在の Windows に搭載された WSL は POSIX 準拠を超えた Linux 互換機能であるためとても人気があります。ちなみに POSIX がアメリカ政府の納入条件だったのは過去の話で現在は廃止されています。当時のコンピュータメーカーが Unix に独自機能を搭載してベンダーロックインを目指していたのはある意味正しかったのかもしれません。Linux と GNU コマンドがおおむね POSIX に準拠して作られ Unix から Linux へ乗り換えがしやすくなった結果、現在の Linux の普及につながり商用 Unix の世界は大きく衰退したからです。

勘違いされやすいのですが、POSIX は当初から Unix(風の OS)を作るための標準規格としては作られませんでした。POSIX はアプリケーションの移植が焦点であり、POSIX が想定している OS には Unix 以外の OS も含まれています。POSIX は Unix を手本にして作られましたが、Unix が持っていたシステム管理機能(ハードウェア管理機能やユーザー管理機能)はばっさり削除されています。これらの機能が POSIX に含まれていると、POSIX に準拠して動くアプリケーションを増やしたいだけなのに Unix を作らなければならなくなってしまいますよね? POSIX の範囲ではシステムにログインする機能もユーザーを作成する機能もありません。これらはアプリケーションを動かすうえで標準化する必要がないためです。そもそもシステム管理機能はコンピュータメーカーが販売する商用 Unix によって違うのが当たり前なので標準化しようと思ってもできません。POSIX は標準化できないものは標準化しません。特にグラフィカルユーザインタフェースやデータベースインターフェースは明確に対象外と記載されています。POSIX の機能だけではまともに Unix/Linux を使うことや、その上で動くソフトウェア開発は不十分なのです。

OS の機能は元より環境依存が激しい機能です。だからこそ(ある程度の)標準化が必要になりました。特定の OS に依存していない多くのコマンドやプログラミング言語は POSIX の標準化の対象外です。なぜなら OS の機能ではないからです。POSIX にとってはそれらはアプリケーションです。多くのコマンドやプログラミング言語が POSIX で標準化されていないからと言って、シェルスクリプトや C 言語を使わなければ POSIX に標準したアプリケーションが作れないということはありません。どのようなコマンドや言語を使ったとしても内部で OS の機能を使う時に POSIX インターフェースを呼び出していれば POSIX 準拠のアプリケーションと言えます。例えば curl や jq や SQLite は POSIX コマンドではありませんが、POSIX 準拠のコマンドで移植性が高く(インストールすれば)どの環境でも使えるコマンドとして有名です。このような POSIX コマンド以外の POSIX 準拠コマンドやプログラミング言語を作るために、POSIX 標準規格が作られました。今では POSIX のおかげで多くのコマンドやプログラミング言語があらゆる OS で動くようになり、それらを利用することで簡単にどの環境でも動くシェルスクリプトやアプリケーションを作ることができるようになったのです。

POSIXの改定履歴

POSIX に関する標準規格はいくつもあり混乱するため、ここにPOSIXの改定履歴をまとめました。

2000 年頃まで

初期の POSIX の標準化は IEEE の PASC (Portable Application Standards Committee) によって行われています。

POSIX は 1988 年(C 言語インターフェース)または 1992 年(シェルとコマンド)に標準化されて以降、何度か機能追加を含む改定が行われました。初期の POSIX 規格は POSIX.n(または IEEE 1003.n)のような命名規則でした。例えば POSIX.1 が C言語インターフェース、POSIX.2 がシェルとコマンドといった感じです。そして POSIX.1-1988 のように後ろにハイフンで年号を付けたものが標準規格の名前でした。また POSIX.2 への追加機能は POSIX.2a-1992 のように n のあとにアルファベットが付加されていました。このような命名規則は 2000 年頃まで使われました。この頃の POSIX の活動は活発で、最終的にどこまでの活動を想定していたのかはっきり調べきれていませんが、検索すると POSIX.27(C++ バインディング)のようなものも計画にあったようです。ただしその多くが撤回されるか POSIX.1 に取り込まれて消えました。

コア機能 追加機能 XPG / SUS ←別の標準規格→ SVID
1984 (/usr/group)
1985 XPG1 (Issue 1) SVID1
1987 XPG2 (Issue 2) SVID2
1988 POSIX.1-1988
1989 XPG3 (Issue 3) SVID3
1990 POSIX.1-1990
1992 POSIX.2-1992 POSIX.2a-1992 XPG4 (Issue 4)
1993 POSIX.1b-1993
1994 POSIX.2d-1994 XPG4v2 (Issue 4 version 2)
1995 POSIX.1c-1995 SUSv1 (UNIX 95) SVID4
1996 POSIX.1-1996
1997 SUSv2 (UNIX 98, Issue 5)
1998 POSIX.13-1998
1999 POSIX.1d-1999
POSIX.2b draft
2000 POSIX.1g-2000
POSIX.1j-2000
POSIX.1q-2000

POSIX は特定の Unix ベンダーのための標準規格ではありません。中立的で当時の二大 Unix であった BSD Unix と UNIX System V の両方を考慮して作られた標準規格です。両方を考慮しているのであれば、POSIX に準拠すれば BSD Unix と UNIX System V の両方で動くようになると思うかもしれませんが、結局のところ両方に共通するものしか標準化ができないということなので、POSIX で標準化された範囲でできることは少なくなります。標準規格は POSIX だけではありません。XPG (X/Open Portability Guide) は X/Open という団体による標準規格です。X/Open は POSIX よりも広い範囲の技術を対象にしており、その範囲には例えば SQL やウインドウシステムなどが含まれています。ただし X/Open が標準規格のすべてを一から策定しているというわけではなく、他の標準規格を参照して作られています。XPG4 の中の一部である基本仕様は X/Open CAE Specification (CAE = Common Applications Environment) と呼ばれており、POSIX の標準化の範囲に近く C 言語インターフェースやシェルやコマンドなどがドキュメント化されています。

X/Open は AT&T から Unix の権利を買い取った Novell から Unix ブランドを譲渡され、他の団体との合併などを経て The Open Group と名前を変え、現在は SUS (Single UNIX Specification) という標準規格を発行しています。この SUS が Unix であると認められるために必要な標準規格です。例えば SUSv1 に認定された Unix は UNIX 95 というブランド名を名乗ることができます。SUS は X/Open CAE Specification の後継としてそのドキュメントの一部から構成されています。つまり XPG は幅広い技術を対象としていましたが、SUS の対象とする範囲はそれよりも小さく POSIX の対象範囲を少し超える程度に近づきました。ちなみに XPG は OS のインターフェースとして SVID (System V Interface Definition) という別の標準規格を参照しており、SVID はその名の通り UNIX System V のインターフェースを定義した AT&T が策定した標準規格です。そのため XPG と SUS で標準化された OS のインターフェースは System V 系 Unix に近いものとなっています。

2000 年以降

POSIX は元は IEEE による標準規格ですが、現在(1997年以降)は POSIX の改定作業を行うために設立された共同技術ワーキンググループである Austin Group によって改定作業が行われています。POSIX と呼ばれている標準規格は、IEEE (PASC)、ISO/IEC (JTC 1/SC 22)、The Open Group の3つの団体がそれぞれ発行している標準規格のベースとして採用しており、それぞれの組織のメンバーが Austin Group に参加しています。Austin Group の議長は The Open Group の Andrew Josey です。ちなみに Austin Group へは誰でも無料で参加できる(メーリングリストへの登録が必要)ので、誰でも POSIX の標準規格の内容に口を出すことができます。もちろん各 Unix/Linux の関係者で参加している人もいるはずです。誰でも新しい標準規格を POSIX に提案することができますが「発明」(今までに誰も実装したことがない素晴らしい新機能)は採用されないことに注意してください。POSIX の標準化とは新しい機能を作ることではなく、既存の実装の動作を明文化することです。

SUS は元は POSIX とは独立した標準規格でしたが、Austin Group によって POSIX との統合作業が行われ、POSIX.1-2001 で POSIX 標準規格の拡張規格として統合されました。IEEE が発行する標準規格は有料ですが、The Open Group は SUS として無料で公開しており、POSIX と統合された SUS という形で私達は無料で POSIX 標準規格書を参照することができます。SUS のみに存在していた機能は、XSI オプションと呼ばれる拡張機能として「POSIX に準拠するだけなら実装は必須ではない仕様」として記載されています。また SUS には POSIX には組み込まれなかった X/Open Curses といった仕様もあります。POSIX に含まれている XSI オプションは元は UNIX System V の仕様です。SUS が Unix と認められるために満たさなければいけない仕様とは(BSD Unix ではなく)UNIX System V との互換性を実現するための仕様であることを意味しています。言い換えると XSI オプションは BSD 系 Unix との互換性が低い部分です。BSD 系 Unix では XSI オプションは実装されていない可能性があるだけではなく、標準化された内容とは異なる仕様で実装されている可能性もあります。XSI オプションが何かを知らないと、POSIX 標準規格に(XSI オプションとして)記載されているのだから移植性がある機能なのだろう勘違いしがちですが、XSI オプションの部分は移植性がないかもしれない機能なのです。Ubuntu のとあるページにはこうあります。

We recommend that developers of shell scripts adhere to the POSIX standard, omitting those items flagged as XSI extensions.

私達はシェルスクリプトの開発者たちは POSIX 標準に準拠し、XSI 拡張としてフラグがつけられた項目を省略することをおすすめします。

POSIX.1-2001 以降では従来の POSIX.n という命名規則がほぼなくなり、POSIX.2 は POSIX.1 に統合されました。つまり現在の POSIX 標準規格は POSIX.1-xxxx という命名規則のみです(リアルタイムコントローラーシステムのプロファイルを定めた POSIX.13 を除く)。この頃にはもう POSIX の開発はメンテナンスのようなものとなり大きな機能追加を行うという考えはなくなっているようです。UNIX の標準規格は POSIX の標準規格に対応して SUSv3、SUSv4 のような名前となっています。SUS が POSIX に統合された POSIX.1-2001 (SUSv3) の時点では多くの XSI オプションがありましたが、POSIX.1-2008 (SUSv4) では多くの XSI オプションが標準機能へと変更になっています。これは元々 UNIX System V との互換性の仕様だったものが BSD 系 Unix や Linux でも実装されて、移植性があるから XSI オプションとする必要がなくなったことを意味しています。逆に言えば現在も XSI オプションのまま残っているものは移植性に関してさらに注意が必要だということです。

Year 標準規格 細かい修正版 SUS(POSIX に統合された拡張規格)
2001 POSIX.1-2001 SUSv3 (UNIX 03, Issue 6)
2003 POSIX.1-2001 TC1 : 2003 Edition
2004 POSIX.1-2001 TC2 : 2004 Edition
2008 POSIX.1-2008 SUSv4 (UNIX V7, Issue 7)
2013 POSIX.1-2008 TC1 : 2013 Edition
2016 POSIX.1-2008 TC2 : 2016 Edition
2017 POSIX.1-2017 : 2018 Edition
2024 POSIX.1-2024 SUSv5(仮) (UNIX V8, Issue 8)
:
20xx POSIX.1-20xx SUSv6(仮) (UNIX V9, Issue 9)

細かい修正版 (TC: Technical Corrigendum) では、標準規格の間違いの訂正を行ったり、将来の大規模な標準規格の改定で行われるかもしれない内容を「将来の方向性」(FUTURE DIRECTIONS) に追記したりしています。「将来の方向性」が実現するには各Unix/Linuxの実装者がその方向性に同意して実装しなければいけないので必ずしも実現するとは限りません。あくまで POSIX が考えている将来像です。POSIX.1-2017 は 2018 年に標準規格が期限切れになるのを防ぐために公開された改定版です。厳密には今回の改定である POSIX.1-2024 の一つ前は POSIX.1-2017 になるのですが、POSIX.1-2017 の内容は POSIX.1-2008 2016 Edition (POSIX.1-2008 + TC1 + TC2) と同一であるため本記事では大幅な改定には含めておらず、今回の改定は 16 年ぶりの大幅な改定ということになります。

ちなみに Austin Group では POSIX 標準規格の各バージョンはコードネームのような感じで Issue N のような名前で呼ばれています。これは The Open Group の前身である X/Open 時代から継続している名前です(XPG1 = Issue 1)。細かい改訂版は区別することなく Issue N と呼ばれており、正式な標準規格の名前を使うよりもわかりやすいです。次回(10年後ぐらい?)の改定版は Issue 9 と呼ばれています。Issue N は標準規格の CHANGE HISTORY での改定履歴などでも使われています。ちなみに Solaris 11 では /usr/xpg4/bin、/usr/xpg6/bin、/usr/xpg7/bin のようなディレクトリにそれぞれの標準規格のバージョンに適合したコマンドがインストールされていますが、xpg6 や xpg7 のような名称は標準規格の名前としては正確なものではありません。

なぜOS標準コマンドは充実しないのか?

ここまで読んでくれた人ならすでに気づいていると思いますが、Unix/Linux の OS 標準コマンドが便利で充実した物にならないのは、OS の開発者がそれらを作る必要がないからです。POSIX の移植性の要は C 言語インターフェースです。Unix/Linux の OS の開発者からすれば C 言語インターフェースさえ標準化されていれば、それ使って誰でもさまざまなアプリケーションが作れるわけで、自分たちで OS のコマンドを充実させる必要がありません。

OS 標準コマンドは OS の一部であるため環境依存が激しくなりやすいという問題もあります。特に BSD 系 Unix や System V 系 Unix では OS に組み込まれたコマンドは、それぞれ別々に開発していこうとする傾向にあります。(いくつかの例外もありますが)コードの修正を上流のプロジェクトに集めて OS とは独立して育てていこうとはしません。元々 OS の開発というのはそういうものです。POSIX で OS のインターフェースが標準化されたと言っても、POSIX が OS の仕様を作っているわけではないため、1990 年代以降の OS の開発は、それぞれの OS で独立した開発です。

各 OS の開発者は、自分たちにとって必要なコマンドの実装は行いますが、どの環境でも動く移植性のある新しい OS のコマンドをみんなで作っていこうという考えは持っていません(人によるとは思いますが)。せいぜい既存のコマンドに追加する機能を、他の Unix/Linux のコマンドを真似して実装する程度です。移植性がある新しい OS のコマンドが充実しないのだから POSIX で新しいコマンドが標準化されることもありません。今回の大幅な改定でも実際には何年も前からどこかで実装されていた機能が大半です。標準化されたのは長い時間をかけてどの環境でも使えるようになった移植性のあるわずかなコマンドや機能だけです。

シェルスクリプトの分野がほとんど 1990 年頃のままで発展していないのは、POSIX で標準化された OS のコマンドは標準化されているから良いものなんでしょ?という考えや、移植性が高いコマンドをインストールするのが面倒だからと最初から使えるコマンドでそれだけを使ってシェルスクリプトを書くのが良いことだという間違った考えが広まっているからです。OS 標準コマンドはこれからも充実することはありませんし後方互換性を維持するために大きく改良されることはありません。したがって人類が目指すべきなのは脱 POSIX コマンドです。POSIX コマンドがなくなることはありませんがいつまでも不便なままなので、それを利用してシェルスクリプトを書くのは、特定の OS に依存したシェルスクリプトを作るときだけで十分です。一般のプログラマは POSIX を意識する必要をなくしていくべきです。将来的に POSIX を意識する必要をなくすために、今はまだ最低限(完璧ではないが便利という意味)の移植性を解説した POSIX を知る必要があります。

シェルは何が変わったか?

最初に説明した通りシェルが変わったと言ってもそれはあくまでPOSIXで標準化されているシェル (POSIX sh) が変わったという話です。追加された機能はすでに bash や ksh などで実装済みで、人によってはおなじみの機能ばかりです。

シェル言語自体の機能

ここではシェル言語自体に追加された機能を紹介します。

ANSI C 文字列を解釈する $'...'(ダラーシングルクォート)の追加

$'...'(ダラーシングルクォート)は ANSI C のバックスラッシュを用いたエスケープシーケンスを解釈する文字列です。

$ printf '%s\n' $'Hello\nWorld'
Hello
World

bash、ksh93、zshはかなり昔から対応しています。FreeBSD shとNetBSD sh は最新版では対応しています。ただし dash や OpenBSD sh ではまだ対応していません。おそらくそのうち対応するでしょう。

2023-06-23 追記 dashに $'...' の機能が 2024-06-22 に追加されました。おそらく v0.5.13 以降で対応することでしょう。またマルチバイト文字への対応も行われています。

対応していないシェルでは printf コマンドを使用してエスケープシーケンスを解釈することができますが、変数に代入したりコマンドの引数に使用する場合はサブシェルを伴うコマンド置換を使用する必要があるため遅くなります。ダラーシングルクォートはそのような問題を解決したものです。

余談ですが、 $'...' は tcsh でも 2022年2月2日にリリースされた 6.24.00 以降で対応しています。

case のフォールスルー (;&) の追加

多くのシェルが対応している、case コマンドのフォールスルー (;&) が標準化されました。以下のコードは、foo と bar の両方を出力します。

case foo in
  *foo*) echo foo ;& # 次の行のコマンドも実行される
  *bar*) echo bar ;; # bar にはマッチしていないが実行される
esac

ちなみに bash はフォールスルーに似ているが次の条件判定も行う ;&& が実装されていますが、こちらは POSIX では標準化されていません。おそらく ksh と zsh で実装されていないから候補にならなかったのではないかと考えられます。

単語分割、IFS の仕様が詳細に定義される

信じられないことにこれまで単語分割、または IFS シェル変数の仕様が明らかに不十分でした。POSIX.1-2024 では詳細に定義されており、書き直しに近いレベルでかなりの量の文章が追加されています。

どおりで POSIX 眺めていても IFS の細かい仕様がわからんかったはずだよ......

クォートを推奨する文字の変更

シェル言語は文字をクォートせずにそのまま書くことができますが、一部の文字(記号)はクォートせずに使うと特別な意味を持ちます。多くの文字がクォートが必要になっているため、逆にクォートせずに使える文字を例示したほうが簡単です。ここに挙げた記号以外はクォートすることを推奨します。よくわかなければ記号はすべてクォートする方針でもよいでしょう。

クォートせずに使用可能な文字
+ - . / : @ _

実装が必須ではないブレース展開への考慮

一部のシェルはブレース展開({1..10}{a,b,c}.txt のようなもの)を実装していますが、これまでの POSIX では文法的に受け入れられていませんでした。この拡張機能の実装が可能になるように文法が修正されました。また「ブレース展開をサポートしているシェルではすべての標準的な単語展開の前に実行すべきである」と明記されました。ブレース展開がその他の展開のよりも後に実行されると予期せぬ結果をもたらす可能性があるからです。

ブレース展開が他の展開より後で実行された場合の予期せぬ結果
$ zsh -c 'a="1." b=".10"; echo {$a$b}'
1 2 3 4 5 6 7 8 9 10

bash や POSIX モードの ksh93u+m では POSIX で標準化されている通りブレース展開は最初に行われるので、このような予期せぬ結果は発生しません。ただし次のように数値の終了値に変数が使えないなど不便に感じる場合があります。この問題は eval コマンドを使って回避することができます。

POSIX に準拠したオプション機能のブレース展開の挙動
$ bash -c 'n=10; echo {1..$n}'
{1..10}

$ ksh -o posix -c 'n=10; echo {1..$n}' # ksh93u+m(ksh93は非対応)
{1..10}

$ bash -c 'n=10; eval "echo {1..$n}"'
1 2 3 4 5 6 7 8 9 10

POSIX モードではない ksh や zsh では次のような動作になります。

POSIX に準拠していないオプション機能のブレース展開の挙動
$ ksh -c 'n=10; echo {1..$n}'
1 2 3 4 5 6 7 8 9 10

$ zsh -c 'n=10; echo {1..$n}'
1 2 3 4 5 6 7 8 9 10

繰り返しますが、ここまで仕様が定義されていながらもブレース展開の実装は POSIX.1-2024 では必須ではありません。ただしブレース展開は POSIX で拒否されているわけではなく、誰も標準規格を書いておらず、詳細な議論(複数のシェルの動作を矛盾なく共通の文章で定義できるか)が行われていないというだけなので、将来の POSIX で標準化される可能性が高い機能と考えられます。

実装が必須ではない {fd}>file への考慮

bash と ksh で対応しているファイルディスクリプタ番号 10 以上を扱うための以下のような構文が許可されました。

( echo "[fd: $fd] test" >&"$fd" ) {fd}>/tmp/log.txt

この機能も POSIX.1-2024 ではオプション機能であり実装は必須ではありませんが、拡張機能として実装しても POSIX に違反しないように文法が許可されています。

将来の方向性: 「.*」が「.」「..」に展開しないようになる

現在のシェルの実装では .*... に展開される場合と展開されない場合があります。多くのシェルではデフォルトで ... に展開されますが、bash 5.2以降、ksh、mksh、zsh では展開されません。

dash、bash 5.1まで、yashなど
$ echo .*
. . .file.txt
bash 5.2以降、ksh、mksh、zshなど
$ echo .*
.file.txt

パス名一覧に ... が含まれることに意味はなく、バグの原因になるとして、POSIX.1-2024 では「... を無視する場合がある」と明記され、将来のバージョンでは「... を無視することを要求するかもしれない」ことが記載されました。

bash は 5.2 以降でデフォルトで ... に展開されなくなりました。そのためこれより ... に展開されるシェルがあることに気づきにくくなります。... に展開されないのは方向性としては正しいと思いますが、移植性があるシェルスクリプトを書こうとする人にとっては、この仕様に気づかないことによるバグが発生する可能性があります。なお以前の動作に戻すには shopt -u globskipdots を実行します。

補足ですが、ls -a... を含む隠しファイルと通常のファイルの一覧を出力することができますが、ls -A を使用すれば ... を無視することができます。-A オプションは POSIX.1-2008 で標準化されています。

コマンドの分類の詳細化

シェルに組み込まれたコマンドは、これまで「特殊シェルビルトインコマンド」と「通常のシェルビルトインコマンド」の2つの分類しかありませんでした。この分類は今も変わっていませんが、宣言コマンドと内在コマンドという新たな分類が追加されました。シェルに組み込まれたコマンドは「特殊シェルビルトインコマンド」「内在コマンド」「通常のシェルビルトインコマンド」のいずれかに分類され、その中に「宣言コマンド」に該当するコマンドがあるという関係です。

宣言コマンド (Declaration Utilities)

宣言コマンドとは「変数への代入を行う形になっているタイプのコマンド」で、POSIX では exportreadonly、特別な場合の command が宣言コマンドとして定義されており、その他に実装が追加の宣言コマンドを提供している場合があります(declaretypesetlocal など)

宣言コマンドという区別が必要な理由は通常のコマンド呼び出しと宣言コマンドで引数の解釈が異なるためです。具体的な例を見ていきましょう。

通常のコマンド env の場合(bashで実行)
$ var='123 VAR2=456'
$ env VAR1=$var sh -c 'echo "$VAR1 : $VAR2"'
123 : 456
宣言コマンド export の場合(bashで実行)
$ var='123 VAR2=456'
$ export VAR1=$var && sh -c 'echo "$VAR1 : $VAR2"'
123 VAR2=456 :

どちらも環境変数に値を設定してその値を出力するコードですが結果が異なっています。これは単語分割による挙動の違いです。通常のコマンドは変数を参照するときにダブルクォートされていない場合は単語分割が行われます。つまりスペース(正確には IFS シェル変数で指定された文字)で分割され複数の引数が生成されます。そのため前者の env コマンドの例は次のように解釈されます。

通常のコマンドは、単語分割により複数の引数に分割される
$ var='123 VAR2=456'
$ env VAR1=$var sh -c 'echo "$VAR1 : $VAR2"'
$ env VAR1=123 VAR2=456 sh -c 'echo "$VAR1 : $VAR2"'

一方で宣言コマンドの場合は単語分割が行われません。つまり後者の export コマンドの場合は次のように解釈されます。

宣言コマンドは、単語分割は行われず複数の引数に分割されない
$ var='123 VAR2=456'
$ export VAR1=$var && sh -c 'echo "$VAR1 : $VAR2"'
$ export VAR1='123 VAR2=456' && sh -c 'echo "$VAR1 : $VAR2"'

ちなみに単語分割が行われないのは通常の変数代入でも同じです。

通常の変数代入は単語分割が行われない(宣言コマンドと同等の動作)
$ var="123 VAR2=456"
$ VAR1=$var && echo "$VAR1 : $VAR2"
123 VAR2=456 :

つまり変数への代入の形をした宣言コマンドは、通常の変数への代入と同じ挙動をするということです。他にも宣言コマンドでは次のように配列を扱ったりすることができます。通常のコマンドではこのような引数の書き方をすると文法エラーになるため宣言コマンドという区別が必要なのです。

bashの場合
foo() {
  local ary=(1 2 3)
}

宣言コマンドは POSIX シェルの元になった ksh88 の時点ですでに存在していた概念ですが、見逃されていたのか POSIX で標準化されていませんでした。ちなみに Bourne シェルには変数への代入の形をしたコマンドはありません。export コマンドや readonly コマンドは変数の属性を変えることしかできず、同時に代入することはできませんでした。つまり宣言コマンドは ksh88 で新しく追加された機能ということです。bash、mksh、ksh、zsh、FreeBSD sh、OpenBSD shでも ksh88 と同様の動作を行いますが、すべてのシェルが宣言コマンドになっているわけではなく、dash 0.5.10 とそれ以前、NetBSD sh(NetBSD 10.0時点)、yash(2.56時点)では宣言コマンドではない挙動をします。そのため現時点では export などで代入値として変数を使う場合はダブルクォートしておいたほうが安全です。もちろん通常の変数代入であればダブルクォートはなくても構いません。

# 現時点では宣言コマンドでもダブルクォートしておいた方が安全
export VAR1="$var"

# 通常の変数代入ならダブルクォートはなくても良い
VAR1=$var

内在コマンド (Intrinsic Utilities)

「内在コマンド」は Intrinsic Utilities に対して私が付けた訳です。Intrinsic は(シェルに)「本来備わっている」という意味がここでは一番的確だと思いますが、「本質(的)コマンド」とか「固有コマンド」ではすっきりしないので(自分でもあまり満足していないのですが)内在コマンドと訳しています。内部コマンドとも訳せると思いますが、それだとシェルビルトインコマンドと混同してしまうので。

内在コマンドの説明の前にまず前提知識としてシェルビルトインコマンドの話です。シェルに組み込まれたコマンドをシェルビルトインコマンドと呼びますが、大きく分けて「特殊シェルビルトインコマンド」と「通常のシェルビルトインコマンド」の2つに分類することができます。特殊シェルビルトインコマンドはシェルの機能と深く結びついている以下のコマンドです。

特殊シェルビルトインコマンド
.    :      break     continue    eval     exec     exit    export
readonly    return    set         shift    times    trap    unset

特殊シェルビルトインコマンドは iffor といったシェルのキーワード(予約語)に近いものですが、特殊シェルビルトインコマンドは「コマンドの文法」、つまりコマンド名と引数を意味する単語の並びという単純な形をしているものです。それに対してキーワードは単純な単語の並びではなく特殊な解釈(fi で終わったり in を使ったり)を行います。また特殊シェルビルトインコマンドは(POSIX での定義では)同名のシェル関数で再定義できないという特徴を持っています。実際にはシェルによっては特殊シェルビルトインコマンドでもシェル関数で再定義できてしまうのですが、bash では POSIX 準拠モードにすると再定義できなくなります。

これまでは特殊シェルビルトインコマンド以外のコマンドはすべて「通常のシェルビルトインコマンド」というのが POSIX での標準化された定義でした。通常のシェルビルトインコマンドのコマンド名は POSIX では規定されていません。これは通常のシェルビルトインコマンドは、パフォーマンスなどを理由にシェル開発者の判断で組み込んでいるもので、本質的には外部コマンドと同じものという扱いだからです。何をシェルに組み込むかはシェル開発者の自由です。

POSIX にとって通常のシェルビルトインコマンドはシェルに組み込まれているという点を除いて外部コマンドとの違いはありません。これは「シェルビルトインコマンドは外部コマンドと同じように環境変数 PATH を使ったコマンドの検索処理が行われる」ことを意味しています。つまり多くの人が考えているであろう「シェルビルトインコマンドと外部コマンドで同じ名前のコマンドがある場合、シェルビルトインコマンドの方が優先される」は POSIX の定義的には違うということです。もっとわかりやすく言えば、環境変数 PATH を使ってシェルビルトインコマンドよりも外部コマンドを優先させることができます。この方法についてはマイナーすぎる話で、多分別の記事を書くと思うのでここでは省略しますが、興味がある方は POSIX.1-2024 を読んでみてください。おそらく POSIX.1-2024 を読んでもわからないと思います。シェル依存の動作で具体的なことが書かれていないからです。

さて環境変数 PATH を使って「通常のシェルビルトインコマンド」よりも外部コマンドの方を優先させることができるという体で話を進めますが、現実のシェルの実装は一部のコマンドに対しては外部コマンドを優先させることができませんでした。環境変数 PATH を使ったコマンド検索を行わないコマンド、それがシェルに本来備わっている「内在コマンド」です。内在コマンドとして定義されたコマンドには次のようなものがあります。

内在コマンド
alias   bg     cd     command   fc       fg      getopts   hash
jobs    kill   read   type      ulimit   umask   unalias   wait

これらはいずれもシェルのプロセス自体に影響を与えたりシェルのプロセスの情報を必要とするものです。外部コマンドで実装するのが(不可能ではないものの)難しいため、現実のシェルの実装は外部コマンドを優先させる方法を用意していません。個人的にはこれらを特殊シェルビルトインコマンドとしてカテゴリする道もあったのではないかと考えますが、特殊シェルビルトインコマンドは内在コマンドとは別の特徴を持っており、別のカテゴリを作ったほうが良い(標準化の手間が少ない)と考えたのでしょう。

シェルの実装によっては POSIX で定義されたコマンド以外の内在コマンドを持っていることがありますが、POSIX では原則としてこれら以外を内在コマンドとして実装しないように推奨しています。どうあっても POSIX は通常のシェルビルトインコマンドと外部コマンドを区別したくないようです。ユーザーが環境変数 PATH を変更して依存する外部コマンドの実装を変更しようとしたとき、シェルビルトインコマンドが呼び出されることによって外部コマンドの呼び出しが不可能になるのであれば、異なるシェルの間でシェルスクリプトに移植性をもたせることができないという考えからのようです。しかしながら特定のシェルで動かすと決めたシェルスクリプトではシェルにコマンドが内蔵されていたほうが環境依存しなくなるという皮肉な現実があります。

echo コマンド

echo コマンドは文字列を出力するおなじみのコマンドです。必ずしもシェルに組み込む必要はありませんが、(おもにパフォーマンス上の理由で)すべての POSIX シェルに組み込まれてるためここではシェルの機能として扱います。ただしシェルによって動作が異なり移植性が低いコマンドでもあります。

エスケープシーケンスの解釈を制御する-eや-Eが未指定として標準化

今まで echo コマンドの -e オプションや -E オプションは POSIX で言及されていませんでした。つまり文字列として出力する必要があり、多くのシェルは POSIX に準拠していない状態でした。POSIX.1-2024 で -e-E-n と同じく「未指定として標準化」されました。これはシェルの実装やどのような動作でも良い(POSIX に違反しない)ことを意味しています。-e-E-n がどのような動作を行うオプションであるかは POSIX で規定されていないことに注意してください。これらの動作は拡張機能として各シェルが自由に動作を決めることができます。また echo コマンドの引数にバックスラッシュが含まれる場合、その動作は(今まで通り)未指定です。シェルによってデフォルトでエスケープシーケンスを解釈したり解釈しなかったりするので注意してください。

回避策: 移植性のあるechoコマンド相当の実装

移植性が重要な場合 echo コマンドを使うのは推奨できません。代わりに printf コマンドを使いたいところですが、mksh では printf コマンドが外部コマンドなので遅くなります。次のようなシェル関数を定義することで、移植性の問題を解決しつつパフォーマンス低下を避けることができます。

if [ "${KSH_VERSION:-}" ]; then
  put() {
    IFS=" $IFS" && set -- "${*:-}" && IFS=${IFS# }
    print -nr -- "$1"
  }
  putln() {
    IFS=" $IFS" && set -- "${*:-}" && IFS=${IFS# }
    print -r -- "$1"
  }
else
  put() {
    IFS=" $IFS" && set -- "${*:-}" && IFS=${IFS# }
    printf '%s' "$1"
  }
  putln() {
    IFS=" $IFS" && set -- "${*:-}" && IFS=${IFS# }
    printf '%s\n' "$1"
  }
fi

put foo bar baz     # echo -n と同等
putln foo bar baz   # echo と同等

cd コマンド

cd コマンドはディレクトリを移動するコマンドです。

引数のディレクトリ名に空文字を指定した場合にエラーになる

cd コマンドの引数のディレクトリ名に空文字を指定したときにエラーになるように規定されました。これまでは空文字を指定したときの動作は未指定でした。つまり現実のシェルではシェルによって動作が異なっているということを意味します。

ksh93u+mでの動作
$ cd ""
ksh: cd: bad directory

一部のシェルでは cd コマンドの引数のディレクトリ名が空文字の場合、カレントディレクトリへの移動を意味します。しかし ls コマンドなどではディレクトリ名が空文字の場合、カレントディレクトリを意味しません。空文字をエラーにするという仕様の変更は、安全性の点からは妥当なものであると考えられますが、問題は現在いくつかのシェルはエラーにならないという点です。つまり POSIX を読んで空文字を指定した場合にエラーになると思い込んでいると、実際のシェルではそうではないためバグの原因となってしまいます。現実に使われているすべてのシェルが空文字を指定した場合にエラーになるのには時間がかかるでしょう。

なお、空文字を指定してもエラーにならない、つまり現時点で POSIX.1-2024 に準拠していないシェルには、dash、bash、mksh、yash、zsh、Busybox ash、FreeBSD sh、NetBSD sh、OpenBSD sh、ksh88 とほとんどです。逆にエラーにするシェルは ksh93(比較的新しいバージョン)、ksh93u+m、Bourne シェルとわずかしかありません。現実の実装の明文化とは言えないため、この変更は必要だったのか疑問が残ります。また仕様を変更する前に公開されるはずの、将来の方向性として空文字をエラーにするという記述が見当たらないので、標準化プロセスに何かしらのミスがあったのではないかと感じています(本来公開されるはずの TC3 が撤回された?)。

2024-06-30 追記 bash (5.3?) で POSIX.1-2024 準拠に修正されるようです

現在のディレクトリ名が不明な場合にエラーにする-eオプション

cd コマンドの -e オプションは、-P オプションを指定したときのみ指定できるオプションです。-P オプションは物理的なディレクトリ (physical directory) に移動するためのオプションですが、ディレクトリの移動が正しく行われたのに現在のディレクトリ名を取得できない状況があります。例えば対話シェルでカレントディレクトリを削除した場合です。この場合ディレクトリは削除されますが、そのディレクトリを参照しているシェルはディレクトリを参照している限りアクセス可能です。これはオープンしているファイルを削除しても、ファイルをオープンしている限り参照できるのと同じ理屈です。パスは消えますが本当にファイルが削除されるのはファイルをクローズした段階です。このような場合にディレクトリは移動できるのにディレクトリ名を取得できない状況になります。

現在のディレクトリ名は PWD シェル変数から取得することができますが、シェルスクリプトにとって必ずしも必要な情報ではありません。正しく移動できていればそれで十分な場合もあります。しかしもちろんディレクトリ名が必要な場合もあります。-e オプションを指定しない場合、cd コマンドは指定したディレクトリに正しく移動できたかどうかのみを考慮して終了ステータスを決定します。-e オプションを指定した場合はさらに現在のディレクトリ名が取得可能(PWD シェル変数に正しく代入できたか)まで考慮して終了ステータスを決定します。

printf コマンド

printf コマンドは echo コマンドの移植性問題を解決するために作られた文字列を出力するためのコマンドです。必ずしもシェルに組み込む必要はなく、実際に mksh では組み込まれていませんが、大半のシェルに組み込まれているためここではシェルの機能として扱います。

引数位置を指定した参照

printf コマンドで %s の代わりに %2$s のような指定(n$)で参照する引数の位置を指定できるようになりました。この機能は ksh93 と zsh で以前から実装されていますが、bash 5.2 時点では実装されていません。

$ printf 'こんにちは, %2$s %1$s\n' Koichi Nakashima
こんにちは, Nakashima Koichi

上記の例を見てわかるように、この機能はシェルスクリプトの国際化対応に必要な機能(言語によって単語の順番が異なるため)として標準化されました。

POSIX の将来のバージョンでは、指定した位置の引数が存在しない場合はエラーとすることを要求するかもしれないことが記載されています。

将来の方向性: %b と %q の追加

将来の方向性として printf コマンドに %b%q 書式の追加が記載されました。将来の方向性は必ずしも実現するとは限りません。POSIX が(誰かからの提案により)シェルに実装して欲しいと考えている提案のようなもので、実現するかどうかはシェル実装者が実装するなどして標準化するに値すると POSIX が考えた場合です。

%b は数値を2進数に変換して出力する書式です。現在の実装では %b はエスケープシーケンスを解釈する書式として別の意味で使用されているため、新しい書式を使用するための -C オプションのようなものがあると良いと POSIX は提案しています。現在の実装に -C オプションを実装しているシェルはないためすぐに標準化はできません。%b の追加は ISO C 標準の次のバージョンの printf 関数の仕様に対応してのもので、C言語の printf 関数と挙動が異なると混乱するというのが提案の動機のようです。

%q は文字列をシェルの入力として扱えるようにエスケープするための書式です。提案の時期が遅くまた上記の問題(-C オプションの追加が行われるかどうか)に関連しているため標準化に至りませんでしたが、すでに bash、ksh、mksh、zsh で実装されているため標準化される可能性は高いでしょう。

read コマンド

read コマンドはデータを入力するためのコマンドです。

改行の代わりにヌル文字などを終端文字として使う-dオプションの追加

find コマンドの -print0 で出力されたヌル文字終端データを読み込むには、例えば xargs コマンドに -0 オプションを指定する必要がありますが、それと同じように read コマンドの -d オプションで空文字を指定すると、ヌル文字終端データを読み込むことができるようになりました。

$ find .  -print0 | while IFS= read -d '' line; do echo "$line"; done

ヌル文字以外の文字を扱うこともできます。

$ echo foo:bar:baz: | while IFS= read -d : line; do echo "$line"; done
foo
bar
baz

この機能は以前からbash、mksh、ksh93、zshで使用可能でした。ただし ksh93 ではバグでヌル文字を指定することができません。このバグは ksh93u+m で修正されています。

set コマンド

set コマンドはシェルオプションの設定や位置パラメータの設定を行うためのコマンドです。

# シェルオプションの設定
set -o errexit

# 位置パラメータの設定
set -- a b c
echo "$1, $2, $3" # => a, b, c

パイプラインの途中のエラーを取得する-o pipefailの追加

set -o pipefail を実行していない場合、パイプライン全体の終了ステータスは「最後のコマンド」のものとなります。この動作を変更し「最後にエラーになったコマンド」の終了ステータスにするのが set -o pipefail です。ちなみに -o オプションによる長い名前での指定方法は Bourne シェルでは使えませんでしたが、POSIX シェルでは以前から標準化されています。

デフォルトでは最後のコマンドの終了ステータス
$ echo "abc" | grep "d" | tr a-z A-Z
$ echo $?
0
pipefailを有効にすると最後にエラーになったコマンドの終了ステータスになる
$ (set -o pipefail; echo "abc" | grep "d" | tr a-z A-Z)
$ echo $?
1

この機能については多くのシェルで実装済みであり、bash、ksh93、zshに関してはかなり昔から、その他のシェルでは FreeBSD sh は2019年6月の FreeBSD 11.3 から(FreeBSD 11.x はすでにサポート終了)、NetBSD sh や OpenBSD sh もすでにサポートしています。一番の現実的な懸念点は dash ですが 2024-04-05 に対応が完了されており、おそらく次の v0.5.13 で対応するでしょう。

シェルスクリプトでの(pipefailを有効にした)パイプラインのエラー処理の一般的な書き方はこのようになります。

#!/bin/sh

set -o pipefail

if echo "abc" | grep "d" | tr a-z A-Z; then
  echo 正常終了
else
  echo エラー終了
fi

注意点として、移植性が必要な場合 PIPESTATUS 配列は使用しないようにしてくださいPIPESTATUS 配列は bash と mksh(R40以降)でしか使えない機能です。パイプライン全体のどこかでエラーが起きたかを調べたいだけなら PIPESTATUS 配列を調べる必要はありません。

回避策: sh-pipestatus - PIPESTATUSとpipefailの代替実装

もし pipefail にまだ対応していないシェルで pipefail の機能を使用したい場合は、sh-pipestatus というライブラリを作っているのでこれを使用してください。次のような感じで簡単に pipefail 機能と同等のことを行うことができます。

#!/bin/sh

. ./pipestatus.sh

if pipe -fail 'echo "abc" | grep "d" | tr a-z A-Z' "$@"; then
  echo 正常終了
else
  echo エラー終了
fi

ちなみに sh-pipestatus の本来の機能は、名前の通り bash の PIPESTATUS 配列機能をエミュレートするためのライブラリです。PIPESTATUS 配列の代わりに PIPESTS 変数にパイプラインの各コマンドの終了ステータスをスペース区切りで代入します。PIPESTATUS 配列は POSIX で標準化されていないので、もし必要な場合はこのライブラリを使うことができます。

おまけで、パイプを使用する時に問題になる SIGPIPE によるエラーを無視するための igpipe シェル関数も実装しています。

set -o pipefail
seq 100000 | cat | head >/dev/null
echo $? # => 141

igpipe seq 100000 | igpipe cat | head >/dev/null
echo $? # => 0

test / [ ... ] コマンド

test([ ... ])コマンドにいくつかの演算子が追加されました。この議論の中で ksh93 で実装され bash や zsh などにも広まった [[ ... ]] を標準化しようという意見もでましたが、[ ... ][[ ... ]] にはほとんど違いがなく、標準化しても移植性は高まるとは言えず、現在のシェルの実装の間に違いがありすぎるとして却下されたようです。それよりも [ ... ] に足りない演算子を追加した方が良いという結論により行われました。ちなみに = の別名としての == の追加はメリットがないなどの理由で合意に至らず含まれませんでした。そして [[ ...]] にはあったが [ ... ] にはなかった追加の演算子が [ ... ] に対しても広く実装されたため標準化されることになりました。

文字列の大小を比較するための <, > 演算子の追加

文字列の大小を比較するための < > 演算子が追加されました。シェルの文法である [[ ... ]] とは違って [ ... ] はコマンドなので < > 演算子はエスケープする必要があることに注意してください。エスケープがなければリダイレクト文字として解釈されてしまいます。

< > はバックスラッシュでエスケープするかクォートすること
$ [ a \< b ] && echo ok
ok

$ [ a '<' b ] && echo ok
ok

ファイルを比較するための -ef, -nt, -ot 演算子の追加

ファイルが同じかどうかを比較する -ef と更新日時を比較するための -nt-ot 演算子が追加されました。

-ef (equal files) ・・・ ファイルが同一(iノードが同じ)であれば真
-nt (newer than) ・・・ 左のファイルが右よりも新しければ真
-ot (older than) ・・・ 左のファイルが右よりも古ければ真

-a (AND) / -o (OR) 演算子の削除

以前より非推奨となっていた -a (AND) と -o (OR) 演算子が POSIX から削除されました。POSIX から削除されたということの意味は、実際のシェルから削除することを要請しているのではなく、移植性がない機能なので(移植性があるシェルスクリプトを書きたい人は)使用しないように推奨しているということです。実際のシェルは互換性を保つ必要があるため、一般的には POSIX から削除されたからと言ってシェルが持つ機能から削除することはまずありません。マイナーなシェルが削除したり設定により警告を出すようにすることはあるかもしれませんが。

削除された理由については、Oils (Oils for Unix) プロジェクトの下記の記事が参考になります。

What Does [ -a -a -a -a ] Mean?

詳細は上記のサイトに書いてありますが、[ -a -a -a -a ] にはいくつもの解釈があります。これは歴史的なシェル(ksh88など)が -a が「式1 -a 式2: AND」と「-a ファイル名: ファイルが存在してるか?」という文脈によって異なる2つの意味を持ってたあったからです。ちなみに -o にも「式1 -o 式2: OR」と「-o オプション名: シェルオプションが有効か?」の2つの意味があります。POSIX では -a ファイル名-o オプション名 の用法は標準化されていません。POSIX で標準化されていないと言っても、実際のシェルは対応しているわけで広く使われていた AND と OR の用法は非推奨でありながらも標準化されていましたが、このたびめでたく POSIX から削除されたという流れです。AND と OR がなくなったということは ( ... ) を使う意味もありませんので、こちらも削除されています。結論としては移植性があるシェルスクリプトを書きたいのであれば -a-o は使わず、シェルの機能である &&|| を使いましょうということです。

補足ですが、上記サイトの Oils (Oils for Unix) プロジェクト(以前の名前は Oil shell プロジェクト)は、bash 互換のシェルの実装である OSH の開発とレガシーフリーなより良いシェルの実装である YSH (以前の名前は Oil 言語)を開発しているサイトです。

trap コマンド

trap コマンドはシグナルハンドラの設定、つまり任意のシグナルを受け取ったときに、指定したコマンド(シェル関数を含む)を実行するように設定するコマンドです。

# シグナルハンドラの設定
# 補足: 設定にはシグナル番号ではなくシグナル名を使うこと
# シグナル名は大文字でSIGプレフィックスなし
# シグナル番号はXSIオプションなので移植性がない
trap 'sigint_handler' INT

EXITシグナルハンドラが実行される条件の明確化

EXIT シグナルハンドラは正常終了時に実行される疑似シグナル(本当のシグナルではない)ですが、シェルによっては他のシグナルで異常終了したときにも実行される場合があること明確になりました。

実行中にCTRL+Cを押すと?
$ dash -c 'trap "echo Exit" EXIT; sleep 3'
^C

$ bash -c 'trap "echo Exit" EXIT; sleep 3'
^CExit

$ ksh -c 'trap "echo Exit" EXIT; sleep 3'
^CExit

$ mksh -c 'trap "echo Exit" EXIT; sleep 3'
^CExit

$ yash -c 'trap "echo Exit" EXIT; sleep 3'
^C

$ zsh -c 'trap "echo Exit" EXIT; sleep 3'
^C

指定したシグナルのハンドラだけを出力する-pオプションの追加

これまでは trap コマンドに引数を何も渡さずに呼び出すことで現在設定されているすべてのシグナルハンドラをシェルに再入力可能な形で出力することができました。

bashでの実行結果
$ trap "echo int" INT
$ trap "echo quit" QUIT

$ trap
trap -- 'echo int' SIGINT
trap -- 'echo quit' SIGQUIT

この出力は、現在のシグナルハンドラの状態を一旦変数に退避して、シグナルハンドラを変更し、元に戻すという使い方ができます。

save_traps=$(trap) # 現在のシグナルハンドラ設定の保存

... # ここでなにか処理をする

eval "$save_traps" # 元のシグナルハンドラ設定のリストア

ただし、すべてのシグナルハンドラの出力しかできないため少々不便な場合がありました。-p オプションを指定すると、指定したシグナルハンドラのみを出力することができます。

bashでの実行結果
$ trap "echo int" INT
$ trap "echo quit" QUIT

$ trap -p INT
trap -- 'echo int' SIGINT

-p オプションはまだ対応していないシェルが結構ありますが、それよりも重要な問題は、save_traps=$(trap) で現在のシグナルハンドラを変数に保存できないシェルがあるということです。具体的には現時点での dash、mksh、zsh、OpenBSD sh です。これはコマンド置換がサブシェルによって実行され(無視以外の)シグナルハンドラの設定がサブシェルに継承されないためです。サブシェルに入ると(無視以外の)のシグナルハンドラがデフォルトに戻ることは POSIX でも定義された動作ですが、シグナルハンドラを変更することがない単一の trap コマンドだけの場合は戻す必要がないという例外規定があります。もしどうしても現在のシグナルハンドラの設定を変数に保存したい場合は、コマンド置換を使わずに一度ファイルに出力するなどの対策が必要です。

ulimit コマンド

ulimit コマンドはプロセスが使用するリソースを制御するためのコマンドです。

設定項目を指定する多数のオプションの追加

次のようなオプションが追加されました。これまでは -f(ファイルの最大サイズ)しか標準化されておらずしかも XSI オプションでした。POSIX.1-2024 では XSI オプションから Base(基本機能)に変更されました。

意味
-H 強い制限を設定する
-S 弱い制限を設定する
-a 現在の制限を全て表示する
-c 生成されるコアファイルの最大サイズ
-d データセグメントの最大サイズ
-n オープン可能なファイルディスクリプターの最大数
-s 最大スタックサイズ
-t CPU 時間の最大秒数(XSI オプション)
-v 使用可能な最大仮想メモリ量

コマンドは何が変わったか?

通常シェルに組み込まれることはない外部コマンドについての変更事項です。

awk コマンド

awk コマンドは文字列のパターンを認識して処理を行うためのプログラミング言語です。次のような使い方をします。

awk -f スクリプト [-f スクリプト]... [データファイル | 変数割当]...

awk -f myscript file1.txt var=123 file2.txt var=456 file3.txt

補足ですが、awk の正規表現は拡張正規表現を使いますが、歴史的に {m}{m,n} には対応しておらず、POSIX で実装が要求されてからも(gawk や 商用 Unix を除く)一部の実装は長らく対応していませんでした。しかし nawk では 20190305 から、mawk では 1.3.4 20200717 から対応しており 2020 年頃以降のバージョンであれば問題なく利用可能となっています。OS のサポート期間を考えるともう少し注意が必要です。

動かない実装が存在した
$ echo 123 | awk '/[0-9]{3}/'
123

nextfileステートメントの追加

現在の入力ファイルを処理を終了し、次のファイルを読み込むための nextfille ステートメントが標準化されました。

すべてのファイルの一行目のみを出力する
$ awk '{ print $0; nextfile }' test1.sh test2.sh test3.sh
#!/bin/sh
#!/bin/sh
#!/bin/sh

fflush関数の追加

標準出力のバッファを書き出す(フラッシュする)fflush 関数が標準化されました。

# コマンドの出力を別のコマンド(下記の例では cat)に出力すると、
# フルバッファリングが行われるため、すぐに出力されない
ping google.com | gawk '{ print toupper($0); }' | cat

# ping コマンドの本来の動作の通り、1秒間隔で出力される
ping google.com | gawk '{ print toupper($0); fflush(); }' | cat

srand関数を利用したUNIX時間の取得

これまでは srand 関数の引数(シード値)を省略したときのデフォルトの値は時間であることが明記されていましたが、時間が何であるかは定義されていませんでした。これが UNIX 時間(エポックからの秒数)であることが明確になりました。これは awk コマンドを使用した UNIX 時間の取得に移植性があることを意味しています。

UNIX時間の取得
$ awk 'BEGIN { print srand(srand()) }'

【❌️ 注意: 某所で紹介されている間違った方法】
$ awk 'BEGIN { print srand() + srand() }'
GNU awk と nawk と Busybox awk では 1秒ずれます
mawk では指数形式で表示されます

この動作は昔から移植性がありましたが、比較的新しい awk の実装である goawk 1.27.0 では現在この動作を行っていないので注意してください。おそらく次のバージョンぐらいで修正されるはずです。

length関数で配列サイズの取得が可能に

従来、POSIX では length 関数は文字列の長さを取得する関数としてしか定義されていませんでしたが、多くの実装で配列のサイズを取得することが可能であることからこの機能が標準化されました。

$ awk 'BEGIN { a[1]=1; a[2]=2; a[3]=3; print length(a) }'
3

delete関数で配列を空にすることが可能に

従来、POSIX では delete 関数は変数の削除を行いましたが、配列をすべて削除することはできませんでした。多くの実装で配列を削除できることからこの機能が標準化されました。

$ awk 'BEGIN { a[1]=1; a[2]=2; a[3]=3; delete a; print length(a) }'
0

なお、これまでの awk では次のような方法で配列を空にすることができます。

for (index in ary)
  delete ary[index]

# または

split("", ary)

c99 / c17 コマンド(C言語コンパイラ)

c99 および c17 は C言語コンパイラです。CD (C-Language Development Utilities) とマークが付けられているオプション機能なので、どこにでもインストールされているとは限りません。なお機能の違いについての詳細は調べていません。

c99コマンドがc17コマンドへ変更

c99 コマンドが c17 コマンドに変更になりました。POSIX では ISO C 標準を参照しており、コンパイラ名は POSIX の(大幅な)改定時に最新の C 言語標準規格に合わせて変更されることになっています。ちなみに最新の C23 はまだ 2024 年公開予定となっており間に合いませんでした。近く公開されるでしょうから、次の POSIX の大幅な改定では再度 C 言語コンパイラのコマンド名が変更になるのは確実です。

compress / uncompress / zcat コマンド

compress / uncompress / zcat コマンドは圧縮と展開を行うためのコマンドです。これらのコマンドは XSI (X/Open System Interfaces) とマークされているオプション機能となっており POSIX では必須ではないのでインストールされていなかったとしても不思議ではありません。ただし UNIX の認証を取得する場合には必須です。以前の POSIX ではこの3つのコマンドは別々のページで説明されていましたが、POSIX.1-2024 では同じページにまとめられました。

compressコマンドに展開を行う-dオプションの追加

ページの構成が変わっているので比較しづらいですが、compress コマンドに -d オプションが追加されました。これはuncompress コマンド相当の動作を行うコマンドです。また以前から標準化されていた-c (標準出力への出力)オプションと併用することで zcat コマンド相当の動作を行うことができます。つまり uncompress コマンドと zcat コマンドを使わずに compress コマンドだけですべてを行うことができるようになったということです。これがページが統合された理由です。

-d オプションを指定したときは「適切な展開アルゴリズムを使って元のデータに戻す」ということになっているので、展開時の形式は自動判定です。圧縮形式を指定する -g オプションや -m オプションは、POSIX では展開時に指定できないことになっています。

ちなみに、圧縮コマンドが -d オプションで展開できるのは、(POSIXで標準化されていない)gzipbzip2xzlzmazstd などでも広く使われている一般的な習慣です。

gzip圧縮形式を使用する-gオプションの追加

従来これらのコマンドは Z 圧縮形式を扱うコマンドとして標準化されていましたが、gzip 圧縮形式を使用する -g オプションが追加されました。-g オプションを使用する gzip 圧縮形式の対応は「POSIXによる発明」ではありません。1997 年の OpenBSD 2.2 (ドキュメントには 2.1 と書いてある?)で実装された機能です。

また、展開に関しては uncompress コマンド、zcat コマンドともに Linux (GNU) だけではなく BSD も含めて多くの環境で以前から gzip 形式に対応しています。展開は gzip 形式に対応しているのに、圧縮は対応してないという非対称な状態でした。

ちなみに、これらのコマンドが複数の圧縮形式に対応するのは「1 つのことだけを行うという UNIX 哲学的に反している」という意見と共に gzip コマンドを標準化するべきだという意見が寄せられましたが(意見を言うのが遅すぎたのに加え)、7年前にすでに議論済みの話であり標準規格の開発者は当時も今も新しいコマンドを追加するよりも別の圧縮形式に対応するほうが優れていると考えているという理由で却下されました。実際問題、圧縮アルゴリズムが違うだけでそれ以外の機能が同じコマンドをそれぞれ保守していくよりも、一つのコマンドで異なる圧縮アルゴリズムをサポートするほうが、利用者にとっても開発者にとってもシンプルで簡単です。1 つのことはライブラリ関数レベルでやれば十分ですし、異なるアルゴリズムの処理を別のコマンドに分離したければ、コマンド内部から別のコマンドを呼び出せばよいだけです。POSIX はインターフェースを規定するものであってコマンドの内部実装を規定しません。

新たな形式に拡張可能な-mオプションの追加

POSIX で標準化されている対応形式は従来の Z 形式と gzip 形式のみです。圧縮形式の指定は gzip 形式を指定する -g オプションの他に -m オプションが追加されました。-m algo という指定方法で引数 (algo) に圧縮形式を指定します。

Algorithm algo Filename Suffix
Adaptive LZW lzw .Z
RFC1951 DEFLATE deflate .gz
Synonym for DEFLATE gzip .gz

-m オプションはこれまでの実装には存在しなかったオプションです。POSIX による発明にように思えるかもしれませんが、「新しい機能」を発明したわけではなく、今までの (OpenBSD compress の) 機能に別のオプションを定義しただけという扱いです。既存の機能とほぼ同じ機能のオプションを POSIX が追加するということはあります(xargs コマンドにおける -l オプションを標準化た -L オプションなど)。

-m オプションを追加した理由は、既存の uncompress コマンドや zcat コマンドの実装が複数の形式に対応しているという現状を踏まえて、その一般的な慣習を compress コマンドにも取り入れるためです。つまり新たな圧縮形式に実装が対応できる余地を与えるために用意されたものです。-m オプションの導入により拡張性が向上しました。新しい圧縮形式への対応が一般的になれば、将来の POSIX で標準化されるかもしれません。今後はこれらのコマンドは複数の圧縮形式を同じ方法で扱うためのフロントエンドコマンドという位置づけになるでしょう。

date コマンド

date コマンドはシステム日時の取得と設定を行うコマンドです。システム日時の設定に限り XSI オプション機能です。

UNIX時間(%s)などの書式への対応

これまで欠けていた %s などの書式に対応するようになりました。また date コマンドが対応すべき書式は、date コマンドで定義されるのではなく C言語の strftime() 関数で定義されるように変更になり、今までの date コマンドで対応していた書式以上のものが使えるように標準化されています。

回避策: UNIX時間と日時の相互変換や曜日の取得関数

date コマンドの +%s 書式はすでに多くの環境で実装されていますが、もし気になる場合は、UNIX 時間を取得するシェル関数および、日常の日付とUNIX時間を相互に変換するシェル関数を作っているのでこちらを使用してください。

dd コマンド

dd コマンドはファイルの変換とコピーを行うコマンドです。ディスクを初期化したりするのに使用されるため危険なシステム管理系のコマンドと勘違いされることがありますが、/dev/sda のようなデバイスへの書き込みは root 権限があれば cp コマンドでもできるわけで、dd コマンドが特別危険なわけではありません。コマンドライン引数の書式が特徴的ですが他のコマンドと対して違うものではありません。ちなみに dd コマンドの引数が特徴的なのは、元は Unix とメインフレームとの間でのデータ交換用のコマンドで、メインフレームのジョブ制御言語(JCL)の DD 文に由来しているためと言われています。

すべてのデータを確実に読み込むiflag=fullblock

iflag=fullblock は入力のブロックがいっぱいになるまで蓄積してから処理を実行するためのフラグです。iflag=fullblock を指定せずに count= を指定してパイプからデータを読み取るとデータが途切れる場合があります。このフラグの効果は次のような例で確認することができます。

$ cat /dev/urandom | dd bs=1048576 count=1 | wc -c
0+1 records in
0+1 records out
65536 bytes (66 kB, 64 KiB) copied, 0.000959738 s, 68.3 MB/s
65536

$ cat /dev/zero | dd bs=1048576 count=1 iflag=fullblock | wc -c
1048576
1+0 records in
1+0 records out
1048576 bytes (1.0 MB, 1.0 MiB) copied, 0.00114991 s, 912 MB/s

上記のコードでは /dev/zero の中身をパイプで dd コマンドに渡していますが、前者は bs で指定した 1 MB ではなく 64 KB を読み取っただけで終了しています。読み取ったデータをパイプでさらに wc コマンドに渡していますが、当然その wc コマンドが処理するデータサイズも 64 KB です。一方後者は iflag=fullblock の指定により 1 MB溜まるまでまってから処理を行うため、指定したサイズを確実に読み取ることができます。この決から分かる通りパイプからの入力は iflag=fullblock を指定しないかぎり読み取れる保証はありませんbs=1 count=1024 と指定することで指定した正確なバイト数読み込むことができますが、1バイトずつの読み込みになるので当然処理は遅くなります。この問題は POSIX.1-2024 でも説明されています。

Using the count= operand of dd with a pipe or FIFO as the input can lead to surprising results, since these file types are prone to encountering short reads for any input block size other than 1. Unless the iflags=fullblock operand is in effect, dd will stop after the specified number of reads, rather than full input blocks, and therefore can often result in fewer bytes being output than the product of the count and input block size.

パイプまたは FIFO を入力として ddcount= オペランドを使用すると、これらのファイルタイプでは、1 以外の入力ブロックサイズで短い読み取りが発生しやすいため、驚くような結果になることがある。iflags=fullblock オペランドが有効でない限り dd は完全な入力ブロックではなく指定された読み取り回数で停止するため、出力されるバイト数は、count と入力ブロックサイズの積よりも少なくなることがよくある。

注意 iflags は POSIX 標準規格のスペルミスで正しくは iflag です。iflags を持った実装は存在しません。このスペルミスは修正されるはずです。

ちなみに dd コマンドを使用して指定したバイト数を読み込むのであれば、POSIX.1-2024 で標準化された tail -c を使用したほうが良いでしょう。この問題は「「dd bs=サイズ count=1」で パイプから入力するとデータが途切れる罠に注意せよ!」でも詳しく解説しています。

将来の方向性: SIGINFO への対応

Linux の dd コマンドは USR1 シグナルを送信すると転送データのステータスを出力することができます。

Linuxの場合
$ dd if=/dev/random of=/dev/null bs=1024 count=10000000 &
[1] 1437922

$ kill -s USR1 $!
929781+0 records in
929780+0 records out
952094720 bytes (952 MB, 908 MiB) copied, 3.53758 s, 269 MB/s

FreeBSD では USR1 シグナルの代わりに INFO シグナルを使用します。

FreeBSDの場合
$ dd if=/dev/random of=/dev/null bs=1024 count=10000000 &

$ kill -s INFO $!
430270+0 records in
430270+0 records out
440596480 bytes transferred in 1.933599 secs (227863429 bytes/sec)

現在の Linux (や、おそらく System V 系 Unix)には INFO シグナルは実装されていません。将来の方向性として POSIX では BSD 系 Unix に存在する INFO シグナルを標準化し、dd コマンドはそれに対応することが明記されています。

find コマンド

find コマンドはファイルを検索し、見つけたファイル名の出力および見つけたファイルに対して(-exec を使って指定した)任意のコマンドを実行するためのコマンドです。

find ... 検索条件 ... -exec 任意のコマンド +

大文字と小文字を区別しない-iname評価式の追加

-name と同じですが大文字と小文字を区別しません。

異なるファイルシステムを除く-mount評価式の追加

以前から POSIX で標準化されていた -xdev 評価式と似た機能を持つ -mount 評価式が追加されました。GNU findでは -mount-xdev の別名であると書かれていますが、POSIX で標準化された内容にはわずかに違いがあります。細かい検証はしていないので詳細は調べてください。

ヌル文字終端で出力する-print0評価式の追加

検索したファイル名を改行終端ではなくヌル文字終端で出力する評価式です。xargs -0read -d "" などで読み込むことができます。

fort77 コマンド(Fortran 77コンパイラ)の削除

Fortran 77 のコンパイラです。以前の POSIX では FD (FORTRAN Development Utilities) とマークが付けられているオプション機能で、実装は必須ではありませんでした。このコマンドは POSIX.1-2024 で削除されました。POSIX.1-2024 に対応する Fortran コンパイラが存在しないからのようです。

gettext / ngettext / xgettext / msgfmt コマンド(新規)

gettext / ngettext / xgettext / msgfmt コマンドは POSIX.1-2024 で新しく追加された国際化関係のコマンドです。gettextngettextmsgfmt は POSIX で必須コマンドとなっていることに注意してください。つまり gettext はもはや GNU や Linux だけのものではありません。

gettext 由来の新しい国際化対応

従来 POSIX では catgets の仕組みを使った国際化機能が POSIX.1-2001 で XSI 拡張機能として追加され、POSIX.1-2008 で基本機能となっていました。補足ですがに catgets で翻訳を行うには POSIX でも標準化されている gencat コマンドを使って翻訳ファイルを作成します。XSI 拡張機能であることからわかるように、元は System V 系 Unix で広く使われていた国際化機能です。POSIX.1-2024 では別の仕組みである gettext を使った国際化機能が追加されました。gettext は元はサン・マイクロシステムズで開発された国際化機能ですが、現在は GNU gettext として GNU 関連のコマンドや Linux などで広く使われています。これらの国際化機能はシェルスクリプト限定の機能ではなく、どちらかと言えば C 言語やその他の言語のための仕組みであることに注意してください。

余談ですが、gettext の標準化は 2021年10月に亡くなった Joerg Schilling(cdrtools や Schily Bourne Shell の開発者)が、POSIX にプログラムを追加するのにどのような労力が必要なのかを理解してもらうために Hasso Plattner Institute の学生と一緒に POSIX 標準規格の提案を書いたそうです。なかなか面白い試みですね。詳細は以下を参照してください。

言語翻訳の gettext / ngettext コマンド

gettext コマンドと ngettext コマンドは、英語のメッセージを引数に(翻訳ファイルがあれば)それに対応する翻訳メッセージを出力するコマンドです。ちなみに ngettext コマンドは数値の単数形と複数形で異なる翻訳メッセージに変換することができるコマンドです。

翻訳ファイルを生成する xgettext / msgfmt コマンド

xgettext コマンドと msgfmt コマンドは翻訳ファイルを作成するためのコマンドです。xgettext コマンドはソースコードから翻訳対象の文字列を抽出して PO ファイルを作成するコマンドです。POSIX では CD (C-Language Development Utilities) とマークが付けられており、C 言語用の開発用コマンドとして C 言語のことしか考慮されていません。ただし実際の GNU xgettext コマンドはシェルスクリプトを含むさまざまな言語のソースコードに対応しています。POSIX の xgettext は C 言語専用ですが、国際化にはテキストファイル形式の PO ファイルさえあればよいので、シェルスクリプトでも移植性がある形で国際化対応を行うことは可能です。

msgfmt コマンドは xgettext コマンドから生成した PO ファイル(を元に翻訳したファイル)から、MO ファイルを生成するコマンドです。MO ファイルはバイナリ形式でシステム依存する可能性があるため、(インストール時などに)環境ごとに生成する必要があります。開発時のみに使うコマンドではないためオプションではなく必須コマンドとなっています。

回避策: シェルスクリプト用の国際化ライブラリsh-i18n

シェルスクリプトの国際化は従来の catgets の仕組みには ksh93 のみが対応していました。GNU gettext を使った翻訳の仕組みは bash がネイティブに対応(bash 専用の $"..." を使用して翻訳します)していますが、この方法は現在非推奨で、GNU が提供する gettext.sh シェル関数ライブラリを使用した方法が推奨されています。この gettext.sh シェル関数ライブラリでは内部的に gettext コマンドや ngettext コマンドを使用しています。ただし内部で(POSIX で標準化されていない)envsubst コマンドを使用しており、 gettext.sh シェル関数ライブラリは POSIX コマンドだけでは動作しません。

このような現状であるため、移植性があるシェルスクリプトで国際化対応を行うのは難しい状況です。そこで私がシェルスクリプト用の国際化対応シェル関数ライブラリである sh-i18n を作成しています。このシェル関数ライブラリは、gettext.shgettext コマンドや ngettext コマンドを直接使うよりもも使用方法が簡単で、すべての POSIX シェルをサポートしており高い移植性がある(例えば gettextngettext コマンドがインストールされていない環境でも翻訳ができないだけで動作する)ので、もしシェルスクリプトで国際化対応をしたいという人は、これを使用するとよいでしょう。

sh-i18n は内部的に gettext コマンドや ngettext コマンドを使用して翻訳しています。したがって翻訳ファイルを作るには xgettext コマンドと msgfmt コマンドを使います。

head コマンド

head コマンドはファイルの冒頭部分を出力するコマンドです。

読み込むバイト数を指定する-cオプションの追加

ファイルの最初から指定したバイト数を出力する -c オプションが標準化されました。head コマンドは原則としてテキストファイルを扱うコマンドですが、-c オプションを指定した場合はバイナリデータとして扱うことが明記されています。

$ echo 1234567890 | head -c 5
12345

注意: AIX では末尾に改行が追加される

ちなみに tail コマンドは以前から -c オプションが標準化されています。head コマンドと tail コマンドの機能が対照的ではないのは、そもそもこの 2 つのコマンドはルーツが異なり別々の場所で開発されているからです。head コマンドは sed コマンドで代替できる (head -n 5 = sed 5q) ため、Research Unix(Unix の創始者達のオリジナルの Unix)では、長らく不要なものとして考えられていました。この話については別の機会に紹介したいと思います。

logger コマンド

logger コマンドはシステムログにメッセージを書き込むコマンドです。

追加情報を記録する -f, -i, -p, -t オプションの追加

これまで POSIX で標準化されていた logger コマンドにはオプションは一切なく、メッセージのみが指定できました。

意味
-f file 指定したファイルの内容をログに出力
-i logger のプロセス ID をログに出力
-p priority 優先度(err、warning、info、debug など)をログに出力
-t tag (ユーザー名の代わりに)タグで指定した文字列を出力

mailx コマンド

mailx コマンドはメールを送信したり、届いたメールを閲覧するためのコマンドです。実際の mailx コマンドはメールを閲覧する機能を持っていると思いますが、POSIX では閲覧機能は UP (User Portability Utilities) と指定されているオプションで、閲覧機能の実装が必須なのは User Portability Utilities をサポートしているシステムのみです。

空メールを送信しない-Eオプションの追加

このオプションはプログラムからメッセージを生成してパイプ経由で mailx コマンドに渡してメール送信する処理で、エラーの場合にメールを送信しない方法として利用することができます。つまりエラーの場合はメッセージを何も出力しないことでメールの送信がキャンセルされます。

make コマンド

make コマンドは特定のファイルから別のファイルを生成するときに使用するコマンドです。SD (Software Development Utilities) とマークが付けられているオプション機能なので、どこにでもインストールされているとは限りません。ここに挙げたもの以外にもいろいろな機能が追加されているので詳しくは POSIX を参照してください。

並列実行のための-jオプションの追加

並列実行のための -j オプションが標準化されました。関連して .NOTPARALLEL.WAIT 特殊ターゲットが追加されています。

ファイル名でないことを明示する.PHONY特殊ターゲットの追加

ターゲットがファイル名やディレクトリ名でないことを明示する .PHONY 特殊ターゲットの追加されました。

ps コマンド

ps コマンドはプロセスのステータスを報告するコマンドです。このコマンドは XSI オプションとなっているコマンドラインオプションが多く、移植性には注意が必要です。

広い幅で表示する-wオプションの追加

一行をより広い幅で表示する -w オプションが標準化されました。

Batch Environment関連のコマンドの削除

POSIX 2d-1994 で追加された Batch Environment 関連のコマンドが削除されました。

qalter  qdel     qhold  qmove  qmsg  qrerun
qrls    qselect  qsig   qstat  qsub

削除理由を明確に書いてある所を見つけていないのですが、ほぼ間違いなく実際には使われていないコマンドだからでしょう。

readlink / realpath コマンド(新規)

readlink コマンドと realpath コマンドは POSIX.1-2024 で新しく追加されたコマンドです。

シンボリックリンク先を取得するコマンドの追加

広い環境で実装され、どの環境にでもあると言っても過言ではないコマンドとなった readlink コマンドと realpath コマンドが標準化されました。どちらも似たような機能を持っていますが、POSIX で標準化されたこの二つのコマンドの違いは明確でコマンド名のとおりとなっています。

  • readlink ・・・ シンボリックリンクの中身を読み取る
    • オプション -n のみ
  • realpath ・・・ シンボリックリンクを再帰的に解決し実体のパスを取得する
    • オプション -e-E のみ

歴史的にこの二つのコマンドは異なるルーツを持つコマンドで別々の場所で誕生し機能が強化されてきました。realpath コマンドは BSD で使われてきたコマンドで、readlink コマンドは Linux で使われてきたコマンドだと勘違いされることがありますが、それは真実ではありません。realpathreadlink も単純な機能なので同じようなものが何度も再発明されているようです。realpath コマンドは 1996 年当時 dwww パッケージの一部として利用可能であったものが、2002 年に Debian 3.0 で単独のパッケージとしてインストール可能になりました。そのときには BSD (OpenBSD と NetBSD)では readlink コマンドが登場していました。realpath が昔から BSD で使えたのは FreeBSD だけです。BSD と言ったときに FreeBSD のことしか考えていないのは多くの例で見かけます。

Year realpath readlink
1996 dwww パッケージの一部
1997 OpenBSD 2.1, OpenBSD 2.2 (-f)
2001 FreeBSD 4.3
2002 Debian 3.0 NetBSD 1.6
2003 GNU Coreutils 4.5.5 (-f)
2004 FreeBSD 4.10
2007 NetBSD 4.0 (-f)
2012 GNU Coreutils 8.15 FreeBSD 8.3 (-f), Mac OS X 10.8
2022 OpenBSD 7.1, macOS 13.0 macOS 12.3 (-f)
2023 NetBSD 10

readlink コマンドの -f オプション(GNU だけではなく macOS などでも対応した)は realpath コマンドと同等の動作を行いますが POSIX では標準化されていません。POSIX の範囲でシンボリックリンクを解決したい場合に使うコマンドは realpath です。

回避策: POSIXコマンドのみを使ったreadlink -fの実装

realpath コマンド(または readlink -f)はシェルスクリプトの実体の場所を見つけるために必要なコマンドです。このコマンドが標準化されておらず、以前の環境では標準インストールされていない場合もあったため、以前の POSIX で標準化された範囲の機能(ls コマンド)のみを使って readlinkf シェル関数を実装しています。ほとんど readlink -f と互換の動作を行いシンボリックリンクが循環参照している場合のエラー処理にも対応しています。もし必要な方はこれを使用してください。

rm コマンド

rm コマンドはファイルまたはディレクトリを削除するためのコマンドです。

空のディレクトリを削除可能にする-dオプションの追加

rm コマンドに -d オプションが追加されました。 -d オプションを指定すると引数がファイルまたはディレクトリの場合に削除します。ディレクトリを削除する場合はその中身は空でなければいけません。従来はディレクトリの削除には rmdir コマンドを使っていましたが rmdir コマンドはディレクトリしか削除できないの対して rm -d はファイルとディレクトリの両方を削除することができるという違いがあります。

以前から rm -r でディレクトリとその中身の再帰的な削除ができましたが、rm -r は(デフォルトでは)異なるファイルシステムであっても削除できてしまいます。これを防ぐ --one-file-system は GNU コマンドの拡張機能です。rm -dfind コマンドの -xdev 評価式と組み合わせてることで、--one-file-system と同等のことを行う方法として、すでに多くの実装が対応しているため標準化されました。つまり find コマンドで再帰的に見つけたファイルとディレクトリの両方を削除するには、rm コマンドに -d オプションが必要であるという話です。

$ find . -xdev -depth ! -name . -exec rm -d {} +

-d オプションの追加を受けて、私達は考え方を少し変える必要があるでしょう。それは rm コマンドはファイルシステムから「削除全般を行うコマンド」になったということです。これまで私達は rm コマンドはファイルを削除するコマンドで、rmdir はディレクトリを削除するコマンドで、ファイルかディレクトリでコマンドを使い分けるものだと考えていたはずです(なのに rm -r でディレクトリも削除できるという矛盾がありましたが、それが解消されました)。rmdir コマンドは mkdir コマンドと対を成すコマンドで、この二つは「空のディレクトリを(-p オプションで再帰的に)作成または削除する時に使うコマンド」です。もちろんディレクトリの削除に rmdir コマンドを使うのは非推奨と言うつもりはありません。シフトキーを押す手間(何かを勘違いしていた)ハイフンに指を伸ばすことを考えれば rmdir コマンドのほうが入力が簡単でしょう。

何を削除したか知らせる-vオプションの追加

何を削除したか知らせる -v オプションが追加されました。

sed コマンド

sed コマンドはストリーミング型のテキストエディタで、テキストエディタのコマンドを並べたスクリプトを使ってテキストをストリーミングで書き替えるコマンドです。次のような使い方をします。

sed -f スクリプト [-f スクリプト]... ファイル...

sed -f myscript file1.txt file2.txt file3.txt

拡張正規表現を有効にする -E オプションの対応

grep コマンドと同じように、sed コマンドも -E オプションで拡張正規表現が使えるようになりました。過去には sed コマンドは -r オプションで拡張正規表現を有効にしていましたが、昔から -E オプションにも対応しており、BSD 系 Unix で -E オプションが普及したために POSIX で標準化されました。

sコマンドに大文字小文字の区別をしないiフラグの追加

s コマンドに大文字小文字の区別をしない i フラグが追加されました。GNU だけではなく、macOS、FreeBSD、NetBSD などでもすでに対応しています。

$ echo Aa | sed 's/a/@/gi'
@@

補足ですが一部の実装は小文字の i の代わりに大文字 I(のみ)を同じ意味で使用していることが POSIX に記載されています。大文字 I は POSIX では標準化されていません。

将来の方向性: 正規表現中のバックスラッシュエスケープ文字の対応

現在の POSIX では正規表現の中にバックスラッシュでエスケープされた文字を使うことはできません。将来的にこの機能を標準化するかもしれないことが明記されました。

\tをタブと解釈する(現在の POSIX では非対応だが GNU、FreeBSD、macOS などで動作する)
$ printf "Hello\tWorld\n" | sed 's/\t/:/g'
Hello:World

正規表現中のバックスラッシュエスケープ文字は従来は GNU コマンドのみが対応していましたが、現在は macOS や FreeBSD や NetBSD (OpenBSD は現時点では非対応)でも対応するように強化されています。この流れを受けて将来の POSIX は標準化を検討しているようです。そして将来この動作が標準化された時に、ブラケット式の中のバックスラッシュがエスケープの意味に変わるかもしれないため、ブラケット式の中では2文字のバックスラッシュを使用すべきであると明記されています。

# 現在の POSIX では [ ] の中は \ と t の意味だが
# GNU や(最近の)macOS や FreeBSD や NetBSD ではタブの意味になる
echo 'test' | sed 's/[\t]/@/g' # => @es@ または test

# POSIX は将来の方向性の内容、および現在の一部の環境のために
# この書き方を推奨している。現在の POSIX の動作では2番目の
# バックスラッシュはただ冗長なだけなので問題は発生しない
echo 'test' | sed 's/[\\t]/@/g' # => @es@

補足ですが置換文字列の中の \n については POSIX は将来の方向性も含めて何も記載されていないようですが、現在の実装では多くの環境が \n を改行の意味として使用します(同様に \t をタブなど)。これは最近広まってきた仕様ですが、便利な拡張機能なのでこの動作も将来の POSIX で標準化される可能性が高いと思われます。

GNU、FreeBSD、NetBSD、macOSでの現在の動作
$ echo "foo:bar" | sed 's/:/\n/g'
foo
bar

将来の方向性: 基本正規表現の \+\?\| 対応

sed コマンド以外にも当てはまる POSIX で標準化されている正規表現全般の話です。

基本正規表現 (BRE) と拡張正規表現 (ERE) で同じ意味となる表現を対応付けると次のようになります。

BRE  ^  $  .  [  ]  *        \{  \}  \(  \)     \  \1...\9
ERE  ^  $  .  [  ]  *  +  ?   {   }   (   )  |  \

POSIX BRE には +?| に相当する機能がありません。POSIX.1-2024 では将来これらに相当する \+\?\| の実装を要求するかもしれないことが明記されました。実際、これらは GNU コマンドだけではなく BSD 系 Unix の grep コマンドなどですでに対応しており基本正規表現の拡張は進んでいます。移植性が認められれば POSIX で標準化されるというのは道理です。なお拡張が進んでも POSIX 拡張正規表現で後方参照の実装が要求されることはないでしょう。これは拡張正規表現で使用されている正規表現のアルゴリズムの都合で実装が難しいからです。将来的に POSIX 正規表現は名前に反して基本正規表現のほうが拡張正規表現よりも機能が上になる時が来るかもしれません。

補足ですが「拡張正規表現で使えない後方参照」というのは sed の置換文字列(s/正規表現/置換文字列/)での参照のことではなく、正規表現の中で用いる後方参照のことなので注意してください。置換文字列での正規表現にマッチした部分の参照は拡張正規表現でも問題なく使えるので、sed コマンドで -E オプションを指定したとしても、置換文字列での参照が POSIX 準拠していないことになるわけではありません。

POSIX.1-2024に準拠した書き方
$ echo abc | sed -E 's/(.)/[\1]/g'
[a][b][c]

sort コマンド

sort コマンドはファイルを並べ替えるコマンドです。

一時ファイルの作成場所を環境変数TMPDIRで指定する

sort コマンドは一般的にメモリに入らないような大きなファイルをマージソートと呼ばれるアルゴリズムを使用して並び替えます。マージソートはすべてのデータをメモリ内で並び替えるのではなく、あるサイズごとのソート済みファイルを作成し、そのファイルをマージ(併合)することで全体を並び替えるソート方法です。メモリを多く消費しなくてすみますがその代わりに一時ファイルを使用します。つまりディスク容量が足りなければエラーになってしまいます。もしディスク容量や読み書き速度のために別のディレクトリを使用したい場合、環境変数 TMPDIR で指定することができます。

stty コマンド

stty コマンドはターミナルドライバの設定を変更し、入力したキーに対する反応を制御するためのコマンドです。

ターミナルのウインドウサイズの設定と取得

ターミナルのウインドウサイズの設定と取得を行うための rowscolssize が追加されました。

tail コマンド

tail コマンドはファイルの末尾を出力するコマンドです。

ファイルを逆順に出力する-rオプションの追加

BSD 系 Unix や Solaris 11 などで広く実装されているファイルを逆順に出力するための -r オプションが標準化されました。ただし現時点での GNU 版には実装されていません。GNU 版では昔からファイルを逆順に出力するための tac コマンドが使われています。「ファイルを逆順に出力する機能」は tail コマンドの「ファイルの末尾を順番に出力する機能」とは異なるため Unix 哲学的に tail コマンドに -r オプションを含めるべきではないという考えからです。しかし POSIX に準拠するために追加することには肯定的であるため、おそらく近いうちに実装されると思われます。POSIX では tac コマンドと -r オプションのどちらを標準化するか投票を行ったのですが、他の Unix が tac コマンドの実装するのに期待するよりも、GNU なら素早く実装してくれるに違いないという考えから -r オプションが採用されたようです。

timeout コマンド(新規)

timeout コマンドは POSIX.1-2024 で新しく追加されたコマンドです。

時間制限付きでコマンドを実行する

timeout コマンドは時間制限付きでコマンドを実行するコマンドです。

3秒経ったら自動的に停止する
$ timeout 3 ping google.com
PING google.com (142.251.42.174) 56(84) bytes of data.
64 bytes from nrt12s46-in-f14.1e100.net (142.251.42.174): icmp_seq=1 ttl=55 time=26.1 ms
64 bytes from nrt12s46-in-f14.1e100.net (142.251.42.174): icmp_seq=2 ttl=55 time=23.1 ms
64 bytes from nrt12s46-in-f14.1e100.net (142.251.42.174): icmp_seq=3 ttl=55 time=23.3 ms

timeout コマンドが便利なコマンドでシェルスクリプトで実装するのが難しいというのはわかるので、あったほうが良いというのはそのとおりなのですが、対話的な用途で使うプログラムで使うものとしか思えずシェルスクリプトにとって役に立つものではないので、POSIX の方針的に標準化する必要があったのか疑問です。議論もほとんどなく標準化されています。

多くの人はタイムアウト付き入力に使いたいのではないかなと思うのでサンプルコードです。read コマンドではなく head -n 1 で一行の入力を行っているのは read コマンドがシェルビルトインコマンドなので timeout コマンドから呼び出すことができないためです。

$ input=$(timeout -f 3 head -n 1)

【現時点では GNU 版は -f オプションに非対応なので --foregroud を使う】
$ input=$(timeout --foreground 3 head -n 1)

余談ですが、System V 系 Unix では一行を読み込んで一行を出力するための line コマンドを持ってます。しかし POSIX では line コマンドを標準化する代わりに read コマンドに行をそのまま(バックスラッシュを解釈せずに)読み込むための -r オプションを追加したという経緯があります。read コマンドで行をそのまま読み込むときはいちいちと IFS= read -r と書かなければいけなくて面倒なのですが、元々 read コマンドは単純な行を読み込むためのコマンドではなく(ある種のフォーマットを持った)データを読み込むためのコマンドということなのでしょう。

uuencode / uudecode コマンド

uuencode コマンドと uudecode コマンドはその名の通り uuencode と呼ばれるエンコーディングに、データをエンコードを行うコマンドとデコードを行うコマンドです。本来は。

base64 エンコードを行う -m オプションの追加

base64 エンコーディングは uuencode とは別のエンコーディングですが、どちらもメールにバイナリファイルを埋め込むという用途で使われてきたという点で共通点があります。uuencode という名前からは反していますが、現在のほとんど(すべて?)の実装が -m オプションで base64 エンコード・デコードを行う機能を持っているという現実から、この機能が POSIX で標準化されました。

$ echo test | uuencode -
begin 664 -
%=&5S=`H`
`
end

$ echo test | uuencode -m -
begin-base64 664 -
dGVzdAo=
====

変換データを見るとわかるように(メールに埋め込む時に必要な)ヘッダが付いているため、通常の base64 エンコーディングとは異なります。ただし最初と最後の行を取り除けば通常の base64 エンコーディングとして使用可能です。これは sed コマンドなどを用いて簡単に行うことができます。

$ echo test | uuencode -m - | sed '1d;$d'
dGVzdAo=

(POSIX で標準化されていない base64 コマンドと同じ結果)
$ echo test | base64
dGVzdAo=

回避策: POSIXコマンドのみを使ったbase64の実装

おそらく相当古い環境でない限り、-m オプションは実装されているようです。Solaris や AIX にも実装されています。特に使う理由はないような気もしますが、POSIXコマンドのみ(かつ uuencode / uudecode の -m オプションを使用しないで)base64 を実装しているので興味がある方は参照してみてください。

xargs コマンド

xargs コマンドは「多すぎる引数を分割して複数回のコマンド呼び出しに分けて呼び出すためのコマンド」です。補足ですが、xargs コマンドの仕様は複雑で問題が起きやすいので、入力がファイル名の場合は xargs と組み合わせることはせず、可能な限り find ... -exec {} + を使用するようにしてください

【推奨: ファイル名を安全に扱うことでき速い】
$ find . -name "*.txt" -exec touch {} +

【推奨しない: 上記と同等の方法(安全に扱おうとすると冗長になる)】
$ find . -name "*.txt" -print0 | xargs -0 -r touch

-0 オプションの追加

xargs コマンドの -0 オプションは find コマンドの -print0 などで出力したヌル文字終端文字列を読み取るためのオプションです。これはスペースや改行が含まれている文字列を適切に扱うためのものです。ヌル文字区切りではないので注意してください。つまりテキストファイルの最後の行に改行が必要なように最後にヌル文字が必要です。

【正しい(ヌル文字は終端文字)】
foo<NUL>bar<NUL>baz<NUL>

【間違い(ヌル文字は区切り文字ではない)】
foo<NUL>bar<NUL>baz

POSIX では末尾のヌル文字以外(つまり上記の間違いの末尾の「baz」)は無視することが推奨されています。通信切断などで「途切れた壊れたデータ」の可能性があるからです。ただし現在の実装は(おそらくすべて)末尾のヌル文字以外を無視しませんし、無視しなくても良いことになっています。

If the standard input is not empty and does not end with a null byte, xargs should ignore the trailing non-null bytes (as this can signal incomplete data) but may use them as the last argument passed to utility.

POSIX でも最初は末尾のヌル文字以外を無視しないという内容だったのですが、不完全なデータの可能性があるとして異議申し立てが行われそれが採用されました。テキストデータの最終行に改行がない場合、最終行が無視されることがありますが、それがヌル文字終端の場合にも発生する可能性が考えられるということです。

ヌル文字が連続している場合、それぞれが個別の区切り文字として扱われます。これが POSIX.1-2024 で指定された動作であり、GNU 版、Solaris 11版、OpenBSD 版ではそのとおりに動作します。しかし(現時点の)FreeBSD 版と NetBSD 版とm acOS 版は無視されるので注意してください。変更すると互換性が保てないため将来 POSIX.1-2024 に準拠する仕様変更は行われないのではないかと考えています。

GNU 版、Solaris 11版、OpenBSD 版はPOSIX.1-2024準拠
$ printf 'foo\0bar\0\0baz\0' | xargs -0 -n 1
foo
bar

baz
FreeBSD 版、NetBSD 版、macOS 版はPOSIX.1-2024に準拠していない
$ printf 'foo\0bar\0\0baz\0' | xargs -0 -n 1
foo
bar
baz

引数がないときにコマンド呼び出さない-rオプションの追加

GNU 版と System V 系の xargs コマンドは引数がなくてもコマンドを呼び出します。しかし BSD 系の xargs コマンドは引数がない時にコマンドを呼び出しません。

GNU版やSystem V系では引数がなくてもコマンドを呼びだす
$ cat /dev/null | xargs printf "[%s]\n"
[]
BSD系では引数がない時にコマンドを呼びださない。
$ cat /dev/null | xargs printf "[%s]\n"

伝統的な xargs コマンドの仕様は「引数がなくてもコマンドを呼び出す」ほうです。これは xargs の本来の機能が「多すぎる引数を分割して複数回のコマンド呼び出しに分けて呼び出すためのコマンド」であることから理解できる動作です。つまり touch コマンドの引数がなくても touch コマンドは実行できますよね?ということです。

新しく標準化された -r オプションを使用すると、BSD 系のようにコマンドを呼び出さなくなります。BSD 系では -r オプションは何もしないオプションとして実装されています。

BSD版では引数がない時にコマンドを呼びださない
$ cat /dev/null | xargs -r printf "[%s]\n"

ちなみに POSIX.1-2024 では -r オプションが指定されていないときはちょうど一回呼び出されると規定されているので、現在の BSD 系の実装は POSIX.1-2024 に準拠していないことになると思われます。

将来の方向性: ファイル名に改行文字を使えないようにする

ファイル名を扱ういくつかのコマンドに対して、次のような文章に該当する実装を行うように奨励されました。つまりファイル名の一部に改行文字が含めることができないようにしようという流れです。ファイル名に含まれる改行文字はファイル名をパイプで別のコマンドに渡したときに問題が発生し、場合によっては脆弱性を含む重大な問題に発展してしまう可能性があります。

Austin Group Defect 251 is applied, encouraging implementations to disallow the creation of filenames containing any bytes that have the encoded value of a <newline> character.

Austin Group Defect 251 が適用され、<newline> 文字のエンコード値を持つバイトを含むファイル名の作成を禁止するような実装を奨励する。

Austin Group Defect 251 is applied, encouraging implementations to report an error if a utility is directed to display a pathname that contains any bytes that have the encoded value of a <newline> character when <newline> is a terminator or separator in the output format being used.

Austin Group Defect 251 が適用され、もし使用されている出力フォーマットで <newline> がターミネーターまたはセパレーターである場合に、<newline> 文字のエンコード値を持つバイトを含むパス名を表示するようにユーティリティが指示された場合、エラーを報告するような実装を奨励する。

「実装を奨励する (encouraging implementations)」と強制力がないような文章になっているのは、POSIX がそのような実装を強く要求しているわけではないという意図の表れで、実装するかどうかは実装者が決めることで、POSIX の立場としては既存の慣行ではない新しい機能に対して提案しかできないためです。

Austin Group Defect 251 とは 0000251: Forbid newline, or even bytes 1 through 31 (inclusive), in filenames のことです。元々の提案は改行文字を含む制御文字まで考慮されていましたが、長い議論を経て特に問題が大きい改行のみの禁止となりました。現実の実装がこれに従うかどうかはまだ未知数ですが、個人的には実現して欲しいですね。誰も改行文字をファイル名に含めるようなことはしませんし問題が起きるとは思えません。どの Unix/Linux も今までやっていなかったというだけです。そして今まで誰もやっていなかったので POSIX で標準化することもできませんでした。

将来のPOSIXで追加されるかもしれない機能

POSIX の改定は POSIX.1-2024 で終わったわけではありません。次の改定である Issue 9 への改定作業はすでに始まっています。おそらく改定が行われるのはこれまでのペースから 10 年後ぐらいになるでしょう。ここでは私が追加される可能性が高いと思った機能をいくつかを紹介しますが、現時点での情報であり公開されるまでに内容が変わったり場合によっては拒否されるかもしれないので注意してください。もっとも POSIX は現在の実装を明文化するものであるため、現在と互換性がない仕様に変わってしまうことはまずありません。

xargs コマンド

並列実行のための-Pオプションの追加

xargs コマンドに -P(並列実行)オプションの追加が提案されました。そしてすでにこの提案は受け入れられています。驚くべきことに、この機能はわずか2カ月(一度受け入れられたのは1ヶ月程度)で標準化されました。もし提案が早ければ POSIX.1-2024 に間に合っていた可能性が高いです。ちょっと残念ですが標準化されていないというだけで使おうと思えば使えるわけで別に支障はありません。

この内容は POSIX.1-2024 の TC1(Technical Corrigendum 1: 技術訂正事項 1)として(おそらく何年後かの)小規模な改定で「将来の方向性」に追記される予定です。

余談: 「POSIXによって能力を制限されている」とかいう意味不明な話

余談ですが、この話で思い出すのは以下の記事です。

コアを多数搭載するCPUは「POSIX」によって能力を制限されているとの指摘

この記事では次のように POSIX がマルチコアCPUの能力を制限する要因となっていると書かれていますが、そんなことはありません。POSIX は拡張機能の実装を禁止していないからです。(補足: 「POSIX がマルチコアCPUの能力を制限する要因となっている」と書いているのは Gigazine であり、元記事にはそのように書いてないようにも読めますが、POSIX.2 を制限と捉えているようなので Gigazine の解釈もあながち間違ったものではないでしょう。)

システム管理者のチャールズ・フィッシャー氏は「POSIXがマルチコアCPUの能力を制限する要因となっている」と指摘し、「xargs」コマンドを例として具体的な説明を行っています。
(中略)
POSIXによって並列処理の制限を受けることは「現代において異質」なことだとフィッシャー氏は指摘。

POSIX は(シェルスクリプトを含む)アプリケーション開発者に対して、どの機能が移植性があるかを明確にしているだけです。POSIX は OS やコマンドの実装者に対して「何かをしてはいけないという制限」ではなく(新しく OS やコマンドを実装する人にとっての)最低条件です。実装者は POSIX を超える拡張機能を自由に実装することができ、移植性が高いと認めれば POSIX はその機能を標準化します。その証拠に -P オプションは多くの環境で実装されたため POSIX で標準化されました。POSIX で標準化していなかったことに対しての文句を言うのであれば、その文句を言う先は POSIX ではなく -P オプションを実装していない (特に商用の)Unix 開発者です。その Unix がオープンソースであれば誰でも -P オプションを実装して持ち込むことができます。そして POSIX 標準規格だって誰でも提案できるわけですから、POSIX に文句を言うぐらいなら自分が標準規格を書いて POSIX に提案しろという話です。残念ながら -P の標準化は 10 年後に持ち越されましたが、この記事の主張は的外れで時代遅れのものとなりました。元記事で言及されている「-0 のない xargs」は POSIX.1-2024 から見るともう過去の話です。

元記事では現在は存在しない「POSIX.2 では多くのツールを 1970 年代のままにしておく必要がある」と書いていますが、そのようなことをしろなんて POSIX は言っておらず、1970 年代のままだとしたら、それは Unix 開発者の怠慢です。そもそも POSIX は 1988 年に策定されたので 1970 年代というのもおかしな話ですが。POSIX は Unix 開発者に実装しろとも実装するなとも命令を出すことはありません。標準規格があったとしても、それに準拠するかどうかは Unix 開発者が決めることです。更に言うならば 「POSIX.2」という名前の標準規格はすでにありません。また「POSIX は多くの分野で絶対的な標準と見なされています。」と書いてありますが、誰も絶対的な標準とはみなしていません。SELinux と systemd が POSIX をわずかに上回ったといいますが、そもそも SELinux や systemd の分野は OS 依存で当たり前の領域であり、アプリケーションを移植する上でなんの関係もないので POSIX の標準化の対象範囲ではありません。POSIX は拡張機能の実装を許可しており、拡張機能が POSIX で標準化されるのは拡張機能が普及してからです。GNU xargs の拡張機能は「POSIX.2 に準拠していない」という表現は適切ではなく、単に「POSIX.2 で標準化されていない(POSIX が自由に実装を許可している)拡張機能」というだけです。そもそも商用 UNIX であっても GNU xargs をインストールして使うことはできるわけで「マルチコア CPU の能力を制限する要因となっている」のは「OS に標準インストールされているコマンドしか使わないぞ」という馬鹿げた考え方です。Gigazine が書いている内容は元記事よりもさらに意味不明なものとなっていますが、元記事の内容も POSIX を理解しているとは言えません。

mktemp コマンド(新規)

一時ファイルやディレクトリを作成する mktemp コマンドの追加

mktemp コマンドは一時ファイルや一時ディレクトリをセキュリティ的に安全な方法で作成するためのコマンドです。特に反対意見があるわけではないのですが、提案された時期が遅く保留状態になっています。mktemp コマンドは多くの環境で実装されており簡単な使い方であれば移植性があるため、私はこの提案は受け入れられるだろうと考えています。

余談ですが、mktemp コマンドは POSIX による標準化の初期に提案されているのですが、標準化されなかったのはシェルに追加された set -C (noclobber) オプションによって置き換えることが可能であるからというのが理由です。しかし今回の提案でも説明されている通り、シェルスクリプトで mktemp コマンド相当のものを正しく作るのは大変です。

回避策: 安全に一時ファイルを作成するシェル関数の実装

もし mktemp コマンドが入っていない環境、もしくは POSIX コマンド以外を使用したくない場合は、その代わりとなる maketemp シェル関数を作成しているのでこちらを使用してください。セキュリティ的に安全な実装になっており、さらに一時 FIFO ファイルの作成にも対応しています。他にも m4 コマンドを使用した一時ファイルの作成方法もあるようですが未検証です。

awk コマンド

FSが空文字の場合に一文字単位で区切る

awk コマンドで FS が空文字の時に一文字単位で区切るという機能の標準化が提案されました。この機能は最新の gawkmawknawkbusybox awkgoawk で実装ずみで、特に議論する内容がなかったのか、提案はすでにあっさりと受け入れられています。

$ echo test | awk -F '' '{print $1 " " $2 " " $3 " " $4 }'
t e s t

printf コマンド

数値を3桁区切りにする書式の追加

printf コマンドに数値をカンマで3桁ごとに区切る機能、より正確にはロケール依存の桁区切り文字(区切り文字はカンマとは限りません)を使って、ロケール依存の桁数ごと(桁数は3桁とは限りません)に区切る機能の標準化が提案されました。

$ printf "%'d\n" 10000
10,000

この提案の議論は進んでいませんが、もともと別件の電話会議で話題になっており、すでに広く実装されているが POSIX.1-2024 に含めるには遅すぎるという理由で採用されなかったものです。話題になったうえでの提案なので、おそらく採用されるのではないかと考えられます。

getconf コマンド

すべての設定値を出力する-aオプションの追加

getconf コマンドにすべての設定値を出力する -a オプションの追加が提案されました。-a オプションはすでに多くの環境で実装されており、すでに提案は受け入れられています。ただし現時点はまだ詳細に関しての議論が続いているため、最終的に受け入れられる可能性が高いですが何かしらの変更が入る可能性があります。

さいごに

今回の改定は(標準規格の期限切れを防ぐための POSIX.1-2017 を除いて)2008 年から 16 年ぶりという、長い期間がかかった改定となりました。あまりにも長かったため、POSIX は時代遅れになった標準規格だとか大幅な改定がない標準規格だと思われていたりもしたようですが、そんな事はありません。今回は少し長く改定されていなかったと言うだけです。電話会議は(特別な場合を除いて)毎週二回行われており、改定作業は今も継続して行われています。そしてシェルにもコマンドにも新しい機能は追加され便利に改良されてつづけています。そうです、POSIX も改定するしシェルスクリプトの世界も変わるのです。シェルスクリプトが変わらないと思っている(言っている)人は、単にシェルスクリプトの書き方が変わっていることを知らなかったり、古い書き方に固執して新しい書き方に変えようとしていないだけなのです。

POSIX 標準規格に関わるメンバー(つまりメーリングリストに登録して発言した人々)は標準規格のバグの指摘や新しく提案された機能に対して、シェルやコマンドに関する動作について調べたり議論を重ねながら標準規格を改定しています。標準規格を読むのは大変ですが、そこには現実のシェルやコマンドの問題が詳細に正確に書かれています。そのようにして策定された POSIX 標準規格を注意深く読んで対応すれば移植性が高いシェルスクリプトを書くことができます。どの環境にでもすぐにと言うわけにはいかないかもしれませんが、新しい機能を使うとこれまでできなかったことや不便だったことが解消されます。古いシェルやコマンドや古い POSIX 標準規格の制限からくる回避策を捨て、知識をアップデートして新しい書き方へと変えていきましょう。やりたいことが簡単にできなければシェルスクリプトで書く意味はありません。

167
157
5

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

Comments

No comments

Let's comment your feelings that are more than good

167
157

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Login to continue?

Login or Sign up with social account

Login or Sign up with your email address