共通テーマ
実例でわかる awk: 第 1 回
不思議な名前を持つすばらしい言語のご紹介
コンテンツシリーズ
このコンテンツは全#シリーズのパート#です: 共通テーマ
このコンテンツはシリーズの一部分です:共通テーマ
このシリーズの続きに乞うご期待。
awk を擁護する
この連載記事を通して、皆さんに熟練した awk プログラマーになってもらいたいと思います。awk とは、けっしてかわいくもないし、特にかっこいい名前でもありません。GNU 版の awk の名前である gawk に至っては明らかに変な名前 (訳注: gawk は英語で「のろま」「気の利かない人」等の意味があります) です。この言語になじみのない人であれば「awk」と聞くと、非常に知識の豊富な UNIX の第一人者でさえも頭がおかしくなりそうなほどの (そしてコーヒーを淹れに行きながら、繰り返し「kill -9!」と叫んでしまうような)、あまりにも時代に逆行した古めかしい乱雑なコードを思い浮かべるかもしれません。
確かに、awk は立派な名前ではありません。しかし awk はすばらしい言語です。awk は、テキスト処理やレポート作成に適していますが、たくさんの洗練された機能を備えているため、基幹業務のプログラミングにも利用できます。また一部の言語とは違い、awk の構文は親しみやすく、C、Python、および bash などの言語の長所を取り入れています (しかし厳密には、awk は Python や bash より前に開発されています)。awk は一度習得すれば、戦略的なコーディングの重要な部分になり得る言語です。
awk の第1歩
では awk で遊びながら、その仕組みを見てみましょう。コマンド行に次のコマンドを入力してください。
$ awk '{ print }' /etc/passwd
画面に /etc/passwd ファイルの内容が表示されるはずです。さて awk が何をしたのか説明しましょう。awk を呼び出した時、/etc/passwd
を入力ファイルに指定しました。awk を実行すると、print コマンドが /etc/passwd
の各行に順次、実行されます。すべての出力は標準出力へ送られ、/etc/passwd
の内容を書き出したのと同じ結果が得られます。次に、{ print }
コード・ブロックの説明をします。C 言語と同様、awk
は中括弧を使用してコードをブロックにまとめます。この例では、ブロックの中には 1 個の print コマンドしかありません。awk では print
だけが単体でコマンドとして使われた場合、現在行のすべての内容が出力されます。
ここに、最初の例とまったく同じ動作をする awk の別の例をご紹介します。
$ awk '{ print $0 }' /etc/passwd
変数 $0
は現在行全体を意味するので、print
と
print $0
はまったく同じ動作となります。awk
では以下のように、入力データとは一切関係のないデータを出力するプログラムを作成することもできます。
$ awk '{ print "" }' /etc/passwd
print コマンドにストリング ““ を指定すると、ブランク行が出力されます。このスクリプトをテストしてみると、/etc/passwd ファイルの行の数だけ、ブランク行が出力されることがわかります。繰り返しになりますが、このような結果が得られるのは awk が入力ファイルの各行に対してスクリプトを実行するためです。別の例をもう 1 つ示します。
$ awk '{ print "hiya" }' /etc/passwd
このスクリプトを実行すると、画面が hiya で埋まります。
複数フィールド
awk は、複数の論理フィールドに分割されたテキストの処理が得意であり、awk のスクリプトの中から各フィールドを容易に参照することができます。次のスクリプトは、使用中のシステムに存在するユーザー・アカウントの一覧表を出力します。
$ awk -F":" '{ print $1 }' /etc/passwd
上の例では -F オプションを使用して、”:” をフィールド区切り文字に指定しています。awk が print $1
コマンドを実行すると、入力ファイルの各行の 1 番目のフィールドが出力されます。別の例をもう 1 つ示します。
$ awk -F":" '{ print $1 $3 }' /etc/passwd
以下に示すのは、このスクリプトで作成される出力の抜粋です。
halt7 operator11 root0 shutdown6 sync5 bin1 ....etc.
ご覧のとおり、/etc/passwd ファイルの 1 番目と 3 番目のフィールドが出力され、それぞれのフィールドは、ユーザー名とユーザー ID
に該当します。ところで、このスクリプトは動きはしましたが、まだ完全ではありません。2 つの出力フィールドの間にスペースがありません!bash や Python
でのプログラミングに慣れている方は、print $1 $3
コマンドが、2
つのフィールドの間にスペースを挿入してくれると考えたかもしれません。しかし、awk プログラムでは 2
つのストリングを連続して記述した場合、間にスペースは入らずにストリングは連結されます。次のコマンドでは、フィールドの間にスペースが挿入されます。
$ awk -F":" '{ print $1 " " $3 }' /etc/passwd
このようにすると、 $1
と $3
が
“ “
で連結され、判読可能な出力を作成できます。もちろん、必要ならばテキスト・ラベルを挿入することもできます。
$ awk -F":" '{ print "username: " $1 "\t\tuid:" $3 }' /etc/passwd
出力は次のようになります。
username: halt uid:7 username: operator uid:11 username: root uid:0 username: shutdown uid:6 username: sync uid:5 username: bin uid:1 ....etc.
外部スクリプト
スクリプトをコマンド行から引数として awk に渡す方法は、小さな 1 行のスクリプトであれば非常に便利です。しかし複雑な複数行のプログラムとなると、どうしてもスクリプトを外部ファイルとして作成する必要がでてきます。そのような場合は、-f オプションを使用して、スクリプト・ファイルの名前を awk へ渡します。
$ awk -f myscript.awk myfile.in
スクリプトを、専用のテキスト・ファイルに記述することで、awk の機能をさらに活用することができます。たとえば、次の複数行のスクリプトは、先ほど紹介した、/etc/passwd の各行の最初のフィールドを出力する 1 行のスクリプトと同じ動きをします。
BEGIN { FS=":" } { print $1 }
この 2 つの方法の違いは、フィールド区切り文字の設定方法です。このスクリプトでは、フィールド区切り文字はコードの中で設定されています (FS 変数の設定による)。一方、先ほどの例では、コマンド行から awk に対し -F”:” オプションを渡すことで FS を設定しています。一般的には、フィールド区切り文字はスクリプトの中で設定したほうが良いでしょう。それは単純に、コマンド行から入力する引数が 1 つ減るからです。FS 変数については、この記事の後半でさらに詳しく説明します。
BEGIN ブロックと END ブロック
通常、awk は入力行 1 行ごとにスクリプトの各ブロックを実行します。しかし、awk が入力ファイルのテキストの処理を開始する前に、初期化用のコードを実行する必要のあるプログラムは多数あります。そのような場合のために、BEGIN ブロックを定義することができます。前の例では BEGIN ブロックを使用しました。BEGIN ブロックは、入力ファイルの処理の開始前に実行されるため、FS (フィールド区切り文字) 変数の初期化、見出しの出力、あるいはプログラム内で参照するグローバル変数の初期化に非常に適しています。
awk には、もう 1 つ特別なブロックがあります。それは END ブロックです。このブロックは、入力ファイルのすべての行の処理が終了した後、実行されます。一般的に END ブロックは、最終的な計算や、出力ストリームの最後に合計を出力するために使用されます。
正規表現とブロック
awk
では正規表現を使うと、特定の行に対してだけコードの個々のブロックを実行することができます。処理される行は、正規表現と現在行が一致したか否かによって決まります。次の例は、文字列
foo
が含まれている行だけを出力するスクリプトです。
/foo/ { print }
もちろん、より複雑な正規表現も使用できます。次の例は、浮動小数点数が含まれている行だけを出力するスクリプトです。
/[0-9]+\.[0-9]*/ { print }
式とブロック
実行対象とするコード・ブロックを選択する方法は、他にもいろいろあります。コード・ブロックの前にいろいろなブール式を指定して、特定のブロックを実行するかどうかを制御することができます。ブール式が真と評価された場合にだけ、ブロックが実行されます。次のスクリプトは、1
番目のフィールドが fred
と一致するすべての行の、3 番目のフィールドを出力する例です。現在行の 1 番目のフィールドが
fred
と一致しない場合、ファイル処理が続行され、現在行に対する print 文は実行されません。
$1 == "fred" { print $3 }
awk では、”==“、”<“、”>“、”<=“、”>=“、および “!=“
といった一般的なものを含め、あらゆる比較演算子を使用できます。それに加えて「一致する」を意味する “~” と、「一致しない」を意味する “!~”
演算子が使えます。これらの演算子は、左側に変数を、右側に正規表現を指定して使用します。次の例は、5 番目のフィールドに文字列
root
が含まれている行の、3 番目のフィールドだけを出力します。
$5 ~ /root/ { print $3 }
条件文
awk では、C 言語に似た非常に便利な if 文を使用することができます。前述のスクリプトを、if
文を使って書き直すことができます。
{ if ( $5 ~ /root/ ) { print $3 } }
この 2
つのスクリプトの機能はまったく同じです。前の例では、ブール式はブロックの外側に置かれています。一方、後の例では、ブロックはすべての入力行に対して実行されており、if
文を使って print コマンドを実行する行を選択しています。どちらの方法も使用できるので、スクリプトの他の部分と最も調和する方法を選択することができます。
次の例は、より複雑な if
文の例です。ご覧のとおり、複雑なネスト化された条件の if 文であっても、構成は C 言語のそれと非常によく似ています。
{ if ( $1 == "foo" ) { if ( $2 == "foo" ) { print "uno" } else { print "one" } } else if ($1 == "bar" ) { print "two" } else { print "three" } }
次のようなコードも、if
文を使用した形に変換することができます。
! /matchme/ { print $1 $3 $4 }
上記のコードは次のように変換されます。
{ if ( $0 !~ /matchme/ ) { print $1 $3 $4 } }
どちらのスクリプトも、文字列 matchme
を含んでいない行だけを出力します。これも、ご自分のコードに一番適した方法を選ぶことができます。両方とも、機能は同じです。
awk では、ブール演算子 “||” (論理和) と “&&” (論理積) も使うことができ、より複雑なブール式を作成することができます。
( $1 == "foo" ) && ( $2 == "bar" ) { print }
この例は、第 1 フィールドが foo
と等しく、かつ 第 2 フィールドが
bar
と等しい行だけを出力します。
数値変数
これまで出力してきたのは、ストリング、行全体、または特定のフィールドのいずれかでした。しかし、awk では整数や浮動小数点数の計算を行うこともできます。算術式を使うと、ファイルに含まれるブランク行の数を求めるスクリプトを容易に書くことができます。次の例がその処理を行うものです。
BEGIN { x=0 } /^$/ { x=x+1 } END { print "I found " x " blank lines. :)" }
BEGIN ブロックで、整数変数 x
をゼロに初期化します。その後、ブランク行が出てくる度に
x=x+1
という文が実行され、x
は加算されていきます。すべての行を処理した後、END ブロックが実行され、ブランク行の数を示している最終合計が出力されます。
ストリング指向の変数
awk 変数のすばらしさとして、その「簡便さとストリング指向」があげられます。awk 変数が「ストリング指向」であるというのは、内部ではすべての変数がストリングとして格納されるからです。同時にawk 変数が「簡便」であるというのは、変数を使って算術演算を行えるからです。変数に有効な数値ストリングが保持されていれば、ストリングから数値への変換操作は自動的に行われます。次の例で確認してみましょう。
x="1.01" # We just set x to contain the *string* "1.01" x=x+1 # We just added one to a *string* print x # Incidentally, these are comments :)
awk の出力は以下のようになります。
2.01
これは興味深い結果です。変数 x にストリング値 1.01 を代入したにもかかわらず、x に 1 を加算することができました。bash や Python
でこのようなことはできません。まず第一に bash は浮動小数点数演算をサポートしていません。また、bash
にも「ストリング指向」の変数はありますが、「簡便」ではありません。bash で算術演算をするには、式を見苦しい $( )
構文で囲まなければなりません。Python の場合は、演算をする前に、ストリングの 1.01
を浮動小数点数に明示的に変換しなければなりません。この作業は何ら難しいものではありませんが、余計なステップです。awk
なら、すべて自動的に行われるので、コードを見映えよく、簡潔に作成することができます。入力行の 1 番目のフィールドを 2 乗し、その値に 1
を加えるには、次のスクリプトを使用できます。
{ print ($1^2)+1 }
ちょっと試してみれば、算術式を実行するときに、変数に有効な数値が保持されていないと、その変数は数値のゼロとして処理されることがわかると思います。
いろいろな演算子
awk のもう一つのすばらしい点は、通常の算術演算子がすべてそろっていることです。標準的な加算、減算、乗算、および除算に加え、先ほど紹介した指数演算子 “^”、剰余 (モジュロー) 演算子 “%”、そしてこのほかにも C 言語から取り入れた多くの便利な代入演算子を使用できます。
代入演算子には、前置型および後置型の増分演算子と減分演算子
(i++, --foo
)、加算、減算、乗算および除算の代入演算子
(a+=3, b*=2, c/=2.2, d-=6.2
)
があります。しかし、それだけではありません。便利な剰余代入演算子や指数代入演算子 (a^=2, b%=4
)
もあります。
フィールド区切り文字
awk 固有の特殊変数もあります。awk の機能を細かく調整するために使用するものや、入力に関する貴重な情報を収集して読み取るためのものがあります。特殊変数の 1 つ、FS についてはすでに簡単に触れました。先ほど説明したように、この変数を使うと awk がフィールドの区切りを判断するのに使用する文字列を指定することができます。入力に /etc/passwd を使った例では、FS に “:” を設定しました。この例ではうまくいきましたが、FS はもっといろいろな使い方ができます。
FS の値は、1 文字に制限されているわけではありません。任意の長さの文字パターンを指定した正規表現を設定することもできます。1 つ、または複数のタブで区切られたフィールドを処理する場合、次のように FS を設定します。
FS="\t+"
上の例では正規表現の特殊文字 “+” を使用しています。これは「直前の文字の 1 つ、または複数の繰り返し」を意味します。
フィールドが空白 (1 つ、または複数のスペースあるいはタブ) によって区切られている場合、FS を次のような正規表現に設定することがあります。
FS="[[:space:]+]"
この指定は間違ってはいませんが、必要ないのです。なぜでしょうか。特に指定しないかぎり、FS には長さ 1 のスペース文字が設定されていて、それは「1 つ、または複数のスペースあるいはタブ」の意味に解釈されるからです。この例に限って言えば、FS のデフォルト値が、まさに必要としていた設定だったのです!
正規表現の複合も問題なく行うことができます。”foo” という単語とそれに続く 3 桁の数字でレコードが区切られている場合でも、次の正規表現を使ってデータをうまく分解することができます。
FS="foo[0-9][0-9][0-9]"
フィールド数
次に説明する 2 つの変数は、通常、値を代入することはなく読み取り専用として、入力に関する役立つ情報を得るために使用するものです。最初の変数は NF 変数で、「フィールド数」変数とも呼ばれています。この変数には、awk によって現行レコードのフィールド数が自動的に設定されます。NF 変数を使うと、特定の入力行だけを表示することができます。
NF == 3 { print "this particular record has three fields: " $0 }
もちろん、次の例のように、条件文の中で NF 変数を使用することもできます。
{ if ( NF > 2 ) { print $1 " " $2 ":" $3 } }
レコード番号
レコード番号 (NR) 変数も便利な変数です。この変数には現行レコードのレコード番号が常に設定されています (awk では、最初のレコードをレコード番号 1 とカウントします)。これまでは、1 行が 1 レコードである入力ファイルを扱ってきました。その場合、NR は現在行の行番号も意味しています。しかし今後の連載で、複数行にまたがるレコードを処理するようになると、NR と行番号が違ってくるので注意してください。NF 変数と同様に、NR 変数を使って入力の特定の行だけを出力することができます。
(NR < 10 ) || (NR > 100) { print "We are on record number 1-9 or 101+" }
以下に示すのは、別の例です。
{ #skip header if ( NR > 10 ) { print "ok, now for the real information!" } }
awk には、このほかにもさまざまな目的に使用できる変数があります。それらの変数はこのあとの記事で説明します。はじめての awk の探検も、もう終わりです。これからの連載では、より高度な awk の機能をご紹介しましょう。そして、実用的な awk アプリケーションでこの連載を締めくくりたいと思っています。それまでの間、どうしてももっと勉強しておきたいという方は、次の参考文献に目を通されるとよいでしょう。
ダウンロード可能なリソース
関連トピック
- もし古典的な本がお好みなら、O'Reilly 社から出版されている『sed & awkプログラミング 改訂版』をお勧めします。
- comp.lang.awk FAQ も忘れずに調べるとよいでしょう。ここにも、さらにたくさんの awk のリンクがあります。
- Patrick Hartigan 氏の awk tutorial には、便利な awk スクリプトがまとめられています。
- Thompson の TAWK Compiler は、awk のスクリプトをコンパイルして高速なバイナリー実行ファイルを生成します。TAWK Compiler には Windows 用と DOS 用のバージョンがあります。