UECジャーナル

ユニケージエンジニアの作法その七 パイプ化が難しい処理はコマンド化せよ

医者の薬も(さじ)加減、前半を省略してよく「さじ加減」と言われることの方が多いだろうか。ご存知のとおり、よく効くと言われる薬も量が適切でなければ効き目がないという意味で、何事も加減が大事という教訓を説くものである。

冒頭でこの(ことわざ)を持ち出したのは、今回伝える作法が、過去に伝えた作法に対する1つの例外となるものだからだ。その作法とは、3番目に紹介した作法「関数の使用は控えよ」である。

作法その三とは何であったか

関数化することで次の問題が生じる。

関数化すると、上から下へ素直に読めるプログラムでなくなってしまい、一部goto文同様の欠点が生じる。
関数化して処理を共通化すると、呼出し元の一部だけ特別な処理を加える必要が生じた時に苦労する。
関数化すると、関数内に仕様変更が生じた時、すべての呼び出し元に副作用が生じぬよう確認せねばならない。

関数化には相当の覚悟がいる。ただし、関数化は好ましくないが、状況によってはコマンド化は認められる。まさに、医者の薬もさじ加減ということだ。今回の作法は、そのさじ加減の事例の1つである。

日付の書式を検査する例を考える

次のテキストを見てもらいたい。これは各地域に多数の店舗を持つ、ある小売業者の日毎売り上げデータだ。第1列から順に「店舗コード」、「売上日」、「売上額」という3つの列から構成されている。このファイルをテキスト1とする。

0237 20130203 1206510
0238 20130203 5731192
0301 20130203 2209873
0302 20137203 3217394
0201 20130204 6203342
0202 20130204 2310113
0203 20130204 4463859

ところが、一部の店舗に設置された装置には不具合があり、日付が壊れた状態で報告が上がってくることがごく稀にあった。そのような記録が見つかった場合は、当該店舗に問い合わせて正しい売上日を書き込む。

システム開発者の仕事は、前述のテキストファイルを元にして、全店舗の日毎の総売上を求めることなのだが、今説明したようにして売上日が不正な書式になっていると正しい計算ができない。よってそのようなデータが含まれていないかどうかを検査するための処理を追加する必要がある。

次のリスト1は、このような経緯で追加した日付の検査処理を抜粋したものである。

#!/bin/sh
# リスト1. 日付の正当性を検査し、処理するプログラム

