awk
プログラム
この章ではあなたが読むためのawk
プログラムの寄せ集めを提供する。
この章には二つのセクションがある。最初の章は
幾つかの一般的なPOSIXユーティリティのawk
バージョンを提供し、二番目の章は面白いプログラムの
福袋(grab-bag)である。
これらのプログラムの多くは
セクション awk
の関数ライブラリを参照.
にあるライブラリ関数を使用している。
このセクションでは、幾つかのawk
によって実現したPOSIXユーティリテ
ィがある。これらのプログラムをawk
で作成しなおすということは、アル
ゴリズムが非常に明確に説明されているのと一般的にプログラムコードが簡潔か
つ単純であるので楽しいことである。これはawk
がそれをするのに適して
いるからである。
これらのプログラムが、今あなたの使っているシステムのそれを置き換えるため
のものではないことに注意して欲しい。これらのプログラムの目的は、"現実世
界"の作業におけるawk
言語プログラミングを説明することである。
プログラムはアルファベット順に並んでいる。
cut
ユーティリティは、標準入力からの入力中にあるキャラクタかフィー
ルドに対して選択もしくは"カット"(cut)を行い、その結果を標準出力に送る。
cut
はキャラクタのリストか、フィールドのリストを切り出すことができ
る。デフォルトでは、フィールドはタブで区切られているとみなされるが、コマ
ンドラインオプションで指定することによりフィールドデリミッタ(filed
delimiter)、言い換えればフィールドを区切るキャラクタを指定することもできる。
cut
の定義するフィールドはawk
程には汎用性はない。
一般的なcut
の使用はwho
の出力からログオンしているユーザーの
ログイン名だけを切り出すといったものだろう。たとえば、次の例に挙げるパイ
プラインは、ログオンしているユーザーのソートされ、重複が取り除かれたリス
トを生成する。
who | cut -c1-8 | sort | uniq
cut
のオプションには以下に挙げるのものがある
-c list
-f list
-d delim
-s
awk
によるcut
は、getopt
ライブラリ関数
(セクション コマンドラインオプションの処理を参照),
と
join
ライブラリ関数
(セクション Merging an Array Into a Stringを参照).
を使っている。
プログラムは、プログラムのオプションと使用法を出力
(そのあとプログラムを終了させる)するuasge
という関数から
始まっている。
usage
は不正な引数が渡されたときに呼び出される。
# cut.awk -- awkによるcut # Arnold Robbins, arnold@gnu.org, Public Domain # May 1993 # Options: # -f list Cut fields切り取るフィールド # -d c Field delimiter characterフィールド区切りのキャラクタ # -c list Cut characters切り取るキャラクタ # # -s Suppress lines without the delimiter character 区切りキャラクタがない行の出力を抑制する function usage( e1, e2) { e1 = "usage: cut [-f list] [-d c] [-s] [files...]" e2 = "usage: cut [-c list] [files...]" print e1 > "/dev/stderr" print e2 > "/dev/stderr" exit 1 }
変数e1
とe2
は関数が
ページ
にフィットするために使われれている。
次にくるのはBEGIN
ルールで、ここではコマンドラインオプションの解析
を行っている。FS
にはcut
のデフォルトのキャラクタである単一
のタブキャラクタをセットし、出力フィールドセパレータも入力セパレータと同
じにする。それからgetopt
はコマンドラインオプションを処理するため
に使われている。by_fields
かby_chars
という変数のいずれかが
セットされ、フィールドによって処理するのかキャラクタによって処理するのか
を指示する。キャラクタによって切り出しを行うとき、出力のフィールドセパレ
ータは空文字列である。
BEGIN \ { FS = "\t" # デフォルト OFS = FS while ((c = getopt(ARGC, ARGV, "sf:c:d:")) != -1) { if (c == "f") { by_fields = 1 fieldlist = Optarg } else if (c == "c") { by_chars = 1 fieldlist = Optarg OFS = "" } else if (c == "d") { if (length(Optarg) > 1) { printf("Using first character of %s" \ " for delimiter\n", Optarg) > "/dev/stderr" Optarg = substr(Optarg, 1, 1) } FS = Optarg OFS = FS if (FS == " ") # awkのsemanticsを上書きする FS = "[ ]" } else if (c == "s") suppress++ else usage() } for (i = 1; i < Optind; i++) ARGV[i] = ""
特に注意するのはフィールドデリミッタがスペースであるときである。
" "
(単一のスペース)をFS
の値にするというのは正しくない。
awk
はこれによってスペース、タブ、改行の連続(一つでも良い)を、フィールド
を分割するものとして扱う。我々はここでは個々のスペースで分割を行うことを
望んでいる。また、getopt
が処理した後では、awk
がそれらのオ
プションをファイル名として扱わないようにするためにARGV
の一番目か
らOptind
までの要素をクリアする必要がある
ことにも注意すること。
コマンドラインオプションを処理した後で、プログラムはそれらのオプションが
正しいものであることを検査する。`-c'と`-f'はいずれか一つだけが
使われ、かつどちらの場合もフィールドリストが必要である。
set_fieldlist
やset_charlist
はそれぞれフィールドリストやキャ
ラクタリストを取り出すために呼び出される。
if (by_fields && by_chars) usage() if (by_fields == 0 && by_chars == 0) by_fields = 1 # デフォルト if (fieldlist == "") { print "cut: needs list for -c or -f" > "/dev/stderr" exit 1 } if (by_fields) set_fieldlist() else set_charlist() }
下にあるコードはset+fieldlist
である。最初にフィールドリストを
カンマで分割し、配列にしている。それから配列の各要素を
それが範囲指定であるかをチェックして、範囲指定であれば
それをさらに分割する。
続いて範囲指定が最初の数字が二番目の数字より小さいかどうかをチェックし、
リスト中の各数字はflist
という配列に加えていって
出力するフィールドの単純なリストを作成する。
そして、プログラムはawk
にフィールド分割を行わせる。
function set_fieldlist( n, m, i, j, k, f, g) { n = split(fieldlist, f, ",") j = 1 # index in flist for (i = 1; i <= n; i++) { if (index(f[i], "-") != 0) { # 範囲 m = split(f[i], g, "-") if (m != 2 || g[1] >= g[2]) { printf("bad field list: %s\n", f[i]) > "/dev/stderr" exit 1 } for (k = g[1]; k <= g[2]; k++) flist[j++] = k } else flist[j++] = f[i] } nfields = j - 1 }
seet_charlist
関数はset_fieldlist
よりも複雑である。ここでは
固定長の入力を記述するgawk
のFIELDWIDTHS
(セクション 固定長データの読み込みを参照)を使っている。キャラク
タリストを使うとき、我々はこれを使う。
FIELDWIDTHS
の設定は、単純なフィールドの設定よりも
複雑である。出力するフィールドを記録しつつ、余計なキャラクタはスキップし
なければならない。例えば、1番目から8番め、15番め、22番目から35番目までの
キャラクタを取り出したいとする。これは`-c 1-8,15,22-35'という指定を
すれば良い。このときFIELDWIDTHS
は"8 6 1 6 14"
という値
になる。これには5つのフィールドがあり、出力すべきものは$1
、$3
、
$code{$5}である。間にあるフィールドは"埋め草"(filler)であり、無
視されるデータである。
flist
は、出力するフィールドのリストであり、
t
は埋め草も含めた完全なフィールドのリストである。
function set_charlist( field, i, j, f, g, t, filler, last, len) { field = 1 # count total fields フィールドの合計をカウントする n = split(fieldlist, f, ",") j = 1 # index in flist flistの添え字 for (i = 1; i <= n; i++) { if (index(f[i], "-") != 0) { # 範囲 m = split(f[i], g, "-") if (m != 2 || g[1] >= g[2]) { printf("bad character list: %s\n", f[i]) > "/dev/stderr" exit 1 } len = g[2] - g[1] + 1 if (g[1] > 1) # 埋め草の長さを計算する filler = g[1] - last - 1 else filler = 0 if (filler) t[field++] = filler t[field++] = len # フィールドの長さ last = g[2] flist[j++] = field - 1 } else { if (f[i] > 1) filler = f[i] - last - 1 else filler = 0 if (filler) t[field++] = filler t[field++] = 1 last = f[i] flist[j++] = field - 1 } } FIELDWIDTHS = join(t, 1, field - 1) nfields = j - 1 }
次にあるコード片は実際にデータを処理するルールである。`-s'オプショ
ンが指定されている場合、suppress
が真になる。最初のif
文は入
力レコードにフィールドセパレータが含まれているかどうかをチェックしている。
cut
がフィールドを処理し、suppress
が真であり、フィールドセ
パレータがレコードにない場合には、そのレコードはスキップされる。
レコードが正当なものであれば、gawk
はこの時点でFS
にあるキャ
ラクタか、FIELDWIDTHS
を使った固定長フィールドでのレコードのフィー
ルドへの分割は完了している。ループは出力すべきフィールドのリストを参照し、
対応するフィールドにデータがあればそのフィールドが出力される。同様に次の
フィールドがデータを持っていれば、間にセパレータキャラクタが出力される。
{ if (by_fields && suppress && $0 !~ FS) next for (i = 1; i <= nfields; i++) { if ($flist[i] != "") { printf "%s", $flist[i] if (i < nfields && $flist[i+1] != "") printf "%s", OFS } } print "" }
このcut
では、gawk
のFIELDWIDTHS
変数の
キャラクタベースの取り出しに依存している。
他のawk
処理系では、substr
を使って同じことが実現できるが
(セクション Built-in Functions for String Manipulationを参照)、
それは実行するには実際骨の折れる作業となるだろう。
FIELDWIDTHS
変数は、キャラクタによって入力行を分けて
取り出すような問題の優雅な解決策を提供する。
egrep
ユーティリティはファイルからパターンを検索するが、
awk
で使えるものとほぼ同じ正規表現を使っている
(セクション 正規表現定数を参照)。
egrep
は次のように使用する。
egrep [ options ] 'pattern' files ...
patterは正規表現である。典型的な使い方では、この正規表現はシェルが
メタキャラクタをファイル名のワイルドカードと見なして展開してしまうのを防
ぐためにクォートされている。通常、egrep
は正規表現がマッチした行を
出力する。コマンドラインには複数のファイル名を置くことができ、出力にはパ
ターンが見つかったファイルの名前がコロンの前に置かれる。
オプションは以下の通り。
-c
-s
-v
egrep
は
与えられたパターンにマッチしなかったときに、その行を出力し、
パターンがマッチしなかったときに成功の終了コードを返して
終了する。
-i
-l
-e pattern
このバージョンでは、getopt
ライブラリ関数と
(セクション コマンドラインオプションの処理を参照)、
file transitionライブラリプログラム
(セクション Noting Data File Boundariesを参照)
を使っている。
このプログラムはコメントで始まり、次いで
getopt
を使ってコマンドライン引数を処理する
BEGIN
ルールがくる。
`-i'(太小文字の無視)オプションはgawk
では
とても簡単である。ただ単にIGNORECASE
変数を
(セクション 組み込み変数を参照)使うだけで良い。
# egrep.awk -- awkを使ってegrepをシミュレートする # Arnold Robbins, arnold@gnu.org, Public Domain # May 1993 # Options: # -c 行を数える # -s 静かにする。終了コードを使用する。 # -v テストを逆にして、マッチしなかったときに成功 # -i 大小文字の違いを無視する # -l ファイル名だけを出力する # -e 引数がパターンであることを明示する BEGIN { while ((c = getopt(ARGC, ARGV, "ce:svil")) != -1) { if (c == "c") count_only++ else if (c == "s") no_print++ else if (c == "v") invert++ else if (c == "i") IGNORECASE = 1 else if (c == "l") filenames_only++ else if (c == "e") pattern = Optarg else usage() }
以下のコード片はegrep
特有の動作を扱うものである。`-e'を使っ
てパターンが指定されていなかった場合、最初のオプションでないコマンドライ
ン引数がパターンとして使用される。awk
のコマンドライン引数は
ARGV[Optind]
までクリアされ、awk
がそれらの引数をファイルと
して扱わないようにする。ファイルが指定されていない場合には標準入力が使わ
れ、複数のファイルが指定されている場合にはマッチした行を出力するときにフ
ァイル名を前置するようにする。
最後の二行は、gawk
では不要なのでコメントアウトしている。
この二行は他のawk
を使う場合にはコメントをはずす
必要がある。
if (pattern == "") pattern = ARGV[Optind++] for (i = 1; i < Optind; i++) ARGV[i] = "" if (Optind >= ARGC) { ARGV[1] = "-" ARGC = 2 } else if (ARGC - Optind > 1) do_filenames++ # if (IGNORECASE) # pattern = tolower(pattern) }
以下のコード片はgawk
では使わないのでコメントアウトしている
行である。
このルールは
`-i'オプションが指定されたときに
入力された行を小文字に変換するものだが、
gawk
では必要ではないのでコメントアウトされている。
#{ # if (IGNORECASE) # $0 = tolower($0) #}
beginfile
という関数は、新しいファイルが処理されたときに
`ftran.awk'から呼び出される。この例は非常に単純で、fcount
と
いう変数を0にすることが全てである。fcount
はカレントファイルで、
パターンがどれだけマッチしたかということを記録している。
function beginfile(junk) { fcount = 0 }
endfile
は各ファイルを処理し終えたときに呼び出される。これは、ユー
ザーがマッチした行数を希望したときにだけ使用される。no_print
とい
う変数は終了コードが要求されたときにだけ真になる。count_only
はマ
ッチした行数だけを要求されたときに真となる。egrep
はしたがって、出
力とカウントがイネーブルのときだけ行数を出力する。出力のフォーマットは処
理するファイルの数によって調整されなければならない。最後に、fcount
がtotal
に足しこまる。これはパターンが全体でどれだけの行にマッチ
したかということを知るためのものである。
function endfile(file) { if (! no_print && count_only) if (do_filenames) print file ":" fcount else print fcount total += fcount }
以下のルールは、行のマッチング作業のほとんどを行っている。matches
という変数は行がパターンにマッチしたときに真となる。ユーザーがマッチしな
かった行を望んだ場合には、`!'演算子を使ってmatches
の意味を逆
転している。fcount
はmatches
の数だけインクリメントされる。
インクリメントする値は0か1であり、これはマッチが成功したかしなかったかに
よる。行がマッチしなければ、next
文を使って次のレコードへ単に移行
させる。
幾つかの、パフォーマンスを向上させるための二、三行のコードがある。ユーザ
ーが終了ステータスだけを必要としているとき(no_print
が真のとき)に
は行をカウントする必要はなく、ファイルには一行マッチすれば十分でありその
時点でnextfile
を使って次のファイルへ処理を移すことができる。同様
に、ファイル名だけを出力すればいい場合にも行をカウントする必要はなく、フ
ァイル名を出力してしまえばnextfile
を使って次のファイルへ処理を移
すことができる。
最後に各行を出力し、必要であればファイル名とコロンを前置する。
{ matches = ($0 ~ pattern) if (invert) matches = ! matches fcount += matches # 1 か 0 if (! matches) next if (no_print && ! count_only) nextfile if (filenames_only && ! count_only) { print FILENAME nextfile } if (do_filenames && ! count_only) print FILENAME ":" $0 else if (! count_only) print }
END
ルールは正しい終了ステータスを取り扱う。
マッチした行がなければ、終了ステータスは1、あれば0となる。
END \ { if (total == 0) exit 1 exit 0 }
usage
関数は不正なオプションが渡されたときに使用法のメッセージを出
力し、実効を終了する。
function usage( e) { e = "Usage: egrep [-csvil] [-e pat] [files ...]" print e > "/dev/stderr" exit 1 }
e
という変数は関数を印刷されるページに
きちんと納めるために使われている。
プログラミングスタイルに関するのと同じように、END
ルールに
バックスラッシュによる行継続を使って、開きブレースのみを行に置く
ようなことをしたいと思うかもしれない。これは
この章にある
多くの例で使っている関数の記述スタイルにより近づけるものである。
あなたはBEGIN
ルールやEND
ルールを記述するときに
このスタイルを使って書くかどうかを自由に決めることができる。
id
ユーティリティはあるユーザーの実ユーザーID、実効ユーザーID、実
グループID、実効グループID、そして(存在していれば)ユーザーのグループセッ
トをリストアップするものである。id
は本人以外の誰かを指定して実行
した場合には、実効IDと実効グループIDだけを出力するもし可能なら、id
は対応するユーザー名とグループ名を出力する。id
の出力は以下のよ
うなものである。
$ id -| uid=2076(arnold) gid=10(staff) groups=10(staff),4(tty)
この情報は実際にgawk
の特殊ファイル`/dev/user'
から得られたものである
(セクション Special File Names in gawk
を参照)。
しかし、id
ユーティリティは単なる数字の並びよりも
よりわかりやすい出力を行う。
次に挙げるのはシンプルなバージョンの
awk
によるid
である。
この例ではユーザーデータベースライブラリの関数
(セクション Reading the User Databaseを参照)と
グループデータベースライブラリの関数
(セクション Reading the Group Databaseを参照)を
使っている。
プログラムは実に簡単なものであって、BEGIN
ルール中で全てのことを行
っている。ユーザーID番号とグループID番号は`/dev/user'から取得してい
る。もし`/dev/user'がサポートされていなければ、プログラムはその時点
でギブアップする。
プログラムコードは反復的である。実ユーザーID番号のためのユーザーデータベ ースのエントリは`:'を区切りとして分割される。名前は最初のフィールド にある。同様のコードが実効ユーザーID番号とグループ番号のために使われてい る。
# id.awk -- idのawkによる実装 # Arnold Robbins, arnold@gnu.org, Public Domain # May 1993 # output is: # uid=12(foo) euid=34(bar) gid=3(baz) \ # egid=5(blat) groups=9(nine),2(two),1(one) BEGIN \ { if ((getline < "/dev/user") < 0) { err = "id: no /dev/user support - cannot run" print err > "/dev/stderr" exit 1 } close("/dev/user") uid = $1 euid = $2 gid = $3 egid = $4 printf("uid=%d", uid) pw = getpwuid(uid) if (pw != "") { split(pw, a, ":") printf("(%s)", a[1]) } if (euid != uid) { printf(" euid=%d", euid) pw = getpwuid(euid) if (pw != "") { split(pw, a, ":") printf("(%s)", a[1]) } } printf(" gid=%d", gid) pw = getgrgid(gid) if (pw != "") { split(pw, a, ":") printf("(%s)", a[1]) } if (egid != gid) { printf(" egid=%d", egid) pw = getgrgid(egid) if (pw != "") { split(pw, a, ":") printf("(%s)", a[1]) } } if (NF > 4) { printf(" groups="); for (i = 5; i <= NF; i++) { printf("%d", $i) pw = getgrgid($i) if (pw != "") { split(pw, a, ":") printf("(%s)", a[1]) } if (i < NF) printf(",") } } print "" }
split
ユーティリティは大きなテキストファイルを小さな塊に分ける。デ
フォルトでは出力ファイルの名前は`xaa', `xab', ... のようにつけ
られる。分割された各々のファイルは1000行の長さ(最後のファイルを除く)とな
る。分割したファイルの行数を変更するには、コマンドラインにマイナス記号を
前置した数字を置いて指定する。例えば、500行毎にファイルを分割するように
するには`-500'とする。出力ファイルの名前を`myfileaa',
`myfileab',...のように 変更するには、その様なファイル名を指定するコ
マンドライン引数を置く。
以下に挙げたのはawk
によるsplit
である。
このプログラムは
セクション Translating Between Characters and Numbersを参照.
にあるord
関数とchr
関数を使っている。
プログラムは最初にデフォルトの設定を行い、次いでコマンドライン引数が多す ぎないかどうかをチェックする。それからそれぞれの引数のチェックを行う。最 初の引数はマイナス記号に続いた数字であってもよく、もしそういったものなら ば、それは負の数のように見えるので、符号を反転しそれを分割するときの行数 とする。データファイルの名前はスキップし、最後の引数を出力ファイルの prefixとして使用する。
# split.awk -- awkによるsplit # Arnold Robbins, arnold@gnu.org, Public Domain # May 1993 # usage: split [-num] [file] [outname] BEGIN { outfile = "x" # デフォルト count = 1000 if (ARGC > 4) usage() i = 1 if (ARGV[i] ~ /^-[0-9]+$/) { count = -ARGV[i] ARGV[i] = "" i++ } # ファイルではなく標準入力から読み込むときにargvを検査する if (i in ARGV) i++ # データファイル名をスキップする if (i in ARGV) { outfile = ARGV[i] ARGV[i] = "" } s1 = s2 = "a" out = (outfile s1 s2) }
次のルールで作業のほとんどを行っている。tcount
(temporary count)は
出力ファイルにどれだけの行が出力したのかを記録している。もしこれが
count
より大くなったとき、カレントファイルをクローズし、新しいファイル
を始めるときである。s1
とs2
はファイル名につけるsuffixの現在
の値を保持している。これが両方とも`z'であるとき、処理対象のファイル
が大きすぎるということである。そうでない場合にはs1
は次のアルファ
ベットに変わり、s2
はまたa
から再度始まる。
{ if (++tcount > count) { close(out) if (s2 == "z") { if (s1 == "z") { printf("split: %s is too large to split\n", \ FILENAME) > "/dev/stderr" exit 1 } s1 = chr(ord(s1) + 1) s2 = "a" } else s2 = chr(ord(s2) + 1) out = (outfile s1 s2) tcount = 1 } print > out }
usage
関数は単純にエラーメッセージを出力して、実行を終了する。
function usage( e) { e = "usage: split [-num] [file] [outname]" print e > "/dev/stderr" exit 1 }
e
という変数は関数を
ページ
にフィットするよために使われている。
このプログラムはちょっとばかりいい加減である。最後のファイルのクローズを
END
ルール中で行うのではなく、awk
が自動的に行うことに依存し
ている。
tee
プログラムは"pipe fitting" として知られている。tee
は
それに対する標準入力からの入力を標準出力にコピーするとともに、コマンドラ
インで指定されたファイルに複製する。その使用方法は以下の通り、
tee [-a] file ...
`-a'オプションはtee
に対して、開始時に指定したファイルを切り詰め
るのではなくファイルに追加を行うように指示する。
BEGIN
ルールは最初にすべてのコマンドライン引数のコピーをcopy
という名前の配列に作成する。ARGV[0]
は必要でないのでコピーされな
い。tee
は、awk
がARGV
にあるファイル名を入力データと
して扱おうとするのでARGV
を直接は使わない。
最初の引数が`-a'であれば、append
という名前のフラグ変数を
真にセットし、ARGV[1]
とcoppy[1]
を削除する。
ARGC
が2未満であれば、ファイル名が指定されていないという
ことであり、tee
は使用方のメッセージを出力し、
実行を終了する。最後に、awk
は
ARGV[1]
に"-"
がセットされ、ARGC
が2に
セットされたことにより、標準入力から読み込みを行うことを強制される。
# tee.awk -- awkによるtee # Arnold Robbins, arnold@gnu.org, Public Domain # May 1993 # Revised December 1995 BEGIN \ { for (i = 1; i < ARGC; i++) copy[i] = ARGV[i] if (ARGV[1] == "-a") { append = 1 delete ARGV[1] delete copy[1] ARGC-- } if (ARGC < 2) { print "usage: tee [-a] file ..." > "/dev/stderr" exit 1 } ARGV[1] = "-" ARGC = 2 }
パターンがなく、すべての行に対して処理するので、一つのルールで作業の全て を行っている。ルールの本体は、単純にその行をコマンドラインで指定されたフ ァイルに出力してから、標準出力にも出力する。
{ # if文をループの外に出せば高速化できる。 if (append) for (i in copy) print >> copy[i] else for (i in copy) print > copy[i] print }
このコードはループを次の様に書くこともできる。
for (i in copy) if (append) print >> copy[i] else print > copy[i]
これはより簡潔なものではあるが、効率は良くない。`if'はすべてのレコ ードのすべての出力ファイルでテストループの本体を複製することによって、 `if'は入力レコード一つにつき一度だけテストを行うようになる。N個 の入力レコードがあり、M個の入力ファイルがあった場合、最初のやり方 ではN回`if'文が実行されるが、二番目のやり方ではN×M 回も`if'文が実行されてしまうことになる。
最後にEND
ルールですべての出力ファイルをクローズするという
後始末を行う。
END \ { for (i in copy) close(copy[i]) }
uniq
ユーティリティは入力としてソートされた行を標準入力からとり、
(デフォルトでは)重複した行を取り除く。言い換えれば、ユニークな行だけが出
力される。この名前はそこから来ている。uniq
は幾つかのオプションが
あり、その使い方は以下の通り。
uniq [-udc [-n]] [+n] [ input file [ output file ]]
オプションの意味は以下の通り。
-d
-u
-c
-n
awk
のデフォルトと同じであり、スペースやタブ、あるいは改行(の連なり、run)で
区切られた非空白キャラクタ(non-whitespace character)である。
+n
input file
output file
通常のuniq
は`-d'オプションと`-u'オプションの両方が
指定されたかのように動作する。
以下に挙げるのはawk
によるuniq
の実装である。
このプログラムはライブラリ関数のgetopt
(セクション コマンドラインオプションの処理を参照)と
join
(セクション Merging an Array Into a Stringを参照)を
を使っている。
このプログラムはusage
関数と
オプションの一覧とその意味の説明から始まっている。
BEGIN
ルールはコマンドライン上の引数とオプションを扱っている。
`-25'という形式のオプションを、`2'というオプションとそれに対する
`5'という引数のように扱うために、getopt
のトリックを使っている。
二文字以上の数字が与えられた(Optarg
が数字である)場合、Optarg
はオプションの数字と連結され、0を加えることによってそれを数値にする。
もし一つの数字だけがオプションにあれば、Optarg
は必要ではなく、次
回のgetopt
の処理のためにOptind
をデクリメントせねばならない。
このコードは明らかにちょっとトリッキーである。
オプションが一つも与えられていなければデフォルトが採用され、
繰り返しのある行もない行も共に出力することになる。
出力ファイルが指定されていれば、それをoutputfile
に代入する。
それ以前に、outputfile
は標準出力`/dev/stdout'に
初期化されている。
# uniq.awk -- awkで uniq をする # Arnold Robbins, arnold@gnu.org, Public Domain # May 1993 function usage( e) { e = "Usage: uniq [-udc [-n]] [+n] [ in [ out ]]" print e > "/dev/stderr" exit 1 } # -c 行をカウントする。-dと-uをオーバーライドするcount lines. overrides -d and -u # -d 繰り返しのある行だけを出力するonly repeated lines # -u 繰り返しのない行だけを出力するonly non-repeated lines # -n フィールドをn個スキップするskip n fields # +n n個のキャラクタをスキップskip n characters, skip fields first BEGIN \ { count = 1 outputfile = "/dev/stdout" opts = "udc0:1:2:3:4:5:6:7:8:9:" while ((c = getopt(ARGC, ARGV, opts)) != -1) { if (c == "u") non_repeated_only++ else if (c == "d") repeated_only++ else if (c == "c") do_count++ else if (index("0123456789", c) != 0) { # getoptはオプションに引数を要求している。 # -5のようなものに対処するために干渉する if (Optarg ~ /^[0-9]+$/) fcount = (c Optarg) + 0 else { fcount = c + 0 Optind-- } } else usage() } if (ARGV[Optind] ~ /^\+[0-9]+$/) { charcount = substr(ARGV[Optind], 2) + 0 Optind++ } for (i = 1; i < Optind; i++) ARGV[i] = "" if (repeated_only == 0 && non_repeated_only == 0) repeated_only = non_repeated_only = 1 if (ARGC - Optind == 2) { outputfile = ARGV[ARGC - 1] ARGV[ARGC - 1] = "" } }
以下にあるare_equal
という関数は、現在行の$0
と一つ前の行
last
との比較を行っている。この関数はまたフィールドやキャラクタのスキ
ップも処理している。
フィールドカウントやキャラクタカウントが指定されていなければ、
are_equal
は単純にlast
と$0
を文字列として
比較し、その結果を1または0として返す。そうでない場合には複雑になる。
フィールドをスキップしなければならない場合、各行をsplit
(セクション Built-in Functions for String Manipulationを参照)
を使って配列に分割し、それからjoin
を使って必要なフィールドを
組み立てる。連結された行はclast
とcline
に格納される。
スキップするフィールドがなければ、clast
とcline
には
それぞれlast
と$0
の内容がそのままセットされる。
最後に、キャラクタをスキップする必要がある場合に、substr
が
clast
やcline
の先頭のキャラクタcharcount
個スキップする
ために使われる。
function are_equal( n, m, clast, cline, alast, aline) { if (fcount == 0 && charcount == 0) return (last == $0) if (fcount > 0) { n = split(last, alast) m = split($0, aline) clast = join(alast, fcount+1, n) cline = join(aline, fcount+1, m) } else { clast = last cline = $0 } if (charcount) { clast = substr(clast, charcount + 1) cline = substr(cline, charcount + 1) } return (clast == cline) }
次に挙げる二つのルールはプログラムの本体である。最初のルールは
データの最初の行だけに実行される。このルールでは、
次の行に比較すべきものがあるので、last
に$0
をセットしている。
二番目のルールが仕事をする。
equal
という変数の内容は、are_equal
で行った比較の結果に
よって、1か0のいずれかになる。uniq
は行の重複を数え、
行が同じ内容であればcount
という変数をインクリメントする。
内容が異なっていれば行の内容を出力し、count
をリセットする
uniq
が数えることをしていなければ、count
は
二つの行の内容が等しかったときにインクリメントされる。
uniq
が繰り返しの行をカウントしていて二回以上その同じ行が
出現したり、逆にuniq
が繰り返しのない行をカウントしていて
ある行が一度しか出現しなければ、その様な行は出力され、count
が
リセットされる。
同様のロジックが、END
ルールの中で
入力の最後の行を出力するために使われている。
NR == 1 { last = $0 next } { equal = are_equal() if (do_count) { # -d と-uをオーバーライド if (equal) count++ else { printf("%4d %s\n", count, last) > outputfile last = $0 count = 1 # カウンタをリセット } next } if (equal) count++ else { if ((repeated_only && count > 1) || (non_repeated_only && count == 1)) print last > outputfile last = $0 count = 1 } } END { if (do_count) printf("%4d %s\n", count, last) > outputfile else if ((repeated_only && count > 1) || (non_repeated_only && count == 1)) print last > outputfile }
wc
(word count)ユーティリティは、一つ以上の入力ファイルに
対して、行、単語、キャラクタの数をカウントする。その使い方は以下の通り。
wc [-lwc] [ files ... ]
コマンドラインで入力ファイルが指定されなかった場合には、wc
は標準
入力から読み込みを行う。複数のファイルが指定された場合、すべてのファイル
でカウントした合計を同様に出力する。wc
のオプションとその意味は以
下の通り。
-l
-w
awk
が通常行う入力データの分割方法
と同じである。
-c
awk
を使ったwc
の実装はawk
が仕事のほとんどを、つまり
行の単語(つまりはフィールド)への切り分けと、行(レコード)のカウントを
awk
自身が行い、行の長さがどれくらいかということも簡単に知ることが
できるので実にエレガントである。
このバージョンではライブラリ関数の
getopt
(セクション コマンドラインオプションの処理を参照)と
transition 関数
(セクション Noting Data File Boundariesを参照)
を使用している。
このバージョンと伝統的なwc
では大きな違いがある。我々のバージョン
では、常に行、単語、キャラクタの順序でカウントした数を出力する。伝統的な
wc
では`-l'、`-w'、`-c'といったオプションをコマンド
ライン上で使用してして出力する順序を指定することができる。
BEGIN
ルールは引数の処理を行う。コマンドラインで二つ以上のファイル
が指定されたときには変数print_total
の値が真となる。
# wc.awk -- 行、単語、キャラクタを数える # Arnold Robbins, arnold@gnu.org, Public Domain # May 1993 # Options: # -l 行だけを数える # -w 単語だけを数える # -c キャラクタだけを数える # # デフォルトでは、行、単語、キャラクタを数える BEGIN { # getoptに不正なオプションについてはメッセージを出力するよう # にし、ここではそのようなオプションを無視するようにする while ((c = getopt(ARGC, ARGV, "lwc")) != -1) { if (c == "l") do_lines = 1 else if (c == "w") do_words = 1 else if (c == "c") do_chars = 1 } for (i = 1; i < Optind; i++) ARGV[i] = "" # オプションが何もなければ、全てを行う if (! do_lines && ! do_words && ! do_chars) do_lines = do_words = do_chars = 1 print_total = (ARGC - i > 2) }
beginfile
は単純で、行、単語、キャラクタのカウントを0にリセットし、
fname
に入っている処理対象のファイルの名前をセーブする。
関数endfile
は、それまで処理していたファイルで見つかった行、単語、
キャラクタの数をそれまでのそれぞれの合計に加える。その後でそのファイルで
のカウンタの値を出力する。このカウンタはbeginfile
で処理するのを当
てにしている。
function beginfile(file) { chars = lines = words = 0 fname = FILENAME } function endfile(file) { tchars += chars tlines += lines twords += words if (do_lines) printf "\t%d", lines if (do_words) printf "\t%d", words if (do_chars) printf "\t%d", chars printf "\t%s\n", fname }
各行毎に実行される一つのルールがある。そのルールではchars
にレコー
ドの長さを加えている。ここで、改行キャラクタがレコードを分割していて
(RS
の値)レコード自身には含まれていないためさらに1を足す必要がある
lines
は行が読まれる度にインクリメントされ、words
はその行にある
"単語"の数、NF
の値だけ増加される。
(22)
最後に、END
ルールはすべてのファイルでの合計値を
出力する。
# do per line { chars += length($0) + 1 # 改行の分も忘れずに lines++ words += NF } END { if (print_total) { if (do_lines) printf "\t%d", tlines if (do_words) printf "\t%d", twords if (do_chars) printf "\t%d", tchars print "\ttotal" } }
awk
Programsこのセクションは雑多なプログラムの"福袋"である。 我々はあなたがそれらが共に面白くて、楽しいことを見いだすことを 希望する。
長い文章を書くときにありがちなエラーは 間違って単語を重複させてしまうというものである。 テキスト中では "the the program does the following ...." のようなものをしばしばみかけることだろう テキストがオンラインにある場合には、しばしば重複した単語は 行末と次の行の先頭とで発生し、それは非常に見つけにくい場所である。
このプログラム、`dupword.awk'はファイルを一度に一行読み、同じ単語が
隣接していないかをチェックする。同様に、その行の最後の単語を(prev
という変数に)次の行の最初の単語と比較するためにセーブしておく。
最初の二つのステートメントは行の内容を、 "The"から"the"の比較の結果が等しいものになるように 全て小文字にしている。 二番目のステートメントでは アルファベット、数字、空白以外のすべてのキャラクタを 句読点が比較に影響しないように取り去っている。 これによりときとして、本当は違う単語を重複していると報告する ときがあるが、これは滅多にないことである。
# dupword -- テキスト中の重複した単語を探し出す # Arnold Robbins, arnold@gnu.org, Public Domain # December 1991 { $0 = tolower($0) gsub(/[^A-Za-z0-9 \t]/, ""); if ($1 == prev) printf("%s:%d: duplicate %s\n", FILENAME, FNR, $1) for (i = 2; i <= NF; i++) if ($i == $(i-1)) printf("%s:%d: duplicate %s\n", FILENAME, FNR, $i) prev = $NF }
次に挙げるプログラムはシンプルな"アラーム時計"プログラムである。プログ ラムには、時刻とメッセージ(こちらは省略可能)を渡す。指定した時刻になった ときに、このプログラムはメッセージを標準出力に出力する。それに加えて、メ ッセージを繰り返す回数も指定することができ、さらにその繰り返しの間隔も指 定できる。
このプログラムでは
セクション Managing the Time of Dayを参照.
にあるgettimeofday
関数を使っている。
作業の全てはBEGIN
ルールで行っている。最初の部分では引数のチェック
と、ディレイ、カウント、出力メッセージのデフォルト値のセットを行っている。
ユーザーがメッセージを指定しているが、そこにASCIIのベルキャラクタ("警告
"キャラクタとして知られているもの、`\a')が含まれていない場合、それ
をメッセージに追加する(多くのシステムでは、ASCIIのベルキャラクタを出力す
ると警告の類の音を生成する。したがって、ユーザーがコンピュータやターミナ
ルを見ていない場合にアラームが作動したときにシステムが注意を喚起するので
ある)。
# alarm -- アラームをセットする # Arnold Robbins, arnold@gnu.org, Public Domain # May 1993 # usage: alarm time [ "message" [ count [ delay ] ] ] BEGIN \ { # Initial argument sanity checking usage1 = "usage: alarm time ['message' [count [delay]]]" usage2 = sprintf("\t(%s) time ::= hh:mm", ARGV[1]) if (ARGC < 2) { print usage > "/dev/stderr" exit 1 } else if (ARGC == 5) { delay = ARGV[4] + 0 count = ARGV[3] + 0 message = ARGV[2] } else if (ARGC == 4) { count = ARGV[3] + 0 message = ARGV[2] } else if (ARGC == 3) { message = ARGV[2] } else if (ARGV[1] !~ /[0-9]?[0-9]:[0-9][0-9]/) { print usage1 > "/dev/stderr" print usage2 > "/dev/stderr" exit 1 } # set defaults for once we reach the desired time if (delay == 0) delay = 180 # 3 分 if (count == 0) count = 5 if (message == "") message = sprintf("\aIt is now %s!\a", ARGV[1]) else if (index(message, "\a") == 0) message = "\a" message "\a"
次のコードは、アラームの時刻を時間と分に変換し、必要ならば24時間制の時間 にする。そのとき、この時間は真夜中からの経過秒数に変換される。次いで真夜 中からその時点までの経過秒数を計算する。これら二つの間の差は、アラームを 起動するまでどのくらいの長さがあるかということになる。
#目的時刻を分解する split(ARGV[1], atime, ":") hour = atime[1] + 0 # 強制的に数値に変換する minute = atime[2] + 0 # 強制的に数値に変換する #現在時刻を取得する gettimeofday(now) #与えられた時刻が12時間制の時刻で、現在より後、 #例えば午前9時に`alarm 5:30'とされたときに #目的時刻が午後5:30になるように、12を足す。 if (hour < 12 && now["hour"] > hour) hour += 12 #目的時刻を深夜からの秒数で取得する target = (hour * 60 * 60) + (minute * 60) #現在時刻を深夜からの秒数で取得する current = (now["hour"] * 60 * 60) + \ (now["minute"] * 60) + now["second"] #どのくらい寝るのか naptime = target - current if (naptime <= 0) { print "time is in the past!" > "/dev/stderr" exit 1 }
最後にプログラムはsystem
関数
(セクション Built-in Functions for Input/Outputを参照)
を使ってsleep
ユーティリティを呼び出す。
sleep
ユーティリティは単純に与えられた秒数だけ停止する。もし
sleep
ユーティリティの終了ステータスが0でなければ、このプログラム
はsleep
が中断されたとみなし、実行を終了する。sleep
がOKのス
テータス (0)を返した場合には、ループの中でメッセージを出力し、メッセージ
の出力間隔に必要な秒数だけ待つためにsleep
をもう一度使用する。
# zzzzzz..... 中断されたら終わり if (system(sprintf("sleep %d", naptime)) != 0) exit 1 # time to notify! command = sprintf("sleep %d", delay) for (i = 1; i <= count; i++) { print message # sleepコマンドが中断されたら終わり if (system(command) != 0) break } exit 0 }
システムのtr
ユーティリティはキャラクタの変換を行う。例えば、大文
字のキャラクタを小文字に変換するのによく次のように使われる。
generate data | tr '[A-Z]' '[a-z]' | process data ...
tr
ユーティリティにはブラケットで囲まれた二つのキャラクタリストを
渡す。通常は、これらのリストはシェルがファイル名の拡張子として扱うことの
内容にクォートされている。(23)入力を処理するときに、最初のリストにあ
る最初のキャラクタは二番目のリストにある最初のキャラクタに変換され、最初
のリストにある二番目のキャラクタは二番目のリストにある二番目のキャラクタ
に変換され、以後同様な変換が行われる。もし、"from"リストが"to'リスト
よりも長ければ、"to"リストの最後のキャラクタが、"from"の残りのキャラ
クタの変換後のキャラクタとして扱われる。
ちょっと前に、
あるユーザーが、我々に
gawk
に変換を行う関数を追加するように提案をしてきた。
これは(追加することに)反対するような"crepping featurism"であり、
私はそれを証明するために、
キャラクタの変換をユーザーレベルの関数で行うことのできる
後述するようなプログラムを作成した。
このプログラムはシステムのtr
ユーティリティほどには
完璧ではないが、ほとんどの仕事はきちんと行う。
translate
プログラムは標準のawk
の持つ幾つかの弱点の
一つを浮き彫りにする。それは、個々のキャラクタを扱うのが非常に
苦手であり、組込み関数のsubstr
、index
、gsub
を
くり返し使うことが要求されるということである
(セクション Built-in Functions for String Manipulationを参照)。
(24)
二つの関数があり、最初の一つはstranslate
で、
これは三つの引数をとる。
from
to
target
連想配列は変換部分を実に簡単にしている。t_ar
は"from"中のキャラ
クタで添え字付けされる"to"キャラクタを保持している。そして、単純なルー
プを通じてfrom
のキャラクタが一度に一文字ずつ変換されていく。
from
の各キャラクタは、そのキャラクタがtarget
にあれば
gsub
を使って、対応するto
のキャラクタに変換される。
関数translate
は単に$0
をターゲットとしてstranslate
を
呼び出すことだけをしている。メインプログラムは二つのグローバル変数、
FROM
とTO
をコマンドラインからセットして、それからawk
が標準入力から読み込みを行うようにするためにARGV
を変更する。
Finally, the processing rule simply calls translate
for each record.
最後に、単にtranslate
を呼ぶだけのルールが各レコードの処理を行う。
# translate -- trと同じようなことをする # Arnold Robbins, arnold@gnu.org, Public Domain # August 1989 # bugs: does not handle things like: tr A-Z a-z, it has # to be spelled out. However, if `to' is shorter than `from', # the last character in `to' is used for the rest of `from'. function stranslate(from, to, target, lf, lt, t_ar, i, c) { lf = length(from) lt = length(to) for (i = 1; i <= lt; i++) t_ar[substr(from, i, 1)] = substr(to, i, 1) if (lt < lf) for (; i <= lf; i++) t_ar[substr(from, i, 1)] = substr(to, lt, 1) for (i = 1; i <= lf; i++) { c = substr(from, i, 1) if (index(target, c) > 0) gsub(c, t_ar[c], target) } return target } function translate(from, to) { return $0 = stranslate(from, to, $0) } # メインプログラム BEGIN { if (ARGC < 3) { print "usage: translate from to" > "/dev/stderr" exit } FROM = ARGV[1] TO = ARGV[2] ARGC = 2 ARGV[1] = "-" } { translate(FROM, TO) print }
ユーザーレベルの関数でキャラクタの変換をすることは可能であるが、それは必
ずしも能率的ではない。そのため我々は組み込みの機能を追加することを検討し
始めた。しかしながら、このプログラムを書いた後で、我々はSystem V Release 4
のawk
にtoupper
とtolower
が追加されていることを学ん
だ(?)。これらの関数は必要があればキャラクタの大小文字の変換をするような
関数であり、gawk
にこれらの関数をそれのみ(gawk
だけ)で十分な
ことができるように追加することを決めた。
このプログラムの明らかな改良策は、配列t_ar
のセットアップを
BEGIN
ルール中で一度だけ行うということである。しかしながら、これは
"from"と"to"の二つのリストがプログラムの実行中を通じて決して変更され
ないということを仮定したものである。
次に挙げるプログラムは"real world"(25)プログラムである。 このスクリプトは名前とアドレスのリストを入力として読み込み、宛て名ラベル を生成する。各ページは20のラベルがあり、10個ずつの二段組みにされる。アド レスは5行を越えないように保証される。各アドレスは、空行で区切られる。
基本的なアイデアは、ラベル20個分のデータを読むことである。各ラベルの各行
は、line
という配列に格納される。この単一のルールは、line
と
いう配列を満たすことを行い、そして20個のラベルが読み込まれたときにそのペ
ージを出力するということを行っている。
BEGIN
ルールではawk
がレコードを空行で分割するようにするため
にRS
に空文字列をセットする
(セクション 入力をレコードへと分割をするやりかたを参照)。また、一ページの最大行
が(20×5=100)なので、MAXLINES
に100をセットする。
作業のほとんどは関数printpage
で行われている。ラベル行は順番に配列
line
に格納されるが、それは水平方向に印刷する必要がある。つまり、
line[1]
の次はline[6]
であり、line[2]
の次は
line[7]
、という具合になる。二つのループがこれを達成するために使わ
れている。 外側の、i
で制御されるループはデータ10行毎に一段落し、
これはラベルの各列(row)である。内側のj
で制御されるループは列(row)
にある各行を処理する。j
は0から4まで変化し、`i+j'はその列の
j
番目の行である。i+j+5
は次に処理するエントリとなる。出力は
最終的に次のようなものになる。
line 1 line 6 line 2 line 7 line 3 line 8 line 4 line 9 line 5 line 10
21行目と61行目では、ラベルにきちんと印字されるようにするために空行が出力 される。これはプログラムが書かれたときに使っていたラベルに依存するもので ある。同様に、ページの一番上二行と、一番下二行が空行であることに注意する こと。
END
ルールはラベルの最終ページのフラッシュを行っている。
データが、20ラベルの倍数である場合には、フラッシュされるようなデータは
ない。
# labels.awk # Arnold Robbins, arnold@gnu.org, Public Domain # June 1992 # Program to print labels. Each label is 5 lines of data # that may have blank lines. The label sheets have 2 # blank lines at the top and 2 at the bottom. BEGIN { RS = "" ; MAXLINES = 100 } function printpage( i, j) { if (Nlines <= 0) return printf "\n\n" # ヘッダー for (i = 1; i <= Nlines; i += 10) { if (i == 21 || i == 61) print "" for (j = 0; j < 5; j++) { if (i + j > MAXLINES) break printf " %-41s %s\n", line[i+j], line[i+j+5] } print "" } printf "\n\n" # フッター for (i in line) line[i] = "" } # メインルール { if (Count >= 20) { printpage() Count = 0 Nlines = 0 } n = split($0, a, "\n") for (i = 1; i <= n; i++) line[++Nlines] = a[i] for (; i <= 5; i++) line[++Nlines] = "" Count++ } END \ { printpage() }
次のawk
プログラムは入力中にあった単語がどのくらい現れたの数を出力
するものである。このプログラムはawk
の配列の性質が、添え字に文字列
を使った連想的なものであるということを示している。また、
`for @var {x' in array}という構成を例示したものでもある。結局
のところこれは、最少限度の努力で少々複雑な有用な仕事をさせるために、
awk
をどのように他の有益なプログラムと共に使うことをできるかという
ことを示している。若干の説明がプログラムリストの後にある。
awk ' #単語の出現頻度のリストを出力する { for (i = 1; i <= NF; i++) freq[$i]++ } END { for (word in freq) printf "%s\t%d\n", word, freq[word] }'
このプログラムを理解するにあたっての最初の点は、二つのルールがあるという
ことである。最初のルールは、パターンが空であるので入力のすべての行で実行
される。awk
のフィールドアクセス機構
(セクション フィールドの検査を参照) を行の中にある個々の単語を取り出すた
めに使っていて、どのくらいのフィールドがあるかを知るためにNF
(セクション 組み込み変数を参照)という組込み変数を使っている。
入力中にある各単語は、その単語が現れる度に一つ値が増える
freq
という配列の要素のために使われる。
二番目のルールはEND
というパターンを持っているので、入力がつきるま
では実行されない。そこでは最初のアクションで構築したテーブル、
freq
の内容を出力している。
このプログラムは実際のテキストファイルに使用するには幾つかの問題点がある。
awk
の規則を使って検索される。この規則は、フィールドはホワイ
トスペースによって区切られるというものであり、このため入力にあるその他の
キャラクタ(改行を除く)はawk
にとって特別な意味を持つものではない。
これは、句読点キャラクタを単語の一部として数えてしまうということである。
awk
言語はキャラクタの大文字と小文字とを区別する。このため。
`bartender' と `Bartender'は異なる単語として扱われる。標準的なテキ
ストでは、文章の始めにある場合には単語はキャピタライズされるので、これは
望ましいことではない。また、頻度アナライザーはキャピタライズに影響される
べきではない。
これらの問題を解決する方法はより進んだawk
言語の機能を使うことであ
る。第一に、大小文字の区別を取り除くために、tolower
を使用する。次
いで、句読点キャラクタを取り除くためにgsub
を使用する。最後に、
awk
スクリプトの出力に対してsort
ユーティリティを使用する。
次に新しいバージョンのプログラムを挙げる。
# Print list of word frequencies # 単語の出現頻度のリストを出力する。 { $0 = tolower($0) # 大小文字の区別をなくす gsub(/[^a-z0-9_ \t]/, "", $0) # 句読点をとる for (i = 1; i <= NF; i++) freq[$i]++ } END { for (word in freq) printf "%s\t%d\n", word, freq[word] }
このプログラムを`wordref.awk'というファイルにセーブし、データが `file1'というファイルにあったとすると、次のパイプライン
awk -f wordfreq.awk file1 | sort +1 -nr
これは、`file1'中にある単語を出現頻度順に並べた テーブルを出力する。
awk
プログラムは適切にデータを扱って、きちんと並べられては
いない単語テーブルを作り出す。
awk
スクリプトの出力は、その後でsort
ユーティリティによって
ソートされ、ターミナルに出力される。この例でsort
に与えられている
オプションは、ソートユーティリティが各行二番目のフィールド(最初のフィー
ルドはスキップする)を数値と見なした(`15'が`5'の前にくる)ソート
キーにして使って降順(逆順)並べかえを行うように指示している。
sort
で行っていることを、END
アクションを変えることによって
プログラムの中で行うこともできる。
END { sort = "sort +1 -nr" for (word in freq) printf "%s\t%d\n", word, freq[word] | sort close(sort) }
本当のパイプ(true pipe)を持ってないシステム上で ソートをするにはこのやり方を使わなければならないだろう。
sort
プログラムの使い方に関するより詳しい情報は、
一般的なオペレーティングシステムのドキュメントを参照のこと。
uniq
プログラム
(セクション テキストの重複のない行を出力するを参照)
はソートされたデータの重複を取り除く。
さてここで、あるデータファイルから重複したデータを取り除く必要があるが、 その順番はそのままにして置きたいということを望むようなことがあるだろう か?これは、シェルのヒストリーファイルが良い例かもしれない。ヒストリーフ ァイルは、ユーザーが入力したすべてのコマンドのコピーを保持する。そして、 数回同じコマンドを繰り返すことは希なことではない。ユーザーはときどき、ヒ ストリーファイルから重複したエントリを削除して、ヒストリーファイルをコン パクトにしたいと思うかもしれない。その場合にも元のコマンドの順序はそのま まにして置くことが望ましい。
次に挙げる単純なプログラムは、この仕事をこなす。このプログラムは二つの配
列を使い、data
という配列は各行のテキストによって添え字付けされる。
ある行がそれまでに現れていないコマンドでああれば、data[$0]
はゼロ
である。この場合、その行のテキストはlines[count]
に格納される。
lines
の各要素はユニークものであり、また、[lines]
は行の出現
順 序に添え字付けされる。END
ルールは、単に順番に[lines]
の要
素を出力しているだけである。
# histsort.awk -- シェルのヒストリーファイルを小さくする # Arnold Robbins, arnold@gnu.org, Public Domain # May 1993 # Thanks to Byron Rakitzis for the general idea { if (data[$0]++ == 0) lines[++count] = $0 } END { for (i = 1; i <= count; i++) print lines[i] }
このプログラムにはまた、他の便利な情報を生成するための土台を提供する。
例えば、次のようなprint
文をEND
ルールで使うことによって
ある特定のコマンドがどのくらいの回数使われたのかを示すようになる。
print data[lines[i]], lines[i]
これはdata[$0]
が見つかる度にインクリメントされているので
きちんと働く。
この章と一つ前の章
(セクション awk
の関数ライブラリを参照)
では、多くのawk
プログラムを例示している。
もしこれらのプログラムを試してみたいのであれば、
手でこれらのプログラムをタイプしなければならないというのは退屈である。
そこで、Texinfoの入力ファイルの一部を引き抜いて個々のファイルに納める
ことのできるプログラムを提供する。
このマニュアルはGNUプロジェクトのドキュメントフォーマッティング 言語であるTexinfoで記述されている。 TexinfoはFree Software Foundationから入手可能な Texinfo--The GNU Documentation Format で詳しく説明されている。
上述した目的のためには、Texinfoファイルに関して 三つの事柄を知っておけば十分である。
awk
での`\'のようなものである。
`@'という文字そのものはTexinfoソースの中では
`@@'で表わされる。
以下に挙げるプログラム、`extract.awk'はTexinfoソースファイルを
読み、特殊なコメントに基づいて二つの事柄を行う。
`@c system ...'というシーケンスを見つけたときには、
コマンドのテキストを制御行から抜き取り、それを
system
関数に渡すことによりコマンドを実行する
(セクション Built-in Functions for Input/Outputを参照)。
`@c file filename'というシーケンスを見つけたときには、
`@c endfile'というシーケンスに出会うまで
各行をfilenameというファイルに出力する。
`extract.awk'のルールは、`oment'の部分をオプショナルに
することによって`@c'と`@comment'の両方にマッチするように
なっている。`@group'か`@end group'を含んでいる行は
単に削除される。`extract.awk'はライブラリ関数のjoin
(セクション Merging an Array Into a Stringを参照)を
使用している。
オンラインTexinfoソース、Effective AWK Programming(`gawk.texi')にあるサ
ンプルプログラムは全て`file'と`endfile'に囲まれた中に置かれて
いる。gawk
の配布キットでは、サンプルプログラムを展開するためと、
それらをgawk
が見つけることのできる標準的なディレクトリにインスト
ールするために`extract.awk'を使っている。
Texiinfoファイルは以下のような形式である:
... This program has a @code{BEGIN} block, which prints a nice message: @example @c file examples/messages.awk BEGIN @{ print "Don't panic!" @} @c end file @end example これは最後に次のような出力をする: @example @c file examples/messages.awk END @{ print "Always avoid bored archeologists!" @} @c end file @end example ...
`extract.awk'はディレクティブの大小文字の違いを気にしないために、
IGNORECASE
に1をセットすることから始まっている。
最初のルールではsystem
を扱い、与えられたコマンドをチェックし(少な
くともNF
が三つある)、そのコマンドが正しく実行されたことを表わす終
了ステータス0を返すことをチェックする。
# extract.awk -- extract files and run programs # from texinfo files # texinfoファイルからファイルを取り出し、プログラムを実行する # Arnold Robbins, arnold@gnu.org, Public Domain # May 1993 BEGIN { IGNORECASE = 1 } /^@c(omment)?[ \t]+system/ \ { if (NF < 3) { e = (FILENAME ":" FNR) e = (e ": badly formed `system' line") print e > "/dev/stderr" next } $1 = "" $2 = "" stat = system($0) if (stat != 0) { e = (FILENAME ":" FNR) e = (e ": warning: system returned " stat) print e > "/dev/stderr" } }
e
という変数は関数を
page.
ページ
に体裁よく納めるために使っている。
二番目のルールでは、データをファイルに取り出すことを行っている。ディレク ティブで与えられたファイル名の確認を行い、もしそれがそのときに処理してい るファイルでなければ、以前のファイルをクローズする。これは`@c endfile' ではファイル名を与えないでも良いということである(この場合、今行って はいないが診断メッセージを出すべきである)。
`for'ループ中でgetline
(セクション getline
を使った入力を参照).
を使って、行の読み込み作業を行っている。予期しないファイルの終了に対して
は、unexpected_eof
という関数を呼び出す。行が"endfile"の行に
あった場合には、ループを脱出する。注目している行に`@group'か
`@end group'があった場合、それを無視し、次の行の処理に移る
(これらTexinfo制御の行は一ページにまとまるコードのブロックを
保持している。残念ながら、TeX はこの作業を常に正しく行うほどには
賢くない。そのために、ちょっとしたアドバイスをしなければならない)。
作業のほとんどは、それに続く数行で行っている。もし、読み込んだ行に@samp {@}シンボルがなければ、それは直接出力することができる。`@'があっ て、それが先頭にある行は取り除かなければならない。
`@'を取り除くためにsplit
関数
(セクション Built-in Functions for String Manipulationを参照).
を使って、その行はa
という配列の要素に分割される。a
の空に
なっている要素は二つの連続した`@'シンボルが元の行にあったことを示し
ている。二つの空要素(元のファイルの`@@')毎に、`@'シンボル一
つを戻す必要がある。
配列の処理が終わったとき、join
をSUBSEP
の値を引数に
使って呼び出し、配列を単一の行に戻す。
この行は、その後出力先のファイルに出力される。
/^@c(omment)?[ \t]+file/ \ { if (NF != 3) { e = (FILENAME ":" FNR ": badly formed `file' line") print e > "/dev/stderr" next } if ($3 != curfile) { if (curfile != "") close(curfile) curfile = $3 } for (;;) { if ((getline line) <= 0) unexpected_eof() if (line ~ /^@c(omment)?[ \t]+endfile/) break else if (line ~ /^@(end[ \t]+)?group/) continue if (index(line, "@") == 0) { print line > curfile continue } n = split(line, a, "@") # if a[1] == "", means leading @, # don't add one back in. for (i = 2; i <= n; i++) { if (a[i] == "") { # @@だった a[i] = "@" if (a[i+1] == "") i++ } } print join(a, 1, n, SUBSEP) > curfile } }
注意すべきことは、`>'を使ったリダイレクトである。`>'を使った出
力はファイルを一度だけオープンし、ファイルをオープンし続けてその後の出力
はファイルに追加する
(セクション Redirecting Output of print
and printf
を参照)。
このことは、プログラムのテキストと、それ対しての説明をする文(ここでやっ
ているように!)を何の面倒もなしに、簡単に混ぜることができることを可能とす
る。ファイルは新しいデータファイル名に出会ったときか、入力ファイルの最後
にきたときだけクローズされる。
最後にunexpected_eof
という関数が適切な
エラーメッセージを出力し、実行を終了する。
END
ルールは、オープンしたファイルをクローズする
最後のクリーンアップ作業を行っている。
function unexpected_eof() { printf("%s:%d: unexpected EOF or error\n", \ FILENAME, FNR) > "/dev/stderr" exit 1 } END { if (curfile) close(curfile) }
sed
ユーティリティはデータストリームを読み取り、変更を行い、変更し
たデータを出力するプログラム、"ストリームエディタ"(stream editor)であ
る。これはしばしば大きなファイルやパイプラインを通じて他のコマンドが生成
したデータストリームに対して全面的な変更を加えるときに用いられる
sed
はそれ自身が複雑なプログラムでもあるが、
ほとんどの使用目的はパイプラインの真ん中で全面的な置換を行うことである。
command1 < orig.data | sed 's/old/new/g' | command2 > result
この例の`s/old/new/g'はsed
に対して
入力行から`old'という正規表現にマッチするものを検索し、
(見つかったら)それを`new'に
置換する作業を
グローバル(つまり、その行のすべてのマッチするものに対して)
に行えということを指示している。
これは、awk
のgsub
関数と似ている
(セクション Built-in Functions for String Manipulationを参照)。
次のプログラム、`awksed.awk'は、少なくとも二つのコマンドライン引数を 受け付ける。その二つは、テキストから検索するパターンと、それを置き換える パターンである。さらに引数がある場合には、それは処理する対象の ファイル名として扱われる。もしファイル名が一つも与えられなければ、 標準入力が入力元として扱われる。
# awksed.awk -- s/foo/bar/g をprintだけでやる # アイデアを出してくれたMichael Brennanに感謝 # Arnold Robbins, arnold@gnu.org, Public Domain # August 1995 function usage() { print "usage: awksed pat repl [files...]" > "/dev/stderr" exit 1 } BEGIN { # 引数を検査する if (ARGC < 3) usage() RS = ARGV[1] ORS = ARGV[2] # 引数をファイルとして使用しない ARGV[1] = ARGV[2] = "" } # look ma, no hands! { if (RT == "") printf "%s", $0 else print }
このプログラムは
正規表現であるRS
を扱う能力と、実際にレコードを区切ったテキストが
RT
であるという動作に依存している
(セクション 入力をレコードへと分割をするやりかたを参照)。
アイデアの基本は、RS
を検索するパターンにするということである。
gawk
は自動的にそのパターンにマッチしたものの間にある
テキストを$0
にセットする。これは我々がそのままにしておきたいと
考えているテキストである。そして、ORS
に置換後のテキストを
セットすることによって、print
文はそのままにしておきたい
テキストを(変換せずにそのまま)出力し、その後ろに
置換したテキストを出力するのである。
この手順に対して、
レコードがRS
で終わっていない場合の手続きにうまい手段は
ないものだろうか?
print
文を使ってしまうと、出力すべきでない置換テキストを
出力してしまい、結果が正しいものでなくなってしまう。
しかし、ファイルがRS
でマッチしたテキストで終わっていない場合には、
RT
は空文字列になる。この場合、$0
を
printf
を使って出力することが可能である。
(セクション Using printf
Statements for Fancier Printingを参照)。
BEGIN
ルールは、引数が適正な数かどうかのチェック、そこに問題があっ
た場合にusage
を呼び出すなどのセットアップ作業を扱っている。その
後で、RS
とORS
にコマンドライン引数からの値をセットし、ファ
イル名として扱われないようにARGV[1]
とARGV[2]
に空文字列をセ
ットしている(セクション Using ARGC
and ARGV
を参照)。
usage
関数はエラーメッセージを出力し、実行を終了する。
最後に、一つのルールで上述した手順の通り、
RT
の値に従って適切に
print
かprintf
を使って出力を行う。
awk
でライブラリ関数を使うことができると非常に有益である。それはコ
ードの再利用と汎用的な関数の作成を促進しする。プログラムはより小さくなり、
それにより(プログラムの)見通しがよくなる。しかしながら、ライブラリ関数の
使用は、awk
プログラムを書くときにのみ簡単になるのであって、それを
実行するときは複数の`-f'オプションを必要とするので面倒である。もし
gawk
が利用できないのなら、AWKPATH
環境変数が使えず、
awk
数をライブラリディレクトリに置くという
機能(セクション コマンドラインオプションを参照).
もないので、より面倒さが顕著になる。
次のようにプログラムが書ければとても良いだろう。
# ライブラリ関数 @include getopt.awk @include join.awk ... # メインプログラム BEGIN { while ((c = getopt(ARGC, ARGV, "a:b:cde")) != -1) ... ... }
以下に挙げるプログラム`igawk.sh'は今述べたサービスを提供する。
gawk
のAWKPATH
環境変数を使ったファイルの検索をシミュレートし、
ネストしたインクルード、つまり@include
でインクルードされたフ
ァイルがさらに`@include'を含んでいることを許す。igawk
はファ
イルのインクルードを一度だけ行うようになっている。それは、ネストしてイン
クルードすることによってライブラリ関数を二度インクルードすることがないよ
うにするためである。
igawk
は外面的にはgawk
と同じように振る舞うべきである。これ
は、複数の`-f'を使ったソースファイルの指定や、コマンドライン上のプ
ログラムとライブラリファイルとを混ぜてつかるということも含めて、
gawk
のコマンドライン引数の全てを受け付けるべきであるということである。
このプログラムはPOSIXのシェル(sh
)言語を使って記述された。
プログラムの動作の方法は以下の通り。
awk
のソースコードを表わしていな
い引数を後で展開したプログラムを実行するときに使用するためにセーブする。
awk
テキストである引数では、それを展開するテンポラリファイルに出力
する。ここで二つの場合がある。
echo
プログラムが
自動的に行末の改行を付け加える。
gawk
が行うやり方で作業するので、
正しい箇所でファイルの内容がプログラムにインクルードされる
awk
プログラムを実行する。
展開されたプログラムは二番目のテンポラリファイルに出力される。
gawk
にユーザーが元々指定したコマンドライン引数を一緒に
渡して実行させる。
プログラムの最初の部分では、最初の引数が`debug'であったときにシェル
のトレースをオンにするということをしている。そうでない場合にはシェルの
trap
文を使ってプログラムが終了したり、割り込みを受けた場合にテンポ
ラリファイルを消去するようにする。
次の部分にあるループでは、すべてのコマンドライン引数を処理する。 いくつかの重要な事例がある。
--
igawk
に対する引数を終わらせる。この後の引数は評価されることなくユ
ーザーのawk
プログアムに渡される
-W
gawk
に特有のものであることを示す。引数の処
理を早めに行わせるために、`-W'は残りの引数に付け足され、ループは継
続される(これはsh
プログラミングトリックである。もしsh
に不
慣れであっても気にすることはない)。
-v
-F
gawk
に渡される。
-f
--file
--file=
-Wfile=
sed
ユーティリティが、ファイル名の前にある
部分(例えば`--file=')を取り去るために使われる。
--source
--source=
-Wsource=
--version
--version
-Wversion
igawk
のバージョンを出力し、また、
`gawk --version'を実行して使用する gawk
の
バージョン情報を出力して終了する。
`-f', `--file', `-Wfile', `--source',`-Wsource'
のいずれもが指定されなかった場合、最初の非オプション引数が
awk
プログラムとなる。もしコマンドライン引数が
残っていなければ、igawk]はエラーメッセージを出力し、
実行を終了する。引数がある場合には、最初の引数が
`/tmp/ig.s.$$'に出力される。
どのような場合でも、引数が処理された後では
`/tmp/ig.s.$$'の内容は完全なオリジナルawk
プログラム
のテキストとなる。
`$$'はsh
ではカレントプロセスID番号となる。これは、ユニークな
テンポラリファイル名を生成するためにシェルプログラミングでは良く使われて
いる。これによりigawk
を複数のユーザーがテンポラリファイルの重複を
気にすることなしに実行することができるようになる。
#! /bin/sh # igawk -- gawkに似ているが、@includeを処理する # Arnold Robbins, arnold@gnu.org, Public Domain # July 1993 if [ "$1" = debug ] then set -x shift else # exit、ハングアップ、割り込み、quit、終了のときにクリーンアップをする trap 'rm -f /tmp/ig.[se].$$' 0 1 2 3 15 fi while [ $# -ne 0 ] # loop over arguments do case $1 in --) shift; break;; -W) shift set -- -W"$@" continue;; -[vF]) opts="$opts $1 '$2'" shift;; -[vF]*) opts="$opts '$1'" ;; -f) echo @include "$2" >> /tmp/ig.s.$$ shift;; -f*) f=`echo "$1" | sed 's/-f//'` echo @include "$f" >> /tmp/ig.s.$$ ;; -?file=*) # -Wfile or --file f=`echo "$1" | sed 's/-.file=//'` echo @include "$f" >> /tmp/ig.s.$$ ;; -?file) # get arg, $2 echo @include "$2" >> /tmp/ig.s.$$ shift;; -?source=*) # -Wsource or --source t=`echo "$1" | sed 's/-.source=//'` echo "$t" >> /tmp/ig.s.$$ ;; -?source) # get arg, $2 echo "$2" >> /tmp/ig.s.$$ shift;; -?version) echo igawk: version 1.0 1>&2 gawk --version exit 0 ;; -[W-]*) opts="$opts '$1'" ;; *) break;; esac shift done if [ ! -s /tmp/ig.s.$$ ] then if [ -z "$1" ] then echo igawk: no program! 1>&2 exit 1 else echo "$1" > /tmp/ig.s.$$ shift fi fi # この時点で/tmp/ig.s.$$ の内容は最終的に実行するプログラムとなる
awk
プログラムは`@include'ディレクティブを処理するために、
getline
を使って一行ずつプログラムを通読する
(セクション getline
を使った入力を参照)。
入力ファイル名と`@include'文はスタックを使って管理される。
`@include'に出会う度に、カレントファイル名が
スタックに"プッシュ"され、`@include'ディレクティブで指定された
ファイル名がカレントファイル名になる。
各ファイルを処理し終える度に、スタックから"ポップ"して
直前の入力ファイルを再びカレントファイルにする。
このプロセスはオリジナルのファイルをスタックの最初に
置くことから開始する。
pathto
関数はファイルのフルパス名を探す作業を行っている。
これはgawk
がAWKPATH
環境変数を使って検索するときの
動作のシミュレートである
(セクション AWKPATH
環境変数を参照)。
もしファイル名に`/'が含まれていれば、検索にパスは使用されない。
`/'がなければ、ファイル名はパスに含まれているディレクトリの
名前と連結され、その結果作られたファイル名によってファイルのオープンが
できるかどうかを試す。awk
でファイルから読み込みが
できるかどうかをテストできる手段は一つだけしかなく、それは
getline
でそのファイルから読み込みができるか、ということであり
これはpathto
が行っていることである(26)
もしファイルから読み込むことができればそのファイルをクローズし、
その時のファイル名を関数の戻り値として返す。
gawk -- ' # process @include directives # @include指令を処理する function pathto(file, i, t, junk) { if (index(file, "/") != 0) return file for (i = 1; i <= ndirs; i++) { t = (pathlist[i] "/" file) if ((getline junk < t) > 0) { # found it close(t) return t } } return "" }
メインプログラムはBEGIN
ルール一つからなる。最初に行うことはpathto
で使用するpathlist
という配列をセットアップすることである。パスを`:'
で分割した後で、空の要素はカレントディレクトリを表わす"."
に置き換えられる。
BEGIN { path = ENVIRON["AWKPATH"] ndirs = split(path, pathlist, ":") for (i = 1; i <= ndirs; i++) { if (pathlist[i] == "") pathlist[i] = "." }
スタックは`/tmp/ig.s.$$'となるARGV[1]
で初期化される。
メインループがそれに続く。入力行は継続して読み続けられる。
`@include'で始まっていない行はそのまま出力される。
行が`@include'で始まっている場合、ファイル名は$2
にある。
pathto
はフルパスを生成するために呼び出される。
もしここで生成できなければ、エラーメッセージを出力し、
処理を継続する。
次に行うことは、ファイルがすでにインクルードされたものでないかどうか のチェックである。もしそのファイルがすでに出現したものであれば、 警告メッセージを出力する。初めてのものであれば、 新しいファイル名をスタックにプッシュし、処理を継続する。
最後に、getline
が入力ファイルの終端にぶつかったとき、
そのファイルはクローズされ、スタックからポップされる。
stackptr
が0を下回ったとき、プログラムは終了する。
stackptr = 0 input[stackptr] = ARGV[1] # ARGV[1] が最初のファイル for (; stackptr >= 0; stackptr--) { while ((getline < input[stackptr]) > 0) { if (tolower($1) != "@include") { print continue } fpath = pathto($2) if (fpath == "") { printf("igawk:%s:%d: cannot find %s\n", \ input[stackptr], FNR, $2) > "/dev/stderr" continue } if (! (fpath in processed)) { processed[fpath] = input[stackptr] input[++stackptr] = fpath } else print $2, "included in", input[stackptr], \ "already included in", \ processed[fpath] > "/dev/stderr" } close(input[stackptr]) } }' /tmp/ig.s.$$ > /tmp/ig.e.$$
最後のステップは展開したプログラムと、ユーザーが指定した
オリジナルのオプションと
コマンド引数を渡してgawk
を呼び出すことである。
gawk
の終了ステータスは呼び出し元の
igawk
に返される。
eval gawk -f /tmp/ig.e.$$ $opts -- "$@" exit $?
このバージョンのigawk
はこのプログラムに対する私の三つめの試みを表
わしている。プログラムをより良く作業させる三つの重要な単純化がそこにある。
awk
プログラムの組み立てが単純になる。
すべての`@include'の処理は一度だけ実行できる。
pathto
関数はファイルにアクセスできるかどうかをテストするために
getline
を使って読んだ行をセーブしようとしない。
この行をメインプログラムで使うためにセーブしようとすると、
かなり複雑なものになってしまう。
getline
のループをBEGIN
ルールの中で使うことによって、
全てを一ヶ所で行う。これはネストした`include'文の処理の
ために別のループを呼び出す必要がないということである。
同様に、このプログラムはsh
とawk
を組み合わせてプログラミン
グすることはしばしば価値があることだということを示すものである。あなたは
通常、CやC++の低レベルなプログラミングに頼らずに多くのことを達成すること
ができるし、ある種の文字列や引数の取り扱いはawk
よりもシェルを使う
ことによってしばしば簡単になる。
また、igawk
はプログラムに新しい機能を付け加えることが常に必要なわ
けではないということを証明している。igawk
があるので、
`@include'を処理する能力をgawk
自身に追加すべき大きな理由はない。
この追加の例として、検索パスに含まれるディレクトリに 二つのファイルを置いておくことを考えてみよう。
getopt
やassert
のような
デフォルトのライブラリ関数の集合からなる。
gawk
がリリースされたときに
`default.awk'を
システム管理者がいちいちローカル関数を付け加なくとも
更新できるようになる。
あるユーザー
が、gawk
をその起動時に自動的にこれらのファイルを読み込む様に
修正して欲しいという要望を寄せてきた。そうする代わりに、
igawk
をそのような動作をするように変更することは
とても簡単だった。igawk
はネストした@include
ディレクティブを
扱うことができるので、`default.awk'は必要なライブラリ関数を指示する
`@include'文を含むことができた。