「アット・ア・グランス性」確保のための8つの原則:スマートな紳士のためのシェルスクリプト(7)(2/2 ページ)
シェルスクリプトで読みやすく、後から変更しやすいプログラミングを行うには、手続き型のプログラミング言語とは違ったポイントを押さえなくてはならない。筆者はそのコツを「アット・ア・グランス性」と表現している。(編集部)
ほかのファイルはインクルードしない
shでは「.」を使うことで、ほかのシェルスクリプトをその場に展開できる。設定ファイルをどこかに書いておいて「.」で読み込んで反映させるといった、インタラクティブな用途で使われることもある。
だが、シェルスクリプトではこれは使わない方がよい。だいたい「.」で読み込むような用途は、変数のデフォルト値をまとめておいたり、共通して利用できる関数をまとめたライブラリ的な扱い方をするときに使うことになるが、よい効果よりも悪い効果が生じるケースが多い。
必要な機能はスクリプトとしてコピーし、1つのファイルに直接書いておく方がよい。繰り返しになるが、こうしてアット・ア・グランス性を確保することが大切だ。
ほかのファイルをいちいちトレースして追っていくのは作業としてストレスになる。変数のスコープ的にも、シェルにおけるファイルのインクルードはプログラミング的に扱いやすい機能とはいい難い。
実践例:売上データの集計処理に見る違い
では、簡単なスクリプトを書いて、アット・ア・グランス性の有無によってどういった違いが表れるかを見てみよう。まず、次のようなデータを用意する。
$ head data 003 10 001 53 002 13 005 38 003 71 000 27 001 94 008 36 000 24 001 48 $
1列目は商品番号、2列目は売上数が書き込まれた売上データファイルだ。この売上データを集計して「商品番号 本日の日付 売上の総数」を出力するスクリプトを作成する。手続き型のプログラミング言語的な発想でいけば、次のようなシェルスクリプトを書くことが多いのではないかと思う。
#!/bin/sh m0=0; m1=0; m2=0; m3=0; m4=0; m5=0; m6=0; m7=0; m8=0; m9=0; while read p i do case "${p}" in 000) m0=$((${m0} + ${i})) ;; 001) m1=$((${m1} + ${i})) ;; 002) m2=$((${m2} + ${i})) ;; 003) m3=$((${m3} + ${i})) ;; 004) m4=$((${m4} + ${i})) ;; 005) m5=$((${m5} + ${i})) ;; 006) m6=$((${m6} + ${i})) ;; 007) m7=$((${m7} + ${i})) ;; 008) m8=$((${m8} + ${i})) ;; 009) m9=$((${m9} + ${i})) ;; esac done < data for i in 0 1 2 3 4 5 6 7 8 9 do eval echo $(printf "%03.0f" ${i}) $(date +%Y-%M-%d) \${m$i} done
これを実行すると次のようなデータが出力される。シェルスクリプトとしてもけっこうすっきりしているし、そのままCやJavaでも書き換えできそうだ。
000 2012-51-13 4889 001 2012-51-13 5341 002 2012-51-13 4669 003 2012-51-13 4001 004 2012-51-13 4912 005 2012-51-13 4672 006 2012-51-13 5326 007 2012-51-13 4773 008 2012-51-13 5240 009 2012-51-13 5190
一方、今回書いたような規制を考慮してスクリプトを組むと、次のようになる。
#!/bin/sh grep 000 data | awk '{print $2}' | total > sum00 grep 001 data | awk '{print $2}' | total > sum01 grep 002 data | awk '{print $2}' | total > sum02 grep 003 data | awk '{print $2}' | total > sum03 grep 004 data | awk '{print $2}' | total > sum04 grep 005 data | awk '{print $2}' | total > sum05 grep 006 data | awk '{print $2}' | total > sum06 grep 007 data | awk '{print $2}' | total > sum07 grep 008 data | awk '{print $2}' | total > sum08 grep 009 data | awk '{print $2}' | total > sum09 echo 000 $(date +%Y-%M-%d) $(cat sum00) echo 001 $(date +%Y-%M-%d) $(cat sum01) echo 002 $(date +%Y-%M-%d) $(cat sum02) echo 003 $(date +%Y-%M-%d) $(cat sum03) echo 004 $(date +%Y-%M-%d) $(cat sum04) echo 005 $(date +%Y-%M-%d) $(cat sum05) echo 006 $(date +%Y-%M-%d) $(cat sum06) echo 007 $(date +%Y-%M-%d) $(cat sum07) echo 008 $(date +%Y-%M-%d) $(cat sum08) echo 009 $(date +%Y-%M-%d) $(cat sum09) rm sum0[0-9]
ポイントは「total」というコマンドを別途用意しているところにある。
totalは次のような内容になっている。下記の通り、処理が短いのでwhileを使っている。常用するならば、このコマンドはC/C++あたりで書き換えればよい。
#!/bin/sh sum=0 while read i do sum=$(($sum + $i)) done echo $sum
後者のスクリプトであれば、例えば「商品番号003だけは日付を1日ずらして出力してほしい」とか、「商品番号006の売上数を1000増やしてほしい」といった要望があっても、1行書き換えるだけで済み、すぐに応えることができる。
一方、最初のスクリプトで同じことを実現しようとすると、分岐とインデントが増えていってどんどん読みにくくなるだろう。1年後や2年後にスクリプトを読んだとき、なぜこんな処理になっているのかを理解するだけで一苦労で、一瞥して理解できる、といったことにはならないだろう。
また、このスクリプトはバグを誘発しやすい典型的な問題をはらんでいる。例えば次のように書き換えた場合、このスクリプトは機能しなくなる。
#!/bin/sh m0=0; m1=0; m2=0; m3=0; m4=0; m5=0; m6=0; m7=0; m8=0; m9=0; cat data | while read p i do case "${p}" in 000) m0=$((${m0} + ${i})) ;; 001) m1=$((${m1} + ${i})) ;; 002) m2=$((${m2} + ${i})) ;; 003) m3=$((${m3} + ${i})) ;; 004) m4=$((${m4} + ${i})) ;; 005) m5=$((${m5} + ${i})) ;; 006) m6=$((${m6} + ${i})) ;; 007) m7=$((${m7} + ${i})) ;; 008) m8=$((${m8} + ${i})) ;; 009) m9=$((${m9} + ${i})) ;; esac done for i in 0 1 2 3 4 5 6 7 8 9 do eval echo $(printf "%03.0f" ${i}) $(date +%Y-%M-%d) \${m$i} done
この出力結果は次のようになる。売上数がすべて0になってしまう。
000 2012-51-13 0 001 2012-51-13 0 002 2012-51-13 0 003 2012-51-13 0 004 2012-51-13 0 005 2012-51-13 0 006 2012-51-13 0 007 2012-51-13 0 008 2012-51-13 0 009 2012-51-13 0
リダイレクトでファイルの内容をwhileに流し込んでいたところを、cat | で流し込む方法に変えただけでこんな結果になってしまう。先に説明したとおり、パイプが挟まるとサブシェルとして起動されるため、while内部での変数への変更が、シェルに反映されなくなるためだ。
whileの前後にパイプを接続してデータを加工する、というコーディングはシェルスクリプトではしばしば生じるものなので、この問題は結構な頻度で発生する。たとえ「自分はその問題を理解している」と思っていても、そのスクリプトを書き換える別の誰かがこの問題を引き起こす可能性があるし、実際問題が起こることが多い。
最終的に落ち着くところは同じ発想?
もちろん、コーディングスタイルは人それぞれなので、ここに書いてあることは、「こういった理由があって、このような書き方をしている」という参考資料の1つにしてもらえればと思う。物事は適材適所で使うものであって、何らかの指針に無理やり合わせる必要はない。
私はこれまで「一瞥して理解できる方がよい」という発想の下で、このやり方を「アット・ア・グランス性」と呼んでいたが、ほぼ同じような内容がユニバーサル・シェル・プログラミング研究所のサイトにユニケージ開発手法として掲載されている。こちらはデータのレベル分けからペアプロ的な教育手法まで含めたより包括的な内容だが、シェルスクリプトプログラミングの規制に関する発想はここで説明したものによく似ている。ユニケージエンジニアの作法の内容はほとんど一緒になっている。
思うに、シェルスクリプトのような機能のツールを使い続けると、最終的にこのような発想に落ち着くのではないか。似たようなことを考えているスクリプト・マイスターは少なくないのではないだろうか。
注:ちなみに@ITでこの連載を開始してから、ユニバーサル・シェル・プログラミング研究所の方々と縁ができました。シェルで広がる紳士のつながり。人生は不思議なものです :)
著者プロフィール
後藤大地
BSDコンサルティング株式会社取締役。オングス代表取締役。@ITへの寄稿、MYCOMジャーナルにおけるニュース執筆のほか、アプリケーション開発やシステム構築、『改訂第二版 FreeBSDビギナーズバイブル』『D言語パーフェクトガイド』『UNIX本格マスター 基礎編〜Linux&FreeBSDを使いこなすための第一歩〜』など著書多数。
Copyright © ITmedia, Inc. All Rights Reserved.