100% 実践 awk

01/05/30 Naoto Hashimura

awk=“オーク”と読みます。
awkは UNIXの標準ツールとして大抵付いています。

★どういう時にawkを使うか

例えば、簡単な例。psコマンドで次のような結果が出力されました。
-------------------------------------------------------------------------------
$ ps -ef | grep hashi
   hashi  6848  6687  0 12:18:22 pts/2    0:00 vi /doc/md5.html
   hashi  6687  6685  0 11:10:31 pts/2    0:00 -bash
   hashi  6680  6678  0 11:01:31 pts/1    0:00 -bash
-------------------------------------------------------------------------------
※1番目はユーザID、2番目はプロセスID、3番目は親プロセスID…です。
 それぞれ複数のスペースで区切られています。

※BSD系のUNIX使ってる人は、オプション-ef の代わりに -aux とすればOK。


これから、2番目のプロセスIDを抜き取り、>>と<<で括りたい場合

-------------------------------------------------------------------------------
$ ps -ef | grep hashi | awk '{print ">>" $2 "<<"}'
>>6848<<
>>6687<<
>>6680<<
-------------------------------------------------------------------------------
要するに、行で、一個以上のスペース(またはタブ)で区切られた文字列の1番目、2番目、…を それぞれ $1、$2、…で参照できるのです。 これだけでもすっごく便利でしょう?
ちなみに、$0は 1行全体を示しています。

もう少し詳しく解説すると、{ } の中身が、1行読むたびに実行されます。
シングルクォート ' で囲んでるのは、awkの文の一部がシェルによって解釈されるのを防ぐためです。
コマンドライン上でawkを実行する場合、いつも付けておきます。 awkの命令をファイルに書く場合は必要ありません。

printは、文字列を表示するための命令です。文字列はダブルクォート " で括ります。
($1や$2まで "" の中に入れてしまうと、そのまま $1、$2 という文字列が表示されてしまいます。ちょっとマヌケ)
printした後、勝手に改行が出力されます。

上の例の応用として、
-------------------------------------------------------------------------------
$ ps -ef | grep hashi | awk '{print "kill -9 " $2}' | sh
-------------------------------------------------------------------------------
※自分の全てのプロセスがkillされます。危険!



★awkのコマンド構文

awk [-f awk命令ファイル] [-Fセパレータ文字] awk命令 ファイル1 ファイル2...

-f で、awk命令をファイルに書くことができます。
これで、大規模なawkスクリプトを作ってる人もいます。すごいですよね!

デフォルトで文字列の区切りはスペース・タブになっていますが、-Fで変えることができます。後述します。

awk命令の後は、処理したいファイルが来ます。 標準入力から読ませる場合は必要ありません。
ファイル1、ファイル2、のように、複数ファイルを指定すると、次々にそのファイルが処理されていきます。


★最初と最後に一度だけ実行したい処理がある場合

行を読む前に始めに実行したい、また、全ての行を処理した最後に実行したいことがあるとします。
その場合、BEGIN{...} 、または END{...} でそれが実現できます。
-------------------------------------------------------------------------------
$ ps -ef | grep hashi | awk 'BEGIN{print "♪橋村のプロセス番号一覧"}{print $2}END{print "以上。"}'
♪橋村のプロセス番号一覧
6848
6687
6680
以上。
-------------------------------------------------------------------------------
BEGIN{...} は、後述する変数の初期化とかに、END{...} は処理結果を報告したりに使えます。


★文字列の区切り文字に別の文字を指定したい場合

awkのコマンドオプション “-F文字” で実現できます。
(-Fのすぐ後に続けて文字を指定します)

topdomain5.txtという、次の内容のファイルがあるとします。
-------------------------------------------------------------------------------
$ cat topdomain5.txt
com,12140747,15479017,3338270,868016,5993551,Commercial
net,8856687,9383201,526514,64423,2926036,Networks
edu,5022815,5228251,205436,3600,1678553,Educational
jp,1687534,1718935,31401,97,38979,Japan
us,1562391,1642418,80027,75,3118,United States
-------------------------------------------------------------------------------
このファイルから、“com is Commercial” のように、“最初の文字列 is 最後の文字列” と表示するには
--------------------------
$ awk -F, '{print $1 " is " $7}' topdomain5.txt
com is Commercial
net is Networks
edu is Educational
jp is Japan
us is United States
--------------------------
見事、カンマで認識されています。


