オフィシャルドキュメント: The GNU Awk User's Guide (FSF)

AWK サンプルルーティン集

AWK それ自体でプログラムを組むことは少ないが、シェルスクリプト中で引数や文字列処理に使うと便利なものだ。ここでは、筆者がこれまでに BASH や AWK スクリプトの中で使ったり試したことのある処理を、スクリプト例として紹介する。参考になりそうな例があれば随時追加していく。 AWK の正規表現は Perl と同じではないが、事始めとしては Perldoc の perlrequick が役に立つだろう。

意外 ! AWKでは { } が使えない !?

素の呼び出し方では、正規表現 {} は使えない。AWKの呼び出し時に --posix または --re-interval を指定しないと正規表現として受け付けないようにできているのだ。ただし、--posix オプションを指定すると POSIX互換性最優先モードとなり、他にも幾つかの機能が制限されてしまうので、モードはそのままで {} を有効にする後者の方がいいようだ。

ファイルのタイムスタンプを求める

RETM=`ls -l --time-style='+%s' $1 2>&1 | \
   awk '$6 ~ /^[0-9]+$/ {print $6}'`

幾つかのファイルの新旧を比較したかった時に使ったルーティン。これはシェルスクリプト内のサブルーティンの中の処理なので、$1 はサブルーティン呼び出し時の引数 (ファイルパス)。新しいか古いかさえ分かればいいので更新日時を 00:00:00 1970/1/1 からの秒数の形で返させる。

ファイルからコメント行と空行以外の内容を読み込む (出力レコードセパレータ変数 ORS)

tar $OPT -cf $DEST \
  `cat $SRCLIST |awk '{ORS=" "} /^(#.*|$)/ {next} {print}'`

AWK の予約済み変数である ORS は、各レコードを出力する際の区切り文字のこと (Output Record Separator)。ここでは、 # で始まる行と空行をマッチさせて、マッチしたら何も処理せずに次のレコードへ、それ以外だったら stdout へスペース区切りprint している。デフォルトの ORS は "\n" つまり改行だ。なお、この AWK 処理は

awk '{ORS=" "} !/^(#.*|$)/ {print}'

とほぼ同義で、そちらのほうが処理速度が少々速いかもしれない。

パスワードファイルから特定のフィールドだけを取り出す (入力フィールドセパレータ変数 FS)

