なるべく書かないawkの使い方
awkという、古くからのスクリプト言語がある。(1977年生まれ。読み方は「オーク」である。エイ・ダブリュ・ケイではない)man awkをPDFに変換してみると、たったの3ページ強しかない。
$ man -t awk|pstopdf -i -o ~/Downloads/awk.pdf
とてもシンプルな言語仕様ではあるが、awkには必要十分な表現力がある。特にテキストを処理する場面においては、最小限のシンプルな記述で、気の利いた処理を素早くこなす。無駄のないawkワンライナーを見ると、ある種の感動を覚える。awk以降に生まれたスクリプト言語は、少なからずawkの影響を受けていると思われる。
awkを知ることで、間違いなく幸福度は上がると思う。いつかきっと「知ってて良かった」と思える時が来るはず。もっともっと、awkを知りたくなってきた。
基本動作
awkの基本動作は、とってもシンプルである。
- テキストを1行読み込んで、
- 読み込んだ行を空白区切りのデータと解釈して、
- 何らかの処理を行う。
- 以上の動作を、行末まで繰り返す。
- 1、2の動作は、awkが自動的に処理する。
- 3の部分を、自分が書くことになる。
- 例えば以下のようなテキストファイルを作って...
$ cat <abc.txt > a b c > d e f > g h i > EOS $ cat abc.txt a b c d e f g h i
$ cat abc.txt | awk '{print $0}'
a b c
d e f
g h i
つまり...
- a b cを読み込んで、print $0して、
- d e fを読み込んで、print $0して、
- g h iを読み込んで、print $0しているのだ。
- スペース区切りを実感するために、真ん中の列(2列目)だけ出力してみる。
$ cat abc.txt | awk '{print $2}'
b
e
h
つまり...
- $0、$2は、awkが用意した変数である。
- $1には、1列目のデータが入っている。
- $2には、2列目のデータが入っている。
- $nには、n列目のデータが入っている。
- そして、$0にはすべての列、つまり1行全体のデータが入っているのだ。
- ところで、print $1 $2 $3と、print $1,$2,$3は、違う。
$ cat abc.txt | awk '{print $1 $2 $3}' abc def ghi $ cat abc.txt | awk '{print $1,$2,$3}' a b c d e f g h i
条件と処理
- 条件を付加することによって、その条件が満たされた時だけ、何らかの処理を行うようになる。
- 条件は、何らかの処理の直前に書く。
- "e"を含む行の時だけ、print。
$ cat abc.txt | awk '/e/{print $0}'
d e f
- 1列目が"a"の時だけ、print。
$ cat abc.txt | awk '$1=="a"{print $0}'
a b c
- 3行目の時だけ、print。
$ cat abc.txt | awk 'NR==3{print $0}'
g h i
- NRは、awkが用意した変数である。
- 現在処理している行番号が代入されている。
- awkでは、この条件と何らかの処理の組み合わせによって、あらゆることを処理していく。
- 条件 = パターン と呼ばれている。
- 何らかの処理 = アクション と呼ばれている。
- 言い換えれば、 パターンとアクションの組み合わせによって、あらゆることを処理していく。
- そして、'パターン{アクション}'の定義は、複数書くことができる。
- つまり、awkスクリプトとは 'パターン{アクション}' の集合なのだ。
- 例えば、aで始まる行と、gで始まる行を取り出すなら...
$ cat abc.txt | awk '
> /^a/{print $0}
> /^g/{print $0}
> '
a b c
g h i
- 複数行に分けても、1行にまとめても、どちらでもOK。
$ cat abc.txt | awk '/^a/{print $0}/^g/{print $0}'
a b c
g h i
grep+cutとの違い
# 2列目を取り出す $ cat abc.txt | cut -d' ' -f2 b e h # aかgで始まる行を取り出す $ cat abc.txt | grep -E '^a|^g' a b c g h i # aかgで始まる行の2列目を取り出す $ cat abc.txt | grep -E '^a|^g' | cut -d' ' -f2 b h
- ところが、その性能は微妙に違っている。
- その違いが、使い勝手に大きく影響する。
- 例えばlsの出力を加工しようと思った場合...
$ ls -l
total 16
drwx------+ 31 bebe staff 1054 12 4 16:21 Desktop
drwx------+ 450 bebe staff 15300 12 5 10:37 Documents
drwx------+ 332 bebe staff 11288 12 5 14:24 Downloads
drwx------@ 72 bebe staff 2448 10 30 17:44 Library
drwx------+ 9 bebe staff 306 10 30 06:42 Movies
drwx------+ 9 bebe staff 306 2 28 2013 Music
drwx------+ 14 bebe staff 476 11 5 15:51 Pictures
drwxr-xr-x+ 12 bebe staff 408 12 2 16:14 Public
drwxr-xr-x+ 5 bebe staff 170 12 2 16:14 Sites
...中略...
- cutで5列目のバイト数の列だけ取り出すのは、ちょっと苦労する。
- 区切り記号をスペースに変更して、5列目を指定しただけでは、以下のような意図しない結果になってしまう...。
$ ls -l | cut -d' ' -f5
staff
staff
bebe
bebe
bebe
...中略...
- この原因は、cutが指定された区切り記号であるスペースを使って、1文字ずつ厳格に区切ろうとした結果である。
- スペースが連続する部分でも、それぞれのスペースを区切り記号と解釈して、無駄な区切りを増やしているのだ。
- よって、もしcutでバイト数の列だけ取り出すなら、連続するスペースを1つにまとめる処理が必要になる。
- sed 's/ \{1,\}/ /g' によって、連続するスペースを1つにまとめている。
$ ls -l | sed 's/ \{1,\}/ /g' | cut -d' ' -f5
1054
15300
11288
2448
306
306
476
408
170
...中略...
- 一方、awkなら何の苦労もなく、バイト数の列を取り出せる。
$ ls -l | awk '{print $5}'
1054
15300
11288
2448
306
306
476
408
170
...中略...
- awkではデフォルトの区切り記号が、1文字以上の連続する、スペースかタブになっている模様。
- ちなみに、cutのデフォルトの区切り記号は、1文字のタブである。
- そして残念なことに、cutの区切り文字には、1文字以上という正規表現が設定できないのだ...。
awkには、人の感覚に合った区切り文字がデフォルトで設定されている。だから使いやすい!
- コマンド出力されたテキストデータを加工するなら、断然awkを使いたくなる。
集計の技
そして、awkの処理は列を取り出すだけに留まらない。
- 取り出した列は、素早く集計できる。
- ENDは、すべての行を読み込み完了後、1回だけ実行される特殊なパターン(=条件)である。
- ちなみに、最初の行を読み込む前に、1回だけ実行されるBEGINというパターン(=条件)もある。
$ ls -l | awk '{sum+=$5; print $5} END{print "--------\n" sum}'
1054
15300
11288
2448
306
306
476
408
170
136
136
18
343
136
136
340
-
-
-
-
-
-
- -
-
-
-
-
-
$ cat <123.txt > 1 2 3 > 4 5 6 > 7 8 9 > EOS $ cat 123.txt 1 2 3 4 5 6 7 8 9 $ cat 123.txt | awk -v OFS="\t" '{rt=$1+$2+$3; c1+=$1; c2+=$2; c3+=$3; c4+=rt; print $1,$2,$3,"",rt} END{print ""; print c1,c2,c3,"",c4}' 1 2 3 6 4 5 6 15 7 8 9 24 12 15 18 45
$ seq 100 | awk '{n=$1} n%3==0{$1="";$3="Fizz"} n%5==0{$1="";$5="Buzz"} {print $1 $3 $5}'
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
...中略...
これはもう、表計算アプリと同じ感覚である。
- テキスト全体が区切り文字によって区切られた行列データと解釈され、自在に集計できる。
- 単なる行列データではなく、指定した条件にマッチしたデータのみ抽出して、集計できる。
省略の技
awkの心地よさは、省略可能な表現力だと思う。
- 例えば、ここまでprint $0と書いてきたが、多くの場合、$0は省略できる。
- よって、print $0は、printのみでOK。
$ #cat abc.txt | awk '{print $0}' $ cat abc.txt | awk '{print}' a b c d e f g h i
- また、パターンのみでアクションが存在しない場合、パターンがマッチした時、awkは{print}を処理してくれる。
- よって、検索はパターンの指定のみでOK。
$ #cat abc.txt | awk '/e/{print $0}' $ #cat abc.txt | awk '/e/{print}' $ cat abc.txt | awk '/e/' d e f
- さらに、パターンがマッチするとは、条件式の戻り値が0でない状態である。
- 条件式が成立すると、1が返る。
- 条件式が成立しないと、0が返る。
- この仕組みを知ると、パターンとは特に条件式である必要はないことに気付く。
- つまり、何らかの式が0を返せば、続くアクションは実行されず、
- 何らかの式が0以外を返せば、続くアクションが実行されるのだ。
- 式とは、数値のみでも式である。
- よって、すべての行を出力したいのであれば、1と書くだけでもOK。
- 2でも3でも、0以外なら何でもOKなのだけど、一般的によく1が使われるようだ。
- 1に続くアクションがないので{print}が実行されるのだ。
- よって、すべての行を出力したいのであれば、1と書くだけでもOK。
$ #cat abc.txt | awk '{print $0}' $ #cat abc.txt | awk '{print}' $ cat abc.txt | awk '1' a b c d e f g h i
- awk '1'だけのスクリプトでは、その恩恵をほとんど感じないが、
- awkでは各列のデータを加工して、最後にすべてをprintしたくなることがよくある。
- その場合、'{print}'の省略記法として、'1'が使われることが多い。
- 例えば、Fizz Buzz問題の最後の{print $1 $3 $5}は、1に置換えても、許せる範囲の書式で出力されると思う。
$ #seq 100 | awk '{n=$1} n%3==0{$1="";$3="Fizz"} n%5==0{$1="";$5="Buzz"} {print $1 $3 $5}' $ seq 100 | awk '{n=$1} n%3==0{$1="";$3="Fizz"} n%5==0{$1="";$5="Buzz"}1' 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 Fizz Buzz ...中略...
-
- OFS=""を設定しておけば、書式も乱れないはず。
$ seq 100 | awk -v OFS="" '{n=$1} n%3==0{$1="";$3="Fizz"} n%5==0{$1="";$5="Buzz"}1'
- 文字列を置換えるgsubという関数も、すべての行で置き換えが発生するなら、printは不要になる。
- 小文字のaを大文字のAにするには、特定の行しか対象にならないので、printが必要である。
- gsub関数の最後の引数は、置き換え対象の文字列を指定するのだが、省略すると行全体($0)に対して置き換え処理が行われる。
$ #cat abc.txt | awk '{gsub(/a/,"A",$0);print}' $ cat abc.txt | awk '{gsub(/a/,"A");print}' A b c d e f g h i
-
- もし行頭に#を付加する場合なら、すべての行が対象になるので、アクションではなくパターンに書くことで{print}を省略できる。
- 置き換えが発生した場合、gsub関数の戻り値には置き換えした回数が返るので。(必ず置き換えが発生すれば、常に1以上が返る)
$ #cat abc.txt | awk '{gsub(/^/,"#");print}' $ cat abc.txt | awk 'gsub(/^/,"#")' #a b c #d e f #g h i
省略しても、awkが良きに計らい解釈してくれる所が嬉しい。
省略の落とし穴
但し、省略のルールによって、awkに予想外の解釈をされることもある。
$ echo apple orange melon | awk '{if($0 ~ /apple/ || $0 ~ /orange/)print "Hit!"}'
Hit!
- 上記と下記($0 ~ を省略)は、意味的にはまったく同じである。
$ echo apple orange melon | awk '{if(/apple/ || /orange/)print "Hit!"}'
Hit!
- その省略のルールによって、予想外の結果が導かれてしまう...。
$ echo apple orange melon | awk '{if(/apple/ ~ $1)print "Hit!";else print "Unmatched"}'
Unmatched
- $1は"apple"なはずなのに、なぜUnmatchedになってしまうのか?
$ echo apple orange melon | awk '{if(($0 ~ /apple/) ~ $1)print "Hit!";else print "Unmatched"}'
-
- ($0 ~ /apple/)の計算結果は1となり、
- その後(1 ~ $1)が評価されているのだ。
- ならば、appleの前に1を追記すれば、Hit!するはず。思ったとおり!
$ echo 1 apple orange melon | awk '{if(/apple/ ~ $1)print "Hit!";else print "Unmatched"}'
Hit!
気の利いたawkのお節介が誘発する唯一のマイナス面と思われる。気をつけよう!
- 比較対象を明示する時は、正規表現は後に書く必要があるのだ。
- うっかり先に書いてしまうと、相当悩み続けることになりそう。
文字と数値と変数
awkは必要に応じて、文字や数値を良きに計らい自動変換してくれる。
- 文字の足し算も、ちゃんと計算してくれる。
$ echo | awk '{print 1 + 2}' 3 $ echo | awk '{print "1" + "2"}' 3
- 数値に変換できない文字は0と解釈される。
$ echo | awk '{print "a" + "b"}'
0
- 数値同士でも、文字列として結合できる。
$ echo | awk '{print 1 2}'
12
- パターンの戻り値は、0または""がfalse。
$ echo | awk '0{print "Trueです。"}' $ echo | awk '""{print "Trueです。"}'
- 上記以外は、すべてtrue。"0"もtrue。
$ echo | awk '"0"{print "Trueです。"}'
Trueです。
- 変数には最初から、""が代入されている。(と考えることにしている)
- よって、変数の初期化なしで、いきなり数値計算できるのだ!
- 1から10まで足し算してみた。
$ seq 10 | awk '{sum+=$1; print sum}'
1
3
6
10
15
21
28
36
45
55
プログラマーが面倒だと思うことは、ことごとくawk側でうまく取り計らってくれるのだ!
- 文字と数値を区別することなく、ゆるい気分でコードを書けて嬉しい。
- nil・NULLの判定が不要になって嬉しい。
言語仕様
ここまでawkの概要が分かると、詳しい言語仕様を知りたくなる。よく使いそうな部分を抜粋してみた。
組み込み変数
たった7つの変数($数値、NF、NR、FS、RS、OFS、ORS)を覚えておくだけで、かなり便利に使える。
- 今まで行列データと書いてきたが、awkでは、行をRecord(レコード)と呼んでいる。
- 行の中で区切られた1列を、Field(フィールド)と呼んでいる。
- RecordとFieldの意味が分かると、awkの組み込み変数を覚えやすくなるのだ。
$1 ↓ |
FS ↓ |
$2 ↓ |
FS ↓ |
$3 ↓ |
FS ↓ |
(NF=フィールド数) $NF ↓ |
RS ↓ |
|
---|---|---|---|---|---|---|---|---|
フィールド1 | フィールド2 | フィールド3 | フィールド末尾 | \n | ←レコード1(処理中ならNR=1) | |||
フィールド1 | フィールド2 | フィールド3 | フィールド末尾 | \n | ←レコード2(処理中ならNR=2) | |||
フィールド1 | フィールド2 | フィールド3 | フィールド末尾 | \n | ←レコード3(処理中ならNR=3) |
- NF=処理しているレコードのフィールド数(Number of Fields)
- NR=処理しているレコードの先頭からの番号(Number of Record)
- FS=入力時にフィールドを区切る文字(Field Separator)
- RS=入力時にレコードを区切る文字(Record Separator)
- OFS=出力時にフィールドを繋げる文字(Output Field Separator)
- ORS=出力時にレコードを繋げる文字(Output Record Separator)
-
- OFSを設定することで、スペース区切りから、カンマ区切りに変換してみる。
- $1=$1は、OFSの変更を$0に反映させるため、何らかのフィールド操作が必要なのだ。
$ echo A B C | awk '{OFS=",";$1=$1;print}'
A,B,C
-
- OFSを変更しても、フィールド操作が何もないと$0が更新されず、以前のままになる。
$ echo A B C | awk '{OFS=",";print}'
A B C
$で始まるフィールド変数は、配列のように振る舞う!
- $数値は、フィールドの内容が代入された変数である。
- $0には、行全体の内容が代入される。
- $1には、1番目のフィールドの内容が代入される。
- $2には、2番目のフィールドの内容が代入される。
- $NFには、最後のフィールドの内容が代入される。
- $NF以降は、未定義なので""が代入される。
- $NFの例からも分かるように...
- $変数は、変数の値が示すフィールドの内容が代入される。
$ echo A B C D E F G | awk '{num=5; print "$num =", "$" num, "=", $num}'
$num = $5 = E
-
- $(式)は、計算結果が示すフィールドの内容が代入される。
$ echo A B C D E F G | awk '{print $(NF-1)}'
F
-
- $(式)は、基本的に括弧で囲う必要がある。
- 括弧なしでは、$NF=$7="G"と解釈され、その後"G" - 1が計算される。
$ echo A B C D E F G | awk '{print $NF-1}'
-1
-
- "G"は数値に変換できないので0と評価され、0 - 1 = -1 となるのだ。
制御文
if (条件) 真の処理 else 偽の処理 | 条件によって、真偽どちらかの処理を実行する。(else以降は省略可能) |
---|
$ echo A B C D E F G | awk '{if(length > 8)print $1,$2 "..." $NF; else print}' A B...G $ echo A B C D | awk '{if(length > 8)print $1,$2 "..." $NF; else print}' A B C D
- ifやelseの後、複数処理を実行する時は{ }で囲う。
$ echo A B C D E F G | awk '{if(length > 8){s=$1 FS $2 "..." $NF; print s;} else print}'
A B...G
while (継続条件) ループ処理 | 継続条件が真の状態なら、ループ処理を続ける。 |
---|---|
do ループ処理 while (継続条件) | 継続条件が真の状態なら、ループ処理を続ける。(継続条件が偽でも、最低1回はループ処理を実行する) |
for (初期設定; 継続条件; 増減設定) ループ処理 | 継続条件が真の状態なら、ループ処理を続ける。(for = 初期設定と増減設定が付属したwhile) |
for (変数 in 配列) ループ処理 | 配列のキーを順に変数に代入しながら、ループ処理を繰り返す。 |
break | for、while、doのループ処理を抜け出す。 |
continue | for、while、doの次のループ処理へ移行する。 |
$ echo A B C D E F G | awk '{for(i=2;i
B C D E F
- for、while、doの後、複数処理を実行する時は{ }で囲う。
$ echo A B C D E F G | awk '{for(i=2;i
B
BC
BCD
BCDE
BCDEF
getline | awkは1行ずつ自動的に読み込んでくれるが、getlineは1行読み込みを自分で制御する時に使う。 |
---|---|
next | awkの処理を次の行に進める。 |
nextfile | awkの処理を次のファイルに進める。 |
exit [ 式 ] | awkの処理を中断して、式の結果をステータスコードとして返す。(ENDパターンは実行される) |
指定された内容を、出力する。(改行あり) | |
printf | 指定された内容を、指定されたフォーマットで、出力する。(C言語のprintfフォーマットに準ずる) |
- 3桁区切り
- \047=シングルクォートの8進数表記
- シングルクォート内に"%'d\n"と書くため、エスケープする必要があった。
$ echo 1234567| awk '{printf "%\047d\n", $0}'
1,234,567
- 通常print
$ echo A B C D| awk '{printf "%s\n", $0}'
A B C D
- 10桁幅の右寄せ
$ echo A B C D| awk '{printf "|%10s|\n", $0}'
A B C D |
- 10桁幅の左寄せ
$ echo A B C D| awk '{printf "|%-10s|\n", $0}'
A B C D |
delete array[index] | 配列の指定されたindexを削除する。([index]指定なしだと、配列全体を削除する) |
---|
$ echo | awk '{a[1]=100; print a["1"]}' 100 $ echo | awk '{a["dog"]="one!"; a["cat"]="nya-"; for(i in a)print a[i]}' nya- one!
- 未定義の配列内容は、""が返される。
$ echo | awk '{a["dog"]="one!"; a["cat"]="nya-"; delete a["dog"]; print a["cat"], a["dog"]}' nya- $ echo | awk '{a["dog"]="one!"; a["cat"]="nya-"; delete a; print a["cat"], a["dog"]}'
演算子
- 優先順位が高い順の演算子リスト
優先順位 | 演算子 | 意味 |
---|---|---|
高い | ( ) | グルーピング |
$ | フィールドの参照 | |
++ -- | インクリメント、デクリメント | |
^ ** | べき乗(どちらも同じべき乗) | |
+ - ! | プラス、マイナス、論理否定 | |
∗ / % | 乗算、除算、剰余 | |
+ - | 加算と減算 | |
半角スペース | 文字列連接 | |
< <= > >= != == | 関係演算子 | |
~ !~ | 正規表現のマッチ、非マッチ | |
in | 配列への個別アクセス | |
&& | 論理的なAND | |
|| | 論理的なOR | |
? : | 3項演算子(条件 ? 真の処理 : 偽の処理) | |
低い | = += -= *= /= %= ^= | 代入(例:n+=2は、n=n+2と同等) |
関数
- awkが用意する関数一覧
数値関数 | atan2 cos exp int log rand sin sqrt srand |
---|---|
文字関数 | gsub index length match split sprintf sub substr tolower toupper |
その他の関数 | close fflush system |
- 上記の中で、よく使いそうな関数
関数 | 機能 | 引数を省略したときの意味 |
---|---|---|
sub(正規表現, 変換語句, 処理対象の変数) | 正規表現にマッチした部分を、変換語句に、1回だけ置き換える。 | sub(/mac/, "Mac") == sub(/mac/, "Mac", $0) |
gsub(正規表現, 変換語句, 処理対象の変数) | 正規表現にマッチした部分を、変換語句に、すべて置き換える。 | gsub(/mac/, "macintosh") == gsub(/mac/, "macintosh", $0) |
index(文字列, 検索語) | 文字列に含まれる検索語の位置を返す。 | |
length(文字列) | 文字列の長さを返す。 | length == length($0) |
split(文字列, 配列変数, 区切り文字) | 文字列を区切り文字で分解して、配列変数に代入する。 | split("A B C", a) == split("A B C", a, FS) |
sprintf("フォーマット", 値, 値...) | printfの結果を文字列として返す。 | |
tolower(文字列) | 小文字に変換する。 | tolower == tolower($0) |
toupper(文字列) | 大文字に変換する。 | toupper == toupper($0) |
- 大文字・小文字を区別しないで検索したい時、awkはtolowerで全体を小文字に変換してから検索するしかない...。
- あるいは、toupperで全体を大文字に変換してから検索するのだ。
$ echo Apple | awk '/apple/' $ echo Apple | awk 'tolower ~ /apple/' Apple
- 一方、grepなら-iオプションでOK。
$ echo Apple | grep 'apple' $ echo Apple | grep -i 'apple' Apple
参考ページ
さらに詳しいawkの仕様については、以下のページがたいへん参考になりました。(素晴らしい情報に、感謝です!)「awkは書かねぇ、たった一行」って何?
第37話「三方一両損」(落語の小噺の落ち)「お〜かぁ〜(大岡)食わねぇ、たった一膳(越前)」。 第37話「三方一両損」
- つまり「多くは食わねぇ、たった一膳」。
- 転じて「awkは書かねぇ、たった一行」。
- つまり「オーク(多く)は書かねぇ、たった一行」。