38
29

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.

シェルスクリプトの関数の書き方 functionや()の本当の違いとは? 〜 あなたの知らないシェル関数の真実

Last updated at Posted at 2022-06-12

はじめに

シェルスクリプトの関数定義の方法は function() を書くか省略するかの違いで三通りあります。これらは省略可能なだけで同じだと言われることが多いですがそれは本当なのでしょうか?それが本当ならなぜそのような書き方の違いが生まれたのでしょうか?

関数は Bourne シェルの開発者が重要だと考え最後に取り入れた構文です。関数を知らずしてシェルスクリプトを語ることは出来ません。シェルスクリプトが小さい場合は必要ないかもしれませんが、大きくなり後から読み返して何をやってるのかわからなくなる原因の一つは関数を正しく使っていないからです。この話についても詳しく書きたいのですが、今回のテーマではないので関数を使ってメンテナンス性を上げる方法については別の機会にしようと思います。

関数定義の構文の違いは Bourne シェル、Korn シェル、Bash、そして POSIX の関わりによって生まれました。この記事では関数がシェルスクリプトの世界に導入された歴史を紹介しつつ、これらが何が違うのかとか関数についての話を脱線しまくりながらしたいと思います。興味がない人は冒頭部分を少し読んで、後は最後のまとめにジャンプしてください。単に違いやどう書けばいいかを知りたいだけなら悩むことは何もなく話はすぐに終わります。これは知識欲を満たしたい人のための記事です。

どの書き方を使えばいいの?

まず、最初にどの書き方が一番良いのかを答えておきます。この答えは簡単で function をつけないこの書き方が最良です。理由は Bourne シェルを含めどの互換シェルでも同じように動く書き方で、POSIX でも標準化されているからです。

func() {
  ...
}

可読性の点から function をつけた方が良いのではないかという人もいますが、正直言って可読性に違いはありません。単語の後ろに () がついていれば、それは関数です。関数を検索したいのであれば () を検索すれば十分です。

ちなみに関数定義は間にスペースを入れて func ( ) のように書くこともできるのですが、入れないことをおすすめします。なぜなら POSIX - Function Definition Command に「不要である」と書かれているからです。禁止や非推奨とまでは言っていませんが、文法定義の都合上 () が演算子であり、個々のトークンとみなされるため、入れても動くだけです。

The "()" in the function definition command consists of two operators. Therefore, intermixing <blank> characters with the fname, '(', and ')' is allowed, but unnecessary.

翻訳 関数定義コマンドの "()" は 2 つの演算子で構成されています。したがって、fname、'('、')' に<blank> 文字を混在させることができますが不要です。

Ubuntu / Debian や FreeBSD などの /bin/sh として採用されている ash 系のシェルは function には対応していません。移植性を気にしないなら function をつけても良いのですが、なんだかんだで Ubuntu や Debian で /bin/sh を使う場合もあるものです。そもそも function をつける理由はないのですから、それなら一番短く何も考えないで使える書き方に統一した方が良いでしょう?

function の有無で何が違うのか?

さて、では function に対応しているシェルに限定するとして、function の有無で何が違うのでしょうか?その答えは

違いはありません。(大多数の人にとっては)

どの書き方でも意味は同じです・・・と、後ろの「大多数の人にとっては」が気になりますね。正確に言えば bash や zsh を使っている人にとっては同じであるということです。例外は ksh93 です(ksh93 を使ってる人はあまりいませんよね?)。ksh93 だけは function の有無で動作が違うのです。ですが ksh93 の話は最後の方です。まずは Bourne シェルの歴史の話から始めましょう。

注意 この記事では単に ksh93 と書いた場合 ksh93u+ を指すこととします。u+ はバージョン番号です。ksh93 にはいくつかバージョンがありますが、2011 年の中頃(?)ぐらいにリリースされた 93u+ が AT&T 公式の最終バージョンです。

関数はいつシェルに追加された機能なのか?

Bourne シェルでも関数は使えますが、最初のバージョンからあったわけではなく少し遅れて追加された機能です。Bourne シェルにはそれが搭載された UNIX に対応して次のようなバージョンがあります。(ここより引用、一部修正)

  • Version 7 (1979) Bourne シェルの最初のバージョン
    • 制御構造、コマンド置換、{}、()、任意の変数名、trap、eval、パラメータ置換、case
  • System III (1981) #、[!...]、: でのパラメータ置換、set --
  • SVR1 (1983) shift n
  • SVR2 (1984) 関数追加
    • unset、echo、type のビルトイン、ビルトインのリダイレクト
  • SVR3 (1986) モダンな "$@"関数毎の位置パラメータが利用可能になる
    • 8ビットクリーン、getopts
  • SVR4.0 (1989): ジョブコントロール
    • 参考 bash の最初のリリースも同年
  • SVR4.2 (1992): read -r
    • 参考 POSIX.2-1992 でシェルとコマンドが標準化されたのも同年

関数自体の追加は 1984 年で、関数の仕様が今と同じになったのが 1986 年です。余談ですが関数があれば alias はほぼ不要になります。それなのに alias があるのは、単に関数が追加されたのが後だからというのが大きな理由のようです。 alias は Bourne シェルにはありませんでした。(未確認ですが ksh88 で最初に導入され csh を真似たものではないかと思います。)

関連する話として少し awk の話を取り上げます。awk が最初に作られたのは 1977 年で、Broune シェルと同じく Version 7 Unix で最初に配布されました。実は最初の awk にはユーザー定義関数がありません。その後 1985 年から 1988 年にかけて大きく機能追加が行われ、この時にユーザー定義関数が追加されました。こうして誕生したのが new awk (nawk) 、別名は One True awk です。POSIX awk は nawk をベースとしたものです。nawk は macOS の /usr/bin/awk 等で使われており、Debian 系 Linux では original-awk というパッケージで提供されています。なお awk の関数定義の書き方は function func([args]...) です。

Bourne シェルと awk は 1980 年代の中頃、同じ時期に関数定義が追加されたというのは興味深く、また注意すべきポイントです。シェルスクリプト関係の名著として名前が挙がる UNIX プログラミング環境 の原書 The Unix Programming Environment の第一版は 1984 年出版です。そのためこの本には Bourne シェルにも awk にも関数の説明が含まれていません。有名な本でも時間の流れには勝てません。関数に関して注意が必要なのはこの本ぐらいだとは思いますが、その他の本やネット上のシェルスクリプトの解説記事の中には古い Bourne シェルの解説をしていたりすることがあります(Bourne シェルの時点で古いのですが)。シェルスクリプトは何十年も前から変わっていないように見えるかもしれませんが、他の言語と同じで少しずつ変化しています。現在のシェルスクリプトに当てはまらない古い内容の書籍や記事が結構あるので注意が必要です。

Bourne シェルの開発者は関数を重要な機能だと捉えている

シェルスクリプトは、他のスクリプト言語に比べて出来ることが少ないと文句を言う人が結構いますが、それはシェルでやるべきことだけをシェルでやる、そうでないものは別の言語でプログラムを書く、シェルは複数の言語で作られたコマンドを組み合わせるのに使う、というのが Bourne シェルの哲学だからです。Bourne シェルは言語仕様を充実させるのではなく、シェルとして必要な最低限の機能に削ぎ落とすという考えで開発されています。そのため言語仕様が貧弱なのは意図した設計であり、出来ることが少ないと思うのはシェルスクリプトでやるべきでないことをやろうとしているだけです。

関数はそういった哲学の中であっても採用された機能です。すなわちシェルの開発者は関数をシェルにとって重要な機能だと考えていたということに他なりません。関数を使うとコードをシンプルにしたり可読性を高めたり移植性の問題を簡潔に解決したりすることが出来ます。しかし世の中には関数が実装されていない古いシェルしか使ったことがないからなのか知りませんが、シェルスクリプトで関数を使わない(使えない?)人がいるようです。なんのためにシェルに関数が導入されたと思っているのでしょう?関数を知らずして Bourne シェルの哲学を理解できたとは言えません。

