LoginSignup
21
18

どこでも動くシェルスクリプトを書くための ~ POSIXモードの基礎知識(前編)

Last updated at Posted at 2022-01-24

はじめに

みなさん知っての通りシェルスクリプトは環境依存が激しく、どこでも動くシェルスクリプトを書くのはとても大変な作業です。一番の理由は標準的なコマンド(POSIX コマンド含む)に完全な互換性がなく、どこでも共通で使える機能が少ないからですが、もう一つの理由は全く同じ動作をするシェルが一つもないからです。しかしながら一部のシェルで実装されている POSIX モードを使うことである程度この問題を軽減することができます。この記事ではどこでも動くシェルスクリプトを書くのに重要な POSIX モードと各シェルの POSIX モードの対応状況に焦点をあてて詳しく解説したいと思います。

記事が長くなったので前編と後編に分けました。後編(各シェルの POSIX モードの違い)は「どこでも動くシェルスクリプトを書くための ~ POSIXモードの基礎知識(後編)」です。

シェルスクリプトはどこでも動くのではないのか?

「シェルは Linux や UNIX 系 OS に必ずインストールされている。だからシェルスクリプトはどこでも動くに決まってるだろ。」そう主張する人がいるかもしれません。

たしかにシェルは多くの環境にインストールされています。Windows でさえ WSL を "インストールすれば" シェルスクリプトは動きます。しかし「どこでも動く」とはどういうことでしょうか?例えば UNIX で使っていたシェルスクリプトを Linux にそのまま持ってきてエラーが出たり間違った処理結果(バグ)になったのに、これを動いたという人はいないでしょう。シェル自体はインストールされていたとしても、それぞれは異なる開発者が作った別のシェルです。それらに完全な互換性はないので期待したとおりに動くとは限りません。Python2 を Python3 にアップデートしたり Oracle JDK を OpenJDK に変更して動きますか?というのと同じ話です。

一般的には**「どこでも動く」とは「どこでも【期待したとおりに】動く」という意味**です。残念ながらシェルスクリプトは移植性を意識せずに書いてしまったら、どこでも動くようにはなりません。どこでも動くシェルスクリプトを書くにはそれなりの知識と多くのバッドノウハウ(あのコマンドはやオプションはこっちで使えないとか、使えるけど微妙に仕様や出力が違うなどのこと)が必要です。この記事の POSIX モードはその知識の一つです。

ちなみに言語や実行環境を "インストールしたり" 自分で移植性の問題を解決することで「どこでも動く」と言うのであれば、ほぼすべての言語がどこでも動きます。シェルスクリプトだけの特徴ではありません。シェルスクリプトの特徴は「Unix / Linux 系 OS の最小構成でも使える言語」です。

どこでも動くの「どこ」を定義しよう

本題の前にどこでも動くとは具体的に「どこ」で動くことなのかを定義しなければなりません。さすがにすでにサポートが終了している Windows XP や 古い UNUX で動かす需要があるとは思えません。これらを含めて本当にどこでも動くことを目指すとしたらそれは途方も無い努力が必要になり費用対効果で割に合いません。対応する必要がない環境まで「もしかしたら動かす事があるかもしれない」という理由であらゆる環境に対応しようとは考えないでください。そんなことをしてもYAGNI 原則の通り無駄になります。どの環境まで対応するかを検討するのは重要なことです。

「どこ」で動かしたいかは人それぞれ要件によって異なります。これは私ではなくこの記事の読者が決めることですがある程度の候補は挙げられます。

  1. POSIX シェルと Bourne シェル(古い商用 UNIX)の両方で動く
  2. POSIX 準拠を名乗る全てのシェルで動く(yash や zsh 含む)
  3. 組み込み環境も含めて /bin/sh として使われたことがあるシェルで動く
  4. デスクトップ・サーバー系 OS で使われたことがあるシェルで動く
  5. Linux 系 OS で標準的に使われたシェルで動く(dash や bash 等のみ)
  6. bash だけで動く