★変数を使う

awkは変数も使えます。変数名はC言語などと同じく、英数文字・一部の記号を組み合わせて使用します。

変数には、C言語と同様の操作ができます。
などなど。

注意点ですが、シェルやPerl等と違い、変数名には $をつけません。
{a=5; print a;}
(※命令を複数書く場合は、セミコロン ; で区切ります)
とすると、毎行を読むたびに5が表示されますが、
{a=5; print $a;}
とすると、毎行の5番目の文字列(=$5)という意味になってしまいます。
これは使いようによっては便利なんですが。


カレントディレクトリにあるファイルの合計サイズをKbyte単位で表示したいとします。
-------------------------------------------------------------------------------
$ ls -alp | grep -v /
合計 14
-rw-r--r--   1 hashi    users       2251  3月  2日  14:49 form.c
-rw-r--r--   1 hashi    users        601  3月  2日  13:34 tmp.c
-------------------------------------------------------------------------------
※lsコマンドの “-p” オプションは、ファイルがディレクトリの場合、'/' を名前の末尾に付けるもので、また、その次に grepコマンドの “-v” で、'/' の見つかった行を除外します。
 ⇒結果的に、ディレクトリを除いた一覧を表示します。


ファイルサイズは 5番目に来ています。
従って、$5 を使います。
-------------------------------------------------------------------------------
$ ls -alp | grep -v / | awk '{capa+=$5}END{capa/=1024; print "total=" capa "Kbyte";}'
total=2.78516Kbyte
-------------------------------------------------------------------------------
毎行のファイルサイズを 変数capaにためていき、終わりに÷1024して表示しています。


★特別な変数

FS
入力行の、文字列を分けている文字。(フィールドセパレータといいます)
デフォルトはスペースまたはタブになっています。 (FS="[ \t]")
この変数に文字(正規表現も可)を代入すると、それが新たなフィールドセパレータになります。コマンドオプション “-F文字” と同じ効果です。

NF
入力行が、何個の文字列で分けられているかを表します。(フィールド数)

例えば、次の内容のファイルがあるとします。
-------------------------------------------------------------------------------
$ cat /etc/hosts
127.0.0.1      localhost
192.168.1.1    jisaku1.myhome.or.jp     jisaku1
192.168.1.2    ok_vaio
192.168.1.3    fmv5120d5.myhome.or.jp   fmv5120d5
192.168.1.4    fmv466d3
-------------------------------------------------------------------------------
各行の NF を見てみます。
-------------------------------------------------------------------------------
$ awk '{print NF}' /etc/hosts
2
3
2
3
2
-------------------------------------------------------------------------------
テクの1つとして、NFに$をつけると…
-------------------------------------------------------------------------------
$ awk '{print $NF}' /etc/hosts
localhost
jisaku1
ok_vaio
fmv5120d5
fmv466d3
-------------------------------------------------------------------------------
行によってフィールド数がまちまちでも、うまい具合に最後の文字列が取れます。

NR
行番号。
始めの行では1になり、次の行では2、3、4という具合になります。
END{...} で使うと、処理した全行数が分かります。

RS
入力データの行の区切り文字。
デフォルトは、改行文字までを一行だと認識します。(RS="\n" になっている)
が、RSに別の文字を代入することで変更できます。

ORS
printで文字列を出力時、最後に改行が付きますが、ORSでその文字を変更することができます。

FILENAME
現在処理しているファイルの名前。標準入力の場合 “-”になっています。
awkの種類によっては、BEGIN{...} 内でこの変数が使用できない場合があるようです。

OFMT
数値の出力形式。C言語で言うところの printf書式指定と同じです。(多分)
例えば、OFMT="%.2f" とすると、小数の値をprintする時、小数点2桁で表示できます。
デフォルトは小数点5桁くらいになっています。