Bourne シェルの開発者である Steve Bourne は関数の追加について、インタビュー記事「The A-Z of Programming Languages: Bourne shell, or sh 」で次のように答えています。

Have you faced any hard decisions in maintaining the language?

The simple answer to that is I stopped adding things to the language in 1983. The last thing I added to the language was functions. And I don’t know why I didn’t put functions in the first place. At an abstract level, a command script is a function but it also happens to be a file that needs to be kept track of. But the problem with command files is one of performance; otherwise, there’s not a lot of semantic difference between functions and command scripts. The performance issue arises because executing a command script requires a new process to be created via the Unix fork and exec system call; and that’s expensive in the Unix environment. And so most of the performance issues with scripting come from this cost. Functions also provide abstraction without having a fork and exec required to do the implementation. So that was the last thing I added to the language.

中略

In the language design I would certainly have added functions earlier. I am rather surprised that I didn’t do that as part of the original design.

翻訳

言語をメンテナンスする上で、何か難しい決断に直面したことはありますか?

それに対するシンプルな答えは、1983 年に言語に何かを追加するのをやめたことです。私が最後に言語に追加したのは関数です。そして私はなぜ最初から関数を入れなかったのかわかりません。抽象的なレベルでは、コマンドスクリプトは関数ですが、それは追跡する必要のあるファイルでもあります。しかし、コマンドファイルの問題の一つはパフォーマンスであり、それ以外は、関数とコマンドスクリプトの間に意味上の違いはあまりありません。パフォーマンスの問題は、コマンドスクリプトを実行する際に、Unix の fork と exec システムコールを使って新しいプロセスを作成する必要があり、それは Unix 環境ではコストがかかります。スクリプトの性能問題の多くは、このコストから来ています。関数は fork や exec を必要としない抽象化を提供する実装です。これが、私が言語に追加した最後のものです。

(中略)

言語設計では、私はもっと早くに関数を追加していたかもしれません。むしろ最初の設計でそうしなかったことに驚いています。

(翻訳 終わり)

パフォーマンスが良く、少ない管理の手間でコマンドスクリプト(外部コマンドとして別ファイルに独立させたシェルスクリプト)のように抽象化を行えるのが関数です。初期に関数がなかったことに本人が驚いているようなものです。

上記のインタビュー記事は Bourne シェルを理解する上でとても良い記事なので、ぜひ読むことをおすすめします。関数の話だけではなく Bourne シェル以前のコマンドをただ書き連ねることしか出来なかった原始的なシェルを置き換えて、変数を追加し iffor などの制御構造を追加し goto を削除したというような事などが述べられています。これを読まずにシェルスクリプトを語ろうなんてモグリですよ?(煽るスタイル)。その他 BSDCan 2015 のキーノートやその動画 Early days of Unix and design of sh by Stephen R. Bourne も Bourne シェルを理解するのにおすすめです。

関数とコマンドスクリプトの違い

上記のインタビューで出てきた(シェル)関数とコマンドスクリプト(外部コマンドとして別ファイルに独立させたシェルスクリプト)の違いを簡単に説明します。シェルスクリプトの言語(正確に言うならばシェルコマンド言語)は、外部コマンドを言語の「関数」のように使えるようにするために作られた言語です。他の言語はその言語用の関数を呼び出しますが、シェルスクリプトは、いずれかの言語で作られた「コマンド」を関数のように使う言語です。この点でシェルスクリプトは他の一般的なプログラミング言語とは全く異なる思想を持った言語です。

外部コマンドが「関数」に相当すると言うならば、シェル関数は何に当たるのでしょうか?その答えはシェル関数も「関数」です。シェルにとっては外部コマンドもシェル関数もどちらも「関数」としてみなせるように設計されています。インタビューでも語られている「関数とコマンドスクリプトの間に意味上の違いはあまりありません」というのはそういう意味です。処理を抽象化(名前をつけてわかりやすくする事)するときに、シェル関数にするか?それともコマンドスクリプトにするか?と迷う程度の違いでしかありません。

外部コマンドを書く事とシェル関数を書く事は本質的に同じです。上の方でシェル関数を使うのが苦手な人がいると書きましたが、シェルスクリプトでコマンドを書いていながら、シェルスクリプトでシェル関数を書かないというのは理屈に合いません。以下はシェル関数とコマンドスクリプトの細かい違いです。

シェル関数 コマンドスクリプト
定義する場所 実行スクリプトに内に定義 実行スクリプトと別ファイルで定義
パフォーマンス 速い 遅い
変数のスコープ スクリプト内でグローバル コマンドスクリプト内に閉じている
入出力 標準入出力経由 同左
戻り値 終了ステータスを返す 同左

シェル関数は実行スクリプト内に定義する代わりに、別ファイルに定義して . コマンド(または source コマンド)で読み込む方法もあります。これは他の言語で言えばライブラリ的な使い方です。ライブラリにすることでコマンドスクリプトのように実行スクリプトから関数を分離して読みやすくし、さらに再利用を行いやすくすることができます。その一方でコマンドスクリプトとは違って、複数の関数を一つ(ないし複数)のライブラリファイルにまとめることができるので、多数のファイルを散らばらせることもありません。「実行スクリプト内シェル関数」と「コマンドスクリプト」の中間的な使い方です。

シェル関数とコマンドスクリプトはどちらかがもう一方の完全上位機能になっているわけではありません。シェル関数は例えるならプライベート関数のようなものであり、コマンドスクリプトはグローバル関数のようなものと考えるのがわかりやすいでしょう。コマンドスクリプトは別のプロセスとして起動するためモジュールの独立性は高くなります。その一方でオーバーヘッドが加わり呼び出しが遅くなります。コマンド毎にファイルの管理が必要で処理の詳細を知りたくなった時はいちいちファイルを開いて中を確認しなければいけません。シェル関数の特徴はその逆です。実行スクリプトのシェル変数へのアクセスするなどシェル関数でなければ出来ないこともあります。

ちなみに csh (tcsh) には関数の機能はないのでコマンドスクリプトを使うことしか出来ません。有害な csh プログラミング では csh でプログラミングをしてはいけない理由が述べられています。これには関数がないことについては触れられていないようですが、関数がないことも csh を使ってはいけない理由の一つと言えるでしょう。

シェルスクリプト用ライブラリは環境変数 PATH から検索する

ここで一つ豆知識です。外部コマンドはコマンド名だけで実行した時に環境変数 PATH から実行ファイルを探すというのは常識ですが、. コマンド(または source コマンド)もファイル名だけで読み込む場合に環境変数 PATH から検索します。つまりシェルスクリプトにとってはライブラリを検索するパスとしても機能します。他の言語で言えば PERL5LIBRUBYLIB と同じ役目です。

これは POSIX の dot command でも標準化されている動作です。例えば gettext.sh は Debian では /usr/bin/gettext.sh に実体がありますが、他の OS では別の場所にあるかもしれません。しかし . gettext.sh で読み込むという使い方が想定されており PATH さえ通っていれば異なるパスにあったとしても読み込むことが可能です。これは移植性が高いシェルスクリプト用ライブラリの読み込み方法です(その肝心のシェルスクリプト用ライブラリが殆どないわけですが)。

/usr/bin に実行ファイルではないシェルスクリプト用ライブラリがあるのは気持ち悪いかもしれませんが、私はシェルが OS の一部であるという役割を果たすべく、外部コマンドだけではなくシェルスクリプトライブラリもシェルコマンド言語の実行環境に統合するというシェルの哲学を感じています。というか、そもそも環境変数 PATH の定義は「検索パス」であって、実行ファイル限定の検索パスではないのでは?と思っています。まあバイナリファイルじゃないシェルスクリプトが /bin/usr/bin 以下にあるという矛盾もありますし、元々の役割から変化するというのもよくある話なわけで、哲学とか関係なく後付でそうなっただけかもしれません。

