どこでも動くシェルスクリプトを書くための ～ POSIXモードの基礎知識（後編）

## はじめに

一部のシェルには POSIX モードというシェルの動作を POSIX 準拠にして異なるシェル間の互換性問題を軽減する機能が搭載されています。この記事では POSIX モードの各シェルの実装とその方針の違いについて解説します。

この記事は「[どこでも動くシェルスクリプトを書くための ～ POSIXモードの基礎知識（前編）](https://qiita.com/ko1nksm/items/14151d2dc534f558b027)」の後編です。 POSIX モードの解説については前編を参照してください。

## 各シェルのPOSIXモードの方針の違い

各シェルのPOSIXモードの方針の違いの一覧です。

| シェル          | `sh` として採用された実績 | POSIX モードの実装    | 方針 | 開発 |
| --------------- | ------------------------- | --------------------- | ---- | ---- |
| ash 系          | Debian 系 や一部の BSD    | -                     | 基本 | 継続 |
| BusyBox ash     | 組み込み系 Linux          | -                     | 拡張 | 継続 |
| bash            | RedHat 系 や macOS        | あり                  | 拡張 | 継続 |
| ksh             | 商用 UNIX                 | - （ksh93u+m で実装） | 拡張 | 停滞 |
| pdksh           | OpenBSD                   | あり                  | 拡張 | 停滞 |
| mksh            | Android                   | あり                  | 拡張 | 継続 |
| posh            | -                         | -                     | 基本 | 停滞 |
| yash            | -                         | あり                  | 拡張 | 継続 |
| zsh             | -                         | あり                  | 拡張 | 継続 |
| Bourne （参考） | 古い商用 UNIX             | - （POSIX非準拠）     | 基本 | 終了 |

こうしてみると `sh` (`/bin/sh`) にはいろんなシェルが使われているということがわかると思います。前編でも解説しましたが POSIX に準拠しているシェルでも完全に同じ動作をするわけではありません。「どこでも動くシェルスクリプトを書く」ということは、これら多くのシェルに対応させるということであり、これらのシェルで動作確認・テストする必要があることを意味します。

「方針」と「開発」は私の独断と偏見ですが、シェルがどのような方針で開発されており開発状態がどうなのかを表しています。方針の「基本」は POSIX で標準化された規格だけを実装する方針であるか、それ以外の拡張機能を実装していく方針であるかです。例えば ash 系は POSIX の基本機能だけを実装する方針なので開発は「継続」していますが、拡張機能はほとんど追加されません。バグの修正と POSIX の改定に追随するための機能追加ぐらいでしょう。方針が「拡張」で開発「継続」となっているものは拡張機能が多く実装されシェルのバージョンアップで機能が追加されていく傾向にあります。ただし方針が「拡張」でも開発が「停滞」となっているものは、拡張機能は実装されているものの、主に開発体制の理由でこれ以上の機能拡張は行われないでしょう。ただし開発が停滞でも採用している OS ベンダーによるメンテナンスは行われています。Bourne シェルは POSIX に準拠したものではないので参考としての記載ですが方針は「基本」で開発は「終了」[^1]しています。

[^1]: 実は OpenSolaris の Bourne シェルから派生し POSIX に準拠させた Bourne Shell の最新バージョンとも言える [Schily Bourne Shell (bosh)](http://schilytools.sourceforge.net/bosh.html) というシェルがメンテナンスされ続けていました。私はとても好きなシェルなのですが、残念なことに開発者の Jörg Schilling 氏は昨年 2021 年 10 月 10 日に亡くなりました。cdrtools や star （tar 実装の一つ）でも有名な方で、現在 POSIX をメンテナンスしている [Austin Group](https://www.opengroup.org/austin/) でも訃報が伝えられています。

## 各シェルのPOSIXモードの実装の違い

### ash 系

Debian ash (dash)、FreeBSD sh、NetBSD sh、これら ash 系のシェルは 1989 年にリリースされた Almquist Shell をフォークして開発されたシェルです。オリジナルの開発者による初期の ash はリリースされた年からもわかるように POSIX に準拠していない Bourne シェルの再実装ですが、そこから BSD にフォークされ POSIX に準拠していきます。現在使用されている ash 系のシェルは POSIX に準拠した後の ash のフォークです。詳細は「[Ash (Almquist Shell) Variants](https://www.in-ulm.de/~mascheck/various/ash/)」を参照してください。

ほとんどの ash 系のシェルに共通しているのは拡張機能が少ない純粋な POSIX シェルに近い実装という所です。広く普及する前に POSIX シェルとなったため **POSIX モードはありません**。とはいえすべてのシェルが完全に同じ動きをするというわけではなく同じ ash 系であっても細かい違いがあります。

例えば dash ではマルチバイト非対応なのでロケールが UTF-8 であっても文字列の長さはバイトで表されます。Debian / Ubuntu などの dash では POSIX でも規定されている `LINEO` 変数が使用できません（ビルドオプションによるものだと思います）。FreeBSD sh では変数の代入規則が他のシェルと異なります。また `if` の `then` の中身が空でも動きますが、これは POSIX で規定されている動作ではありません（おそらく元々はバグだったが互換性維持に比べて修正する価値が小さいため意図的に放置されていると考えられる）。

```sh
str="あいう"
echo ${#str} # => dash では 9 を返す

# FreeBSD sh では以下の書き方で変数の値を入れ替えられるが
# 他のシェルでは入れ替えられない（未定義の動作）
a=1 b=2
a=$b b=$a
echo "$a $b" # => 2 1

if [ "$value" -eq 0 ]; then
  # FreeBSD sh は then の中身が空（またはコメントのみ）でも動いてしまうが POSIX で
  # 規定された動作ではない（殆どの POSIX シェルや Bourne シェルではエラーになる）
fi
```

拡張機能も少ないながら実装されています。例えば `local` コマンドは POSIX で標準化されていない拡張機能です（正確には `local` コマンドが実装されたのは 1989 年のオリジナルの ash からなので POSIX を拡張したものではありません）。他にも FreeBSD sh でしか実装されていない `builtin` コマンドや NetBSD では `$NETBSD_SHELL` でバージョン番号が取得できたり `read -p` や `echo -e` など POSIX で標準化されてないコマンドやオプションはいくつか実装されていますが、それらの対応状況は同じ ash 系でもバラバラです。

ここから言えることは ash 系で動くからと言って、他のシェルでも同じように動くだろうと決めつけてはいけないということです。少なくとも他のシェルと動作が違う場合は POSIX ではどのように規定されているかを確認しなければいけません。そのための POSIX です。

### BusyBox ash

Busybox ash はその名の通り ash をベースとしているシェルですが、追加のパラメータ展開やプロセス置換など bash の拡張機能の一部を実装しており、純粋な POSIX シェルを目指している他の ash 系のシェルとは方針が異なっています。ただし 他の ash 系と同じく **POSIX モードは実装されていません。**

bash の拡張機能といっても、殆どは ksh や zsh と同様の拡張機能ですが、本当の bash の拡張機能も実装しており、`BASH_XTRACEFD` 変数のように `BASH_` プリフィックスがついた変数や bash 5.0 (2019) でしか実装されていない`EPOCHSECONDS` 変数や `EPOCHREALTIME` 変数などにも対応しています。[ソースコード](https://git.busybox.net/busybox/tree/shell/ash.c)を見れば bash を意識しているのが明らかにわかります。これは POSIX シェルへの対応だけでは、現実のシェルスクリプトを実行するのに力不足で、デファクトスタンダードである bash の拡張機能に対応することでシェルスクリプトの移植性がより高まるという考えからだと思われます。

余談ですが BusyBox の代替を目指す [ToyBox](https://github.com/landley/toybox) というプロジェクトがあり、そちらも同様に bash 互換のシェルを開発する方針です（[詳細](http://landley.net/toybox/roadmap.html)）。現時点ではまだシェルは完成していませんが bash とのある程度の互換性が保たれることになるでしょう。この流れを私は好ましいものと考えており、UNIX の影響力が小さくなった今、BusyBox や ToyBox でサポートされていることが POSIX で標準化されるための新しい基準になっていくのではないかと考えています。

### bash

bash は GNU Project によって作成されたシェルです。Linux ではユーザーランドのコマンドセットとして GNU Project のソフトウェアの多くを採用しているため、Linux を中心にデファクトスタンダードとなった影響力が大きいシェルです。

bash はデフォルトでも POSIX シェルと高い互換性を持っています。しかし bash は POSIX の標準化よりも前に開発が始まったシェルであるため、後から作られた POSIX の規定とは異なる動作を行う部分があります。他の多くのソフトウェアと同様に互換性を重視しているため、POSIX で標準化されたからと言って気軽に POSIX に合わせて動作を変更するようなことはしません。特に影響力が大きい bash がデフォルトの動作を POSIX 準拠に変更してしまったら多くのシェルスクリプトで互換性問題が発生したでしょう。ゆえにデフォルトの動作を維持するために **POSIX モードが実装されています。**POSIX モードにするには `sh` コマンドで実行する他、`set -o posix` または環境変数 `POSIXLY_CORRECT` を使います。
 
bash は Linux だけではなく macOS のシステムシェルとしても使われています。 しばしば CentOS や macOS の `/bin/sh` は bash だと言われたりしますが、厳密には POSIX モードの bash であり、通常の `/bin/bash` で起動した場合と細かい動作が異なるので注意が必要してください。macOS では [macOS 10.15 catalina から bash から zsh に変更になった](https://support.apple.com/ja-jp/HT208050)と言われますが、これはユーザーが使用するログインシェルの話であり `/bin/sh` は今まで通り bash 3.2.57 が使用されています。現時点での bash の最新版は 5.1 で macOS の bash はかなり古いものとなりますが、POSIX で規定された範囲であればほぼ問題なく使えます。ただし Apple によるパッチが適用されており、POSIX モードを有効にしている場合は `echo -n` で `-n` という文字列が出力されるなど bash 本来の動作と異なっています。この動作は POSIX の追加オプション仕様である XSI に準拠した動作です。

bash の POSIX モードの詳細については [Bash POSIX Mode](https://www.gnu.org/software/bash/manual/html_node/Bash-POSIX-Mode.html) を参照すると良いでしょう。

### ksh

ksh には ksh88 と ksh93 の大きく二種類があります。名前からも分かる通り ksh88 はその名の通り、かなり古いシェルですが Solaris 10（2005年1月リリース～2021年1月サポート終了）に含まれており、比較的最近まで使っていた所もあるかもしれません。POSIX シェルは ksh88 をベースにそのサブセットとして策定されましたが、ksh88 の仕様を完全にそのまま採用したわけではなく ksh88 は POSIX に準拠していないとされています（それでも Bourne シェルに比べれば POSIX シェルとしての機能を十分備えています）。一方 ksh93 は POSIX に準拠するシェルとして大幅に書き直されたシェルです。

ksh88 が POSIX に準拠してない動作の例を上げると算術式の 8 進数定数に対応していません。これは ksh93 では POSIX 準拠の動作に変更されました。このように ksh88 と ksh93 は完全な互換性が保たれていません。（[ksh88 と ksh93 で互換性がない点](https://github.com/att/ast/blob/master/src/cmd/ksh93/COMPATIBILITY)）

```sh
echo $((010)) # => 10 （POSIX 準拠では 8 になるべきで ksh93 ではそうなる）
echo $((0x10)) # => 16 （ksh88 でも 16 進数定数には対応している）
```

**AT&T 版の公式の ksh には POSIX モードはありません**が、これは ksh88 は POSIX 登場以前のシェルであるため POSIX モードを搭載できるはずもなく、また ksh93 は 最初から POSIX 準拠のシェルとして開発されたものであるため、あえて POSIX モードを搭載する必要がなかったということなのでしょう。しかしながら ksh93 であっても完全に POSIX に準拠しているというわけではありません。例えば `"$((x))"` と `"$(($x))"` は同じ値になることが [POSIX で規定](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_04)されていますが、ksh93 では以下のように異なる値となります。

```console
$ ksh -c 'x=010; echo $((x)) $(($x))'
10 8
```

ksh93 は 2012 年に ksh93u+ がリリースされましたが、オリジナルの開発者である David Korn は同年に AT&T を去りました。その後も David Korn の手によって 2014 年まで ksh93v-（ベータ版）の開発が続けられましたがそれは不安定なバージョンでした。その後、コミュニティの手によって開発が続けられ 2019 年に ksh2020 として次バージョンがリリースされます。しかし大幅なリファクタリングによって互換性問題が発生し AT&T は ksh2020 のリリースをロールバックしました。現時点で macOS の Homebrew でインストールされるのは ksh2020 ですが、実はこれは破棄されたバージョンで ksh93u+ との互換性が一部失われているバージョンです。

そして（ksh2020ではなく）ksh93u+ から再スタートして Martijn Dekker 主導で開発が進められているのが [ksh93u+m](https://github.com/ksh93/ksh) です。正式版はまだリリースされていませんが現在 [ksh 93u + m 1.0.0-beta.2](https://github.com/ksh93/ksh/releases/tag/v1.0.0-beta.2) がリリースされています。私はこれが ksh93u+ の後継バージョンになると考えています。そしてこの **ksh93u+m には POSIX モードが搭載されています**（ちなみに ksh2020 には POSIX モードは搭載されていないようです）。POSIX モードによって何が変わるかは[こちら](https://github.com/ksh93/ksh/blob/dev/NEWS) を参照すると良いでしょう。POSIX モードを使用するには `sh` で起動するか `set -o posix` を使用します。補足ですが[ここ](https://github.com/ksh93/ksh/issues/20)を見ると libast (kshを含む AT&T のツールキット）は環境変数 `POSIXLY_CORRECT` をすでにサポートしていると書かれており ksh93u+ ではおそらく何も変わらないと思うのですが ksh93u+m ではこの環境変数を参照して POSIX モードを有効にする可能性があります。

まとめると AT&T公式の ksh93u+ は POSIX にほぼ準拠しており拡張機能も多く搭載されているシェルです。しかしながら完全に POSIX に準拠しているわけではなく ksh93u+m の POSIX モードにを使うことでより POSIX 準拠度が高まります。余談ですが ksh93u+m の修正履歴を見ると ksh は UNIX で使われている割に（見つかりづらい）バグが多いなと感じます。これらの修正の多くは元は ksh を採用した OS ベンダーによる修正（それを ksh93u+m に適用している）で、それぞれ OS ベンダーでの異なる挙動（一部の実装にはバグが残っている）につながっているはずで、動作検証の大変さを物語っています。

### pdksh

pdksh は ksh のソースコードが非公開だった時に開発された、パブリックドメインバージョンの ksh です。開発は 1987 年頃から 1999 年まででオリジナルの pdksh はメンテナンスされていません。Linux ではパッケージを提供しているところはなさそうですが OpenBSD では `/bin/sh` として採用されており（メンテナスは OpenBSD が行っています）、FreeBSD では [パッケージ](https://www.freshports.org/shells/pdksh) が提供されています。

pdksh は Bourne シェルのパブリックドメイン版をベースに開発が始まり、その名の通り ksh の代替として使えるように ksh （主に ksh88）の機能の一部を実装しています。しかしここで発生するのが互換性の問題です。当初は ksh88 のクローンでしたが POSIX が標準化され ksh93 が登場しました。pdksh も POSIX に準拠していくことになりますが ksh88 と ksh93 のように異なるシェルを作るのではなく **POSIX モードを実装しました。**また pdksh には 厳密な Bourne シェルモード (Strict Bourne shell mode) も実装されています。（ただし FreeBSD で提供されている pdksh には厳密な Bourne シェルモードが無いようです。）

pdksh は `sh` で起動すると（POSIX モードではなく）厳密な Bourne シェルモード（`set -o sh` 相当）で起動するようです。`set -o posix` または環境変数 `POSIXLY_CORRECT` によって POSIX モードがが有効になります。POSIX モードと厳密な Bourne シェルモードの両方を有効にすることもできます。

オリジナルの pdksh はメンテナスされておらずあえて使う意味もないでしょうから、どこでも動くシェルスクリプトとして気にすべきなのは OpenBSD sh ぐらいだと思います。OpenBSD では独自の修正を行っています。例えば `sh` で起動した場合は環境変数 `$SH_VERSION` からバージョン番号を取得します（`KSH_VERSION` ではありません）。POSIX 準拠度も高くなっており、オリジナルの pdksh では`set -u` で空の `"$@"` を参照するとエラーになるという POSIX 非準拠の動作を行うのですが OpenBSD では修正されています。また ksh88 と同様に pdksh も 8 進数定数に対応していません（POSIX モードを有効にしたとしても）。FreeBSD の pdksh のパッケージも同じです。しかし OpenBSD sh では POSIX モードとは無関係に 8 進数定数に対応しています。pdksh の POSIX モードについては [OpenBSD の man ページ](http://man.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man1/ksh.1)を参照してください。

### mksh

mksh は pdksh から派生したシェルで pdksh の後継シェルを名乗っています。開発が終わった pdksh とは違い今も積極的に開発（新規機能追加）が行われていおり POSIX 準拠度も pdksh よりも高くなっているようです。pdksh の後継であるため **POSIX モードが実装されています**。また厳密な Bourne シェルモードも実装されていますが、これらは pdksh のそれとは微妙に仕様が異なるようです。

pdksh は POSIX に準拠しておらず 8 進数定数に対応していませんでした。OpenBSD sh では POSIX モードとは無関係に 8 進数定数に対応しています。一方 mksh ではデフォルトでは 8 進数定数に対応していません。これは ksh88 / pdksh と互換性を保つためでしょう。そして POSIX モードにすると 8 進数定数に対応します。pdksh とも OpenBSD sh とも異なる挙動です。

mksh の POSIX モードの詳細については [mksh の man ページ](http://www.mirbsd.org/htman/i386/man1/mksh.htm)を参照してください。

### posh

これも pdksh の派生の一つですが、ベースとして使われただけで POSIX で標準化されてない機能を削除しており pdksh との互換性はありません。POSIX シェルの規格 (Issue 6) で実装が必須となっていない機能を取り除いており、例えば `alias` コマンドは削除されています。しかしこれは Issue 7 で必須となりました。posh は 0.14.1 の時点で再実装されておらず、最新の POSIX シェルの規格に追随できていません。

**POSIX モードも削除されています**が pdksh とは違い算術式の 8 進数定数には対応しておりその点は POSIX に準拠したようです。ただし `[ 010 -eq 8 ]` の場合まで 8 進数定数とみなしてしまうというバグを入れてしまっています（posh 以外のシェルは 10 進数として扱われ実行結果は偽です）。他にも POSIX に準拠してない[バグが多く](https://bugs.debian.org/cgi-bin/pkgreport.cgi?pkg=posh;dist=unstable)、開発はほぼ停滞状態なので、正直言って使用するのはおすすめしません。

ちなみに macOS でも Homebrew で posh が提供されていますが、こちらはビルドが完全に壊れています。例えば `cd` コマンドすら正しく機能しません。（もともと posh は macOS に対応してないと私は考えています。）

まあ一言でいえば（現状のままであれば）このシェルは切り捨てで良いです。

### yash

yash はシステムシェル（`/bin/sh`）として採用された実績はおそらくないため、どこでも動くシェルスクリプトを書く上で考慮する必要性は小さいと思いますが、POSIX 準拠度は高く `sh` としても使用できるように作られています。

yash はデフォルト状態で POSIX にほぼ準拠しています。**POSIX モードも実装されていますが、他のシェルとは異なり POSIX で標準化されてない拡張機能を無効にするという動作を行います。**これは yash が POSIX 標準化された 1992 年よりもずっと後に開発された新しいシェルだからだと思われます（2008 年に初版リリース）。最初から POSIX に準拠して開発することが可能だったため、他のシェルのように POSIX 標準化以前との後方互換性を保つために使う必要はなく、POSIX モードを純粋な POSIX シェルに準拠するために使用したのでしょう。

POSIX モードにするには `set -o posixly-correct` を使います。他のシェルとオプション名が異なりますが、英数字以外の文字は無視され名前を短く省略することもできるので、他に `posix` ～で始まるオプションが追加されない限り `set -o posix` でも動作します。そのため私は `posix` が正しい名前だとしばらく勘違いしていましたが厳密には省略せずに指定したほうが良いと思います。

### zsh

zsh もシステムシェル（`/bin/sh`）として採用された実績はおそらくありません。macOS 10.15 Catalina で bash に代わって zsh がデフォルトシェルになったと言われていますが、それはユーザーが使用するシェルの話で `/bin/sh` のことではありません。

zsh は元々 Bourne シェルと csh を合わせた高機能で優れたシェルを開発するのが目的であり、デフォルトでは POSIX シェルとある程度の互換性はありますが完全に準拠しようとはしていません。しかしながら設定によって POSIX 準拠度を高くすることは可能です。（ただ zsh は意図的に優れていると判断して POSIX に準拠していないわけで、それを POSIX に準拠するというのは zsh が考えるメリットを捨ててしまっていることになります。）

zsh の **POSIX モードは他のシェルと違い `set -o posix` ではなく `emulate` コマンドを使用して指定したシェルの動作をエミュレートするという考え方です。**エミュレートできるシェルにはデフォルトの zsh の他に sh、ksh、csh が指定できます（ただし csh のエミュレートに関しては完全なものにはならないと明言されています）。デフォルトの zsh は POSIX に準拠していません。sh または ksh を指定することで POSIX に準拠します。sh と ksh の違いは ksh に関する拡張機能に対する動作の違いです（sh でも拡張機能は無効になりません）。

zsh が ksh をエミュレートすることから想像できるかもしれませんが、zsh も ksh88 との互換性維持の理由でデフォルトでは算術式で 8 進数定数に対応していません。これは POSIX モード（sh または ksh）を有効にすることで対応します。

`emulate` コマンドは実際には `setopt` コマンドで細かく設定できるシェルオプションをまとめて設定するためコマンドです。`emulate -R sh` または `emulate -R ksh` を指定すると POSIX モードになりますが、一つだけ例外があり関数内の `$0` の扱いが POSIX に準拠していません。`setopt POSIX_ARGZERO` で個別にシェルオプションを設定するか、`emulate sh -o POSIX_ARGZERO` （または `emulate ksh -o POSIX_ARGZERO`）で完全な POSIX モードにすることができます。

## 後編のまとめ

この記事では各シェルの POSIX モードの違いについて解説しました。POSIX モードを使用すれば各シェルの互換性の違いをある程度軽減することができます。しかしそれでもなお各シェルには動作が違う部分が残っています。POSIX 準拠度が高い ash 系の間でも細かい違いがありますし、POSIX シェルの元となった ksh にも POSIX に準拠してない動作があります。結局の所 POSIX モードを使ってシェルを POSIX に準拠させたとしても、シェル間の互換性問題を完全に解決することはできません。

POSIX の標準規格には、実際のシェル固有の仕様については書かれていません。書いてあるのはどのシェルでも同じ動作をするものと、同じ動作をしない部分に気をつけろということだけです。dash、bash、ksh といった実際のシェルの仕様は書かれていません。POSIX モードの存在すら書かれていません。ここからわかるのは POSIX だけを参照してシェルスクリプトを書いても、どこでも動くシェルスクリプトにはならないということです。

このような状況でどこでも動くシェルスクリプトを書きたいのであれば、実際のシェルを知ってそれを動かす環境でテストしなければいけません。POSIX に準拠していればどこでも動くようになるという考えは幻想であり机上の空論でしかありません。