UECジャーナル

ユニケージエンジニアの作法その三 関数の使用は控えよ

ユニケージ開発手法では関数を使うことがほとんどない。関数は処理を意味ごとに分けて整理する効果や、処理を再利用できるようにするというメリットがあり、手続き型のプログラミング言語には欠かせない機能だが、ユニケージ開発手法では好ましくない機能となる。

ユニケージ開発手法では上から下へコードを読めるようにすることを基本的な考え方としている。関数はこの規則に適していない。また、ユニケージ開発手法では関数を使わないことのメリットを経験的にも認識している。関数を使う場合と使わない場合の違いは、実際のシェルスクリプトを通じて理解するのがわかりやすい。

次の2つのシェルスクリプトは、どちらも一定期間以上経った古いログファイルを消すためのものだ。前者は関数を使っており、後者は関数を使っていない。ユニケージエンジニアが実際の現場で使っているのは後者だ。

具体的には、削除要件が微妙に異なる4つのディレクトリのファイル削除の処理を実施している。削除要件は次の3点で、4つのディレクトリですべて要件が異なっている。

関数を使用したコード

#!/bin/sh
# ===== 関数定義 =================================
DELETEFILE() {
	ls -1d $1/*					|
	awk '{ print $1, $1 }'				|
	(
	[ $4 -eq 0 ] && cat
	[ $4 -eq 1 ] && grep '\.gz$'
	[ $4 -eq 2 ] && grep -v '\.gz$'
	)						|
	(
	[ $3 -eq 0 ] && awk \
		'{ sub(/^.*\//, "", $2);
		   sub(/\..*/,  "", $2);
		   print $1, $2 }'
	[ $3 -eq 1 ] && awk \
		'{ sub(/^.*\//, "", $2);
		   sub(/_[0-9][0-9][0-9][0-9][0-9][0-9]\..*$/, "", $2);
		   print $1, $2 }'
	[ $3 -eq 2 ] && awk \
		'{ sub(/^.*\//, "", $2);
		   sub(/_[0-9][0-9][0-9][0-9][0-9][0-9]\..*$/, "", $2);
		   sub(/^.*_/, "", $2);
		   print $1, $2 }'
	)						|
	grep -E ' [0-9]{8}$'				|
	awk '$2<'"$2"					|
	xargs rm -f
}

# ===== 初期設定 =================================
OS=$(uname)
[ "FreeBSD" = "$OS" ] && yday=$(date -v-1d -j "+%Y%m%d")
[ "FreeBSD" = "$OS" ] && y7day=$(date -v-7d -j "+%Y%m%d")
[ "FreeBSD" = "$OS" ] && y30day=$(date -v-30d -j "+%Y%m%d")
[ "Linux"   = "$OS" ] && yday=$(date --date '1 day ago' '+%Y%m%d')
[ "Linux"   = "$OS" ] && y7day=$(date --date '7 days ago' '+%Y%m%d')
[ "Linux"   = "$OS" ] && y30day=$(date --date '30 days ago' '+%Y%m%d')

[ "FreeBSD" != "$OS" -a "Linux" = "$OS" ] && exit 1

# ===== メイン ===================================

# 受信dir内は(yyyymmdd.ext)というformatで、
# ここにある一か月以上前のファイルを消す。
DELETEFILE /home/usp/rcv $y30day 0 0

# backup-dir内は(yyyymmdd.ext)というformatで、
# ここにある一か月以上前の圧縮ファイルのみ消す。
DELETEFILE /home/usp/bak $y30day 0 1

# 送信dir内は(yyyymmdd_hhmmss.ext)というformatで、
# ここにある一週間以上前のファイルを消す。
DELETEFILE /home/usp/snd $y7day 1 0

# ログdir内は(title_yyyymmdd_hhmmss.ext)という
# formatで、ここにある昨日以前の無圧縮fileを消す。
DELETEFILE /home/usp/log $yday 2 2

関数を使わないコード

#!/bin/sh
# ===== 初期設定 =================================
OS=$(uname)
[ "FreeBSD" = "$OS" ] && yday=$(date -v-1d -j "+%Y%m%d")
[ "FreeBSD" = "$OS" ] && y7day=$(date -v-7d -j "+%Y%m%d")
[ "FreeBSD" = "$OS" ] && y30day=$(date -v-30d -j "+%Y%m%d")
[ "Linux"   = "$OS" ] && yday=$(date --date '1 day ago' '+%Y%m%d')
[ "Linux"   = "$OS" ] && y7day=$(date --date '7 days ago' '+%Y%m%d')
[ "Linux"   = "$OS" ] && y30day=$(date --date '30 days ago' '+%Y%m%d')