VMUSERS=$(cat $USRSRC |awk -v pa=$POPADM '
  BEGIN{ FS=":" }
  { if($3 == pa) print $1 }
')

例は qmail の「古いメールの削除」セクションで紹介しているシェルスクリプト rmmail_virtual2 の一部。ここで入力源としている $USRSRC は、メール専用のユーザ/パスワードファイルで、各行は、
user:pAssWord:systemuser:/PATH/TO/MAIL/HOME
といった具合に各フィールドがコロン (:) で区切られている。そこで、デフォルトではスペース (" ") である FS をコロンに変更。予約済み変数 FS は、入力レコードをフィールドへと分解する際に AWK が識別に使う「区切り文字 (Field Separetor, デリミタ)」 だ。普通、こうした定義の変更は初めに 1回だけ行えばいいので BEGIN ブロックで記述する。ちなみに、FS は正規表現を使って定義することも可能で、例えば、
FS="[ \t\n]+" (スペース、タブ、改行のいずれか)
FS="[:space:]*:[:space:]*" (前後にスペースがあるかもしれないセミコロン)
などとすることもできる。

&&による多重マッチ

ps -ewo "%p %c %a" \
  |awk "!/detectpid/ && !/grep/ && /$PROG/ && /$TEST/ {print \$1}" |head -n 1

ps の出力から目的とするプロセスの PID を求める。 &&|| を使うことで かなり的確に絞り込める。ただし &&|| を併用する際には注意。他の言語と同様に AWK でも、 || は優先度が低いので、 `/a/ || /b/ && !/c/' と書いた場合「aを含むレコードか、bを含みcを含まないレコード」と評価される。「aまたはbを含み、cを含まないレコード」という評価をさせるには `(/a/ || /b/) && !/c/' と書く。

ディレクトリの総使用量を得る

du=`du -sk $dir | awk {'print $1'}`

tailを使わずに最後の行の特定部位を取り出す

RES=$(ls -l /dir |awk 'END{ sub(/a/,"b",$0); print $2 }')

END ブロックで囲めば、「現在の読み込みレコード」を保持する $0 には最後のレコードしかないので、`tail -n 1' したのと同じ効果が得られる。

文字列の置換 (sub関数)

taropt=`echo $taropt | awk '{ sub(/(--create|-c)/, "-u") } {print}'`

sub() コマンドは、処理中のデータそのものを書き換える。よって 'str=sub(....)' という使い方や 'print sub(....)' は間違いで、それをやると置き換えた数が返ってくるだけ。

文字列の置き換え (gensub関数)

ls -gG --time-style=+%Y%m%d%H%M%S /some/deep/dir/file | awk \
'{ printf "%s,%s,%s\n", $3,$4,gensub(/(.+\/)(.+)$/,"\\2",1,$5) }'

sub() に対して、 gensub() コマンドは置き換え (SUBstitute) の結果としてできあがった文字列を生成 (GENerate) して返してくる。よって、それを直接 print したり変数に代入することが可能だ。上記のスクリプトは /some/deep/dir/file ファイルを ls して、サイズ、タイムスタンプ、ファイル名を出力するのだが、ls の出力するファイル名は /some/deep/dir/file と言う具合にディレクトリ付きなので、 gensub() によって file だけにしている。 sub() とは文法も異なる点に注意。

gensub(regexp, replacement, limit [, input])

という風に置き換え数の上限 (limit部) が入るからだ。 limit部を数でなく "g" または "G" にすると Perl の x=~s/xx/yy/g のように、見つかったピースを全部置き換える。処理対象が処理中のレコード (行) 丸ごと、つまり $0 のつもりなら ,input 部は省略できる。

大文字小文字の変換 (tolower/toupper関数)

LOW=$(awk 'BEGIN { print tolower(ARGV[1])}' $i) 

これはBASHスクリプトの for ループ内から抜き出した一行。手短に、BASH上の変数 $iBEGIN ブロックで直接処理。つまり $i の値は AWK 上では ARGV[1] として捉えられるので、そいつを小文字に変えつつprint している。

正規表現にマッチする文字列だけを取り出す (match関数)

echo -c -C /home -M |awk '{
  match($0, /(-C +|--directory=)([^ ]+)/, fields); print fields[2]
}'

match 関数はカッコ内の表現にマッチした部分を配列 (例では fields) に入れる。この例は /home を出力する。

正規表現にマッチするひとつ前のフィールド文字列を取り出す (フィールド序数の演算)

ldd $PROG |awk '{
  for (i=1; i<=NF; i++) {
    if ($i ~ /^\(0x[[:xdigit:]]+\)$/) {
      if ($(i-1) != "=>") print $(i-1)
      break
    }
  }
}'

特定のプログラムに必要なライブラリを chroot 環境にコピーするために組んだ ldcopy スクリプトの一部。 $PROG (例えば /bin/bash) を ldd ユーティリティに渡せばそのプログラムに必要な共有ライブラリがリストされるが、出力内容が込み入っているので AWK を利用して整形。 ldd ではカーネルにスタティックコンパイルされているライブラリ以外は `登録名 => ファイル実体 (アドレス)' という出力形式になるので、`(アドレス)' のひとつ前のフィールドを狙えば、ライブラリファイルの実体名だけが取り出せる。 NF は、現在のレコードのフィールド総数を格納する AWK 組み込み変数。 POSIX の文字クラス [:xdigit:] は 「16 進数で使われる文字」 を意味する。これは `0-9a-fA-F' に展開されると考えることができ、その外側をさらに文字集合の [] で囲んで使う。なお ldd の出力形式はバージョンによって異なるようなのでご注意。

シェル配列に特定の値があるか調べる

awk 'BEGIN {
  for (i=1; i<ARGC; i++) {
    if (ARGV[i] ~ /^test_string$/) exit 1
  }
  exit 0
}' "${SHELL_ARR[@]}"
 
if [ $? -eq 1 ] ; then
  ...
fi

BEGIN ブロックで BASH 上の配列 $SHELL_ARR の全要素を直に、順繰りに評価させている。 AWK ルーティンは、正規表現でマッチが見つかったところでリターンコード 1 とともに即脱出、ループの最後までマッチがなければコード 0 で終わることになる。 AWK 内の exit ファンクションは AWK が終了するだけであって親のシェルスクリプトが終わるわけではない。 `exit 0' は冗長だが、念のため入れて不特定要因を排除。そして AWK のリターンコードをシェルの特殊変数 $? (直前のコマンドの終了コード) で捕まえる。

ファイルがブロックデバイスかどうか調べる

if [ `file -L $defaultDST | awk '{print $2$3}'` = blockspecial ]; then
  ...
fi

リンク切れのシンボリックリンクを探す

symlinks -vr / |awk '/^dangling/ { printf "%s %s %s\n",$2,$3,$4 }'

Fedora Upgrade HOWTO で紹介しているルーティーン。システム全体 (/) を symlinks ユーティリティで再帰的 (-vr) に検索した結果を AWK に渡す。 symlinks はリンク切れの「壊れた」リンクは頭に `dangling:' を付けて出力するので、そうしたレコードだけを printf 関数で整形して出力させるようにした。

シェル上の変数をAWK内で使いたい

qmail-qread | \
awk -v opt=$OPT -v more=$MORE -v num=$NUM '
  {
    if ( NR%2==1 ) QNUM=$6
    if ( NR%2==0 && $1==opt ) {
      sub(/#/,"",QNUM)
      if ( more == "-D" ) {
	if ( num != "" && num != QNUM ) next
      }
      print QNUM
      if ( more != "-l" ) exit 0
    }
  }
'

これに関しては詳細はどうでもいい。シェル上の変数を AWK の中で使いたい時には、 AWK のコマンドラインオプション `-v name=value' を使って AWK のための変数として再定義してやればよい。 -v オプションはいくつでも指定できる。他に &&による多重マッチの例のように AWK 評価式全体をシングルでなくダブルクォートで囲み $変数 をシェルに直接展開させる方法もあるが、たくさんの記号を \ でエスケープしなければならなくなりデバグが大変になるため、あまりお勧めできない。予約済み変数である NR (Number of input Record) は、その時点までに AWK が読んだレコード数 (行数) を格納するもので、この例ではそれが偶数か奇数かによって処理を分岐させている。

追加ファイルの読み込み (getline < file)

_v_file = "sample.list":
_v_index = 2;
FS = ",";
 
V_nr = 1;
while ((getline < _v_file) > 0) {
    if (length($_v_index) > 0) {
        A_users[V_nr] = $_v_index;
        V_nr++;
    }
}
close(_v_file);

sample.list というカンマ区切りでパラメータの書かれたファイルを読み込んで、2番目のフィールドの値を A_users という別の連想配列に登録するという処理をやっている。こうした別ファイル読み込み処理は、BEGINブロックか ENDブロックの中で行う。メイン処理ブロックと違うのは、カレント処理レコード番号を示す FNR や処理レコード累計を示す NR は使われないということ。ただし、例を見て分かるとおり、フィールドへの分割は通常と同じようにフィールドセパレータ FS を使って自動的に行われる (例えば上記ループ内の A_users[V_nr]=$_v_index は、あらかじめ _v_index=2 と定義してあるので A_users[V_nr]=$2 として評価される)。

AWK内で外部コマンドを実行し結果を変数に代入する

#!/bin/sh
# Suppose this file is named 'extcmd.sh'.
echo $* | awk '{
    cmd = "date --date=\"" $0 "\" +%Y%m%d"
 
   # This may not be what you want.
    s = system(cmd)
    print "s=" s
 
   # This is it!
    cmd | getline t
    close(cmd)
    print "t=" t
}'
exit
実行結果(上記ファイルを extcmd.sh だとして)
user$ extcmd.sh "2 day ago"
20061124
s=0
t=20061124