ちなみにですが、実行ファイルは拡張子なしで作成することをおすすめします。なぜならその実行ファイルの実装言語が何であるかを気にする必要はないからです。例えば Debian では which コマンドはシェルスクリプトで実装されていますが、そんな事は気にしないですよね?もしかしたら将来、別の言語で実装し直すかもしれませんが、言語に合わせた拡張子だとコマンド名が変わってしまい互換性が保てなくなります。拡張子なしなら(POSIX 準拠の)シェル関数名との互換性も実現できます。つまりコマンドスクリプトからシェル関数に変更しても利用側は名前を変更しなくていいということです。一方でライブラリは拡張子を付けることをおすすめします。なぜならライブラリは実装言語を気にするからです。gettext.sh もそうなっていますよね?まあ実行ファイルであっても作業用の簡易スクリプトなどでは拡張子付けて良いと思いますが。

Bourne シェルの後継として開発された Korn シェル

Korn シェル (ksh) は Bourne シェルと同じく AT&T で開発されたシェルです。現在の多くのシェルは Bourne シェル系と言われ Bourne シェルの代替として開発が始まりましたが、機能的には ksh88 のサブセットから拡張されたものと言った方が近いです。「Bourne シェル → ksh88 → bashやその他のシェル」という形で進化していったと考えて概ね間違いではありません。ksh88 は POSIX シェルのベースとして採用されたシェルでもあります。What does it mean to be "sh compatible"? よりシェルの分類図を引用します。

補足 この図では Thompson shell と PWB shell が Bourne Family Shells に含まれていますが、実際には Bourne シェルとの互換性はほとんどありません。Thompson shell はコマンドを書き連ねてそれを実行するだけのものでした。フロー制御(ifgoto 等)が必要になった時、それは外部コマンドで実装されました。パイプやフィルタの概念は 1973 年の Version 3 Thompson shell で導入されたものです。PWB shell には変数やフロー制御が実装され一応はプログラミング言語と呼べるものにはなりましたが Thompson shell との互換性のため制限が大きいシェルでした。これらを踏まえ根本的に作り直したのが Bourne シェルです。したがって Thompson shell と PWB shell を Bourne Family Shells に含めるのは間違いであるというのが私の意見です。しかしそれを差し引いてもこの図はとてもわかり易い図であるため引用しています(載ってないシェルもありますし新たに作った方が良いかもしれないですね)。なお Thompson shell や PWB shell に興味がある方は Origins of the Bourne shell の Predecessors にマニュアルがあります。余談ですがこの図に載っていないシェルの中で有名な fish は(シェル自体は素晴らしいのですが)Bourne シェル・POSIX シェルとの互換性は全くと言っていいほどなく制御命令も関数定義も完全に異なります。

KornShell の年が 1982 年となっていますが、これは開発が始まった年です。最初にリリースされたのは 1986 年の ksh86 (ソースコードはここから)ですが、これは AT&T's "Experimental Toolchest," の一部として実験的に配布されたもので、最初の公式版は ksh88 です。

UNIX シェルは Bourne シェルから Korn シェル(ksh88 または ksh93)や他のシェル(ash や pdksh)へと移行しました(Linux シェルは最初から bash です)。現在 Bourne シェルを使用している UNIX は存在しないと私は考えています。Bourne シェルをデフォルトシェルとして採用している UNIX の発売時期やサポート期限を考えると、そのような UNIX を使っている所自体まず無いのではないでしょうか?Bourne シェルは現在主流の POSIX シェルとは互換性がないので注意してください。詳しい違いは「Bourne Shell(レガシー sh)とPOSIXシェル(sh, bash, etc)の違い」を参照してください。今は無理に Bourne シェルに対応する書き方をする必要はありません。Bourne シェルは切り捨てて POSIX シェルに準拠するのをおすすめします。(ということで Bourne シェルで動かない内容を Bourne シェルの記事として書くのは、もうやめませんか?)

Bourne シェルの開発者 (Steve Bourne) と Korn シェルの開発者 (David Korn) はシェルに対する哲学に違いがあります。Steve Bourne は Bourne シェルはシェルとして完成したとして機能追加を停止しましたが、David Korn は算術展開や追加のパラメータ展開などを後継となる ksh88 で追加しました。Bourne シェルの哲学からは少し逸脱してしまいましたが、それは便利な拡張です。そして、さらに多くの機能追加を行いシェルスクリプトを普通のスクリプト言語として使えるように魔改造したのが ksh93 です。私の意見では ksh93 は少し、いやかなりやりすぎだと思っています。zsh もかなり機能追加されていることで有名ですが ksh93 は zsh と異なる方向性で多くの拡張機能を持っています。とはいっても互換性はあるので、ほとんどの人は ksh93 を少し便利な Bourne シェル / ksh88 として使っていたと思われます(ksh93 の高度な機能の話をしているサイトとか殆どないので・・・)。ksh93 の高度な機能については「夢のシェルスクリプト言語 KornShell (ksh93) 〜すごいぞ!型とクラスは本当にあったんだ!〜」を参照してください。

ksh に関してのもう一つの注意点は、pdksh です。これは ksh88 のパブリックドメインなクローン実装です。例えば AIX の ksh は本家の ksh ですが、OpenBSD ksh (/bin/sh として使われている)は pdksh の後継シェルです。ksh88 も ksh93 も pdksh も ksh というコマンド名で起動しますが中身は別物です。

function は元々 ksh88 の拡張機能

少し訂正をします。関数が追加された SVR2 (1984) の Bourne シェルは、最初に世の中に広く公開されたBourne シェル系のシェルです。しかし最初に実装したのは実は Korn シェルです。ksh86 もしくは ksh88 が公開されたのは、その名の通り 1986 年、1988 年ですが、開発が始まったのは 1982 年です。

最初の Korn シェルの関数定義の構文は function を使う以下の書き方だけです。

function func {
  ...
}

しかしすぐに Bourne シェルが別の形で関数定義を行えるようにしたため、ksh88 では両対応することになりました。このことは David Korn の手によって書かれた ksh - An Extensible High Level Language - 2. History の以下の文章から読み取ることができます。(ちなみにこの文書の全文は ksh - An Extensible High Level Language から読むことが出来ます。こちらもおすすめです。)

Unfortunately, the System V syntax for function definitions was different from that of ksh. In order to maintain compatibility with the System V shell and preserve backward compatibility, I modified ksh to accept either syntax.

翻訳 残念ながら System V の関数定義の構文は ksh のものとは異なっていました。System V シェル(Bourne シェルのこと)との互換性と(ksh の)後方互換性を維持するために、私はどちらの構文も受け付けるために ksh を修正しました。

Boune シェルと Korn シェルの両方がが独立してほぼ同時に関数を実装していた可能性もなくはないですが、後方互換性を維持する必要があったということは ksh の関数は(内部で)ある程度の期間、使われていたのだと思います。したがって実装したのはやはり Korn シェルの方が先なのでしょう。

さて、では ksh88 において function の有無で何が違うのでしょうか?その答えは

違いはありません。

単にどちらの構文も受け付けるようにしただけだからです。

ところで David Korn はなぜ ksh に関数を追加したのでしょうか?その理由を次のように述べています。

Shell functions were added to make it easier to write modular code, since our shell scripts tended to be larger than most shell scripts at that time.

翻訳 シェル関数はモジュール化されたコードを書きやすくするために追加されました。私たちのシェルスクリプトは、当時の一般的なシェルスクリプトよりも大きくなる傾向があったからです。

どの言語でも同じことですが関数は本質的に大きなプログラムを書くときに必要不可欠なものです。本質的に大きいとは、プログラミング技術の未熟さから大きくなっているのではなく、解決しようとする問題の複雑性から大きくなっているという意味です。関数は構造化プログラミングにおいて「抽象化」を行うときに使う道具です。抽象化というのは長いプログラムに意味のある単位で名前をつけて階層化することで、大きな問題を分割統治で解決するテクニックです。