: > $tmp-flag
cat TXTFILE |
# 1:店舗コード 2:売上日 3:売上額
while read TEN DAY URE; do
  # 1)8桁かどうかを調べる
  [ ${#DAY} -ne 8 ] && echo 1
  # 2)数値以外が含まれていないかどうかを調べる
  [ ! -z $(echo $DAY | tr -d [0-9]) ] && echo 1
  # 3)日付として有効な数値かどうかを調べる
  echo $DAY | yobi 1 > /dev/null 2>&1 || echo 1
done > $tmp-flag

# 不正な日付が1つもなければ処理実行
if [ ! -s $tmp-flag ]; then
  #(ここで何らかの処理を実施)
fi

検査しなければならない事項は3つある。

  1. 8桁の文字列であること
  2. その文字列が全部数字で構成されていること
  3. 2月31日のような無効な日付でないこと

この検査処理を、1行毎に外部コマンドも駆使しながら行うため、while文でループを組んでいる。awk(1)を使えば他の外部コマンドもwhile文も必要ないと思うかもしれない。たしかにその指摘はあっているが、各月の最大日数や閏年判定なども自力で行う必要があるのでプログラムコードは逆に増えてしまう。今回の作法ではいかに簡潔で分かりやすいコードにするかが肝要であるため、リスト1のような記述をしている。

要所をコマンド化する

このプログラムをもっと簡潔にできないものか、そう考えた時に一般的に用いられる手段が関数化だ。まとまった処理単位を関数として外に追い出して見やすくする。しかしユニケージエンジニアは同じ目的をシェル関数化ではなく、コマンド化という手段で行う。理由は本記事の最後で述べることにして、まずはコマンド化の具体例を見てもらいたい。

検査1回分をコマンド化

先程のプログラムの中で検査しなければならなかった3つの事項を、別コマンドとして外部に出すという方法をとって書き直したものが次のリスト2-1、2-2である。

#!/bin/sh
# リスト2-1. 日付の正当性チェックをコマンド化(is_date)

# 機能:与えられた文字列が日付8桁形式かどうか
#       チェックする。
# 書式: isdate {yyyymmdd}
# 戻値: 0=正当な日付、1=不正な日付

# 1)8桁かどうかを調べる
[ ${#1} -ne 8 ] && exit 1
# 2)数値以外が含まれていないかどうかを調べる
[ ! -z $(echo $1 | tr -d [0-9]) ] && exit 1
# 3)日付として有効な数値かどうかを調べる
echo $1 | yobi 1 > /dev/null 2>&1 || exit 1
exit 0
#!/bin/sh
# リスト2-2. is_dateコマンドを利用してコードを単純化

: > $tmp-flag
cat TXTFILE |
# 1:店舗コード 2:売上日 3:売上額
whlie read TEN DAY URE; do
  # 正当な日付(YYYYMMDD)かどうかチェック
  is_date $DAY || echo 1
done > $tmp-flag

# 不正な日付が1つもなければ処理実行
if [ ! -s $tmp-flag ]; then
  #(ここで何らかの処理を実施)
fi

リスト2-1がコマンドとして独立させられた3項目の検査処理である。is_dateと名付けており、検査対象の日時文字列を引数としてとる。そしてリスト2-2は、そのis_dateコマンドを使い、リスト1を書き直したものである。

リスト2-2を眺めてもらいたい。リスト1に比べてすっきりしたと感じられることだろう。複雑なコードをリスト2-1に追い出したのだから当たり前ではないかと思うだろうが、妥当性に関する話も本記事の最後で述べることにする。

検査1回分をコマンド化

リスト2-1のコマンド化は検査1回分であったが、今度は全行に渡る検査処理そのものをコマンド化してみる。全行分であるからwhile文も含めて外に出すことになる。それが次のリスト3-1、3-2である。

#!/bin/sh
# リスト3-1. 全行の日付をチェックするコマンド(check_date)

# 機能:与えられたテキストのうち、指定した列が
#       正当な日付(YYYYMMDD)かどうかチェックする。
# 書式: isdate {列番号}
# 戻値: 0=正当な日付、1=不正な日付

self $1 < /dev/stdin |
while read i; do
  # 1)8桁かどうかを調べる
  [ ${#i} -ne 8 ] && exit 1
  # 2)数値以外が含まれていないかどうかを調べる
  [ ! -z $(echo $i | tr -d [0-9]) ] && exit 1
  # 3)日付として有効な数値かどうかを調べる
  echo $i | yobi 1 > /dev/null 2>&1 || exit 1
done > /dev/null
exit 0
#!/bin/sh
# リスト3-2. check_dateコマンドを利用して更に単純化

# 指定列番号が正当な日付(YYYYMMDD)かどうかチェック
check_date 2 < TXTFILE

# 不正な日付が1つもなければ処理実行
if [ $? -eq 0 ]; then
  #(ここで何らかの処理を実施)
fi

リスト3-1は検査全体を行う側であるが、今度は検査対象の元ファイルを受け取り、そしてその中の何列目に検査対象の日付文字列が存在するのかを指定する仕様になっている。そしてリスト3-2はそのコマンドを呼ぶだけになっている。

これはリスト1より簡潔になったリスト2-2以上に簡潔になった。その分をコマンドとしてリスト3-1に追い出しているので当たり前と思うかもしれないが。

パイプの流れを読みやすいようにコマンド化する

リスト1、リスト2、そしてリスト3。この3つのコードの中で、多くの人が最も理解しやすいと感じるコードは果たしてどれであろうか。以前説明した「作法その三」を鑑みればリスト1のような気がするが、筆者個人としてもそうは思えない。つまりリスト1が他と比べて特別理解しやすいコードになってはいないということだろう。だとすれば、それはなぜなのだろうか。

理解しやすいコードの根本的な性質の1つに「上から下へ素直に読める」がある。関数化はこの性質を失わせてしまうため、推奨してはこなかった。だがリスト1は、ループ文を含んでおり、上から下へ素直に読めるコードにはなっていない。ループの中では、検査その1、検査その2、検査その3があり、これが毎行実施されるという記述である。

対してリスト2-2はどうだろうか。こちらもループ文がある。しかしループの中は「日時検査」として1つにまとめられており、これが毎行実施されるという記述になっている。

ループ文は、上から下へ書かれている筋書きを中断させてしまう。ループ文の先頭を目にしたら「以下からループ端までの一連の処理は何度か繰り返される」ということを頭に入れながら読まねばならないからだ。しかしループが小さければ、「ある処理を繰り返す処理」というように、ループ自体を1つの処理と捉える事が出来る。これはむしろ上から下へ素直に読める性質を向上させている。リスト3-2はその考えを更に推し進め、結果としてループそのものを追い出している。

ここではループ文を例にとったが、条件分岐文等、他にも同じことがいえるものがある。これらをシェルスクリプト的に総括すると「パイプ化が難しい処理」といえる。シェルスクリプトや各種Unixコマンドに慣れると、多くの処理がパイプでこなせるようになるが、うまくいかない場合にはコマンド化を検討してみるとよい。

優れたコマンドは名前ですべてを物語る事ができる

その際、熟慮すべきことが1つある。コマンド化しようとしている処理全体が一言で言い表されるかどうかである。リスト2-1のコマンドis_dateは、「引数の日時を検査する」コマンドである。このような分かりやすいまとまりで切り出せるからこそ、上から下へ素直に読めるという性質を維持できるのである。

ユニケージエンジニアが関数化ではなくコマンド化にこだわる理由もここにある。コマンドは関数に比べて呼び出しプログラムからの独立性が高い。コマンドは呼び出し元と変数を共有できない。環境変数も受け取るのみの片方向であるなど、制約が多い。そして各種の標準UNIXコマンドもそうであるように、プログラムの読み手でありコマンドの使い手は、内部構造について通常特に気にならない。

つまり関数化ではなくコマンド化するためには、高い独立性をもって処理を切り出さなくてはならない。この制約によって、みだりに処理が切り出されることは抑えられ、かつ、切り出す場合でも必然的に適切な単位で行われるようになる。

コマンドとは、プログラミングにおける「さじ加減」を、感覚的に理解するために適した存在といえるのではないだろうか。

ユニケージユニバーサル・シェル・プログラミング研究所の登録商標。

usp Tukubaiユニバーサル・シェル・プログラミング研究所の登録商標。

※ 本ページで公開されているプログラムとそれに付随するデータの著作権およびライセンスは、特に断りがない限りOpen usp Tukubai本体と同じMITライセンスに準拠するものとする。

USP MAGAZINE 2013 spring 松浦智之著、「第六回 ユニケージエンジニアの作法」より加筆修正後転載

Last modified: 2014-01-13 00:00:00