awk walking St.4 - igawk (3) -
先日、豆乳鍋を生まれて初めて食べた tmorimoto です。鍋の具材は、白菜、春菊、長ネギ、椎茸、木綿豆腐、鶏肉、豚肉、つくね、銀だら、マロニー。そのまま食べてもおいしいですが、やや味付けが物足りないので、味/ゆずぽんをつけてもグーです。豆乳が魚の生臭さを消してくれるせいか、銀だらがおいしかったです。写真撮っておけば良かった(- -#
リファレンス
awk walking St.2 - igawk (1) -
awk walking St.3 - igawk (2) -
awk でライブラリを使う簡単な方法
An Easy Way to Use Library Functions
前回、igawk の78〜127行目の部分のシェル変数 $expand_prog に展開される awk スクリプトがごにょごにょしているようだと分かりました。今回は、その awk スクリプトの中身を見ていきます。前回同様、この部分を単独の awk スクリプトに分割して、取り合えず実行してみます。大抵のプログラムは、何らかの機能を提供する関数の集まり/組合せなので、細分化していくことで理解し易くなります。 つまりは "「分ける」ことで「分かる」" です。(少し前に受講したロジカルシンキング研修の講師の受け売り)
先ずは $expand_prog の内容を get_include_library.awk として保存します。
$ less -N get_include_library.awk
1 function pathto(file, i, t, junk)
2 {
3 if (index(file, "/") != 0)
4 return file
5
6 for (i = 1; i <= ndirs; i++) {
7 t = (pathlist[i] "/" file)
8 if ((getline junk < t) > 0) {
9 # found it
10 close(t)
11 return t
12 }
13 }
14 return ""
15 }
16 BEGIN {
17 path = ENVIRON["AWKPATH"]
18 ndirs = split(path, pathlist, ":")
19 for (i = 1; i <= ndirs; i++) {
20 if (pathlist[i] == "")
21 pathlist[i] = "."
22 }
23 stackptr = 0
24 input[stackptr] = ARGV[1] # ARGV[1] is first file
25
26 for (; stackptr >= 0; stackptr--) {
27 while ((getline < input[stackptr]) > 0) {
28 if (tolower($1) != "@include") {
29 print
30 continue
31 }
32 fpath = pathto($2)
33 if (fpath == "") {
34 printf("igawk:%s:%d: cannot find %s\n",
35 input[stackptr], FNR, $2) > "/dev/stderr"
36 continue
37 }
38 if (! (fpath in processed)) {
39 processed[fpath] = input[stackptr]
40 input[++stackptr] = fpath # push onto stack
41 } else
42 print $2, "included in", input[stackptr],
43 "already included in",
44 processed[fpath] > "/dev/stderr"
45 }
46 close(input[stackptr])
47 }
48 }
get_include_library.awk に与える引数は、igawk の129〜131行目を見てみると、シェル変数 $program を展開した内容を標準入力からヒアドキュメントで受け取っています。この例では "@include math.awk" になります。
129 processed_program=`gawk -- "$expand_prog" /dev/stdin <<EOF
130 $program
131 EOF
$ awk -f get_include_library.awk /dev/stdin <<EOF
> @include math.awk
> EOF
# ***************************************************************
# math - math main program -
# Author : tmorimoto
# ***************************************************************
# ***************************************************************
# Name : do_addition
# Desc : calculate from "first" to "last" with increment
# Arg : first, last
# Return : answer
# ***************************************************************
function do_addition(first, last,
subtract_number, prev_last, answer) {
subtract_number = 0
if (first != 1) {
subtract_number = do_addition(1, first - 1)
first = 1
}
if (last % 2) {
# "last" is odd number
prev_last = last - 1
answer = (first + prev_last) * (prev_last / 2) + last
}
else {
# "last" is even number
answer = (first + last) * (last / 2)
}
return answer -= subtract_number
}
BEGIN {
if ((ARGC == 3) && \
(ARGV[1] ~ /^[0-9]+$/) && (ARGV[2] ~ /^[0-9]+$/))
printf ("%d から %d まで足すと %d です\n", \
ARGV[1], ARGV[2], do_addition(ARGV[1], ARGV[2]))
else
printf ("Usage : math.awk 1 10\n")
}
実際に実行してみると、math.awk 内部の @include ディレクティブが処理されて、math.awk と libmath.awk の内容が展開されていることが分かります。
ここでドキュメントを見ながら、ソースを追いかけてみます。
1 function pathto(file, i, t, junk)
2 {
3 if (index(file, "/") != 0)
4 return file
5
6 for (i = 1; i <= ndirs; i++) {
7 t = (pathlist[i] "/" file)
8 if ((getline junk < t) > 0) {
9 # found it
10 close(t)
11 return t
12 }
13 }
14 return ""
15 }
16 BEGIN {
17 path = ENVIRON["AWKPATH"]
18 ndirs = split(path, pathlist, ":")
19 for (i = 1; i <= ndirs; i++) {
20 if (pathlist[i] == "")
21 pathlist[i] = "."
22 }
17〜22行目で awk の組み込み変数 AWKPATH を用いて、 awk が検索するパスを pathlist に格納しています。デフォルトでは、カレントディレクトリと "/usr/share/awk" になります。
$ awk 'BEGIN {print ENVIRON["AWKPATH"]}'
.:/usr/share/awk
1〜22行目の部分について、テストドライバを作成して動作を確認してみます。
$ cat pathto_driver.awk
function pathto(file, i, t, junk)
{
if (index(file, "/") != 0) {
return "/ is founded : " file
}
for (i = 1; i <= ndirs; i++) {
t = (pathlist[i] "/" file)
if ((getline junk < t) > 0) {
# found it
close(t)
return "another path is founded : " t
}
}
print ""
}
BEGIN {
path = ENVIRON["AWKPATH"]
ndirs = split(path, pathlist, ":")
for (i = 1; i <= ndirs; i++) {
if (pathlist[i] == "")
pathlist[i] = "."
}
print pathto(ARGV[1])
}
$ awk -f pathto_driver.awk ./math.awk
/ is founded : ./math.awk
$ awk -f pathto_driver.awk math.awk
another path is founded : ./math.awk
$ chmod 333 math.awk
$ ls -l math.awk
--wx-wx-wx 1 tmorimoto users 491 2月 11 13:27 math.awk
$ awk -f pathto_driver.awk math.awk
$ awk -f pathto_driver.awk assert.awk
another path is founded : /usr/share/awk/assert.awk
$ awk -f pathto_driver.awk not_exist.awk
与えられた引数(ファイル名)に対して、"/" を含んでいれば、そのままファイル名として返します(3〜4行目)。そうでなければ(6〜14行目)、デフォルトの AWKPATH のパスを検索して、ファイルの読み込みに成功した場合のみ(8行目)、ファイル名を返します。getline はファイルが読み込み可能かどうかを調べているので、対象のファイルに読み込み権限があることが大前提となります。ファイルの読み込み後、close でファイルをクローズします(10行目)。awk ではファイルやパイプを開いた後、close を使うことで閉じることができます。同時に開けるファイルやパイプの数は、システムによって変わります。awk スクリプトでこれが問題になることは滅多にないと思いますが、お作法として open したら明示的に close しましょう。
次に、@include ディレクティブで指定されたファイルの読み込み部分を見てみます。
23 stackptr = 0
24 input[stackptr] = ARGV[1] # ARGV[1] is first file
25
26 for (; stackptr >= 0; stackptr--) {
27 while ((getline < input[stackptr]) > 0) {
28 if (tolower($1) != "@include") {
29 print
30 continue
31 }
32 fpath = pathto($2)
33 if (fpath == "") {
34 printf("igawk:%s:%d: cannot find %s\n",
35 input[stackptr], FNR, $2) > "/dev/stderr"
36 continue
37 }
38 if (! (fpath in processed)) {
39 processed[fpath] = input[stackptr]
40 input[++stackptr] = fpath # push onto stack
41 } else
42 print $2, "included in", input[stackptr],
43 "already included in",
44 processed[fpath] > "/dev/stderr"
45 }
46 close(input[stackptr])
47 }
48 }
24行目で ARGV[1] を最初の入力ファイルとしてスタック(input配列)に格納します。ARGV[1]は "/dev/stdin" がセットされ、この例では "@include math.awk" が与えられます。27行目の getline でファイルを読み込みます。28〜31行目の箇所で、1番目の列が "@include" 以外ならば、読み込んだ行をそのまま出力します。
32〜37行目で、そうでないならば、pathto() 関数で対象ファイルのパスと存在チェックを行います。ここは先ほど、確認したので大丈夫ですよね。
38〜45行目で対象ファイルのリストを作成して、1度読み込んだファイルかどうかのチェックを行っています。38〜40行目を見ると、現在読み込んでいるファイル名を processed[fpath] に保存し、次に読み込む対象として、input[++stackptr] にファイル名をセットしています。
ここで38行目の (fpath in processed) は、processed[] 配列に追加しようとしている対象ファイルが存在しているかどうかをチェックしています。awk の配列は全て連想配列になるので "if (key in array)" とすることで添字が存在しているかどうかをチェックすることができます。既に読み込んだファイル名をチェックすることで、お互いを "@include" して無限ループに陥る可能性を排除しています。
また、これらの処理をなぜシェル変数に代入して実行しているかの理由がユーザマニュアルに記載されています。日本語マニュアルをそのまま引用すると以下の通りです。
潜在的なセキュリティ問題を防止するために、
展開したプログラムを一時ファイルではなくシェル変数に保存する。
このことにはスクリプトが sh 言語に依存していて
シェルに不慣れな人が理解するのが難しいという短所がある。
明らかにソースの可読性が悪くなることは納得ですが、これも1つのセキュリティ対策とあります。一時ファイルを作成するセキュリティ上の懸念は、IPA さんのサイトにとても分かり易いページがあります。
シンボリックリンクの悪用
最後にもう1点、引用です。
shとawkを組み合わせたプログラミングがしばしば価値ある物であることを示している。
シンプルなツールをいかに組み合わせて目的を遂行するか。それってプログラミングの1つの醍醐味だと思います。awk の楽しさもそこに在ります。




コメント