上の方ほど動く環境が多くなりますが対応するのも大変です。まず 1 はお勧めしません。1 を実現したい場合、Bourne シェルと互換性がある機能だけを使って書かなければいけなくなります。Bourne シェルは POSIX に準拠してない(具体的な違いは「BourneシェルとBourneシェル系(bash等のPOSIX shell)の違いについて」を参照)ので、POSIX に準拠してシェルスクリプトを書いたとしても Bourne シェルとの互換性は保証できません。私は Bourne シェルはもはや対応は不要だと思っています。なぜなら Bourne シェルが使われていた商用 UNIX は、今は基本的に UNIX の認証を受けており POSIX に準拠していることが必須要件になっているからです。すなわち商用 UNIX は POSIX シェル(主に ksh93)を搭載しています。また Bourne シェルが /bin/sh として使われていた UNIX はすでにサポートが終了しているはずです。有名なシェルスクリプトの本1では Bourne シェルを中心として解説されていたりしますがこれらは 20 年ぐらい前に出版された古い本ばかりです。さすがに 20 年も経てば状況は変わります。今更 Bourne シェルの範囲でシェルスクリプトを書く意味はないでしょう。

6 の bash だけの対応では、どこでも動くことにならないと考えるかもしれませんが、bash 自体がどこでも動くシェル(各 OS に移植されている)であるため、単にインストールするだけです。任意のパッケージを入れて良いという条件の元であれば bash だけの対応でもどこでも動くと言うことができます。例えばシェルスクリプトからインターネットにアクセスするために curl か wget をインストールする必要があるならば bash をインストールすることも可能でしょう。「どこでも動く」シェルスクリプトに求められていることは「どの環境 (OS) でも動く」ということであって「どのシェルでも動く」とは限らないはずです。bash で動けば十分なのに他のシェルに対応させるのは無駄な努力でしかありません。要件を見誤ると無駄にコストをかけてしまいます。そういったことを考えてもらうために bash だけの対応でもあえて「どこでも動く」候補にしています。

5 の Linux 系 OS で使われているシェルで動くを挙げているのも同じ理由です。UNIX 全盛期は UNIX もシェルもソースコードが公開されておらず2、特定の会社のコンピュータと組み合わされて提供されていたため、将来それらのコンピュータが使えなくなるかもしれないという不安がつきまといましたが、オープンソース全盛期の今は状況が全く違います。Linux はユーザーが多く、対応している環境もさまざまで、特定の団体によって独占されているものではないので将来的にも安心して使うことができます。BSD 系や商用 UNIX はクラウドでサービスのサポートが良くないので、どこでも(クラウドでも)動くとは言い難い状況ですが Linux であれば「どこでも動く」と言えるでしょう。

この記事では 2 の POSIX シェル全てで動かしたい場合にも対応できるように yash や zsh にも言及しています。私が知る限り yash や zsh が OS のシステムシェル(/bin/sh)として採用されたことはないと思いますが、将来そのような OS が登場する可能性が無いとは言い切れません(しばしば勘違いされていますが macOS の /bin/sh は今でも bash であり zsh ではありません)。現実には 3 か 4 まで対応すれば十分どこでも動くと言ってよいでしょう。そして要件によっては 5 や 6 でも十分です。

ちなみに私は(自分のプロジェクトとして開発しているものに関しては)「どこでも動くシェルスクリプト」の基準に 2 を採用しています。たとえ POSIX に完全に準拠することを目標としていなかったとしても組み込み環境で動かないのに、どこでも動くとは(個人的には)言えませんし、yash や zsh など POSIX シェルを名乗るシェルであれば可能な限り対応したいと思っています。しかしこれを真似する必要はありません(すごく大変なので…)。Bourne シェルは POSIX に準拠してないだけではなく OS ベンダーによって微妙に動作が異なり、包括的で継続したテストを行うには高価な UNIX を搭載したコンピュータをかき集めなければならず事実上動作保証が不可能です。Solaris 10(サポート終了)であれば無料で使えますが、それだけで動くことを確認しても価値が小さく、もはや使われてないはずの Bourne シェルを「どこでも動くシェルスクリプト」の環境に加える必要はないと考えています。

