はじめに
シェルスクリプトに関しての長所と短所をまとめてみました。多くの短所を上げていますが、私はシェルスクリプトを嫌っているわけではなく(むしろ逆)、現在のシェルスクリプトが抱える問題点を明らかにし、シェルスクリプトはどう使うべきか? またはどう使うべきではないか? 問題点があるならばそれを解決することはできないか? を考えるためにまとめています。問題を解決するにはまず問題点を明らかにしなければいけません。
またシェルスクリプトを本来の用途に合わないものに使うと逆に開発が難しくなってしまいます。それは使い方が悪いわけでシェルスクリプトの問題ではありません。間違った使い方によってシェルスクリプトの価値が不当に下げられてしまうことを減らすために、あえて多くの短所をあげています。つまり最初からこんな用途に使おうと思うな。ということです。(使うことを禁止はしませんが、わかった上でやりましょう。実際に私もわかった上でやっています。)
2021-10-11 この記事のコメントに返信という名の「長所に関しての補足」を追加しています。
補足 (2021-10-03 追記) この記事はわかりやすく「長所」と「短所」としてまとめましたが、実際には用途がまるっきり違うというだけの話です。不適切な用途にシェルスクリプトを使おうとするから「短所」に見えてしまうのです。「長所」が活かせるような場所にシェルスクリプトは使います。正直に言えばこれだけの違いがあるのにシェルスクリプトを使うかどうかの選択を間違えるというのがよくわかりません。)
注意(2021-10-03 追記)よく読まずに勘違いされている方がいるようですが、論点は「やってやれない事はない」ではなく「適切な選択なのか?」です。シェルスクリプト or 他の言語の両方です。普通に実現可能な方法があるものを、いろんな回避策(ライブラリやコマンド等)を生み出してまで別の方法でやることに意味はありません。回避策などは「やらずに出来た方がずっと良い」です。実際に可能だとしても本当にそれ(シェルスクリプト or 他の言語)を使ってやる理由はあるのか?その理由は本当に正しいのか?その理由は今回も当てはまるのか?単に今自分が使える技術をいつも選んでいるだけではないのか?を考えるようにしてください。
シェルスクリプトとは
シェルスクリプトとはシェルによって実行されるスクリプト(プログラム)のことです。シェルで使える文法と全く同じものが使え定型的な処理をスクリプトにすることで作業を効率化させることができます。
シェルはいくつも種類がありますが、現在 Unix / Linux でよく使われているシェルの名前は bash、dash、ksh、zsh です。これらのシェルは POSIX で標準化された言語(シェルコマンド言語)をベースとしてそれぞれで独自の拡張を行った言語で書いたスクリプトを実行することができます。これらのシェルはベースの部分は POSIX に準拠しており(例外もそれなりにありますが)基本的に同じように動作します。拡張部分は他のシェルと互換性があったりなかったりとまちまちです。歴史的には 1988 年版の ksh88 を元にそのサブセットが POSIX シェルの仕様となっています。また 他のシェルは ksh の仕様を取り入れたり独自に改良したりしながら今も開発が行われています。なお dash は可能な限り拡張機能を行わない方針であるため純粋な POSIX シェルに一番近いシェルとなっています。
ksh 以前に(主に Unix で)使われていたシェルが Bourne シェルで ksh は Bourne シェルの上位互換のシェルとなっています。同様に bash、dash、zsh なども Bourne シェルの上位互換シェルです。またシェルには Bourne / POSIX シェルと互換性がない tcsh や fish といったシェルもあります。Bourne シェルよりも前に Unix で使われていた Thompson シェルや PWB シェルも Bourne シェルと互換性がありません。
広義のシェルスクリプトは、これらのシェル(Bourne / POSIX シェルと互換性がないものを含む)で動作するスクリプトの事になりますが、一般的にシェルスクリプトと言った場合、Bourne / POSIX シェルと互換性があるものを指していることが多いです。また Windows で使われるコマンドプロンプトや PowerShell もシェルの一種ですが、これらで動くスクリプトがシェルスクリプトとは呼ばれることは少ないです。
POSIX で標準化されているシェルコマンド言語は構造化プログラミングが可能な手続き型言語です。UNIX の最初のシェル(つまり Bourne シェルよりも前)である Thompson シェルはファイルに羅列したコマンドを実行することはできたものの、変数もや関数もなくフロー制御のための goto
コマンドでさえ個別のコマンドとして実装されており、スクリプト言語としては設計されていませんでした。Thompson シェルがプログラミングを行うのに不十分であることが明らかになり、よりプログラミングに適したものにすべく PWB シェルが開発されましたが、Thompson シェルとの互換性を維持する必要から改良は限定的なものとなってしまいました(例 変数は導入されたが 1 文字しか使えないなど)。その後、互換性を切り捨ててプログラミング言語として使えるように再設計されたものが Bourne シェルです。
なお、この記事ではこれ以降、単にシェルスクリプトと書いた場合は POSIX に準拠したシェル(bash、dash、ksh、zsh 等)で動作するシェルスクリプトのみを指すものとし tcsh、fish、PowerShell 等の POSIX シェルと互換性がないシェル用のシェルスクリプトは含めません。
補足 たまに fish を Bourne / POSIX シェルの拡張だと勘違いしている人がいますが、調べるか使ってみればすぐに分かる通り文法的にはほとんど互換性がありません。しかし fish 自体が可能な限り POSIX の文法に従うと書いてあったりします。これが勘違いの原因ではないかと思いますが、なぜ fish がこのようなことを言っているのかよくわかりません。
Whenever possible without breaking the above goals, fish should follow the Posix syntax.
シェルスクリプトの長所と短所
長所
シェルとシェルスクリプトで同じ言語が使える
多くの人にとってシェルはなくてはならないものだと思います。シェルの上で手作業を行いそれを何回か繰り返して面倒になったらそれをそのままスクリプトにスムーズに移行できるのが出来るのがシェルスクリプトの強みです。シェルとシェルスクリプトで別々の言語を使うべきと主張している人もいるようですが、シェルを使える人で、シェルスクリプトで普通にできることであれば、わざわざ別の言語を使う必要はないはずです。
驚くほど短いコードで目的を達成できる
シェルスクリプトはコマンドを実行するためにとても効率的な文法を採用しています。他の言語のように文字列をクォートするのは必須ではなく、引数を区切るカンマや行末のセミコロンも必要ありません。コマンドを実行するのに必要なのはそのコマンド名と引数だけです。他の言語のようにコマンドを実行するための関数は必要ありません。特にパイプ通信は |
で繋ぐだけという究極のシンプルさを実現しています。
コマンドの出力結果がデータと等しい
シェルスクリプトは多くの場合コマンドの実行結果をデータとして利用します。例えば find
コマンドで実行した結果をそのまま複数のファイルリストとして他のコマンドで処理します。コマンドはシェルからも実行するものであり、その出力結果は普段見慣れているものです。特殊な形式というわけでもなく見やすいテキスト出力形式です。そのため型がどうとかフィールド名がどうとかそういう余計なことを考えずにすみます。自分でコマンドを作る際にも各項目をスペースで区切るだけというシンプルかつ人間にも見やすくなるような形式に対応すれば十分です。
これは長所ではありますが「複雑なデータ構造を扱えない」という短所にも繋がります。複雑なものは扱いづらくなりますが、シンプルなのはとことんシンプルにできる。というのがシェルスクリプトです。
コマンドの互換性問題に対応できる
シェルスクリプトでは一般的にシェルコマンドを実行しますが、コマンドは同じ名前でも GNU と BSD でオプションや機能が違ったりするなど環境によって互換性がありません。しかしこれらの違いはシェルスクリプトのシェル関数を使うことである程度解決することができます。それはシェル関数とコマンドが交換可能な設計になっているからです。もし外部コマンドに互換性上の問題があれば、シェル関数を定義しそこでコマンドの違いを吸収することができます。これは他の言語では容易に実現することはできません。
シェルスクリプトはコマンドを使って処理を行う、ならばそのコマンドの問題を解決する機能も持っている。という考えはおかしなものではないと思います。
既存のコマンドを簡単に並列実行できる
シェルスクリプトにはコマンドを並列で実行するための xargs -P
や GNU Parallel
といった専用のコマンドがあります。またシェル自身でもバックグラウンドプロセスを作ることでコマンドを並列に実行させることができます。つまりコマンド自体がマルチコア対応でなくともシェルスクリプトによって CPU の性能を使い切ることができます。
なおパイプでコマンドを繋ぐだけでもある程度の並列実行の効果はありますが、うまく条件が揃わないと効率的に CPU を使い切ることはできませんし、並列数を柔軟に変えることができません。またパイプ間通信のオーバーヘッドにより遅くなってしまいます。I/O 待ちによる細かい CPU の空き時間を埋めるという効果は期待できますが、本格的に並列実行させるには専用のコマンドを使う方が効率的です。
省メモリである
シェルスクリプトとコマンドは基本的に行単位で独立したデータを扱う設計になっており、全体を読み込んで処理するようなことはあまり行いません。一行のデータは小さいためシェルスクリプトらしいコードを書くと、あまりメモリを使用せずにすみます。
これは私の推測ですが古い時代は複数の人が一台のコンピュータを共有していたので、一人が多くのメモリを長時間専有することは問題であったため、そこから自然と一行一データで処理するような言語設計になったのではないのかと思っています。
2021-10-23 追記 「How do pipelines limit memory usage?」より、初期のパイプラインの仕組みはパイプ通信間のバッファにメモリではなくファイルを使用していたということです。小さなプログラムとパイプラインの仕組みは省メモリを実現するためにあったということがわかります。
短所
最初に念を押しておきますが、シェルスクリプトにはここに上げるような多くの短所がありますが、だからといって不可能ということにはなりません。互換性が低くパフォーマンスや生産性が悪いような場合はシェルスクリプトでわざわざやる理由がないというだけです。互換性が低くとも特定のシステムだけを対象とすれば関係ありませんしパフォーマンスの悪さもデータが十分小さければ(ファイルサイズ数十MBなど)ほとんど問題にならない場合もあります。もしどうしてもシェルスクリプトでやらなければいけない理由がある場合は頑張ればできますが、他の言語なら頑張る必要なくできてしまいます。実現可能であるかどうかという話と、適切な選択であるかどうかという話を混同してはいけません。
OS が持つ POSIX 機能の多くが使えない
POSIX は Unix 系 OS が最低限持っていると期待されるポータブルな OS のためのインターフェースであり、アプリケーション開発に必要とされるものです。しかしこれは 主に C 言語から呼び出すための API として設計されており、シェルスクリプトから直接その機能を利用できるものは少ないです。そのため外部コマンド経由で間接的に使用することしかできません。他の言語であれば C 言語ライブラリのバインディングなどがあったり、より使いやすいインターフェースにラッピングされたライブラリがあり POSIX API の多くを呼び出すことが出来ますが、シェルスクリプトではそのようなものがないどころか作ることすらできないのです。
その理由の一つは「POSIX には型がある」からです。シェルスクリプトには(原則として)文字列型しかなく複雑なデータ構造を扱えません。int 型や float 型といったプリミティブな型だけではなく様々な複雑な構造体があります。そのため文字列型しかないシェルスクリプトではそれらの API を直接呼び出すことができません。
もちろん他の言語を使ってコマンドを作れば(遅くなるという問題が残るにしろ)使うことは出来ます。しかし POSIX API の全てがコマンドとして使えるように提供されているとは思えませんし、特に POSIX で規定されているコマンドに限れば最小限のものしかありません。つまりシェルスクリプトだけでは Unix 系 OS の最低限とされる機能すら、そのすべてを引き出すことは出来ないのです。
シェル間の互換性が低い
bash、dash、ksh、zsh など環境ごとに異なるシェルが使われていますが、それぞれのシェルで互換性があると期待できるのは POSIX で標準化されている所のみです。POSIX で標準化されている場合でも、POSIX の仕様自体が複数の異なる実装を許容しているため全てのシェルに完全な互換性はありません。例えば echo
の引数の解釈はシェルによって異なりますし、パイプで繋いだコマンドのうちどこがサブシェルになるかはシェル依存です。そのため複数の環境で動くようなシェルスクリプトの開発には各シェルの実装の違いを詳しく知る必要がありますし、実際の環境でテストしなければ動作保証ができず、可搬性があるシェルスクリプトを作るためには学習コストや検証コストが掛かってしまいます。
シェルコマンド言語の言語仕様が貧弱すぎる
シェルが実行できるスクリプト言語の名前はシェルコマンド言語と言います。このシェルコマンド言語は POSIX で標準化されていますがかなりミニマムな言語仕様に抑えられています。(個人的には bash、ksh、zsh など殆どの POSIX ですでに使える配列や拡張されたパラメータ展開の一部を POSIX で採用すべきだと思っています。)
POSIX で規定されている言語には「シェルコマンド言語 (sh
)」「awk プログラム言語 (awk
)」「C 言語 (c99
)」「FORTRAN (fort77
)」の 4 つがあります。(sed
もチューリング完全であるため言語と言えなくもないのですが、言語のような使い方をしている例は殆どないので省いています。ただし世の中にはsed
だけで作られたチェスやテトリスもあるようです。)
この POSIX で規定された言語の中で、実際 POSIX で言語仕様が標準化されているのはシェルコマンド言語と awk のみです。C 言語と FORTRAN に関しては他の団体で決めたものを採用しているだけです。この違いがシェルコマンド言語(とついでに awk)の機能の貧弱さにつながっているのではないかと考えています。
例えば C 言語には POSIX で採用されている C99 だけでなく C89、C90、C95、C11、C17 と今も改定が続けられています。FORTRAN も 60、77、90、95、2003、2008、2018 と改定が続けられています。かつては POSIX で採用されていた C 言語は C89 でした。おそらく何年か後には新しいバージョンに置き換わるでしょう(FORTRAN は 次の Issue 8 で削除されるようですが)。POSIX の方針とは無関係に C 言語は便利になっていきます。
一方 POSIX は言語仕様を策定する団体ではなく Portable Operating System Interface の略であるとおり「移植可能な OS のインターフェース」を標準化する団体です。シェルコマンド言語に(とても便利で誰からも望まれているような)新たな仕様を追加したとしても移植性は向上しません。言語の標準化団体と POSIX では目指している場所が違うため POSIX がシェルコマンド言語を便利さの点から改良することはありません。POSIX がシェルコマンド言語に新しい仕様を追加するとしたら、すべてのシェルで実装されて十分な移植性があると認めたときぐらいです。C 言語や FORTRAN のように言語仕様を改良する独立した団体は存在しないことが、シェルコマンド言語の言語仕様の貧弱さにつながっているのだと考えています。シェルコマンド言語や awk にも ISO や ANSI などの独立した標準化団体が必要なのかもしれません。
環境毎のコマンドの互換性が低い
シェルスクリプトは多くの場合外部コマンドを呼び出しますが、その外部コマンドも環境によって互換性が低いです。例えば macOS で作ったシェルスクリプトが Linux で動かないなどということが度々発生します。その理由はさまざまなベンダーが同じ名前のコマンドをそれぞれ作っているからです(ブラウザで言えば MS が IE を作って Google が Chrome を作るようなもの)。jq
や git
のようなものは開発している所が一つだけなので互換性の問題はバージョン違いによるものぐらいですが、ps
や tr
と言った基本的で古いコマンドほど複数のベンダーがそれぞれ作っているため互換性が低くなります。
その問題を改善するために生まれたのが POSIX なのですが将来の拡張の余地を残すために最低限の機能しか標準化しておらず、細かい差異をなくそうとはしなかった(細かい挙動を実装に任せた)ため完全な互換性は実現するものにはなりませんでした。そのためどこの環境でも動くようにするには、多くのバッドノウハウが必要になってしまい、単一のコードでどこでも動く他の言語よりも明らかに移植性が低くなってしまいます。
これが他の言語の場合は、コマンドを使って解決するような問題はその言語用のライブラリを使って解決でき、その方が標準出力をパースしてデータを解釈する必要もなくパフォーマンスがよいため、基本的な処理にわざわざコマンドを使う理由がありません。他の言語でコマンドを使うことがあるとすれば、高度な機能を持ったコマンドがあってそれを言語から使うための言語バインディングライブラリがない時ぐらいです。他の言語では単一のコードでどこでも動くようにライブラリが環境の違いを吸収していますが、シェルスクリプトでは単一のコードでどこでも動くようにするには、環境の違いの吸収を自分でやらねばならずとても大変な作業になります。
ちなみに POSIX の方針は(WHATWG の圧力によって?)どのブラウザでも同じように動くようにすべてを厳密に決めることにした W3C とは正反対の方針です(参考 主要ブラウザのHTML5互換テスト結果、W3Cが公表。HTML5時代のブラウザ互換性は、より高くなる)。さらに余談となりますが現在は W3C が公表していた HTML5 は廃止されました(参考 どうしてHTML5が廃止されたのか )。
処理速度が遅い(特に外部コマンドの起動)
シェルスクリプトはインタプリタでありスクリプト言語でもあるため、コンパイラ言語に比べれば遅いものとなります。ただし極端に遅いわけではありません。極端に遅くなってしまう場合の原因の殆どは外部コマンドを多数起動しているからです(例えば jq
で取ってきたデータをループで回し、その中で sed
を呼び出すなど)。外部コマンド自体は(一般的に)C 言語などで実装されているため実行自体は速いのですが、外部コマンドの起動が遅いです。そのためシェルスクリプトでは遅くしないためには外部コマンドの起動をできるだけすくなくする必要があります。
小数の計算やバイナリデータの処理ができない(遅い)
一部のシェルを除き、シェルスクリプトでは整数の計算しかできませんしバイナリデータを扱うことも出来ません。そのためこれらの処理をする場合は外部コマンド(bc
コマンドや od
コマンドなど)に頼ることになります。そうすることで扱うこと自体はできますが、前項の外部コマンドの起動が遅いという問題にぶち当たります。そのため気軽に小数の計算やバイナリデータを扱うことが出来ず、外部コマンドの起動回数を少なくなるように設計するなど他の言語では不要なシェルスクリプト特有の設計をおこなわなければいけません。
複雑なデータ構造を扱えない
基本的にシェルスクリプトでは構造体や階層構造を持った複雑なデータ構造を扱うことができません(ksh だと多少複雑なデータ構造を扱える機能があったりします)。連想配列ですら使えないシェルがありますし POSIX 準拠に限ると普通の配列ですら使うことができません。これらは工夫次第でシェルスクリプトでも実装は可能だと思いますが他の言語ではスマートの実装できるのにたいしてシェルスクリプトだと面倒で開発コストも掛かりパフォーマンスも悪くなってしまいます。
ライブラリやフレームワークが少ない
皆無ではありませんが、他の言語のように有名で広く使われており将来に渡っても自由に使える信頼性の高いライブラリやフレームワークは存在しません。そのため必要なものがあれば自分で作らねばなりません。自分で開発する場合に移植性が高いライブラリを作ろうとするならば自分でシェルやコマンドの互換性の違いを吸収して各環境で動くことをテストせねばならず開発コストが掛かってしまいます。
もちろん自分ライブラリやフレームワーク作れる技術力をつけることは重要ですが、実際の仕事ではライブラリやフレームワークを作ることは直接の仕事にはなりません。すでにある信頼性のあるライブラリやフレームワークを使うことは生産性、信頼性、移植性を上げるために重要なことです。Unix 哲学で有名な「UNIXという考え方―その設計思想と哲学」でも P69「定理6:ソフトウェアの梃子を有効に活用する」で「良いプログラマはよいコードを書く。偉大なプログラマはよいコードを借りてくる」「独自技術症候群を避ける」「コードを他者が梃子として使うのを認める」と他人が作ったコードを利用し、また利用されることの重要さを述べています。しかしながらシェルスクリプトにはライブラリやフレームワークがあまりありません。
補足 もちろん外部コマンド=ライブラリと考えることができ、それらを含めると多くのコマンドが使えるわけですが、シェル関数で作られたライブラリはシェル関数にアクセスできたり小さいデータを扱う際のパフォーマンスがよいというメリットが有るため、外部コマンドだけではは十分ではありません。また他の言語のライブラリは基本的にどの環境でも同じように動作しますが、上記で書いたように環境ごとのコマンドの互換性は低いです。
注意 現時点ではライブラリやフレームワークは少ないですが、将来は解決するであろうと私は推測しています。
ファイルのランダムアクセスができない
ksh を除きファイルのランダムアクセスができません。そのためランダムアクセスのような物が必要な場合、小さいファイルに分けるなどファイル・ディレクトリ構造をランダムアクセスができるような構造に設計する必要があります。しかしそうするとプログラムは複雑なものとなってしまい開発コストが掛かってしまいます。
またファイルのオープンやクローズも重い処理であるため多数の小さいファイルに分けてしまうとパフォーマンスに影響が出てきてしまいますし、効率的にするにはユースケースに依存したディレクトリ構造にする必要があるため将来的な変更に対する柔軟性が失われてしまいます。
ネットワークアクセスができない
bash、ksh、yash、zsh 等一部のシェルでは TCP / UDP 通信ができたりしますが POSIX 準拠の範囲ではネットワークアクセスを行うことができません。また TCP / UDP 通信ができたとしても上位プロトコル(例 HTTP)等のライブラリはないため自分で作らねばならず開発コストが掛かってしまいます。
もちろんネットワーク通信が行えるコマンド(curl
や wget
等)を経由すれば間接的にネットワーク通信を行うことができますが言語でネイティブで実行するのに比べるとで遥かに遅くなってしまいます。データベースサーバーなど多くのサーバーとの通信にはネットワーク通信を使用します。例えば MySQL との通信では他の言語はネイティブライブラリを使って高速にアクセスできますが、シェルスクリプトの場合は、最悪 SQL を実行するたびに毎回起動が遅い mysql
コマンドを実行することになってしまいます。
デスクトップ・スマホアプリなどの GUI を持つアプリの開発はできない
CLI だから当たり前だと思われるかもしれませんが、実は ksh の拡張として dtksh や tksh といった GUI ツールキットと連携できるシェルスクリプトを書くことが出来る実装が存在していました。なので一応シェルスクリプトでグラフィカルなインターフェースを作成することはできたわけです。ただし現在はそのようなことができるシェルは私が知る限り存在しません。(まあユーザーインターフェースを HTML で作り、その HTML をシェルスクリプトを使ってテンプレートから生成するぐらいのことは出来ると思いますが、その場合はユーザーインタフェースを作ってる言語は HTML というべきでしょう。)
本格的なゲームを作るのに適していない
過去には有名な Rogue などキャラクタベースのゲームもありましたし Gnome-Terminal など画像を表示することが出来るターミナルもあるのでノベルゲームやアドベンチャーゲームであれば十分面白いゲームは作れると思います。そもそもゲームの面白さとインターフェースは関係ありません。
ここで言っているのは「それが適切な選択なのか?」という話です。本格的にゲームを作るとしたらグラフィカルな画面や音楽も必要になりますし、スマホ等いろんなプラットフォームへの対応を考える必要もあります。不可能では無いと思いますが「やってみた」程度の意味にしかなりません。
もちろん「挑戦」という意味では面白いとは思います。どうやって音楽を鳴らすか?画像はどうするか?3D描画は可能か?リアルタイムなキー入力は実現できるか?常識とは違う方法で作るといろんな課題が見つかります。それらをアイデアで解決するのは楽しいでしょうし、どうやって解決したかを解説する記事はとても興味深いものになる(ウケる)と思います。
しかしゲームづくりという点ではその先には続きません。なぜなら「シェルスクリプトでゲームを作る」時のさまざまな課題とその解決策は、普通のやり方をしていれば発生しないからです。つまり本来ならば存在するはずがない課題を自分で生み出してそれを自分で解決したに過ぎません。普通にゲームを作るという目的であればすでに確立された効率の良い方法を使うのがベストです。
ウェブアプリケーションの開発に適していない
シェルスクリプトはネットワーク通信ができないためアクセスを待ち受けることができません。そのため他の言語で作ったミドルウェア(ウェブサーバー等)を使うことは必須となります。さらにネットワーク通信ができないため CGI を使うしかありません。CGI はアクセスのたびにプロセス起動します。これは短時間で処理を終える小さなプロセスが多数作成することとなり、外部コマンドの実行が遅いという影響をモロに受けてしまいます。
この CGI というのは 2000 年以前に主に Perl で使われていた技術でエンタープライズの分野ではとっくに過去のものとなっています。現在では負荷が小さいシステムや個人向けの格安なレンタルサーバーでアプリケーションを使うときぐらいにしか使われていません。また昔の技術に詳しい方であれば CGI のパフォーマンスの低さを改善するための FastCGI というのを知っているかもしれませんが、これはシェルスクリプトの一般的な外部コマンドを連携して処理を行うという使い方にまったく当てはまりません。大雑把に言うと FastCGI はアクセス時の外部コマンドの起動を抑制するためのものです。しかしシェルスクリプトではスクリプトの中で多数の外部コマンドを起動してしまうためたかだか最初の 1 回を減らした程度では FastCGI の効果は殆どありません。(そもそも FastCGI に対応したシェルスクリプトフレームワークは存在しないため使えないという根本的な問題があります。)
CGI については以下のスライドがわかりやすいと思います。(ただしこれは Perl のスライドです。ここで挙げられてるパフォーマンス上の問題を解決する技術はシェルスクリプトでは使えません。)
大量のデータを扱う処理が遅く柔軟性がなく信頼性確保が難しい
一般論としてデータの処理速度を上げるためには、ストレージよりも速いメモリを活用することで処理速度をあげます。メモリとストレージの速度の違いは、高速な SSD を使ったとしても数十倍、HDD であれば数百倍以上の差があります。例えばデータベースサーバーであれば多くのメモリを搭載しデータの多くを常にメモリに保持することで高いパフォーマンス性を実現していますが、シェルスクリプトの場合は、コマンドを実行するたびにすべてのデータをメモリ読み込み、処理が終わるとすべてのデータを解放してしまうためメモリの高速性が無駄になってしまいます。(これは逆に言えば「省メモリである」という長所にもなります。)
バッチ処理のような「どちらにしろデータをすべて読み込んで処理する」場合にはシェルスクリプトでも問題ないのですが、ウェブサービスなどで発生するランダムアクセスやデータ更新が伴うような場合には、シェルスクリプトはデータベースサーバーの代わりにはなりません。ネットワークアクセスができないためウェブサービスとデータベースサーバーを分離するという基本的な設計すら実装が困難になってしまいます(参考 3層アーキテクチャ)。それを無理やりシェルスクリプトで実装すると他の言語では不要な設計が必要となり開発コストがかかってしまう上にパフォーマンス上の重大な問題が発生する可能性も高くなってしまいます。
データベース(RDB、NoSQL 等)は多くのデータベース研究者(例 エドガー・F・コッド)の研究を元にしており大小の多くの企業やプログラマーによる発明です。信頼性を確保したり要求の変化に柔軟に対応するなど実際のユースケースを満たすための多くの機能を持っています。Unix によるコマンドの発明も十分晴らしいものですが、さすがにデータ管理の専門家が作ったものを超えることはできません。awk
や sed
によってデータを何度も加工していくやり方は現在のデータがどう加工されているかをいちいち出力して確認しなければわからず、ソースコードから状態を読み取るのがわかりづらくなります。餅は餅屋です。専用のミドルウェアに比べるとシェルスクリプトとシェルコマンドは多くの機能が不足しています。
補足1 RDB で採用されている SQL はデータを柔軟に参照・更新・加工するための専用の言語です。POSIX より長い歴史を持った国際標準規格(SQL86 は 1986 年に登場。参考 標準SQL規格)で、さまざまな製品で同じように使うことができます。多少の方言はありますが、それを言ったらシェルスクリプトはもっと多くの方言があります。SQL はプログラマなら必ず知っておくべきもの一つと言っても過言ではありません。特に業務システム系であれば必ず使用することになるでしょう。
補足2 NoSQL は名前から「SQL を使わないこと」と勘違いするるかもしれませんが、SQL を使う RDB とは別の考えでトランザクション処理等を行うもので SQL を使わなければ NoSQL になるというわけではありません。NoSQL は別の考え方を使うミドルウェアの一つです。シェルスクリプトでは排他制御などをする場合にもコマンドを使わなくてはならないために遅く、また専用のライブラリ使う他の言語に比べると十分な信頼性を実現するために難しいエラー処理が必要となります。
補足3 ミドルウェアは言語だけで独自で実装すると大変な作業になるものを肩代わりして高い信頼性やパフォーマンスを容易に実現するためのものです(参考 ミドルウェア)。例えば RDB であればデータを効率よくメモリに読み込んで簡単なコマンド(SQL)で柔軟にデータの加工して取得することが出来ます。それと同じことを言語やコマンドだけで独自で実装することの大変さは容易に想像できると思います。なぜ言語や OS だけでなくその間に使うミドルウェアが数多く生まれ、そして使われてきたのか?その答えはミドルウェアが重要であり大きなメリットがあるからです。
補足4 ミドルウェアという用語が分かりづらい人がいるかも知れませんが、例えばクラウドサービス(AWS や GCP)で提供しているサービスのほとんどはミドルウェアに該当します。今どき使わないで作るなんてありえないと言っても過言ではありませんね?
クラウドサービスとの連携が難しい
大抵のクラウドサービスではシェルやシェルスクリプトから使えるように CLI コマンドを用意している(例えば GCP であれば gcloud
コマンドや gsutil
コマンド)のですが、アプリケーションから本格的に使うのであれば API を使うことになります。しかしながらシェルスクリプト用の API ライブラリが用意されていることはありません。大抵は HTTP を使った JSON ベースのプロトコルなので curl
コマンドや jq
コマンドを使えば呼び出すことは出来るのですが、やはり外部コマンドの起動が遅いという問題にぶち当たります。言語ネイティブのネットワーク通信んを使った専用のライブラリが用意されている他の言語と比べれば、シェルスクリプトは圧倒的に不利です。
リアルタイム処理に適していない
POSIX ではリアルタイム API が規定されていますが、シェルスクリプトからそれらを直接使うことはできません。他の言語で外部コマンドを作れば一応はシェルスクリプトと連携させることが可能になるとは思いますが、コマンドの起動の遅さがリアルタイム処理に影響し、パイプ間通信においてもバッファがあるため、正確なタイミングを取ることが難しくなります。(バッファは無効にすることもできますが、それはそれで大幅にパフォーマンスが低下します)。また、どちらにしろ他の言語で外部コマンドの開発が必要であるならば、すべてその言語で作った方が楽であり、より正確に動作するのでシェルスクリプトを使うメリットがありません。
注意 リアルタイムシステムというのは決められた時間内に処理が完了することを保証するシステムのことであるため、必ずしも処理が速いという意味にはなりませんが、多くの事例では短い時間で反応しなければいけないものであると私は認識しています。
2021-10-22 追記 「シェルスクリプトのデータ出力タイミングが遅い? それはパイプ通信に起因するバッファリングが原因かもという話」で詳しい説明を追加しています。
シェルスクリプトの短所と思わないもの
文法が独特で可読性が低い
それはシェルスクリプトの文法を勉強していない、もしくは慣れていないだけのことです。変数に代入する時に空白がいらないとか、比較する時の []
に空白が必要で分かりづらいと言われます。たしかに奇妙な文法ですが「たったそれだけのこと」ではありませんか? どの言語にも特徴的な文法はあります。他の言語で言えばセミコロンが必要だとかブロックを作る時にインデントが必要だとかその程度のことです。シェルスクリプトは簡単な言語(?)であるがゆえにろくに勉強しないで使っているからハマるだけです(文法をろくに解説せずに応用編・使い方の例ばかり紹介している本が多いのも原因の一つだとは思いますが)。変数の代入や比較の書き方はシェルスクリプトの文法の教科書があったとすれば最初の十数ページ以内に書いてあるようなものだと思います。この時点でハマっているとしたらシェルスクリプトの文法を全く勉強してないとしか思えません。
シェルスクリプトの文法に慣れていないのであれば(慣れていたとしても) ShellCheck を導入するのがシェルスクリプトを覚える近道です。上記のような空白が不要だったり必要だったりする場合も指摘してくれます。もちろんシェルスクリプトの"文法"について解説している技術書やウェブサイトを見て勉強するのもよいでしょう。英語が得意であれば POSIX のシェルコマンド言語を参考にしても良いと思いますし、開発者が日本人である yash の日本語ドキュメントも大いに参考になると思います。(yash 独自の拡張があるので注意が必要です。)
awk や sed や正規表現が難しい
awk や sed は別のコマンドの話なので厳密にはシェルコマンド言語の話ではありませんが、シェルスクリプトからよく呼び出すコマンドでありシェルスクリプトを書くのであれば使えるようになったほうが良いと思います。難しいというのは事実かもしれませんが、それだけではシェルスクリプトの短所とは言えません。誰しも勉強してないものは難しいと感じるものです。特に正規表現は便利で他の言語でもよく使うものなので覚えるべきでしょう。コマンドによって使える正規表現が微妙に異なるという問題はありますが、他の言語でも同じだったりするのでシェルスクリプト特有の問題とは言い切れません。
ところで最近私が思ってることなのですが、みんな文字列加工に awk
や sed
を使いすぎなのではないでしょうか?実は変数展開(パラメータ展開)を使えば文字列加工は awk や sed などの外部コマンドを使わずに実装することができます。例えば sed
の代わりは ${str//FOO/BAR}
とするだけです。POSIX シェルの範囲では置換がしづらいですが、それでも実装することはできます。
もちろん変数展開ですべてやるべきと言っているわけではなく、標準出力がリスト(複数のデータを複数行で表現されているもの)になっているものは awk
や sed
を使ったほうが良いです。しかしコマンド置換を利用して取得した単一の値の場合は変数展開を使用した方がシンプルでパフォーマンスも良いです。
開発者が少ない
シェルスクリプトに詳しい開発者は少ないです。またパイプを使った関数型言語に近い考えの設計が必要となり、これは他の言語とは異なるため適切にシェルスクリプトを使えるようになるまでの教育コストが掛かってしまいます。実際の開発の現場では「短所」にはなりますが、厳密に言えば人の問題であるためシェルスクリプトの問題には含めません。
テストやデバッグがしづらい
シェルスクリプトはテストがしづらいと言われますが、それはシェルスクリプト自身の問題と言うよりもシェルスクリプトが扱う問題が環境に副作用を与えるものが多いからだと思っています。同様の処理を行うものを他の言語で作ったとしてもテストはしづらくなるでしょう。
もちろんシェルスクリプトにもテストフレームワークはあります。GitHub のスターが多い順(有名順?)に Bats-core、shUnit2、ShellSpec があります。ちなみに ShellSpec は私が開発しているユニットテスト用のテストフレームワークです😁
デバッグも BASH Debugger(kshdb と zshdb もあります)などがあります。これを使えば gdb のようにブレークポイントやステップ実行ができます。(ただし私は小さい関数単位で開発していくスタイルでデバッガを使うほどではないため使っていません)。シェルにも -n
による文法チェック機能が搭載されていますし -x
や -v
オプションを使うことでシェルの実行ログを出力することができます。ちなみに実行ログは普通に使うと出力が多くなりすぎで大きなスクリプトでは実用とは言い難いですが ShellSpec には -x
オプションのサポート機能が搭載されており、関数単位でテストすることでログ出力も関数単位で出力することができるようになっています。
他の言語でコマンドを作り、シェルスクリプトで実行する
ここまで見てきたとおりシェルスクリプトには多数の欠点があります。シェルスクリプトの適用範囲は基本的にバッチ処理です。処理を開始しそのまま止まることなく実行して終了する用途に使います。ウェブサービスやデータベースサーバーやスマホアプリのように起動して待ち続け反応があったらすぐに処理をして結果を返すような用途には全く向いていません。間違った用途に使うことは不可能でなくとも生産性やパフォーマンスや信頼性は大きく低下します。「できなくはない」を採用する理由にしないでください。
それでもなおシェルスクリプトは重要な言語です。一般的にはシェルスクリプトでメインの処理を行うものではありません。いろんなコマンドを実行しそれらを連携させるために使います。コマンドは使えるコマンド(sed
、awk
、jq
等)が見つかれば、もちろんそれらを使ってよいのですが、もし適切なものがなければその時は自分の手で専用のコマンド作ります。そのコマンドを作るための言語は C 言語、Go、Rust、JavaScript、Ruby、Python 等なんでも使うことができます。この時それぞれのコマンドにいろんな機能を詰め込むのではなく、独立して行うことが出来る小さなコマンドにしておきます。そしてそれらをシェルスクリプトで自由に組み替えて呼び出す。これがシェルスクリプトの重要な役割です。コマンド単位で独立していれば、それらを並列実行させることも容易になります。どの言語でコマンドを使ってもその言語用の並列処理コードを書くことなく、シェルスクリプトで並列実行させることが可能になります。つまりシェルスクリプトは他の言語のコードの有用性をより高めることが出来るものなのです。
シェルスクリプトが苦手だからと単一の他の言語で作ろうとする人が多いですが(逆にシェルスクリプトだけで作ろうとする人もいますが・・・ん?私か?)、元々シェルスクリプトは Unix で C 言語で作られたコマンドと組み合わせて使うために作られたものです。シェルスクリプトと他の言語を組み合わせて使うことは最初から想定されている正しいやり方なのです。昔はコマンドを作るのに C 言語ぐらいしか選択肢がありませんでしたが別に C 言語に固執する必要はありません。Unix の時代には C 言語ぐらいしかなかったから C 言語が使われていたというだけで今は C 言語よりも効率的で高い生産性と移植性を実現できる言語がいくつもあります。それらの言語のうち問題を解決するのに適切な言語を使ってシェルで実行できる小さな機能を持ったコマンドを作り、シェルスクリプトで組み合わせて使うのが正しい使い方です。例えば Ruby で作った関数と Python で作った関数を組み合わせることは難しいですが、それぞれをコマンドにしてしまえば標準入出力という共通のインターフェースを介してシェルスクリプトで組み合わせることができます。このようにシェルスクリプトはコマンドが別々の言語で作られていたとしても、それらを組み合わせることが得意でありグルー(接着剤)言語1とも呼ばれています。コマンドを組み合わせるのに必要なのは標準入出力を適切に使用することだけです。(参考 シェルスクリプトのための良いデザイン ~ expr と bc から知る設計の違い ~)
Unix 哲学には「ソフトウェアを梃子(てこ)として使う」「シェルスクリプトによって梃子の効果と移植性を高める」という言葉があります。梃子とは再利用可能なモジュールのことです。ここでいう再利用可能なモジュールとはコマンドのことです。そのコマンドをシェルスクリプトによって価値を高めるというのが Unix 哲学の考え方です。繰り返しますが全てをシェルスクリプトと既存のコマンドだけでやらなければならないということではありません。それは Unix 哲学の考え方ではありません。適切なコマンドがなければ自分で作るというのが重要な事です。コマンドは必ずしも汎用的なもの(他のプロジェクトでも使えるようなもの)にする必要はありません。適切な単位で独立していれば再利用可能(異なるシェルスクリプトからも使えるよう)になります。シェルスクリプトでやるのが難しい処理ならば他の言語で作ればよいのです。他の言語で書くことが出来る人であれば自分でコマンドを作ることは難しいことではないでしょう。
シェルスクリプトはあなたがシェルスクリプト以外の言語で作ったコマンドの価値を引き上げるものです。シェルスクリプトはともだちです。こわくなんかありません。怒りをシェルスクリプトにぶつけるのはやめましょう。(というか単純に便利なものを使わないのはもったいないですよ?)
-
Python もグルー言語と言われていたりしますが、そのためには Python 用に専用のモジュールが必要だったりするため、シェルスクリプトのグルー言語とは意味が異なります。シェルスクリプトの場合は標準入出力というすべての言語で共通で使われているインターフェースを利用します。 ↩