awk でフィールド処理する
awk は行を空白で区切られた「フィールド」に分けて扱える、表形式テキスト向けのツールです。各列は $1 $2 … で参照でき、$0 は行全体、NF は列数、NR は行番号を表します。awk '{print $1}' なら各行の1列目だけを出力します。awk -F: '{print $1}' のように -F で区切り文字を変えれば、コロン区切りの /etc/passwd なども処理できます。awk '$3 > 100' のように条件で行を絞ったり、合計を計算したりと、cut より柔軟な列処理が得意です。
grep が行を探し、sed が行を加工する道具だとすれば、awk は行を「列」に分けて扱う道具です。スペースやタブで区切られた表形式のテキストを、列ごとに取り出したり、値で条件を付けたり、合計や平均を計算したりできます。名前は作者3人の頭文字に由来し、実体は小さなプログラミング言語です。基本の形は awk 'パターン{アクション}' ファイル で、「条件に当たる行に対して、{ } の中の処理を行う」と読みます。パターンを省けば全行が対象になり、アクションを省けば一致した行をそのまま出すなど、両者を組み合わせて挙動を決めます。列を切り出すだけなら cut でも済みますが、列の値で条件を書いたり計算したりできる点で、awk は一段柔軟です。
フィールドという考え方
awk は各行を、空白を区切りとして自動的に列に分解します。この1つ1つの列をフィールドと呼び、先頭から $1 $2 $3 … というフィールド変数で取り出せます。$0 は行全体を表し、$NF は「最後の列」を指します。たとえば awk '{print $1}' なら各行の1列目だけを縦に並べて出力します。なお既定の空白区切りは賢く、スペースやタブが連続していても1つの区切りとして扱い、行頭の空白も無視してくれるので、桁をそろえて整形された表でも素直に列を取れます。区切り文字を空白以外に変えたいときは -F オプションを使い、awk -F: '{print $1}' /etc/passwd のように書けば、コロン区切りの /etc/passwd からユーザー名(1列目)だけを取り出せます。print $1, $NF のようにカンマで区切ると、間にスペースを入れて並べてくれます(root /bin/bash のように)。
組み込み変数 NR と NF
awk には便利な組み込み変数があります。NR はこれまでに読んだ行数、すなわち現在の行番号を表し、NF はその行の列数(フィールド数)を表します。printf 'a b c\nx y\n' | awk '{print NR, NF, $0}' を実行すると、1行目は「1 3 a b c」、2行目は「2 2 x y」と出て、行番号・列数・行全体がひと目で分かります。grep -n 風の行番号付けも awk '{print NR, $0}' で作れます。$NF は列数が行ごとに違っても必ず最後の列を指すので、列の総数が一定でないデータでは、番号で $7 と決め打ちするより $NF を使うほうが安全です。応用として $(NF-1) と書けば「最後から2番目の列」を指せます。なお NR を条件に使うと行の範囲も選べ、awk 'NR>=2 && NR<=4' file は2行目から4行目だけを、awk 'NR%2==0' file なら偶数行だけを出力できます。フィールドと行番号という2つの軸で行と列を自在に指せるのが、awk の見通しのよさです。
パターンで行を選ぶ
awk の最大の強みは、列の値そのものを条件にして行を絞り込めることです。{ } の前に書いた「パターン」が条件として働きます。awk -F: '$3 >= 1000 {print $1, $3}' /etc/passwd は、3列目(UID)が1000以上の行、つまり一般ユーザーだけを抜き出します。grep が「文字のパターンで行を探す」のに対し、awk は「$3 >= 1000」のように数値の大小や比較で行を選べるのが本質的な違いです。条件には文字列の一致や正規表現($1 ~ /^web/ のような書き方)も使えるので、表形式データの絞り込みでは grep より一段踏み込んだことができます。パターンを省略すると、すべての行がアクションの対象になります。
BEGIN・END と集計
awk には、処理の前後に1回だけ走る特別なブロックがあります。BEGIN は全行を読む前に1回、END はすべて読み終えた後に1回実行されます。これを使うと集計が書けます。printf '10\n20\n30\n' | awk '{sum += $1} END {print "合計", sum, "平均", sum/NR}' は、各行で sum に値を足し込み、最後に END で結果を1回だけ出します。このとき NR は最終的に全行数になっているので、sum/NR が平均になります(この例では「合計 60 平均 20」)。BEGIN は逆に、見出し行の出力や、区切り文字・変数の初期値の設定に使います。1行ずつの処理と、最後にまとめる処理を分けて書けるのが awk の集計の型です。
書式出力とよくある失敗
見た目を整えて表にしたいときは printf を使います。printf 'apple 120\nbanana 80\n' | awk '{printf "%-8s %5d\n", $1, $2}' のように書式を指定でき、%-8s は左寄せ8桁の文字列、%5d は右寄せ5桁の整数を表します。print と違って printf は自動で改行しないため、末尾に \n を自分で書く必要があり、これを忘れて出力が1行につながってしまうのがよくある失敗です。また、区切りが空白でないのに -F を指定し忘れて列がうまく取れない、というのも頻出です。もう一つ注意したいのは、awk のプログラムは多くの記号を含むためシングルクォートで囲むのが鉄則だという点で、ダブルクォートで囲むと $1 などがシェル変数として展開されてしまい、まったく違う動きになります。実務では、awk は /etc/passwd のようなコロン区切りファイルの解析、ログの特定列の集計、サイズや件数の合計といった場面で活躍します。「列を取り出すだけなら cut、列の値で条件を付けたり計算したりするなら awk」と使い分けると、テキスト処理の幅が大きく広がります。フィールドという考え方さえ身につけば、表形式のデータはほとんど awk で思いどおりに扱えるようになります。