POSIXモードとは?

シェルの動作を POSIX 準拠に高めるために一部のシェルに搭載されている機能です。POSIX モードを使うと他のシェルとの互換性が高くなりどこでも動くシェルスクリプトを書くときの助けになります。

POSIX モードでシェルスクリプトを動かす方法としては、シバンに #!/bin/sh (まれに #!/usr/bin/env sh)を使って sh でシェルスクリプト実行する方法がよく使われます。なぜこの方法で POSIX モードになるのかは /bin/sh とはなにか?を考えてみるとわかります。/bin/sh の実体は OS によって異なります。CentOS では bash ですが、Debian/Ubuntu では dash です。FreeBSD では FreeBSD sh という ash 系のシェルですし、OpenBSD では pdksh を OpenBSD がメンテナンスして使っています。UNIX では ksh が使われているでしょう。組み込みでは BusyBox ash が使われていたりします。(古い商用 UNIX の場合は Bourne シェルの場合もありますが)

このように /bin/sh の実体は OS によって異なるため、(いろんな環境で動かす汎用の)シェルスクリプトは多くのシェルに対応させなければいけません。POSIX コマンドにも当てはまりますが、同じコマンド(シェル)の実装が多ければ多いほど互換性問題も多くなります。そのため多くのシェルは sh というコマンド名で起動すると POSIX モードを有効にして可能な限り互換性が高くなるように作られているわけです。