[ "FreeBSD" != "$OS" -a "Linux" = "$OS" ] && exit 1

# ===== メイン ===================================

# 受信dir内は(yyyymmdd.ext)というformatで、
# ここにある一か月以上前のファイルを消す。
ls -1d /home/usp/rcv/*					|
awk	'{ print $1, $1 }'				|
awk	'{ sub(/^.*\//, "", $2);
	   sub(/\..*/,  "", $2);
	   print $1, $2 }'				|
grep -E ' [0-9]{8}$'					|
awk	'$2<'"$y30day"					|
xargs rm -f

# backup-dir内は(yyyymmdd.ext)というformatで、
# ここにある一か月以上前の圧縮ファイルのみ消す。
ls -1d /home/usp/bak/*					|
awk	'{ print $1, $1 }'				|
grep '\.gz$'						|
awk	'{ sub(/^.*\//, "", $2);
	   sub(/\..*/,  "", $2);
	   print $1, $2 }'				|
grep -E ' [0-9]{8}$'					|
awk	'$2<'"$y30day"					|
xargs rm -f

# 受信dir内は(yyyymmdd_hhmmss.ext)というformatで、
# ここにある一週間以上前のファイルを消す。
ls -1d /home/usp/snd/*					|
awk	'{ print $1,$1 }'				|
awk 	'{ sub(/^.*\//, "", $2);
           sub(/_[0-9][0-9][0-9][0-9][0-9][0-9]\..*$/, "", $2);
           print $1, $2 }'				|
grep -E ' [0-9]{8}$'					|
awk	'$2<'"$y7day"					|
xargs rm -f

# ログdir内は(title_yyyymmdd_hhmmss.ext)という
# formatで、ここにある昨日以前の無圧縮fileを消す。
ls -1d /home/usp/log/*					|
awk	'{ print $1, $1 }'				|
grep -v '\.gz$'						|
awk 	'{ sub(/^.*\//, "", $2);
	   sub(/_[0-9][0-9][0-9][0-9][0-9][0-9]\..*$/, "", $2);
	   sub(/^.*_/, "", $2);
	   print $1, $2 }'				|
grep -E ' [0-9]{8}$'					|
awk	'$2<'"$yday"					|
xargs rm -f

このシェルスクリプトを初めて読んだ方が動作の内容を理解するまでにどれだけ時間がかかるか検討すると、後者の方が必要な時間は短いことが多い。関数を使わないコードの方は、上から下へ読み進めることができる。関数を使った方は、頻繁に目線を関数の呼び出し部分から関数が記載された部分へと移さなければならない。目線を頻繁にあちこち誘導するという作業は、想像以上に大きなタイムロスとストレスを与える。関数で記載されている処理をすべて覚えるといったことは稀で、関数が使われる度にこのコストが発生する。

関数を使わないことでコードの量は増える。処理の再利用もできない。しかし、コードを理解する時間は短くなり、仕様の変更にも対処しやすくなる。たとえば、仕様の追加が発生し、削除をしたい第5のディレクトリ/home/usp/tmp/ができたとする。そこも削除ルールが他とはまた違っており、今度は".tmp"という拡張子が付いているものに関しては見つけ次第削除してほしいというケースを考える。

関数を使ったコードを方を、たとえば関数の引数を一つ増やして対処するとしよう。関数を変更したらテストをしなければならない。引数を追加したのであれば、関数を利用している側も書き換える必要がある。引数を増やさない場合でも、関数の動作が変わる以上外部テストは必要だ。結局、全体を読み直す必要がある。このようなケースでは関数化していない方が対処しやすい。関数化せずに処理を上から下へ流し、その部分々々だけで処理を完結させておけば、仕様の変更に対処しやすく、後からコードの変更を実施しやすい。

ユニケージ開発手法ではOSが提供しているコマンドやusp Tukubaiがいわば関数としての役割を持っているといえる。自前で関数が必要になるようなケースは、それはもはや新しいコマンドを検討すべき状態といえる。

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

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

USP MAGAZINE Vol.4「第二回 ユニケージエンジニアの作法」より加筆修正後転載。

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

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