シェルスクリプトに向いている用途

シェルスクリプトに向いている用途は、大抵の場合、以下の要件を1つ以上満たしているものだ。

  1. 作りたいものが単機能で、あまり複雑ではない。
  2. 非対話的な処理である。
  3. 既存のコマンドの羅列/組み合わせで容易に解決できる内容である。
  4. 他の言語でいう「メソッドチェーンによるコレクションからコレクションへの変換」で済む処理である。
  5. 扱うデータが「1行1レコードのテキストデータ」である。もしくは、その形式に容易に変換可能である。
  6. 扱うデータの件数(レコード数)が多い。
  7. 事前に扱うデータ群から異常値が取り除かれている。もしくは、異常値や外れ値を単純に取り除くだけでよい(≒エラー処理での高度な「特別扱い」が不要である)。
  8. 実装するにあたり、中間データを作成して使い回したいが、中間データをオンメモリにせずファイルシステム上に置いても問題ない。

作りたいものが単機能で、あまり複雑ではない

シェルスクリプトで作られるツールは、どちらかといえば単機能――特定の機能のみ提供するものだ。これはUnix哲学の影響によるところが大きいのだが、シェルスクリプト自体も、あれもこれも機能を提供するようなツールは実装しにくい。

ただし「コマンドの羅列/組み合わせで容易に解決できる内容」に向いている、という特性より、解決したい課題を整理・分割した際に個々のパーツをコマンドとして実装できるようなら、それらの独自実装コマンドを組み合わせることで複雑な課題を解決できる余地があるし、独自実装コマンド群のラッパースクリプトを書くことで、例えるなら「apt-get(8)にたいするapt(8)」のような関係の、複数の機能を提供するツールを仕立て上げることもできなくはない。

非対話的な処理である

シェルスクリプトの王道は、定型処理の自動化である。自動化においては、対話的な部分の排除が発生する。そのため、シェルスクリプトの多くは非対話的なものとなる。

とはいえ、シェルスクリプトでも、ftp(1)のような対話型操作のツールは作れなくもない。もう少し凝ったUIならtput(1)やdialog(1)で実装できるし、GUIもダイアログ程度ならXdialogで表示できる。

もっとも、シェルスクリプト内でデータを保持して使い回すのが苦手なこともあり、大抵はraspi-configのように「実際の処理は別のコマンドで実行する。シェルスクリプトはユーザ操作を受け取って個々のコマンドを叩くだけ」という、ある種のラッパースクリプトとして実装される。

既存のコマンドの羅列/組み合わせで容易に解決できる内容である

……まあ、シェルスクリプトはグルー言語の中のグルー言語だから、しかたないね ;)

素のシェルスクリプトは、それ自体の表現力は高くない。bash(1)やzsh(1)の独自拡張機能アリアリなら改善されるものの、それでも他のプログラミング言語と比べれば――お察しください。

今も昔もシェルスクリプトの本業は、職人たち(≒コマンド群)に指示して工事を進める現場監督だろう。

他の言語でいう「メソッドチェーンによるコレクションからコレクションへの変換」で済む処理である

「フィルタをパイプで連結してテキストレコードを処理する」とは、つまりそういうことだ。

扱うデータが「1行1レコードのテキストデータ」である。もしくは、その形式に容易に変換可能である

awk(1)はそうでもないが、他のテキストフィルタのことを考えると、「1行1レコード」の形式が圧倒的に扱いやすい。

ということは、元データが他の形式であっても、一旦「1行1レコード」に変換できてしまえば、後はテキストフィルタで活殺自在だといえる。XMLならxmllint(1)、JSONならjq(1)などを駆使して「1行1レコード」のテキストに変換した上でテキストフィルタに流し込むのは常套手段だ。

扱うデータの件数(レコード数)が多い

シェルスクリプトが呼び出すコマンドは、それがシェル組み込みコマンドでもない限り、単体のプログラムな訳で――コマンド呼び出しの度にプロセスの生成・削除が発生する。

Unix環境は比較的プロセス生成コストが低いといえども、他の言語における関数/メソッド呼び出しと比較すれば、やはりコストは高めだ。同じシェルスクリプト内であっても、シェル組み込みコマンドやシェル関数の呼び出しの方が圧倒的に速い。

(記録のために書いておくと、Cygwinではもっとコストがかかる。というのも、大抵のWindows上ではセキュリティ対策ソフトが動作していて、ヒューリスティック検知機能が有効化されている。その手の機能はWindows APIをフックしていて、プログラム実行時(≒プロセス生成時)にAPIフック経由で検知機能を実行していることが多い。そのため、純粋なプロセス生成コスト+αの時間が必要となる)