その他にもいろいろ特殊な変数がありますが、あまり使わないと思います。


★処理する行を選んだり、条件によって処理を行うには

条件によって制御を変えるには、if 構文が使えます。
C言語と同様の形式となっています。
if(条件式){...}else if(条件式){...}else{...}

次のような内容のファイルがあるとします。
-------------------------------------------------------------------------------
$ cat px.lst
wyrm.its.uow.edu.au:8080
xanadu.centrum.dk:8080
xcs.contex.com:80
xena.cable-lynx.net:3128
xmail.eatel.com:8080
xns.codify.com.tw:3128
xxcal-labs.com:8080
yellow.javanet.com:80
yogsothoth.ludexpress.com:8080
yourpalsat.netmeg.net:3128
zam381.zam.kfa-juelich.de:3128
zenith000.hhs.net:80
-------------------------------------------------------------------------------
何のリストかはともかく、どうも “ドメイン名:ポート番号” という形式をしているようです。
例えば、ポート番号が80番と3128番のドメイン名を抽出したいとします。
-------------------------------------------------------------------------------
$ awk -F: '{if(($2==80)||($2==3128)){print $1}}' px.lst
xcs.contex.com
xena.cable-lynx.net
xns.codify.com.tw
yellow.javanet.com
yourpalsat.netmeg.net
zam381.zam.kfa-juelich.de
zenith000.hhs.net
-------------------------------------------------------------------------------
この例のような場合、もっと簡単に
条件式{...}
とも書くことができます。
-------------------------------------------------------------------------------
$ awk -F: '$2==80||$2==3128{print $1}' px.lst
-------------------------------------------------------------------------------

処理する行を選ぶのに、正規表現を含むパターンも使えます。 (今回は正規表現については詳しく触れません)
/パターン/{...} 
とすればOKです。

例えば、上記リストで、トップドメインが “.net” と “.de” のドメイン名を抽出したいとします。
-------------------------------------------------------------------------------
$ awk -F: '/\.(net|de)/{print $1}' px.lst
xena.cable-lynx.net
yourpalsat.netmeg.net
zam381.zam.kfa-juelich.de
zenith000.hhs.net
-------------------------------------------------------------------------------
※パターンの所のドット . に \ を付けないと、ドットが「任意の1文字に合致する」という意味になってしまいます。
※( 文字列 | 文字列 | ... ) で文字列の複数一致ができます。


このパターンに合致するかどうかのチェックは $0 (行の全ての文字列) に対して行われますが、時には $1や$2 だけにと限定したい場合もあります。そういう場合、
$1 ~ /パターン/{...}
のように書くと、$1 に限定されます。 1つ前の例は、
-------------------------------------------------------------------------------
$ awk -F: '$1 ~ /\.(net|de)/{print $1}' px.lst
-------------------------------------------------------------------------------
と書いた方がより親切でしょう。
パターンチェックの意味を逆にするには、
$1 !~ /パターン/{...}
のように、チルダ ~ の前に ! を付けます。
-------------------------------------------------------------------------------
$ awk -F: '$1 !~ /\.(net|de)/{print $1}' px.lst
wyrm.its.uow.edu.au
xanadu.centrum.dk
xcs.contex.com
xmail.eatel.com
xns.codify.com.tw
xxcal-labs.com
yellow.javanet.com
yogsothoth.ludexpress.com
-------------------------------------------------------------------------------
翻訳すると、1番目の文字列 ($1) に “.net” または “.de” が無い行の 1番目の文字列を printせよ! ということです。


★終わりに

もう終わりかい! (^-^; 徹夜で書いてるので眠いっす。。。
awkにはもっともっと便利な命令や機能があります。
(配列、substr や system、getline はよく使われるので書きたかったです)
また、スクリプトを書く時のコツや、その他テクニックもいろいろあります。 次の版でどんどん盛り込みます。


以上、ご質問等はいつでも受け付けます。
awk使いが1人でも増えることを願って・・・


このページの先頭 | 前のページに戻る