POSIX モードに切り替える他の方法としては set -o posix もよく使われます。シェルを sh 以外の名前で起動した場合(#!/bin/bash 等)に POSIX モードにするにはこの方法を使います。また一部のシェルでは環境変数 POSIXLY_CORRECT が設定されている場合にも POSIX モードが有効になります。POSIXLY_CORRECT はおそらく bash (GNU) で初めて導入されたものだと思いますが、対応しているシェルは他にもあります。なお、これらの POSIX モードに変更する方法は、POSIX で標準化されているものではありません。POSIX モードに変更する方法自体がシェル依存です。

POSIX モードには大きく分けて次の 2 つのタイプがあります。

  1. POSIX と矛盾する動作だけを POSIX 準拠に変更し、拡張機能はそのまま使える
  2. POSIX で規定された機能のみを使えるようにし、拡張機能を無効にする

ほとんどのシェルでは 1 を採用しています。2 を採用しているのは yash だけです。少なくない人が勘違いしている気がするのですが、**bash を POSIX モードにしたとしてもほとんどの拡張機能は無効になりません。**具体的に言うと bash の配列などの機能は POSIX モードでも使用可能です。そのため bash 用シェルスクリプトの場合でも常に POSIX モードにして良いのではないかと私は考えています。一部無効になる拡張機能はあるのですが POSIX シェルの仕様と bash の仕様の細かい違いでハマる可能性が低くなります。

なぜシェルはデフォルトでPOSIXモードにしないのか?

答えを先に言うと後方互換性のためです。メジャーなシェルは POSIX で標準化されるよりも前に作られており、後から仕様が策定された POSIX シェルは当時のシェルと完全な互換性がありません。POSIX で標準化されたからといって POSIX 準拠にシェルの動作を変更したら今までのシェルスクリプトが動かなくなる可能性があります。だから後方互換性を重視している OS ベンダーやシェル開発者はデフォルトで POSIX モードに変更するようなことはしませんでした。

UNIX の歴史の初期の頃である 1979 年に Version 7 Unix と共に Bourne シェルがリリースされました。初期の Bourne シェルにはシェル関数に対応しておらず getopts もなく "$@" の仕様も今と異なっていました。Bourne シェルの最終バージョンである SVR4.2 がリリースされたのは 1992 年です(ただしこのバージョンは広く使われておらず一つ前のバージョンは 1989 年です)。POSIX でシェルが標準化されたのも 1992 年なので Bourne シェルが POSIX に準拠できるわけがありません。さらに Bourne シェルは複数のバージョンがあるだけではなく OS ベンダーによって多数の細かい差異があります。(詳細は「The Traditional Bourne Shell Family」参照)

Bourne シェルを開発した AT&T では並行して KornShell (ksh) の開発が 1983 年頃から始まり 1988 年頃に ksh88 がリリースされました。重要な事として当時の ksh はクローズドソースでした。そのため時を同じくして 1987 年に pdksh が 1989 年に bash と ash が、1990 年に zsh と多くの Bourne シェル・KornShell のクローンがオープンソース(またはパブリックドメイン)として開発されました。この開発された年が重要です。POSIX シェルという標準規格が登場するのは 1992 年なので、これらのシェルは(初期は)POSIX シェルと互換性のない Bourne シェルとの互換性を有していたことになります。またこれらのシェルはそれぞれ他のシェルの機能を取り込みながら開発が進みますが、統一仕様がない中でそのような開発が行われたわけで、それぞれのシェルの互換性が低くなるのも仕方のない話です。そしてその問題を解決するために POSIX が生まれたわけです。

POSIX は既存のシェルを可能な限り変更することなく、そのままの形で POSIX 準拠となるような内容で標準規格を策定しました(意味わかります?POSIX は既存のシェルをリスペクトして「その仕様最高!それにしよう採用!」と既存のシェルの動作を標準規格にしただけなんです)が、シェル間の互換性が低下している状況で既存のすべてのシェルと矛盾しない形で標準化することは不可能でした。そのため POSIX シェルと当時のシェルに完全な互換性はありませんでした。POSIX でシェルが標準化された後、各シェルは POSIX 準拠を目指します。しかし今までの動作を POSIX 準拠の動作に変更すると過去のシェルスクリプトが動かなくなってしまいます。後方互換性は何より重要なことです。しばしば勘違いされがちですが POSIX は異なる実装との「移植性」のための規格であって「(後方)互換性」のための規格ではありません。POSIX は POSIX で規定された範囲では互換性を保っているでしょうが、POSIX 登場以前から開発しているシェルは POSIX に準拠することで後方互換性が失われてしまいます。したがって OS ベンダーやシェル開発者の多くは後方互換性を保つためにデフォルトでは POSIX に準拠しませんでした。デフォルトで POSIX モードになっていないのは後方互換性を重視しているからなのです。

つまり、私が言いたい事は OS をアップデートしても今までのソフトウェアが動くのは(POSIX のおかげではなく)OS ベンダーやシェル実装者が後方互換性を保とうと努力した結果であり、デフォルトが POSIX 準拠でない(POSIX モードでない)のは後方互換性を保つのに必要なことで、ユーザーの利益のために意図的にやっていることであり、それを理解せずに POSIX に準拠していないことを非難するなということです。POSIX は考え抜かれた優れた仕様などではなく、移植性があるソフトウェアを開発した人のためのただのガイドラインです。

POSIXモードでもシェル間で完全な互換性はない

POSIX シェルの言語仕様から unspecified, undefined, implementation-defined といった用語を検索してみてください。これらは「POSIX で仕様が決まってないもの」・・・ではなく「POSIX で仕様が指定されていない・未定義・実装定義と決まっているもの」です。(これらの用語の正確な定義は「1.5 Terminology」を参照してください。)

しばしば POSIX 標準規格のことを「Unix 系 OS が最低限実装すべき仕様」と勘違いされていたりしますが、実際には POSIX がこれらを実装すべき最低限の仕様だと言ったことはなく(POSIX 準拠が要件の一つである UNIX 認証を与える商売を Open Group がしてるだけ)、POSIX 標準規格というのは単に既存(当時)の Unix 系 OS で共通で使える物だけではなく、共通で使えない物の注意点をドキュメント化したものにすぎません。

また POSIX が実装依存の動作に対して仕様を定めて動作を統一したり、これまでにない新しい仕様を策定することは基本的にありません。例えばパイプでつながったそれぞれのコマンドがサブシェルで動くかどうかはシェルによって異なりますが POSIX は「シェル依存である」と明記するだけで終わりです。シェル依存や実装の選択肢が複数あるものは意図的にそのまま残されており、定義されていない部分はどう実装しても POSIX 準拠と言えますし言えるようにしています。

POSIX はシェルによって異なる動作を「統一する」のが仕事ではなく「統一してない」と明確にするのが仕事です。ソフトウェアの開発者は POSIX を参照してある機能がシェルによって異なることを知り、移植性に注意してシェルスクリプトを書くというのが正しい POSIX の使い方です。だから POSIX を参照して「シェル依存である」と書いているのを見つけて「なんで POSIX はこんなことすら決めてないのか!?」と非難するのは的外れなのです。POSIX が決めてないのではなくただ現実のシェルの動作を書いただけなのですから。これは「シェル依存であると教えてくれてありがとう」と言うべき所です。POSIX を参照することで移植性があるかどうかを知ることができるのでソフトウェアに移植性を持たせる時に役に立つわけです。

POSIX とは元々このようなものであるため、POSIX だけを参照してどこでも動くソフトウェアを書くことはできません。実際のシェル(bash 等)のドキュメントを読んで POSIX には書かれていない実際の仕様を確認する必要があります。他の言語では一般的に移植性の問題を言語やライブラリが解決してくれますが、残念ながらシェルスクリプトではそれは期待できません。ようするに**シェルスクリプトをどこでも動くようにする(移植性を持たせる)のは私達がやらなければいけない仕事なのです。**POSIX を読んで移植性の問題を知り、その問題を自力で解決していくことが「どこでも動くシェルスクリプトを書く」ということの本質です。

一つ具体例をあげます。以下のコードは POSIX で標準化されている機能だけを使っていますが、ksh では動いて dash では期待したとおりには動かないコードです。(UNIX から Linux へのマイグレーションを想定しています。)

count_lines.sh
#!/bin/sh
cnt=0
cat file.txt | while IFS= read -r line; do
  cnt=((cnt + 1))
done
echo "$cnt"

POSIX を参照するとパイプラインの各コマンドがサブシェルで動くかどうかは「シェル依存という規定」=注意書きを見つけることができます。シェル依存だと教えてくれてありがとう。POSIX をちゃんと読むことで移植性の問題を知り(自力で)解決することができます。どこでも動くシェルスクリプトを書くには POSIX シェルの標準規格だけではなく、いろんなシェルのことを知ってる「シェル博士」にならなければいけません。どこでも動くシェルスクリプトを書くことがどれだけ大変であるかが分かっていただけたでしょうか?

POSIXモードなのに拡張機能が使えるなんておかしくね?

POSIX モードが有効なのに拡張機能が使えるなら POSIX モードの意味がないんじゃね?と思う人がいるかもしれないので、この誤解を正しておこうと思います。それは配列などの POSIX で標準化されていない拡張機能が使えたとしても POSIX 違反にはならないということです。

POSIX 違反というのは POSIX で標準化されている内容と矛盾する動作のことを指します。例えば POSIX ではコマンドを実行する時にそれが見付からなかった場合、終了ステータスは 127 になること規定されています。そのため終了ステータスとして 1 を返した場合は POSIX の規定に反する動作なので POSIX 違反になります(実際に zsh は 4.0 まで終了ステータスとして 1 を返していました)。一方で拡張機能は POSIX シェルの仕様とは矛盾しないので POSIX 違反にはなりません。各シェルは POSIX に矛盾しない範囲で自由に拡張機能を搭載することが出来ます。

ところで各シェルが拡張機能を好き勝手に実装していること対して POSIX は文句を言っているはずだと思ったりしてないでしょうか?実はこれは正反対です。POSIX は標準規格の内容が拡張機能に不要な制限を与えないように、拡張機能を容易に実装できるように POSIX を策定しています。POSIX は「我々がこういう仕様を作った。これは考え抜かれた優れた仕様だ。だからこの仕様に準拠して作れ。それ以外は認めない。勝手な拡張をせずに我々が仕様を作るまで待て」などとは言いません。そもそも POSIX が後から作られたのだから(もちろん全てではありませんが)拡張機能は最初からあったんです。例えば[[ ]]、プロセス置換、配列は ksh88 の時点ですでに存在しており POSIX 標準化後に拡張されたものではありません。

POSIX 自身は新しい機能の仕様を作りません。ということは機能を追加するのは OS ベンダーなどの実装者の仕事です。ある実装に拡張機能として追加された機能が他の実装にも広まって移植性が認められたら POSIX に追加するという流れなので、OS ベンダーが拡張できなくなってしまえば POSIX 自身も機能追加できなくなってしまいます。だから POSIX が拡張機能の実装に文句を言うはずがないし、ましてや拡張機能(OS ベンダーによる独自機能)を廃止していこうなんて考えは微塵もありません。例えば、次期 POSIX (Issue 8) では set -o pipefail が標準規格に追加されます。これは今は拡張機能ですが、多くのシェルで実装され移植性が高いと認められたので POSIX に追加されるわけです。(注意 当然ですが POSIX に追加されたからと言ってすぐにどこでも使えるとは限りません。)

POSIXモードを使った方が良いと考える理由

私は bash では POSIX モードを使った方が良いと考えています。その理由は「POSIX は標準で移植性が高いから」で納得する方に対してはそれでいいのですが「bash でいいじゃん。今どき bash が使えない環境なんてあるの?」で反論されてしまいます。私も bash を使える(インストールできる)という前提であれば bash を使って良いと思っています(個人的には bash が入っていない環境を考慮していますが)。

「bash でいいじゃん」と言っても、実際には /bin/sh が bash ではない環境があります。Ubuntu や Debian がそうです。最近(?)は IoT が流行っていますが Raspberry Pi では Debian 系の Raspberry Pi OS が使われています。これらは bash の代わりに dash が /bin/sh として使われています。もっと小さな環境では bash がインストールされていない場合もあります。例えば組み込み系では BusyBox 内蔵の ash が使われていることがあります。軽量 Docker イメージとして使われる Alpine Linux でも BusyBox ash がシェルとして使われてます。なにかと bash じゃない環境はあったりするものです。そういった環境にも対応させようと思った時、簡単なシェルスクリプトであれば bash をインストールするのではなく dash や ash を使おうと思うのではないでしょうか?そういう場合に普段から POSIX モードを使っていれば、細かい動作の違いにハマる可能性が減ります。また、そもそもシェルスクリプトに bash のような複雑なシェルは(インタラクティブシェルとしては必要ですが)シェルスクリプトには必要ないのでは?とも思っています。POSIX シェルの方が仕様が小さいため覚えることも少なくてすみます。複雑なことは他の言語(Python など)を使うべきでシェルスクリプトはそのつなぎです。つなぎとして使うシェルスクリプトは覚えることは少ない方がいいのでは無いでしょうか?

「でも POSIX シェルだと機能が少な過ぎで、配列すら使えないし、配列を使うなら bash しか無いよね?」と思うかもしれません。ここでの話は「POSIX シェルを使う」ではなく「POSIX モードを使う」です。すでに説明したとおり POSIX モードでも拡張機能は使えます。私が減らそうと言ってるのはシェルによって異なる動作です。シバンに #!/bin/bash (#!/usr/bin/env bash) を使って bash を使いつつ POSIX モードにすれば、配列などの拡張機能を使いながら POSIX 準拠の動作に変更することができます。

「いやいや、シバンに bash 使うなら bash でしか動かないじゃん。意味なくない?」と思うでしょう。はい、では質問します。その bash、どこが開発した bash でしょうか?

bash は GNU Project が開発している・・・とは限りません。実は bash を作っている所は複数あります。正確には bash 互換シェルです。

まず macOS で使われている bash。まあこれは GNU bash の古い 3.2.57 を Apple がパッチ当てているだけなので一応 GNU 製ですが GNU bash と少々動作が異なります。例えば POSIX モードで echo -n を実行すると Apple bash では -n と出力されます。これは UNIX = POSIX(XSI オプション含む)準拠の動作です。これ以外の bash には BusyBox ash があります。後編で詳しく説明しますが BusyBox ash は ash 系のシェルでありながら bash の拡張機能を実装しており bash 互換になろうとしています(Windows 移植版の busybox-w32 では実際に bash 実行ファイルを生成します)。またライセンス上の理由で BusyBox (GPLv2) の代替を目指している ToyBox (0BSD) は Roadmap で明確に「we'd like to provide a good replacement for the Bash shell」と bash の代替シェル (toysh) を提供すると公表しています3。新しい文法を持った Unix シェルに移行させようとする野心的なプロジェクトである Oil (Shell) では、bash から Oil への移行パスを提供するために bash と互換性のある osh を開発しています。シバンに bash を指定しているからと言って GNU bash で動かすとは限らない・・・ようになる可能性があると私は考えています。

実は bash の拡張機能の多くは Unix で使われている ksh93 からの移植であり一部同じ機能が使えます。zsh も ksh エミュレートモードを備えています。現在の POSIX シェルは 機能が少ない ksh88 のサブセットですが、デファクトスタンダードと言える bash の代替シェルの普及とともに、私は POSIX シェルの未来は bash のサブセットへと変わっていくであろうと予想しています。bash のサブセット(どこまで含まれるかはまだ未知のことですが)に高い移植性があると認められれば POSIX 標準規格に追加することが出来ます。この兆しはすでに現れており、先程 Issue 8 で追加されると言った set -o pipefail がその一つです。

これは数年スパンの話ではありません。10 年 20 年かかってしまうかもしれない話です。そんな先の話はどうでもいいよと言う人が大半でしょう。これは私が予想しているシェルの未来であって、そうならないかもしれません。なので私も急いで対応が必要なものとは思いません。しかし GNU bash 以外の bash 互換シェルが広く使われだした時に POSIX モードはそういう場合の互換性も実現してくれるはずです。bash 専用のシェルスクリプトで拡張機能を使っていても良いと思います。その上で将来の移植性のために今から POSIX モードを使っていた方が良いのではないかというのが私の考えです。

前編のまとめ

この記事では「どこでも動くシェルスクリプトを書くための基礎知識」として POSIX モードの基礎知識を詳しく解説しました。後編では各シェルが持っている POSIX モードの違いについて解説しています。

POSIX モードを使用することでシェル間の互換性は高まり、移植性のあるシェルスクリプトを書く時の問題が軽減されます。しかしながら POSIX モードでも完璧な互換性は実現できません。互換性問題の発生で破棄された ksh2020(後編参照)のように新しいバージョンのシェルにアップデートしたら動かなくなることだってあります。またこの記事では言及しませんでしたがソフトウェアにはバグがつきものです。POSIX 準拠のシェルであってもバグは避けられません。「どこでも動くシェルスクリプトを書く」というのはそういうバグがあるシェルにも対応するということです。そういった現実の世界で「どこでも動く」と主張するには結局の所、実際の環境で動作確認を行うしかありません。/bin/sh として使われてるシェルの種類が多ければ多いほど「どこでも動く」と主張するためにはシェル間の互換性問題を自分の手で解決していく必要があります。

シェル自体は確かにどこでも使えますが「どこでも動くシェルスクリプト」を書くのは思うより大変なことです。不可能とは言いませんが、頑張ればできるというならどの言語でも当たり前のことです。この記事は(私のように)どこでも動くシェルスクリプトを書きたい人のために POSIX モードについて詳しく解決しましたが、私は何が何でも POSIX で規定されている範囲や、そもそもシェルスクリプトでやるべきだとは思っていません。シェルスクリプトでソフトウェアを作るメリットなんて OS の最小構成でも動く程度のものです。シェルスクリプトでは POSIX (C 言語 API)の一部の機能しか使えず OS が本来持っている基本機能や性能を 100% 引き出せません4。簡易スクリプトやプロトタイプならまだしも、大きなソフトウェアの場合はシェルスクリプトでは生産性が低くなります。私は POSIX 準拠のシェルスクリプトでやりたいことがあるのでやっていますが、それはあくまで私の都合によるものです。

もしどうしてもシェルスクリプトでどこでも動くソフトウェアを作りたいのであれば本気になる必要があります。多くの人がシェルやバージョン間の互換性問題を回避するためのバッドノウハウを公開していることからも明らかなように、POSIX で標準化されているものだけを使ったとしても、たまたまある環境で動いたシェルスクリプトが他の環境や異なるバージョンで動く保証などありません。**POSIX に準拠していればきっとどこでも動くだろうという無責任な考えで作られたテストコードもろくにないシェルスクリプトに重要なシステムを任せたいと思う人はいません。**多数の環境でしっかりとしたテストを行う必要があります。それがわかっているから私はシェルスクリプト用のテストフレームワークを開発しています。

  1. 例えば「入門UNIXシェルプログラミング」がシェルスクリプトの良書としてよく挙げられますが、これは日本語版でおよそ 20 年前の 2003 年に出版され、本家英語版「Portable Shell Programming: An Extensive Collection of Bourne Shell Examples」は 1995 年(27 年前)に出版されたとても古い本です。さらに補足説明すると、この本の著者は数年後に POSIX に準拠した改訂版「Portable Shell Programming Using the Bash Shell and Korn Shell」を出版する予定だったようです。大人の事情でそれができなかったようですが、つまり「入門UNIXシェルプログラミング」は良書ではありますが POSIX に準拠してない Bourne シェル用の本で、著者が改訂の必要があると考えていたということです。他の技術書(特にオライリー)も古いものが多いのでその原著の出版日はいつであるかを調べてください。変化が小さいシェルスクリプトといえど 20 年以上、30 年近くも前の古い常識は今は当てはまりません。(もちろん出版日が新しいにもかかわらず Bourne シェルがメインだったり POSIX シェルとの区別がついてないような本は良くない本です。)

  2. Bourne シェルのソースコードは 2005 年 に OpenSolaris とともに公開、ksh は 2000 年に独自ライセンスで公開され 2005 年からオープンソースです。

  3. ふと思いついたのですが toysh が完全な bash になるとは思いませんが、機能が少ない bash 3.2.57 と十分な互換性が実現されれば Apple は古い bash を捨てて toysh を /bin/sh (/bin/bash) の実体に変更するかもしれませんね。bash 3.2.57 はさすがに古すぎるのでこれに依存し続けるのは Apple にとってもリスクが高いはずです。BusyBox も GPLv2 / GPLv3 の論争の末 GPLv2 だけになったので Apple 的にライセンス上問題ないはずですが 0BSD の ToyBox の方がより適切でしょう。

  4. 例えばリアルタイム API はシェルスクリプトからは事実上使うことが出来ません。リアルタイム API は以前は POSIX のオプション機能でしたが POSIX.1-2008 で必須機能に組み込まれたので、いまや OS の基本機能の一つです。(参考 「POSIX.1-2001 では任意 (optional) とされていたインターフェイスの多くが 2008 年版の標準では必須 (mandatory) になる。」)

21
18
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
21
18