複数の外部コマンドをパイプで連結して使う場合、連結した個々のコマンドのプロセスが生成される。コマンドが増えれば増えるほど、プロセス生成コストがかさむ。このコストをペイするには、やはりそこそこの件数(レコード数)のデータを処理する必要がある。コマンド10個の処理にたいして元データがたった1件だったなら、赤字確定だ。

だから個人的には、1行分のデータをecho(1)を使って「パイプで連結した複数のフィルタ」に流し込むようなやり方は、好きになれない。そうするしかないことも多いのだが、なんかもにょる。

さらにいえば、while readでテキストを1行ずつ読み込んだ上でecho(1)を使って「パイプで連結した複数のフィルタ」に流し込む方法は、もっと好きになれない。そもそもシェルのループ構文中で外部コマンドを呼び出すこと自体が筋が悪い。大抵のテキストフィルタは勝手に複数行のテキストを処理してくれるものだから、while readを外して一気にフィルタに流し込めないものか、あれこれ考えてしまう。

まあしかし、プロセス生成コストに関しては、もう少し慎重に検討すべき事項だろう。なぜなら大抵のテキストフィルタはシンプルなので、実行ファイルが小さく、プログラム起動時のロード時間が短くて済む傾向にあるからだ。他のスクリプト言語では、処理系自体が大き目でロード時間がかかることがある。この辺は、もう実運用環境で計測してみないと分からない話だ。

事前に扱うデータ群から異常値が取り除かれている。もしくは、異常値や外れ値を単純に取り除くだけでよい(≒エラー処理での高度な「特別扱い」が不要である)

他の言語で「メソッドチェーンによるコレクションからコレクションへの変換」というコードを書いたとき、メソッドチェーンの中にエラー処理をうまい具合に埋め込むことが可能なものか、よく考えてみるべきだ。ちょっと厳しくないだろうか?

つまり、「パイプで連結した複数のフィルタ」にテキストレコードを流し込む前に、事前に異常値を取り除く作業を行う(各種エラー処理はそこで行う)べきなのだ。フィルタには、異常値が取り除かれたデータを送り込むべきである。

あえて言うなら、異常値や外れ値を単純に取り除くだけならば、SQLLINQでいう「Whereによるフィルタリング」のような要領で、そういう役割を果たすフィルタをパイプで連結することで実現できるが……逆に言えば、その程度が限度である。

実装するにあたり、中間データを作成して使い回したいが、中間データをオンメモリにせずファイルシステム上に置いても問題ない

素のシェルスクリプトには言語機能としての配列は存在しない*1bash(1)やzsh(1)などの独自拡張機アリアリなら配列が使えるが、他の言語と比べると制限がある。

じゃけん、一時ファイルを使いましょうね――という話。

シェルスクリプトに向いていないことは?

シェルスクリプトに向いていない(または別のツールの方が向いている)ことは色々とある。

シェルスクリプト自体の表現力は高くないし、変数や配列などの「状態を保持する」ための機能は癖が強くて扱いにくい。なので、ゴリゴリとコードを書いてアルゴリズムを実装するような用途には向いていない。

どちらかといえば、そのようなケースでは「コアの部分を独立したコマンドとして別の言語で実装し、シェルスクリプトでラッピングする」というアプローチをとるべきだろう。

例えば、以前XSLT-PFというツールのJava版を作ったときには、中核部分をJavaで実装しつつシェルスクリプトでラッピングして、コマンドのオプション処理などはシェルスクリプト側で行うようにした。

最近だとppbinというツールをPythonで実装したが、これも「コアの部分を独立したコマンドとして別の言語で実装」の一例で、これを他のテキストフィルタ等と組み合わせて、バイナリファイルからある言語の配列形式に変換するツールを作っていたりする。

先に書いたが、シェルスクリプトは基本的に「現場監督」が本業だ。だから、職人たち(≒コマンド群)を連れてくるか、もしくは養成(≒コマンドを自作)したうえで、彼らに作業を割り振って工事を進める(≒コマンドを組み立てて処理をする)のが、もっとも効率がよい。シェルスクリプト自身に職人としての役割を担わせるのは、非効率だ。この点は、作るものが非対話型のコマンドだろうと対話型のツールだろうと同じだ。

もっとも、「現場監督」に徹するケースでも、例えば「ファイルAを入力としてファイルBを生成する」というような作業を大量に行うとか、「途中で失敗したら中断する」とか、そのような要求がある場合にはMakefileの方が向いている。make(1)は、あれはあれで非常に興味深いツールだ。

*1:ただし配列っぽいものをシミュレートすることはできる。