シェルスクリプトで読みやすく、後から変更しやすいプログラミングを行うには、手続き型のプログラミング言語とは違ったポイントを押さえなくてはならない。筆者はそのコツを「アット・ア・グランス性」と表現している。(編集部)
シェルスクリプトのプログラミングには、手続き型のプログラミング言語とは違った癖がある。
CやJava、または人気のほかのスクリプト言語を使ってきたユーザーがシェルスクリプトを記述する場合、意識する、しないにかかわらず、これまでのプログラミング言語の流儀をスクリプトの書き方に持ち込むものだ。しかし、シェルスクリプトではそのアプローチはあまりうまくいかない。ちょっとばかり視点を変える必要がある。
シェルスクリプトは、一見すると手続き型のプログラミング言語のように見える。実際、そういった使い方ができる。しかしながらこれまでの経験から、そうした手続き型的なアプローチはシェルスクリプトの場合にはあまりうまくいかないことが多い。
今回の話は、多くのシェルスクリプトユーザーにとって、とても奇妙なアイデアに見えるかもしれない。今回のシェルスクリプトプログラミングのポイントは次の8つだ。
私はこの特徴を表現する言葉として「アット・ア・グランス性」という言葉を使っている。
アット・ア・グランス(at a glance)というのは「ひと目見て」とか「一見して」「一瞥して」といった意味の言葉で、シェルスクリプトプログラミングのコツを表現する適切な言葉ではないかと考えている。一瞥してすぐに分かるようなコード、これがシェルスクリプトプログラミングのコツだ。
次に、項目ごとにその要点を説明していく。
シェルスクリプトプログラミングは、処理の流れをできるだけ1本のストーリーで組み立てて、簡単で分かりやすいコマンドをパイプで組み合わせるといった仕組みにする方が理解しやすく、仕様の変更にも強くなる。
以降でそれぞれ細かい項目を説明する。今回の記事の最後には、データを処理するシェルスクリプトを2つの違う発想で組み立てたものを示し、その差を説明する。
シェルスクリプトプログラミングのコツの1つは、「関数を使わない」ことにあると思う。
手続き型プログラミングでは関数を作成し、必要に応じて関数を呼び出すことで処理を進めていく。関数を作成して処理そのものやソースコードを再利用することで、プログラミングの効率がアップする。関数は意味や機能ごとにまとめられ「ライブラリ」と呼ばれるようになる。
しかしながら、シェルスクリプトでこれをやると、実際のところ、わけが分からなくなる。作っている段階やスクリプトを整理する段階ではよいのだが、シェルスクリプトでは変数のスコープが分かりにくいところがある。パイプをかませるだけで処理がサブプロセスに入るなど、理解しにくい。また、変数のスコープも実はそれほど直感的ではない。気を付けないと変数の扱いを勘違いすることが多い。
シェルスクリプトではOSの提供する「コマンド」が関数に相当する、と考えると分かりやすい。もし関数を定義する必要が生じるのであれば、それは別のファイルでシェルスクリプトを組んで「コマンド」として扱った方がよいし、必要があればCでコマンドとして組み上げる方がよい。
もちろん、関数を作り、ライブラリのようにまとめあげて使い、うまくいっている例もある。OSのシェルスクリプトフレームワークなどがその代表的な例だ。
しかしこの例は開発の「堅さ」が違う。開発者が個人で開発に使うのと、OSのフレームワークとしてシェルスクリプトを使うのとでは、開発時における堅さが異なる。OSで使うようなシェルスクリプトの関数は、一度コミットされたら、以降変更される可能性は非常に低い。そうした堅い使用例ではシェルスクリプトの関数も利用できるし、利用されるシーンも限定されたものとなるので、悪い面よりもよい面が強く出る。
分岐させると、一瞥して理解することが難しくなる。
シェルスクリプトでの分岐は、ifまたはcase、&&や||で実施することになるが、これはなるべく使わない方がよい。データを工夫すれば分岐処理そのものをなくすことができるし、どうしても分岐が必要な場合でも、デフォルト値を設定することでelifやelseを削除できる。
また、elseやelifを削除するためのテクニックとして、すべての条件を記述するという方法もある。「条件 && コマンド」のように、いったん分岐してelseするのではなく、分岐の対象となるすべてのコマンドに対して分岐条件を展開する。
これも手続き型的なプログラミングテクニックからすると奇妙なコーディングに見えるが、やってみるとこちらの方が何かと変更にも強く、一瞥して理解しやすい。
forやwhileの繰り返しは便利だが、これもできれば避けた方がよい。特にwhileはプログラミング上、とても理解しにくい状況を生むので、できれば避けたい。
繰り返しは結局視線をまた上へ戻す作業を発生させるし、そこを理解するのに頭の中で条件を判定しながら読む必要がある。しばしばサブシェルが起動して処理が別プロセスに入るシェルでは、繰り返し個所は理解を難しくする場所になりやすい。
シェルではサブシェルには変数が引き継がれる。しかし、サブシェルで変数の値を書き換えても、元のシェルにはその変更は反映されない。別のプロセスとして実行されているのだから当然だ。つまり、cat | whileのように1つでもパイプを挟むと、この処理は丸ごとサブシェルへ移るため、whileの内部で変数を変更しても元のシェルには反映されない。
パイプで接続した段階でサブシェルがfork(2)される、という動作を知らないと、なぜwhileの中での値の変更ができないのかと理解に苦しむことになる。結局、これを回避するために標準出力やファイルにデータを出してまた別途データを加工するといった処理を追加することになり、コードが煩雑になる。
数行程度であれば、whileの繰り返しは効果が見込めることもある。短く明快であれば一瞥して理解できるからだ。このあたりもアット・ア・グランス性ということになる。
しかし、ある程度長くなるようであれば別の処理を検討する方がよい。変数を変更するような処理であればwhileは避けるべきだ。例えばいったんファイルにデータを吐き出して処理をするなり、xargs(1)を使うなり、パイプやファイルを活用することで上から下へ処理が流れるスクリプトを書くことができる。
処理はできるだけその行の近辺で完結し、ほかの行へ影響を及ぼさない方がよい。処理の区切りごとにデータをファイルへ書き出し、次はそのファイルから処理を始める、という作り方をするとこれを実現しやすい。
いちいちデータをファイルに書き出すというやり方は、手続き型プログラミング的な発想からすればかなり奇妙なことのように思えるが、シェルスクリプトではこの使い方の方がうまくことが運ぶ。ファイルへの書き出しの容易さ、ファイルの読み込みの簡単さはシェルスクリプトやUNIXコマンドの特徴といえる。
これまで説明してきた方法を実践すると、上から下に読み進めることで内容を理解できるストイックなスクリプトが出来上がる。処理するためのストーリーがどうしても何度も分岐し、ここで説明したような、いわゆる規制を実現できない場合、まず処理対象となるデータをもっと処理しやすい形式へ加工することが必要といえる。
また、既存のコマンドではここで書いたような規制がうまく実現できない場合、特定の動作をするコマンドを追加することで、それを回避できることもある。シェルにおけるコマンドは、手続き型における関数のようなものであり、無理に既存のコマンドを使い回そうとするのではなく、存在しない場合には作った方がよい。
シェルスクリプトはプログラミング言語としての機能は限られたものだが、意外に機能が複雑で、慣れてくるとすごくトリッキーな記述ができるようになる。
シェルスクリプトの中級者にさしかかると陥りがちなことの1つに、何でもかんでも高度な書き方をするようになる、というのがあるが、これはやめた方がよい。だいたい後になって苦労することになるのは、ほかならない自分自身だ。
簡単な例で比較してみよう。次のデータを得たいケースを考える。
+1 +2 +3 +4 +5 -6 -7 -8 -9 -10
手続き型のプログラミングに慣れていれば、次のようなコードが浮かんでくるのではないだろうか。
#!/bin/sh for i in $(seq 1 10) do if [ 6 -gt ${i} ] then echo +${i} else echo -${i} fi done
だが、シェルスクリプトの制御構文と変数、コマンドの組み合わせは、お世辞にも読みやすいとはいい難い。ここでは次のように書いた方がよい。
#!/bin/sh echo '+1' echo '+2' echo '+3' echo '+4' echo '+5' echo '-6' echo '-7' echo '-8' echo '-9' echo '-10'
この方が書き換えに強い。例えば、次のようなデータを得たいといったように仕様が変わったとする。
+1 +2 +30 +4 +5 -6 -7 -8 -9 -10
この場合、上のコードは例えば次のように書き換えることになるだろう。
#!/bin/sh for i in $(seq 1 10) do if [ 3 == ${i} ] then echo +${i}0 elif [ 6 -gt ${i} ] then echo +${i} else echo -${i} fi done
こうなると、コードを読んでも何がしたいのか、ちょっとよく分からない状況になりつつある。一方、愚直なまでにechoを繰り返した2つ目のコードは、次のように1個所書き換えるだけで済む。
#!/bin/sh echo '+1' echo '+2' echo '+30' echo '+4' echo '+5' echo '-6' echo '-7' echo '-8' echo '-9' echo '-10'
このようにシェルスクリプトでは、書き方の違いひとつで、読みやすさや後からの変更のしやすさが格段に違ってくる。繰り返し処理の展開でコード量が増えることが問題だというのであれば、それはおそらくシェルで処理するのではなく、専用にコマンドを作成した方がよいタイミングに来ているといえる。
Copyright © ITmedia, Inc. All Rights Reserved.