AWK(おーく)は, 任意の機能をもつフィルタプログラムを手軽に作成するためのスクリプト言語である. 文法はC言語に似ているので,3J 学生にとっては,なじみやすいだろう.
オンラインマニュアル gawkや 参考文献 AWK の第一歩 も参考にしながら, 作業を進めよう.
とにかく,AWK を使えば簡単に, フィルタプログラムを構成できることを体験してみよう.
AWK が得意とする入力データは, 表(テーブル,table)形式のテキストデータである. なお,表データの各行(横方向)のデータ集合をレコード(record), 各列(縦方向)のデータ集合をフィールド(field)と呼ぶ.
表形式データの例として,成績表を考えよう.
Name | Eng | Math | Phys |
---|---|---|---|
Aho | 90 | 100 | 80 |
Weinberger | 80 | 100 | 95 |
Kernighan | 85 | 95 | 100 |
表データをテキストファイルとして記録する場合, 各フィールドの間は,通常,スペースやタブによって区切る. たとえば,上の成績表をテキストデータとして表わすと次のようになる. サンプルデータファイル table.txt:
Aho 90 100 80 Weinberger 80 100 95 Kernighan 85 95 100
AWK はレコード単位に処理を実行する. つまり,1 行のデータを読み,そのレコードに対して処理を実行する. そして,すべてのレコードに対して,この処理を繰り返す.
各レコードに対する処理は,Cのようなブロック構造をもち, 一般に次のような形式で記述される:
パターン { アクション } # パターンに一致した行だけに対してアクションを実行
パターン(正規表現や条件式)にマッチしたレコードに対してだけ, アクション(処理)を実行する.
また,デフォルトのパターンの場合,すべてのレコードを処理対象とする:
{ アクション } # すべての行に対してアクションを実行
while (レコード文字列を入力) { if (レコード文字列 == パターン) { アクション } { アクション } }
AWK では,while や if の記述は不要であり, コードを非常に短く書ける.
アクションの部分には, 計算式や制御構造などをCと同様な文法で記述できる.
レコード内の各フィールドを参照するには, フィールド変数「$番号」を使う. 上の成績表の例では,$1 が Name, $2 が Eng,... のフィールドを表わす. また,$0 はレコード全体を表わす.
なお,普通の変数には $ を付けない.
では, サンプルデータ table.txt に対して, 単純な AWK コマンドを実行してみよう:
$ cat table.txt Aho 90 100 80 Weinberger ... ... $ awk '{ print $0 }' table.txt # すべてのフィールドを表示 Aho 90 100 80 ... $ awk '{ print $2 }' table.txt # 2 番目のフィールドだけを表示 90 ...
これらは,cat コマンドおよび cut コマンドと同様な機能をもつフィルタの例だ.
次は,正規表現のパターンを使ってみよう:
$ awk '/n.*n/ { print $0 }' table.txt Kernighan 85 95 100
これは,grep 'n.*n' table.txt みたいなフィルタだ. なお,記号「/」でクォートされたパターンが正規表現とみなされる.
そして,条件式のパターンも使ってみよう:
$ awk '$4 >= 90 { print $0 }' table.txt Weinberger 80 100 95 Kernighan 85 95 100
第4フィールド(Phys)が 90 点以上のレコードだけを抽出した.
さて,以上のような機能をもつフィルタプログラムを「C言語で実装せよ」 と言われると大変そうだ. はたしてソースコードは何行になってしまうのだろうか...? AWK なら 1 行で出来た!!
AWK は,前のセクションのようにコマンドライン上で実行することもできるが, 処理が複雑な場合やその処理を何度も使い回したい場合には, スクリプトファイルに記述しておくと便利である.
まず,cat コマンドのクローンを例として, AWK スクリプトファイル cat.awk を作ってみよう:
#!/usr/bin/awk -f # 説明:cat のような AWK スクリプト # 使い方:cat.awk ファイル名 ... { print $0; # レコード全体を出力 }
なお,シェルスクリプトの場合と同様, 記号 # から行末まではコメントであり省略してもよいが, 第1行だけは単なるコメントではなく,実行に必要なものなので削除しないこと. そして,実行許可についてもシェルと同様:
$ chmod +x cat.awk
では,table.txt を入力として実行してみよう:
$ ./cat.awk table.txt Aho 90 100 80 ... # cat table.txt と同じ結果
このように,AWK スクリプトのコマンドライン引数は, 自動的に入力ファイルとして処理される.
また,複数のファイル名を指定してもよい. その場合,すべてのファイルに対して処理が繰り返される. 同じファイルを3個使った場合:
$ ./cat.awk table.txt table.txt table.txt Aho 90 100 80 ... Aho 90 100 80 ... Aho 90 100 80 ... # cat table.txt table.txt table.txt と同じ結果
cut クローンな AWK スクリプト cut.awk は次のようになる:
#!/usr/bin/awk -f # 説明:cut のような AWK スクリプト # 使い方:cut.awk f=フィールド番号 ファイル名 ... { print $f; # f 番目のフィールドだけ出力 }
このバージョンでは, 切り出すフィールドをコマンドラインで指定できる. たとえば,第2フィールドを抽出したければ:
$ ./cut.awk f=2 table.txt 90 80 85
処理内容的には,変数の使い方が特殊なので, ちょっと理解しづらいかもしれない. コマンドライン引数に,たとえば f=2 が指定されると, スクリプト内の変数 f に 2 が代入される. そのため,$f は $2 に置換され, 結果的に print $2 を実行することになる.
同様に grep クローンを作ってみよう:
#!/usr/bin/awk -f # 説明:grep のような AWK スクリプト # 使い方:grep.awk p=正規表現パターン ファイル名 ... $0 ~ p { # 正規表現に一致したレコードに対して... print $0; # レコードを出力 }
$ ./grep.awk p='n.*n' table.txt Kernighan 85 95 100
なお,条件演算子「~」は,「==」の正規表現版である. 正規表現パターンが文字列定数の場合, 前出のとおり,「/正規表現/」と書いていた. 一方,正規表現パターンが変数の場合には, このスクリプトのように,条件式として記述する必要がある.
もう少し複雑にしてみよう. 次は,wc コマンドのクローン:
#!/usr/bin/awk -f # 説明:wc のような AWK スクリプト # 使い方:wc.awk ファイル名 ... BEGIN { # 前処理 n = 0; # 単語数を初期化(この行は省略可) } { # 各レコードに対して... n += NF; # 単語数をカウント } END { # 後処理 print NR, n; # 行数と単語数を出力 }
このように複数個のブロックを設置できる. これは,Cプログラムで if ブロックを複数個書き並べていることと同じだ.
なお,パターン BEGIN および END のブロックは, それぞれ,最初のレコードの前および最後のレコードの後に処理される. (つまり,Cプログラムの場合,メインループの外部の処理.) その他のブロックは各レコードに対して処理される. (つまり,Cプログラムの場合,メインループの内部の処理.)
また,変数 NR には, データ全体のレコード数(Number of Records), 変数 NF には, そのレコードのフィールド数(Number of Fields)が自動的に代入されている.
なお,変数 NR は, 空行(NF == 0 のレコード)も数えてしまう. wc の場合には,これでも構わない. しかし,場合によっては(他のプログラムでは), NR をそのまま使うだけでは, 意図しない結果になるかもしれない. 注意しよう.
空行を取り除くスクリプトの断片:
NF > 0 { print $0; nr++; # nr:空行以外の行数(ユーザ変数) }
なお,これらの組み込み変数(NR や NF)の値は, 自動的に設定される. これらも変数なので,ユーザ変数と同様, 書き換え可能ではあるが,それはトラブルの元だ.やめておこう.
AWK のオンラインマニュアルには, 他にも多くの機能や使用例が紹介されている. 目を通しておこう:
$ man gawk
サンプルデータ table.txt を入力として, 各レコード内の平均点(個人の平均)および 全レコードの平均点(科目の平均)の欄を追加して出力する AWK スクリプト average.awk を作成せよ.
実行例:
$ ./average.awk table.txt Aho 90 100 80 90 Weinberger 80 100 95 91.6667 Kernighan 85 95 100 93.3333 average 85 98.3333 91.6667 91.6667
なお,フィールドの表示位置の整列については難しいので, 多少は,表示が乱れてもよい.
もし余裕があれば,HTML の <table> へ変換して出力してみよう. そうすれば,ブラウザが出力データを整列表示してくれる. HTML 版の実行例:
$ ./average.awk table.txt > average.html $ firefox !$ &
Aho | 90 | 100 | 80 | 90 |
Weinberger | 80 | 100 | 95 | 91.6667 |
Kernighan | 85 | 95 | 100 | 93.3333 |
average | 85 | 98.3333 | 91.6667 | 91.6667 |
---|
ヒント:
{ print データ データ ... }
(c) 2018, yanagawa@kushiro-ct.ac.jp