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'文を含むことができた。