正しいのは、コメント "This is it!" の下に書いた、パイプで getline関数に送るやり方だ。 AWK では system() という関数で外部コマンドを実行することもできるが、上記の "This may not be what you want" とコメントした部分を見て分かるように、system() の場合の "date --date ..." はまさにスクリプトの外部で勝手に走って独自にアウトプットを行い (実行結果の 1行目)、AWK内変数 s にはその終了コードが入るだけだ (実行結果の 2行目)。

なお、コマンドが、引数のない "date" のみであれば直接書いてもいいが、例のように引数を持たせる場合には、あらかじめ AWK内変数 (例では cmd) に定義してから変数経由で呼んでやる必要がある。正確に言うと、コマンド全体をダブルクォートで囲んでもできるのだが、close() の際にも一字一句違わぬコマンドを (やはりダブルクォートして) もう一度書かなければならず現実的でない。例のようにインプットによってコマンドが変化する場合はなおさらだ。

AWK内で外部コマンドを実行し複数行の結果を受け取りたい (print cmd |& "sh")

上記のやり方では、外部コマンドから受け取れる回答は 1行だけだ。複数行に渡る回答を全て受け取りたい場合には、シェルへ双方向のパイプを開き、回答行が無くなるまでループで受け取ってからパイプを閉じる、という処理が必要になる。

_v_cmd = "ls -l " _v_path;
print _v_cmd |& "sh";
close("sh", "to");
while (("sh" |& getline _v_line) > 0) {
    _v_c = split(_v_line, _a_vals);
    printf("Permission=%s, Size=%d\n", _a_vals[1], _a_vals[5]);
}
close("sh");