補足ですが、構造化プログラミングについては「翻訳:構造化プログラミングを最初に提唱した文書」がとても詳しいです。こちらではダイクストラの「7.4 STRUCTURED PROGRAMMING(1969)」の翻訳もされています。構造化プログラミングを「GOTO 禁止」の事だと勘違いしている人は必ず読んでください。

ksh88 が function を使う構文を採用した理由(推測)

ksh88 がなぜこのような構文を採用したか?については、私の推測になりますが互換性のためだと考えています。私は { を関数名の右に書くスタイルなのですが、関数名の下に書くスタイルは Bourne シェルと文法的に互換性があります。

function my_func
{
  ...
}

この書き方は Bourne シェルでは次のように解釈されます。

# コマンド名 引数
function my_func

# ただの波括弧
{
  ...
}

もちろん Bourne シェルに function コマンドはありませんが、無いからこそ言語のキーワードではなく、単なるコマンド名として認識されます。文法的に互換性があるので、シェルにスクリプトを読み込んだ時点ではエラーにはなりません。これにより Korn シェルスクリプトを Bourne シェルで実行してしまった場合にわかりやすいエラーメッセージを出力させることが可能になります。例えば以下のようにします。

{
  function_supported() { false; }
  function function_supported
  { true; }
} 2>/dev/null # エラーを出力させないため

if function_supported; then
  echo "Function supported"
else
  echo "Function not supported"
  exit 1 # このシェルには対応していませんとか言って終了する
fi

また ksh88 が関数名の後ろに () をつけない理由も説明できます。以下の書き方だと Bourne シェルでは構文エラーになるからです。

# コマンド名 引数() は文法エラー
function function_supported()
{ true; }

実際の所、構文エラーを回避するだけなら eval を使えば良いのですが、Bourne シェルとの互換性を持たせるためにこのような構文を採用したのではないのか?というのが私の推測です。

ksh88 が func() に対応した理由

すでに David Korn の文章に答えは書いてありますが、ksh88 が func() の構文に対応したのは Bourne シェルが関数の定義として func() を採用してしまったからです。せっかく Korn シェルが(互換性を考慮して?)function func の構文を採用したというのにですね。

Korn シェルは Bourne シェルの後継となるべく開発が進められていたので Bourne シェルがそうしたのであれば Korn シェルもそれ対応しなければいけません。かくして ksh88 で同じ意味を持つ function funcfunc() の二つの構文が実装されてしまいました。

ksh88 で登場したローカル変数

関数を語る上でのもう一つの登場人物はローカル変数です。ローカル変数は ksh88 で登場しました(正確には ksh86 の開発時から local variable という単語が登場しています)。Bourne シェルには関数は実装されましたがローカル変数はないので注意してください。ksh88 でローカル変数を作るには typeset コマンドを使います。

foo() {
  typeset var=1
}
var=0

foo
echo "$var" # => 0

typeset はシェルの変数に「属性」を付けるためのコマンドです。シェルには exportreadonly と言ったビルトインコマンドがありますが、これらも内部的にはシェルに「エクスポート属性をつけるコマンド」「読み取り専用属性をつけるコマンド」として扱われています。typeset は属性をつけるコマンド(他にもあります)の汎用版で、typeset -xexport と同じようにエクスポート属性を、typeset -rreadonly と同じように読み取り専用属性を付けることが出来ます。そして関数内でオプション無しの typeset を実行するとその変数にローカル変数属性がつきます。

他のプログラミング言語の経験からすると exportreadonly は言語の文法(キーワード・予約語)のように思うかもしれませんが、実際には変数の属性を変更するコマンドなのです(と言いつつ zsh ではこれらは reserved word として扱われているようなのがややこしい所ですが)。type コマンドを使うと特定の単語がどのようにシェルで扱われているかを調べることが出来ます。ちなみに Bourne シェルでは export VAR=1 のような代入を伴う書き方はできず export VAR のように属性をつけるためだけにしか使えませんでした。

ksh88 はその後の改訂版で 1990 年頃(?)に local コマンドが使えるようになります。これは実際にはただの typeset コマンドの別名 (alias local=typeset) で、しかも文書化されていません。しかしエイリアス定義一覧を出力すればすぐに見つけることが出来ますし、bash を始め他のシェルでは 文書化された local コマンドが知られており、ksh88 ユーザーにとっても使ってみたら動いたというノリでローカル変数は local コマンドで定義するものという認識だった人も多かったのではないかと思います。

ksh88 では変数以外もローカル化される

さて、前項ではわかりやすく「ローカル変数」と書きましたが、正確には ksh88 が実装していたのは「ローカルスコープ」です。なにが違うのかと言うと変数以外にもローカル化される、つまり関数を抜けると元に戻るものがあります。全てを完全に把握しているわけではないのですが、少なくとも trapset (シェルオプションの変更)はローカル化されました。カレントディレクトリの変更や umask はローカル化されませんでした。

foo() {
  # 以下の変更は関数を抜けると元に戻る
  typeset var=1
  set -u
  trap "echo foo" INT
}
var=0
set +u
trap "echo main" INT

foo

echo "$var" # => 0
echo "$-" # => h (u がない)
trap # => trap "echo main" INT

Bourne シェルや POSIX の標準規格の範囲にはグローバルスコープしか無いため(POSIX に準拠している限り)settrap はこのような動作を行いません。つまり ksh88 はこの点において POSIX に準拠していないということになります。(無効にする方法がありませんし拡張機能と考えるには無理があると思います。)

多くのシェルのローカル変数はダイナミックスコープ

殆どのプログラミング言語はスタティックスコープ(一般的にはレキシカルスコープという呼び名の方が使われている気がします)を採用しているのですが、ksh88、そして bash や多くの互換シェルではダイナミックスコープを採用しています。この違いを知らないとシェルのローカル変数がおかしな動きをしていると勘違いしてしまうでしょう。下記のコードを参照してください。

foo() {
  typeset var=1
  bar
}
bar() {
  var=2
}

var=0

foo
echo "$var" # => 0

bar
echo "$var" # => 2

foo 関数は内部で bar 関数を呼び出しています。その bar 関数の中で var 変数に 2 を代入しています。しかし foo 関数経由で bar 関数を間接的に呼び出した時と bar 関数を直接呼び出したときで、戻ってきた時の変数の値が違います。これがダイナミックスコープの特徴です。ソースコードの字句(レキシカル)を見ただけでは、それがローカル変数なのかグローバル変数なのか静的(スタティック)に決定せず、実行時に動的(ダイナミック)に決まるのでダイナミックスコープというわけです。

この動作を理解するには「変数にローカル変数属性がつく」という話の延長で考えるのがわかりやすいのではないかと思います。foo 関数を呼び出した時、foo 関数内部で var 変数にローカル変数属性が付与されます。それ以降はずっとローカル変数属性がついた状態です。注意が必要なのはローカル属性を付けた foo 関数から抜ける時に自動的に変数の値が元に戻りローカル変数属性が外れるという所です。

ダイナミックスコープは実装が簡単ということで、初期のプログラミング言語で使われてることが多かったようです。例えば Perl は 1994 年の 5.0 まではスタティックスコープの my がなく、ダイナミックスコープの local しかありませんでした。また Python も当初はダイナミックスコープだったようです。古くに開発され大きな変更が行われてないシェルスクリプトは、今もダイナミックスコープが使われている数少ない言語です。

bash で function func() が追加された

Bourne シェル と ksh88 から多くの互換シェルが誕生しました、その中の一つである bash は Bourne Again Shell の略の通り Bourne シェルのクローンとして開発が始まりました。bash は拡張機能を多く持っているようなイメージがありますが、実際には ksh93 や zsh に比べればおとなしいもので POSIX 準拠度も高いです。bash の最初のリリースは 1989 年であり ksh88 との互換性も考慮されていました。そのため、Bourne シェルスタイルの func() だけではなく、Korn シェルスタイルの function func にも対応しました。

この時 bash は(少なくともマニュアル上は)functionfunc() の前に付けることができる省略可能なキーワードとして認識されていた可能性があります。

bash 3.2 の マニュアルより

[ function ] name () compound-command [redirection]

実際の実装はマニュアルとは異なり function を書いた場合は () は省略可能です。でなければ ksh との移植性が実現できません。しかし上記には () が省略可能であるという情報が含まれていません。

現在のマニュアルは以下のように修正されています。

bash 5.0 のマニュアルより

name () compound-command [redirection]
function name [()] compound-command [redirection]

Bourne / POSIX シェルスタイルと ksh スタイルに二つに対応しており ksh スタイルは省略可能な () を付けることが可能という書き方に改められています。これに従い関数定義を書くと以下の三つになります。

# Bourne シェルまたは POSIX シェルスタイル
func() { ... }

# Korn シェルスタイル
function func { ... }

# bash で許可されたスタイル
function func() { ... }

function func() という書き方は、アクシデントでサポートされてしまったと difference between "function foo() {}" and "foo() {}" で書かれているのですが、私は疑問の余地があると考えています。bash 3.2 のドキュメントの間違いから何かしらの混乱があった可能性がありますが、awk のユーザー定義関数の書き方である function func([args]...) を真似ただけかもしれないとも思っています(そして将来的に引数をサポートしたかったのかも?)。

function func() はすでに多くのシェルが対応しています。移植性の点から使うのは推奨はしませんが、これを廃止する合理的な理由があるとは思えず、互換性維持の方が重要であることを考えると将来使えなくなる可能性はまずありえないでしょう。

余談ですが func() の書き方は C 言語がルーツで間違いないでしょう。引数なし戻り値の C 言語の関数の定義は void func(void) になりますが、この void は(非推奨で警告されますが)省略可能です。JavaScript が広く普及した現在では関数の定義に function がつく方が自然と感じる人が多いと思いますが、当時は C 言語が主流で JavaScript はなく nawk も時期的に普及しているとは思えないので、func() の方が好まれていたのだと思います。

ということで function func() は bash が(アクシデントで?)サポートした構文です。シェルスクリプトの関数は引数リストがないのになんで () が必要なんだ?と気になってしまうかもしれませんが、このような流れでなぜかサポートされてしまったというのが結論です。冗長なのはそのとおりで、元々は関数定義は () をつけるか function をつけるかの違いはあれどシンプルなものでした。

POSIX はローカル変数を導入しなかった

POSIX ではローカル変数は標準化されていません。ローカル変数の導入は、検討されなかったのではなく検討された上で見送られたものです。理由を一言で結論を言うならば、まだローカル変数が広く使われていなかったからです。経緯の詳細については Function Definition Command に書かれています。

POSIX というのは新しい規格を自分たちで作っていく団体ではなく、既存の動作(歴史的慣習)を標準規格としてまとめるというのが原則です。そのため望まれていた機能であっても広く使われていなければ標準化されないこともありますし、各実装の細かい違いを矛盾が無い言葉で文書化できなければ、標準化が見送られることもあります。ローカル変数はまさにこれが理由で、別にローカル変数を標準化したくなかったわけではないのです。

さて面白いのは初期の提案です。

The description of functions in an early proposal was based on the notion that functions should behave like miniature shell scripts; that is, except for sharing variables, most elements of an execution environment should behave as if they were a new execution environment, and changes to these should be local to the function. For example, traps and options should be reset on entry to the function, and any changes to them do not affect the traps or options of the caller. There were numerous objections to this basic idea, and the opponents asserted that functions were intended to be a convenient mechanism for grouping common commands that were to be executed in the current execution environment, similar to the execution of the dot special built-in.

翻訳 関数の説明の初期の提案は、関数は小型のシェルスクリプトのように振る舞うべきだという概念がベースでした。つまり変数の共有を除いて、実行環境のほとんどの要素は新しい実行環境であるかのように振る舞い、またこれらの変更は関数にローカル化すべきだということです。例えば、トラップやオプション(set コマンド等で設定するシェルのオプションのこと)は関数への入力時にリセットされるべきで、それに対する変更は呼び出し元のトラップやオプションに影響しません。この基本的な考え方には数多くの異論があり、反対派の主張は、関数は現在の実行環境で実行される共通のコマンドをグループ化するための便利な仕組みであり、ドットスペシャルビルトイン(. コマンドのこと)の実行と似ているというものでした。

これはまさに「シェルスクリプトの関数はコマンドスクリプトと本質的に同じもの」という考え方です。ksh88 でトラップやオプションが関数内にローカル化されてる仕様もこの考え方で説明できます。しかしほとんどの要素を新しい実行環境であるかのように振る舞わせたいのであれば、実際にコマンドスクリプトにすれば良いだけなので必要ありません(と書かれています)。

結局の所、初期の提案に近い動きをするのは ksh88 しか存在せず、歴史的慣習とまではいえません。当時の時点ではローカル変数を標準化することの合意が取れなかったようで POSIX で標準化されませんでした。残念なことです。ただしローカル変数の標準化が断念されたというわけではありません。POSIX では将来のために local をローカル変数のために予約しておくべきとしました。そしてローカル変数の標準化の検討は引き続き行われています。

ちなみにサブシェルを使えばローカル変数が実現できると思うかもしれませんが、その場合サブシェルに含まれる全ての変更がローカル化されてしまい、グローバル変数も使えなくなってしまうのでそれはそれで不便です。工夫すればサブシェルから変数を戻せないことはないのですが、やっぱり面倒です。

関数のエクスポートとその仕組み

さて少し話が脱線しますが、POSIX のドキュメントからは関数のエクスポートに関しても標準化が検討されていたことがわかります。親プロセスのシェルで定義した関数が、子プロセスのシェルでも自動的に定義されるという仕組みです。関数のエクスポートは現在は bash だけ(?)が使うことが出来ます。後述しますが、この機能は bash のバグにより Shellshock と呼ばれる脆弱性攻撃に悪用されることになります。

関数のエクスポートは必須な機能とまでは言えないものの便利な使い方があります。例えばあるシェルスクリプトが、別の(内部的な)シェルスクリプトを呼び出す時、親シェルスクリプトで定義した関数をそのまま子シェルスクリプトで使うことができるのでコードの重複を避けることができます。

関数がコマンドよりも優先されることを利用して動的なパッチを当てることが出来ます。例えばシェルスクリプト内部で wget コマンドを使用しているが、その環境にインストールされてないので curl コマンドでエミュレートしたいと言った場合に、新たに作った親シェルスクリプトで wget 関数をエクスポートすることですげ替えることが出来ます。コマンドのすげ替えはセキュリティ上問題があるのではないかと思うかもしれませんが、これは環境変数 PATH を変更して実行するのと大差なく、ユーザーが自分の環境変数を完全に制御できている限りセキュリティ上の問題は発生しません。

また次のようにして外部コマンド(以下の例では xargs)からシェル関数を呼び出すことが可能になります。

#!/usr/bin/env bash
func() {
    echo "[$1]"
}
export -f func
seq 10 | xargs -I{} bash -c 'func {}'

私は関数のエクスポートを bash の拡張機能だと思っていたのですが、POSIX のドキュメントからどうやら元々は Ninth Edition のシェルが提供していた機能のようです(軽く調べたのですが実装されてる根拠を見つけられませんでした)。Ninth Edition は 1986 年なので bash よりも前の話です。

There was little historical practice. The Ninth Edition shell provided them, but there was controversy over how well it worked.

また ksh も typeset -xf で関数エクスポートできるようですが、使い方が間違っているのか削除された機能なのかうまく動作させられませんでした。ksh88 でならどうにか動かすことができましたが、環境変数を使わない方法で実装されており、エクスポートされた関数を使うスクリプトはシバンなしでなければならいというような分かりづらい制限がありました。

関数のエクスポートは環境変数を利用して行われます。例えば func() { ... } をエクスポートすると以下の環境変数が定義されます。そしてシェル起動時にこのような環境変数が定義されていれば自動的にその関数が定義されます。

# POSIX で検討されていた形式(注意 ()まで含めて環境変数名)
func()='{ ... }'

# 旧 bash 形式(Shellshock 対策以前)
func='() { ... }'

# bash 形式(Shellshock 対策以降)
BASH_FUNC_func%%='() { ... }'

旧 bash 形式の場合に疑問になったのが func 関数と同じ名前の func 変数の両方をエクスポートしたらどうなるのだろう?です。なんと環境変数に同じ名前の項目が二つ作られ、関数も変数も両方とも正しく使うことが出来ました。同じ名前の環境変数って作れるんですね……。(参考 同じ名前の複数の環境変数に注意する

実際にどのように環境変数がエクスポートされているを知りたい場合は以下のようにして調べることが出来ます。env コマンドを使うのがポイントです。setexport では表示されません。

bash -c 'func() { echo func; }; export -f func; env'

ちなみに printenv でも表示されますが、これは 1980 年に env コマンドが作られる前に、1979 年にビル・ジョイが BSD で作ったコマンドで、POSIX では env コマンドが標準化されたので printenv は忘れて良いです。一応 printenv には指定した名前の環境変数名だけを出力する機能があったりしますが不要でしょう

環境変数を使って関数をエクスポートするという仕組みなので、次のようなコードで bash を使わずに関数のエクスポート相当のことを行うことも出来ます。

env "BASH_FUNC_func%%=() { echo func; }" bash -c 'func'

関数のエクスポート機能は POSIX では歴史的な慣習は殆どなくセキュリティ問題の懸念があるという理由で削除されました。この削除というのは POSIX の標準規格の内容から削除されたというだけで、実装が禁止されたわけではありません。POSIX は拡張機能の実装を許しているからです。ちなみに私はユーザーが持つ環境変数を権限のないユーザーが読み書きできることがセキュリティ上の問題であって、関数のエクスポート自体は予期せぬ動作を引き起こす可能性があるものの、セキュリティ上の問題には当たらないと考えています。

関数のエクスポートには便利な使い方がありますが、工夫すれば別の方法で実現することも可能なので、どうしても必要というものではありません。移植性のために必要ないなら使わないことを推奨しますが、互換性を維持する必要があるので bash がこの機能を完全に削除することはないでしょう。

Shellshock とは何だったのか?

当時は大騒ぎになりましたが 2014 年の話なのでもう随分昔の話になります。関数のエクスポートは Shellshock として知られるバグによって脆弱性攻撃に利用されました。

これは外部から不正な値を持つ環境変数を設定させる手段があることが攻撃のキーになっています。一般的に環境変数というものは外部から勝手に設定されるようなものではありません。環境変数が完全に自分の制御下にあれば、関数のエクスポートの機能があっても大きな問題にはなりませんでした。

ここで問題になったのがウェブサーバー上でプログラムを実行する CGI と呼ばれる(古い)仕組みです。古い仕組みと言っても CGI が誕生したのは bash よりも後です。従って bash が CGI の仕様を考慮するのは難しい話です。CGI から実行するプログラムには Perl スクリプトがよく使われましたが、標準入出力が使える言語ならシェルスクリプトでも何でも構いません。CGI は手軽であるため小規模なシステム等では今も使われていたりしますが、パフォーマンスが低く現在は本格的なシステムでは別の仕組みが使われています。ちなみにプログラミング言語としてシェルスクリプトを使う場合は、シェルスクリプトの制限上 CGI しか選択肢になりません。

CGI にはプログラムを起動する前に外部からのリクエストで環境変数を設定するという仕様がありました。さすがに自由な環境変数名に設定できるわけではなく環境変数名には HTTP_ プリフィックスが追加されます。例えば HTTP ヘッダとして User-Agent が送れられてきた場合、環境変数 HTTP_USER_AGENT にその値が設定されます。クライアントからは自由な名前の HTTP ヘッダを送ることが出来ます。すなわち foo-bar-baz: () { ... } という HTTP ヘッダを送ると HTTP_foo_bar_baz=() { ... } という環境変数が定義されます。ここで重要なのは、この形式が Shellshock 対策以前の bash の関数のエクスポートで使われていた環境変数の形式であるというところです。

bash の実装の筋が悪かった点は、関数定義かどうかを環境変数名ではなく、環境変数の値だけで判定する仕組みだった事です。値の最初が () { で始まっていれば、それを関数定義とみなしてしまいます。もしこれが POSIX で検討されていた func()={ ... } 形式であれば、HTTP ヘッダで許されない文字である () が含まれていますし、Shellshock 対策以降の BASH_FUNC_func%%='() { ... }' のような形式であれば、環境変数名で関数定義かどうか判断できる上に、CGI では頭に必ず HTTP_ が追加されるため大きな問題にはならなかったでしょう。ちなみに RFC7230 によると HTTP ヘッダに % は使用可能な文字のようです。

しかし、この段階ならはまだ大きな問題は発生しません。CGI 経由の場合 HTTP_ で始まる環境変数しか定義されないので、シェルスクリプトから HTTP_ で始まるコマンドを呼び出していない限り任意のコードを実行する方法がないからです。Shellshock の重大なバグは関数定義の後、() { ... }; ここ に書いたコードを関数定義時に実行してしまうという点です(少し異なる別の問題もあるのですが省略します)。このバグによってクライアントから不正な HTTP ヘッダが送り込まれ悪意のある環境変数が定義され、CGI から直接シェルスクリプトを実行するか、別の言語で書かれた CGI スクリプトからシェルスクリプトやシェル経由で外部コマンドを実行したタイミングで不正なコードが発動してしまいます。

もちろん今は Shellshock の問題は解決しており CGI 経由で攻撃が成立することはありません。しかしながら bash の関数のエクスポートの機能自体がなくなったわけではないことに注意してください。もし何かしらの方法で外部から任意の環境変数を設定して任意のコマンドを実行できるような仕組みを作ってしまった場合、例えば BASH_FUNC_date%%='() { ... }' という環境変数が設定されてしまうと date コマンドを独自の悪意があるコードにすげ替えられてしまいます。自分で脆弱性を作ってしまわないように注意してください。

# 環境変数が設定された状態で実行すると予期せぬコードが実行される
$ env "BASH_FUNC_date%%=() { echo fake; }" bash -c 'date'
fake

上記のコードを冗長に書いた場合

$ cat << HERE >./script.sh
#!/usr/bin/env bash
date
HERE

$ env "BASH_FUNC_date%%=() { echo fake; }" bash
$ ./script.sh
fake

関数は動的に定義され再定義や削除ができる

意外と知られてないのではないかと思いますが、シェルの関数は実行時に動的に定義され、再定義や削除を行うこともできます。以下のコードは POSIX のドキュメントからの抜粋です。

# If variable i is equal to "yes",
# define function foo to be ls -l
#
[ "$i" = yes ] && foo() {
    ls -l
}

上記のコードでは、変数 i が "yes" の時だけ関数を定義しています。この例だと何に使えるんだろうと言った感じですが、以下のように環境によるコマンドの違いを吸収する処理に使うことが出来ます。

# 互換性吸収のためのラッパー関数
if command -v gdate >/dev/null; then
  date() { gdate "$@"; }
fi

# 単一のコードで Linux と Mac で動く
date --date tomorrow # --date オプションは GNU 拡張機能

上記のコードで重要なのはラッパー関数とそれを使ったメインのコードが分離されている所です。さらにこれをライブラリ化すると再利用可能となり、可読性や保守性が向上します。

. portability.sh

date --date tomorrow

他には sed で使用する正規表現をデフォルトで拡張正規表現としたい場合、以下のようにします。

sed() { command sed -E "$@"; }

ポイントは command コマンドです。このコマンドは引数に一致する名前の、関数以外(外部コマンドやシェルビルトインコマンド)を呼び出します。これによって再帰呼び出しになるのを防いでいます。

関数を再定義したり削除したりすることもできます。

# 文字列から関数を定義することが出来る
eval 'func() { echo 1; }'
func # => 1

# 別の内容で再定義することが出来る
func() { echo 2; }
func # => 2

# サブシェルを使うと一時的な再定義が行える
(
  func() { echo 3; }
  func # => 3
)
func # => 2 (元の関数に戻っているかのような動作になる)

unset -f func # 関数の削除

補足ですが、関数の再定義や削除は Bourne シェルでも行うことができます。しかし変数名と関数名の名前空間は同じなので foo 変数と foo 関数の両方を同時に定義することはできません。変数名と関数名の名前空間は分離しなければいけないと POSIX で規定されているため、この点でも Bourne シェルは POSIX に準拠していません。

The implementation shall maintain separate name spaces for functions and variables.

関数を再定義できたり削除できたりするような柔軟性は(コンパイラ言語ではなく)スクリプト言語によく見られる特徴です。他のスクリプト言語ではさらに柔軟なことが出来るものが多いですが、シェルも意外といろんなことが出来るということがわかったのではないでしょうか?一方で awk はスクリプト言語ですが、このようなことはできません。awk は文字列処理に強いですがスクリプト言語としての柔軟性はシェルの方が上です。

関数の再定義は一般的にはあまり使うことが無いテクニックですが、例えば単体テストを行う際に外部コマンドをテスト用の処理を行う関数にすげ替えたりする、いわゆるモックを行うのに便利です。これはシェル関数が外部コマンドと本質的に同じという性質を持っているために可能となっています。他の言語で外部コマンドをモックするのは一筋縄ではいかないでしょう。シェルスクリプトはテストしづらいと言われますが、実は外部コマンドを使用するスクリプトのテストはシェルスクリプトの方が得意なのです。よく構造化されたシェルスクリプトはテストも容易になります。

ksh93 ではスタティックスコープに変更になった

さて、長い脱線から話を戻し、ここで衝撃の事実をお伝えします。ksh88 でダイナミックスコープのローカル変数が実装され、他のシェルにもそれが移植され、今ではシェルスクリプトの世界はダイナミックスコープというのが常識です。

しかし ksh88 はダイナミックスコープであると文書化されていなかったというのです。そして ksh93 ではスタティックスコープへと変更になります。もちろん互換性は保たれていません。正直信じがたい話なのですが、それを示すような記述はいくつか見つかります。

KSH-93 Frequently Asked Questions

Q28. How are variables scoped in ksh?

A28. The scoping of variables was not defined for ksh88 but in ksh93
static scoping was specified.

RELEASE

94-12-31 +Variables inside functions are now statically scoped.
The previous behavior was never documented.

いくら文書化していないからと言って、互換性を切り捨ててまでこのような変更を行うのだろうか?と戸惑いましたが、もう一つの話として POSIX はダイナミックスコープであることに難色を示したようです。(StackExchange - List of shells that support local keyword for defining local variables

POSIX initially objected to specifying typeset on the ground that it was dynamic scoping. So ksh93 (a rewrite of ksh in 1993 by David Korn) switched to static scoping instead. Also in ksh93, as opposed to ksh88, local scoping is only done for functions declared with the ksh syntax (function f {...}), not the Bourne syntax (f() {...}) and the local alias was removed.

翻訳 POSIX は当初 typeset を指定することに対して「ダイナミックスコープである」という理由で反対していました。そこで ksh93(David Korn が 1993 年に書き直した ksh)では、代わりにスタティックスコープに変更されました。また ksh93 では、ksh88 とは対照的に、ローカルスコープは Bourne の構文(f() {...})ではなく、ksh の構文(function f {...})で宣言した関数に対してのみ行われ、local エイリアスは削除されました。

この件に関して他のソースを探したのですが見つけられませんでした。StackExchange の回答なので信頼性に疑問の余地があるかもしれませんが、回答者の Stéphane Chazelas は Shellshock 脆弱性の発見者であり(伏線回収)、POSIX 標準規格の改定が行われている Austin Group で数多くの発言をしている方なので安易に切り捨てることは出来ないでしょう。まあ標準化の検討で色々意見はでているはずなので、ダイナミックスコープに対する反対意見があったとしても不思議ではありません。(ちなみに先程は書きませんでしたが 「bash の function func() という書き方はアクシデントでサポートされた」と回答しているのも Stéphane Chazelas です。世界は狭いです。)

ということで、私の推測ですが以下のような理由で ksh88 との完全互換を目指すのではなく POSIX に準拠し多くの機能を追加した ksh93 を新しく開発することを選んだのだと考えられます。

  • スタティックスコープが望まれた
  • (たまたま?)文書化されていない動作だったから変更してもよいだろう
  • トラップやシェルオプションの動作はどちらにしろ POSIX に準拠していなかった
  • 他にも多くの機能を追加したかったので作り直したくなった

ksh93 で function をつけた時の動作の違い

ksh88 から ksh93 での仕様変更ですが、まずは function をつけない場合です。function をつけないというのはすなわち Bourne シェルの関数定義と同じで func() { ... } という書き方です。POSIX ではこの書き方が移植性があるとして標準化されました。

ksh88 では typeset による変数定義だけではなく trapset による変更が関数内にローカル化されていました。これは POSIX に準拠した動作ではありませんでした。したがって ksh93 ではこの仕様が取り除かれ func() { ... } はローカルスコープを持たないものになりました。

また ksh88 では local コマンドは typeset の文書化されていないエイリアスでしたが、おそらく POSIX で local コマンドが標準化されるというのを見越してエイリアスが削除されました。これによって POSIX で標準化された時に、互換性問題を気にすることなく POSIX に準拠した local コマンドを再実装することが可能になります。

次に function を付けた場合、すなわち元々 ksh88 の最初の関数定義の構文である function func { ... } はスタティックスコープを持つものへと変わりました。このことは以下の FAQ からもわかります。

KSH-93 Frequently Asked Questions

Q18. What is the difference between function name and name()?

A18. In ksh88 these were the same. However, the POSIX standard choose foo() for functions and defined System V Release 2 semantics to them so that there are no local variables and so that traps are not scoped. ksh93 keeps the ksh88 semantics for functions defined as function name, and has changed the name() semantics to match the POSIX semantics. Clearly, function name is more useful.

ksh88 と ksh93 の違いをまとめるとこういうことです。

  • ksh88
    • func() { ... }function func { ... } は全く同じ
    • 関数はダイナミックスコープを持つ
    • trapset もローカル化される
    • ローカル変数の定義には typeset を使う
    • 1990 年頃に local コマンドが typeset のエイリアスとして文書化されずに定義される
  • ksh93
    • func() { ... } はスコープを持たない
    • 従って trapset はローカル化されない
    • function func { ... } はスタティックスコープを持つ
    • ローカル変数の定義には typeset を使う
    • local コマンドは存在しない

func() { ... }function func { ... } の実際の動作の違いは以下のようになります。func() { .. } はスコープを持たなくなったので typeset の効果が無い = 関数の中での変更が関数の外に影響しているという所がポイントです。

func() {
  typeset var=1
}
var=0
func
echo "$var" # => 1

function func {
  typeset var=1
}
var=0
func
echo "$var" # => 0

さらにこれがダイナミックスコープではなくスタティックスコープであることを見てみましょう。

function foo {
  typeset var=1
  bar
}
function bar {
  var=2
}

var=0

foo
echo "$var" # => 2(bash 等では異なり 0)

bar
echo "$var" # => 2(bash 等でも同じく 2)

foo 関数では var 変数はローカル変数であるもの、その変数のスコープは foo 関数に閉じており、bar 関数の var とは無関係です。そして bar 関数では typeset がないのでグローバル変数となっています。ソースコードの字句(レキシカル)からローカル変数なのかグローバル変数なのか静的(スタティック)に決まっており、ゆえにスタティックスコープ(またはレキシカルスコープ)です。

なお上記のコードは bash、mksh、yash、zsh でもそのまま動作します。しかしこっそり紛れ込ませたコメントの通り、動作はしますが異なる結果を出力します。これは他のシェルではダイナミックスコープだからです。このような違いがあるため、移植性を考慮するのであれば function foo { ... } を使用するのはおすすめしません。また ksh93 では function foo() { ... } は動かず、dash では POSIX で標準化されている foo() { ... } だけしか動きません。

現状は ksh93 以外のシェルは foo() { ... } + local コマンドでダイナミックスコープのローカル変数を使うことができるのですが、ksh93 だけは function foo { ... } + typeset でスタティックスコープのローカル変数を使うしか無いという状況です。

ksh93u+m の local 対応について

もし POSIX で local コマンドの合意が取れて標準化され、ksh93 でも実装されていたら、どんなに良かっただろうかと思います。ローカル変数がないばかりに移植性がある大きなプログラムを書く時に、パフォーマンスへの考慮や、移植性の注意点、奇妙なテクニックが必要になってしまいます。

しかしまだ希望はあります。ksh93 は ksh93u+ で終わったわけではないからです。AT&T 公式の ksh93 は ksh93u+ が最終版ですが、現在その後継として ksh93u+m が開発されています。その ksh93u+m では local コマンドの実装が計画されています。

ksh93u+m の開発者は Martijn Dekker で Modernish の開発者としても知られています。 Modernish は POSIX シェル用のライブラリで、より安全な変数展開やコマンド展開やループなど、堅牢で移植性と可読性が高い言語構造を提供しています。端的に言うともっと簡単にシェルスクリプトが書けるようになるライブラリです。このようなシェルコマンド言語そのものを改善するようなライブラリを作る場合、シェルのマイナーな機能を駆使する必要があります。実は ksh93 はそのようなマイナーな機能に多くのバグを含んでいます(その他のシェルも多少なりバグはあります)。ksh93u+m ではそれら多数のバグが修正されており、もっとも不具合が少なく安定性が高い ksh93 になることは確実です。

ksh93 のバージョン番号は今まで後ろにアルファベット(と記号)を追加したものでした。それが ksh 93u+m 1.0.0 と変わります。この名前は 10 年以上もの長い間最新版だった ksh93u+ に敬意を示したもので(m は modified の m?)、これからは 93u+m の部分が名前となり、バージョン番号はセマンティック バージョニングに準拠した 1.0.0 へと変わります。つまり ksh93u+ から大きく変わることなく POSIX 準拠と安定性を高めるという意味が込められています。

実は ksh93u+m が登場するまでの間に、開発版の ksh93v- から開発を継続し ksh2020 をリリースしたプロジェクトがあったのですが、多くの修正で互換性が保てず失敗して撤回された歴史があります。破棄されたプロジェクトなので詳細は省きたい所ですが、現時点で Homebrew でインストールされる ksh は未だに ksh2020 です。そのため少しだけ注意点を書いておくと、なんと local コマンドが実装されています。ただしこれは function func { ... } でしか使えずスタティックスコープです。試していませんが bash 互換モードでビルドすればダイナミックスコープで動くらしいです。しかしデフォルトはスタティックスコープでありながら、その場合でも local コマンドが使えるという移植性の点からは混乱を引き起こしやすいものでした。

ksh93u+m の現時点での最新版は ksh 93u+m 1.0.0-beta.2 でまだベータ版です。完成はしていませんが、次期 Debian などで採用されることになっており、これが ksh93u+ の後継バージョンとみなされるのはほぼ確実です。1.0.0 になるまでに local コマンドが実装されるかはまだ未定ですが、将来 local コマンドが実装されれば、すべてのシェルで使えるようになり、POSIX で標準化される可能性も大いにあります。

さいごに

この記事を書く前に私は function の有無でどのような違いがあるのかを書いている日本語のページを探しました。しかしそれを詳しく書いてあるページを見つけることが出来ませんでした。英語でなら詳しい情報がありますが日本語サイトでは bash 前提で動作に違いはないと書いて終わっているか、dash との互換性を気にして POSIX 準拠の書き方を推奨するぐらいのことしか書かれていませんでした。

今回の記事で一番注意が必要なシェルは ksh です。ksh は商用 UNIX でよく使われてるシェルのはずですが、ksh93 への移行はなかなか進まなかったとも言われているようなので、ksh93 ではなくて ksh88 を長く使っていた人が多いのかもしれません。更に古い環境の場合は Bourne シェルを使っていたのでしょう。その後 UNIX から Linux や BSD に乗り換えていたりすれば bash や ash 系のシェルを使うことになるので ksh93 の利用者が少なく、スタティックスコープへと変更された話を見かけないのも理解できます。あとは単純に商用 UNIX は情報自体が少ないので、クローズドな世界なのだろうなという気がしています。

まあいずれにしろシェルスクリプトの移植性問題について本気で取り組んでいる人は少ないという事なのでしょう。私も数年前までそうでした。シェルスクリプトの移植性問題を調べれば調べるほど辛いことがわかるので、私も普段は bash を使うことを推奨しています。可能であればそれが一番楽です。もしインストールされてない環境であればインストールするだけです。あとは人によって必要なら POSIX で標準化されている範囲の機能だけを使って書いて、POSIX 標準化以前の Bourne シェルや ksh88 のことはもう忘れましょう。もちろんそれだけで、どこでも動くようになるという単純な話ではありません。POSIX で標準化されている範囲の時点で完全な移植性が実現されてないからです。そこで移植性を確保し生産性を高めるためのラッパーライブラリを作る・・・ということで関数の重要性につながってきます。

この記事に書かなかった関数にまつわる話に、ちょっと変わった関数定義の話があります。例えば func() ( ... ){} の代わりに () を使う)や func() for ... done{} なし)等です。その理屈や移植性などの話をすると終わらないので、この記事では割愛します。私も書けることは知っていますが特にメリットがなく、動作や意図がわかりづらくなったりするので、どちらにしろこのような書き方はしません。

まとめ

さて、では最後に(長い話を飛ばした人のために)この記事の内容をまとめたいと思います。本文は色々と脱線したので話が長くなってしまいましたが、関数の成り立ちや移植性については以下の内容を把握していれば十分だと思います。

最初に ksh88 に function func { ... } が追加され関数が使えるようになりました。すぐに Bourne シェルでも関数が使えるようになりましたが func() { ... } という異なる構文を採用しました。Bourne シェルとの移植性のために ksh88 は両対応になりました。この二つに動作の違いはありません。ksh88 の変数のスコープは文書化されていませんでしたがダイナミックスコープでした。

ksh88 の実装は多くのシェルに移植されていきました。local コマンドも普及していきました。しかし POSIX では関数は採用されたものの、ローカル変数は時期尚早として採用されませんでした。ダイナミックスコープであることに難色を示したとの情報もあります。

現在 POSIX 標準規格では func() { ... } のみが規定され、これが最も移植性が高いスタイルです。ローカル変数は POSIX では標準化されませんでしたが、全ての POSIX 準拠シェルで実装されています。dash のような最小限の POSIX 準拠シェルでは func() { ... } にのみに対応していますが、それ以外の多くのシェルでは func() { ... }function func { ... }function func() { ... } に対応しており意味は全く同じです。

例外は ksh93 です。ksh93 は function func() { ... } には対応せず、ローカル関数が使えるのは function func { ... } を使った場合のみ、しかも local コマンドはなく typeset で指定せねばならずスタティックスコープが使われていると他のシェルと実装が全く異なります。将来 ksh93u+m で local コマンドが実装され移植性が実現されることが期待されます。

移植性を考える時、何も考えなくて使えるのは func() { ... } だけです。厳密に POSIX に準拠しようと思わない限りローカル変数も使うことも可能です。考慮しなければならないのは ksh93 に対応するかどうかだけです。ksh93 に対応して移植性を実現するのは大変です。ローカル変数はないものとして考えグローバル変数だけを使うかサブシェルを使ってどうにかするのが楽でしょう。

参考リンク

38
29
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?