連載
» 2012年08月17日 00時00分 UPDATE

スマートな紳士のためのシェルスクリプト(7):「アット・ア・グランス性」確保のための8つの原則 (2/2)

[後藤大地,BSDコンサルティング株式会社]
前のページへ 1|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でこの連載を開始してから、ユニバーサル・シェル・プログラミング研究所の方々と縁ができました。シェルで広がる紳士のつながり。人生は不思議なものです :)


著者プロフィール

後藤大地

goto.jpg

BSDコンサルティング株式会社取締役。オングス代表取締役。@ITへの寄稿、MYCOMジャーナルにおけるニュース執筆のほか、アプリケーション開発やシステム構築、『改訂第二版 FreeBSDビギナーズバイブル』『D言語パーフェクトガイド』『UNIX本格マスター 基礎編〜Linux&FreeBSDを使いこなすための第一歩〜』など著書多数。



前のページへ 1|2       

Copyright© 2017 ITmedia, Inc. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。