上記の例の説明をしよう。 1行目では変数 _v_path を使って、あるディレクトリのファイルリストを得るコマンドを作成している。出来たコマンド文字列を、次の行で "sh" に向かってプリントしているのだが、これはつまり「このコマンドを実行せよ」とシェルに命令していることになる。この時、パイプが "|&" になっていることで、AWKとシェルの間で双方向の接続が結ばれる。そして次の close("sh", "to") で「行き」の接続だけを閉じている。その下からはループで、シェルとの接続からの「帰り」を AWKビルトイン関数 getline で順次 1 行ずつ _v_line に代入して某か処理を行い、最後に close("sh") でシェルとの接続を完全に閉じて終わりだ。

注意は、この場合 close しなければならないのは _v_cmd ではなく "sh" であることと、 AWKに引数として渡したファイルが自動的に FS デリミタによってフィールドに分割されるのとは異なり、勝手にフィールドに分けてはくれないので、必要ならば上記のように split 関数を使って明示的に分割しなければならないという点だ。

条件分岐, ループ, BEGINによる直接入力

awk 'BEGIN {
	C = 0; 
	K = 0; 
	for (i = 1; i < ARGC; i++) { 
		if (ARGV[i] ~ /^g=/) { 
			sub(/^g=/, "", ARGV[i]); 
			G = ARGV[i]; 
		} 
		else if (C == 0) { 
			D = ARGV[i]; 
			C++; 
		} 
		else { 
			S[K] = ARGV[i]; 
			K++; 
		} 
	} 
	if (G == "") G = 0; 
	print G; 
	if (D != "") print D; 
	for (i in S) { print S[i] }; 
}' $@

このルーティンを使ったシェルスクリプトはお蔵入りになったので処理自体にはあまり実用性はない。引数を AWK で処理してみようと思っただけで、同じことをやるなら BASH 内蔵の for ループと case を利用したほうが妥当。 AWK は、処理を BEGIN ブロックに入れると引数を直接処理することができる。ここでの引数は BASH スクリプト自体に渡した引数全体 ($@)。引数の数は ARGC、引数を参照するには ARGV 配列を使う。 ARGV[0] は `AWK' 自身。本当の引数は ARGV[1] からで、その中には AWK へのオプション (例えば -v var=value といったもの) は含まれない。

後日、BEGIN による直接入力の代わりに、もうちょっとスマートなやり方に気付いた。 BASH のヒアストリング機能と AWK の NF (Number of Field) を使う。下のような塩梅だ;

awk <<< "$*" '{
    for (i = 1; i <= NF; i++) {
        print $i
    }
}'

複数のファイルパスから重複を除いて出力

function checklink () {
  awk -v d=$CHROOTDIR 'BEGIN{
    for (i=1; i<ARGC; i++) {
      sub(/\/[^/]+$/, "", ARGV[i]);
      printf "%s%s\n", d,ARGV[i];
    }
  }' $@ |sort |uniq
}

chroot 環境構築で使用した、BASH スクリプト ldcopy の中のファンクション。ファンクションに与える引数は、コピー元ファイルパス (/lib/libdl.so.2 /lib/libtermcap.so.2 /lib/tls/libc.so.6 といったようなリスト)。組み込み変数 ARGC$@ の数を数え、 各値 ( ARGV[n] ) をループ処理。重複を無くす役目は uniq ユーティリティが担っている。 uniqsort 済みの集合しか扱えない。

lsのパーミション表示をchmod用に変換

function sym2mod () {
  ls -ld $1 |awk '
    {
      if ($1 ~ /^l/) exit 1
      sub(/d/, "", $1);
      u = substr($1, 1, 3);
      gsub(/-/, "", u);
      sub(/s/, "xs", u);
      sub(/S/, "s", u);
      g = substr($1, 4, 3);
      gsub(/-/, "", g);
      sub(/s/, "xs", g);
      sub(/S/, "s", g);
      o = substr($1, 7);
      gsub(/-/, "", o);
      sub(/t/, "xt", o);
      sub(/T/, "t", o);
    }
    { printf "u=%s,g=%s,o=%s\n", u, g, o }
  '
  return $?
}

chroot jail で紹介している ldcopy スクリプトの一部。元のディレクトリとスクリプト中で作成したそのコピーのパーミションを揃えたかったので組んだ。ファンクションの引数は /lib/tls のようなディレクトリ名のフルパス。 ls の出力する `drwxr-Sr-x' といったパーミション表示を、 chmod に引数として直接与えることが可能な `u=rwx,g=rs,o=rx' といった形式に変換する。 AWK ルーティンで最初の if 節は、そのディレクトリがシンボリックリンクだった場合 (lrwxrwxrwx) と、ディレクトリが存在しないケース (`ls: no such file or ..' という ls のエラー出力) にぶつかったら即処理をやめるため。 UN*X のパーミションについてはこちらで詳しく考察している。