Awk basics - ストリームエディタの発展系 Awk の基礎

[Awk における拡張正規表現]
[2014/03/01新規] [2014/09/12更新]

Contents

主な形式

awk [-v assignment]... 'script' [file...]

簡単な説明

Awk は『レコード』と呼ばれる行を一つずつ入力する。Awk スクリプトを指定することにより様々な処理をすることができる。

まず、入力行は、レコードセパレータである改行コード '\n' が取り除かれ '$0' に入る。

また、用途によっては全く不要なことであるが、フィールドセパレータである空白を区切りとして '$1', '$2', 〜 にその行の『フィールド』群が入る。

Awk スクリプトは「パターンとそのアクション」の組み合わせで書く。そこでは拡張正規表現、論理演算、算術演算、さらには制御構文を書くことができる。加えて、組み込み関数および「ユーザ定義関数とそのアクション」の組み合わせも利用できる。

ちなみに、sed では可能な、正規表現内の後方参照は Awk ではできないので注意。

Awk の実装にはオリジナルの awk の他に、The One True Awk と呼ばれる nawk やその派生および GNU Awk 等があり、知らないとそれらの方言に悩まされる。それを避けるために POSIX Awk についてまずは学ぶべきである。ここでは特に断らない限り POSIX Awk について述べる。

Awk スクリプトの概要

パターンは 'BEGIN', 'END' のような最初のレコードの前と、最後のレコードの後を表す特殊パターンの他に、真偽値を得る「式」も書けて、アクションは、パターンもしくはパターンの範囲「式, 式」にマッチするときに処理される。これは sed の正規表現や行番号、もしくはそれらの範囲を表す「アドレス」を拡張したものとなっている。

例えば以下は、HTML の 'pre' タグを含むそれに囲まれた行を表示する Awk スクリプトである。

	awk '/^<pre>/,/<\/pre>$/'

「パターンとアクション」のパターンを省略した場合、すべてのレコードでアクションが処理される。一方、アクションを省略した場合、'print' 文が省略されたものとして処理される。'print''print $0' と等価である。

例えば以下は、ソースコードを HTML にペーストできるように「<」から「&lt;」への変換等を行なう Awk スクリプトである。

	awk '{ gsub(/&/, "\\&amp;"); gsub(/</, "\\&lt;" ); gsub(/>/, "\\&gt;"); print }'

アクションは文のリストであり、文は代入式、制御構文、関数呼び出し、'{' 文のリスト '}' である。文の区切りは改行か ';' で区切る。シェルや C/C++ の文と似ているが '}' 直前の ';' が省略できることが異なる。

利用者は、Awk スクリプトでは型なしの変数をいくつも使用することが出来るが、すべては大域変数である。局所変数となり得るのは、ユーザ関数定義での引数のみであり、呼び出し側が使っていない余分な引数が局所変数としてよく使用される。

制御構文

以下の制御構文が使用できる。

'switch' は POSIX Awk ではサポートされないが GNU Awk では可能。'goto' は POSIX Awk でも GNU Awk でもサポートされない。

さらに以下の出力構文がある。

演算子

Awk の演算子は C/C++ のそれとおよそ同じだが、その型は基本的に、論理演算と浮動小数点の数値演算、そして文字列の正規表現検査のみである。よって、整数型にあるビット演算に関しては未サポートであり、GNU Awk では組み込み関数でサポートされる。特に注意すべき演算子を以下にあげる。

AwkC/C++備考
expr1 expr2string(expr1) + string(expr2)文字列の連結
expr1 ^ expr2pow(expr1, expr2)C/C++ の XOR ではなく、指数関数
lvalue ^= exprlvalue = pow(lvalue, expr)C/C++ の XOR 代入ではなく、指数関数の代入
expr1 ~ expr2regex_search(expr1, , regex(expr2, ...))C/C++ の ビット否定ではなく、正規表現のマッチ
expr1 !~ expr2!regex_search(expr1, , regex(expr2, ...))C/C++ の ビット否定ではなく、正規表現のマッチの否定

特殊変数

POSIX Awk における特殊変数は以下の通りである。

エスケープ文字

POSIX Awk におけるエスケープ文字は以下の通りである。

例えば '(' そのものを表したい場合、'match(, "")' 等のダブルクォート内では、'\\(' としなければならない。さもなくば '\(''(' と同じくグループを表すことになってしまう。一方、'/' で括られた正規表現のパターン内では '\(' とすればよい。

組み込み関数

算術関数

GNU 拡張ビット演算関数

配列

Awk の配列は C/C++ のようなリニアなアドレッシングの配列ではなく、ハッシュ辞書いわゆる連想配列であるので、添字が 0 から始まろうが 1 から始まろうが関係ないし、添字が文字列でも構わない。さらには、添字をカンマで区切ることによって疑似的に多次元配列も扱える。そのとき添字に SUBSEP である '^\' を使うと意図した指示にはならないので注意。

文字列関数

入出力関数

このように、また、組み込み関数には、ファイル関係の関数が存在しない。よって、ファイルを操作するには system で外部コマンドを呼び出すか、Perl のような別の言語に移行するか、GNU Awk の拡張機能を組み込む必要がある。

例題

代表的な Unix コマンドに相当する Awk スクリプトを以下にあげる。ちなみに、特に断らない限り以下すべては GNU Awk (gawk) でも動作する。

`cat`

	awk '{ print }'

このように、cat と同じ awk スクリプトはパターンを省略した以上のようになるが、「{ print $0 }」でもよいし、アクションを省略した「!0」でもよい。

[sed] [Awk] [Perl] [Ruby] [Python]

`head -n 1`

	awk 'NR == 1'

このように、head -n 1 と同じ awk スクリプトはアクション「{ print }」を省略した「NR == 1」となる。

[sed] [Awk] [Perl] [Ruby] [Python]

`tail -n 1`

	awk 'END{ print }'

このように、tail -n 1 と同じ awk スクリプトは「END{ print }」となる。

[sed] [Awk] [Perl] [Ruby] [Python]
	awk '!(NR > 8)'

このように、head -n 8 と同じ awk スクリプトは以上のようになるが、「NR <= 8」でもよい。

[sed] [Awk] [Perl] [Ruby] [Python]

`tail -n 8`

さて、`head -n 1`, `tail -n 1`, `head -n n` は以上のように大変簡単であるが、`tail -n n` は少し工夫が必要であり、以下のようになる。

	awk -v n=8 '
{
  A[NR%n] = $0
}
END{
  for (i=0; i<n; i++)
    print A[(NR+i+1)%n]
}'

ここでは、配列にラウンドロビン的に行を格納していき、最後にそれらを出力している。

このように、tail -n 8 と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`wc -l`

	awk 'END{ print NR }'

このように、wc -l と同じ awk スクリプトは「END{ print NR }」となる。

[sed] [Awk] [Perl] [Ruby] [Python]

`wc -c`

sed では大変面倒になる `wc -c`awk だと算術演算があるので簡単である。

	awk '{ l += length()+1 } END{ print l }'

ちなみに '+1' は省かれた改行コードの分である。

このように、wc -c と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`wc -w`

sed では大変面倒になる `wc -w`awk だと算術演算があるので簡単であるが、少々工夫が必要であり、以下のようになる。

	awk '
{
  h = $0
  while (match(h, "[^\t ]+")) {
    w++
    h = substr(h, RSTART+RLENGTH)
  }
}
END{
  print w
}'

この方法は他に応用が効くのでこれでもよいのだが、組み込み関数 'gsub' が置換の数をカウントしてくれるので、この場合は以下の方が簡単である。

	awk '
{
  w += gsub(/[^\t ]+/, "")
}
END{
  print w
}'

このように、wc -w と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`grep '^$'`

	awk '/^$/'

このように、基本正規表現の grep '^$' と同じ awk スクリプトは「/^$/」となるが、Awk では基本正規表現はサポートされないので、他のパターンでは拡張正規表現に書き直す必要がある。

[sed] [Awk] [Perl] [Ruby] [Python]

`grep -v '^$'`

	awk '!/^$/'

このように、マッチの否定、基本正規表現の grep -v '^$' と同じ awk スクリプトは「!/^$/」となるが、Awk では基本正規表現はサポートされないので、他のパターンでは拡張正規表現に書き直す必要がある。

[sed] [Awk] [Perl] [Ruby] [Python]

`grep -E '^.+'`

	awk '/^.+/'

このように、拡張正規表現の grep -E '^.+' と同じ awk スクリプトは「/^.+/」となる。

[sed] [Awk] [Perl] [Ruby] [Python]

`grep -E -v '^.+'`

	awk '!/^.+/'

このように、拡張正規表現のマッチの否定、grep -E -v '^.+' と同じ awk スクリプトは「!/^.+/」となる。

[sed] [Awk] [Perl] [Ruby] [Python]

`cut -d ':' -f 1,6`

	awk -F ':' -v OFS=':' '{ if (NF >= 6) print $1, $6; else print; }'

ここで "-F ':'""-v FS=':'" と等価である。

このように、区切り ':' のフィールド切り取り、cut -d ':' -f 1,6 但しフィールド数不足の行はそのまま出力と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`cut -d ':' -f 1,6 -s`

	awk -F ':' -v OFS=':' 'NF >= 6{ print $1, $6 }'

ここで "-F ':'""-v FS=':'" と等価である。

このように、区切り ':' のフィールド切り取り、cut -d ':' -f 1,6 但しフィールド数不足の行は出力しないと同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`fold -b`

この例、1行あたりの文字数(既定値は80)を越えたら改行を挿入する awk スクリプトを示そう。まずは正規表現を使わない方法:

	awk -v w=80 '
{
  h = $0
  while (h != 0) {
    if (length(h) > w) {
      print substr(h, 1, w)
      h = substr(h, w+1)
    }
    else {
      print h
      h = 0
    }
  }
}'

もしくは、正規表現を使った方法:

	awk -v w=80 '
{
  h = $0
  while (match(h, "^.{" w "}")) {
    print substr(h, RSTART, RLENGTH)
    h = substr(h, RSTART+RLENGTH)
  }
  print h
}'

後者の方が少しだけ単純だが、The One True Awk のような実装では動作しない。

このように、fold -b と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`tee filename`

	awk '{ print; print > "filename.out" }'

このように、標準入力を標準出力とファイルに書き出し、tee と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`tr 'A-Za-z' 'N-ZA-Mn-za-m'`

この例、ROT13(と呼ばれる暗号化と言うより難読化)は tr コマンドを使うと表題のように簡単に実現できる。しかし、awk には sed'y' コマンドもなく、少々難しいが以下のようになる。

	awk -v f='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' -v t='NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm' '
BEGIN{
  r = sprintf("[%s]", f)
  for (i=1; i<=length(f); i++)
    m[substr(f, i, 1)] = substr(t, i, 1)
}
{
  for (i=1; i<=length(); i++) {
    c = substr($0, i, 1)
    if (match(c, r) != 0)
      printf("%c", m[c])
    else
      printf("%c", c)
  }
  print ""
}'

このように、文字置換、tr 'A-Za-z' 'N-ZA-Mn-za-m' と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`cat -n`

sed ではいささか面倒になる `cat -n`awk だと極めて簡単である。

	awk '{ printf("%6d\t%s" ORS, NR, $0) }'

このように、cat -n と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`cat -b`

sed では大変面倒になる `cat -b`awk だと極めて簡単である。以下の「;」は空の文であることに注意。

	awk '/^$/;!/^$/{ i++; printf("%6d\t%s" ORS, i, $0) }'

このように、cat -b と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`uniq`, `uniq -d`, `uniq -u`

ところで、これらは GNU sedGNU Awk で示される好例となっている。

  1. `uniq` - 重複する行を一行にする in GNU sed
  2. `uniq -d` - 重複する行のみを一行にして表示する in GNU sed
  3. `uniq -u` - 重複しない行のみを表示する in GNU sed
  4. `uniq [-du]` - 以上すべてに対応 in GNU Awk

sed のいずれも 'N' コマンドで入力行をパターンスペースの末尾に '\n' に続いて追加し、正規表現にて '^\(.*\)\n\1$' のように後方参照 '\1' を利用していることにある。awk においては、基本的には以下に示す例と同じである。

これらを awk で実現すると、単純に文字列の比較と制御の組み合わせとなり、順に以下のようになる。

	awk 'NR == 1{ h = $0; print; next } { if (h != $0) { h = $0; print } }'
	awk 'NR == 1{ h = $0; next } { if (h != $0) { h = $0; d = 0; } else { if (!d) print; d = !0 } }'
	awk 'NR == 1{ h = $0; next } { if (h != $0) { if (!d) print h; h = $0; d = 0 } else d = !0; } END{ if (!d) print h }'

このように、uniq, uniq -d, uniq -u と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`expand`

このタブを複数の空白に置換するコマンドを awk で実現するのは少々難しいが、以下のようになる。

	awk -v n=8 '
{
  h = $0
  while (match(h, "^([^\t]{0," n-1 "}\t|[^\t]{" n "})")) {
    u = substr(h, RSTART, RLENGTH)
    h = substr(h, RSTART+RLENGTH)
    p = index(u, "\t")
    if (!p) p = RLENGTH + 1
    u = substr(u, 1, p-1)
    for (i=0; i<n-(p-1); i++) u = u " "
    printf("%s", u)
  }
  print h
}'

しかし、The One True Awk のような実装では動作しない。よって、機能消極的な実装では以下のようになる。

	awk -v n=8 '
{
  l = 0
  for (i=1; i<=length(); i++) {
    c = substr($0, i, 1)
    d = (c == "\t") ? n - (l % n) : 1
    if (c == "\t") {
      c = ""
      for (j=0; j<d; j++)
        c = c " "
    }
    printf("%s", c)
    l += d
  }
  print ""
}'

両者の効率と柔軟性におけるメリット・デメリットを考えてみるのも興味深い。個人的には、前者の方が判り易くてよいと思うが、例えば、'\b' が一文字戻るとみなす処理を加えるには後者の方が簡単である。

このように、expand と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`unexpand -a`

この複数の空白をタブに置換するコマンドを awk で実現するのは少々難しいが、以下のようになる。

	awk -v n=8 '
{
  h = $0
  while (match(h, "^([^\t]{0," n-1 "}\t|[^\t]{" n "})")) {
    u = substr(h, RSTART, RLENGTH)
    h = substr(h, RSTART+RLENGTH)
    if (match(h, "^ ")) {
      if (match(u, " {1,}$"))
        u = substr(u, 1, RSTART-1) "\t"
    }
    else {
      if (match(u, "^[^\t]{" n-1 "}\t$"))
        u = substr(u, RSTART, RLENGTH-1) " "
      if (match(u, " {2,}$"))
        u = substr(u, 1, RSTART-1) "\t"
    }
    printf("%s", u)
  }
  print h
}'

ちなみに、最初の 'if' 文は、完全に `unexpand -a` の挙動を再現するためのものである。

しかし、The One True Awk のような実装では動作しない。よって、機能消極的な実装では以下のようになる。

	awk -v n=8 '
BEGIN{ for (i=0; i<n-1; i++) r = r "[^\t]"; }
{
  buf = ""
  l = 0
  for (i=1; i<=length(); i++) {
    c = substr($0, i, 1)
    d = (c == "\t") ? n - (l % n) : 1
    l += d
    buf = buf c
    if (l % n == 0) {
      if (substr($0, i+1, 1) == " ") {
        if (match(buf, " +$"))
          buf = substr(buf, 1, RSTART-1) "\t"
      }
      else {
        if (match(buf, "^" r "\t$"))
          buf = substr(buf, 1, length(buf)-1) " "
        if (match(buf, "  +$"))
          buf = substr(buf, 1, RSTART-1) "\t"
      }
      printf("%s", buf)
      buf = ""
    }
  }
  print buf
}'

両者の効率と柔軟性におけるメリット・デメリットを考えてみるのも興味深い。個人的には、前者の方が判り易くてよいと思うが、例えば、'\b' が一文字戻るとみなす処理を加えるには後者の方が簡単である。

このように、unexpand -a と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`rev`

行毎に文字列を反転する BSD rev(1) コマンド。実用したことはないが、sed で実現するには秀逸な技法が必要であった。しかし、awk では極めて単純に、文字列を逆順に取り出して出力すればよい。

	awk '
{
  for (i=length(); i>0; i--)
    printf("%c", substr($0, i, 1))
  print ""
}'

このように、BSD rev(1) と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`tac`, `tail -r`

最終行から先頭行まで逆順で出力する GNU tac コマンド、`tail -r` に同じ。これも実用したことはないが、GNU sed における、好ましくない実現例のように、awk における好ましくない実装は以下の通りである。

	awk '
NR == 1{ b = $0 }
NR != 1{ b = $0 ORS b }
END{ print b }'

変数に行を逆順に連結して最後にそれを出力するという処理なので、メモリが足りなくなるか、仮想メモリで非常に遅くなるだろう。確かに他の方法も思いつかないが、そもそも 'fseek' 等がない awk にそぐわないことをさせるなら C や perl 等の他の言語を採用すべきだ。

[sed] [Awk] [Perl] [Ruby] [Python]

`fold`

この例、1行あたりの制御コードを考慮した文字数(既定値は80)を越えたら改行を挿入する awk スクリプトを示そう。これはカウントが必要になるので sed では困難な好例となっている。

	awk -v n=8 -v w=80 '
{
  l = 0
  for (i=1; i<=length(); i++) {
    c = substr($0, i, 1)
    d = (c == "\b") ? ((l > 0) ? -1 : 0) : (c == "\r") ? -l : (c == "\t") ? n - (l % n) : 1
    if (l+d > w) {
      print ""
      l = d
    }
    else
      l += d
    printf("%s", c)
  }
  print ""
}'

このように、fold と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`fold -s`

この例、1行あたりの制御コードを考慮した文字数(既定値は80)を越えようとするブランクに変わり改行を挿入する awk スクリプトを示そう。これもカウントが必要になるので sed では困難な好例となっているだけでなく、awk でもかなり複雑にならざるを得ない。

	awk -v n=8 -v w=80 '
function increment(l, c)
{
  return (c == "\b") ? ((l > 0) ? -1 : 0) : (c == "\r") ? -l : (c == "\t") ? n - (l % n) : 1
}
{
  buf = ""
  l = len = 0
  if ($0 == "") { print; next }
  for (i=1; i<=length(); i++) {
    c = substr($0, i, 1)
    if (l + increment(l, c) > w) {
      j = len
      while (--j >= 0 && !match(substr(buf, j+1, 1), "[\t-\r ]"));
      space = j
      if (space != -1) {
        space++
        printf("%.*s" ORS, space, buf)
        buf = substr(buf, space+1, len - space)
        len -= space;
        l = 0;
        for (j=0; j<len; j++)
          l += increment(l, substr(buf, j+1, 1))
      }
      else {
        printf("%.*s" ORS, len, buf)
        l = len = 0
      }
    }
    l += increment(l, c)
    buf = substr(buf, 1, len) c
    len++
  }
  if (len != 0)
    printf("%.*s" ORS, len, buf)
}'

ここでユーザ定義関数を効果的に使用していることに注目したい。しかし、もっと簡単になるような気もする…

このように、fold -s と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`strings -a [-n 4] [-t d|o|x]`

この例、ファイルの連続した印字可能な4文字以上の文字列を表示する awk スクリプトを示す。この例では strings.sh という実行権のついたファイルに記述するものとする。

#!/bin/ksh
n=4
t=
while [ "$1" != "" ]; do
  case "$1" in
  -n)	shift; n="$1"   ;;
  -t)	shift; t="$1"   ;;
  *)	break   ;;
  esac
  shift
done
${AWK:-awk} -v n=$n -v t=$t '
BEGIN{
  if (t != "") {
    if (t == "x")
      fmt = "%x %s\n"
    else if (t == "o")
      fmt = "%o %s\n"
    else
      fmt = "%d %s\n"
  }
}
{
  if (FNR == 1) p = 0
  o = 0
  h = $0
  while (match(h, "[\\\\\\f[:print:]]{" n ",}")) {
    if (t != "")
      printf(fmt, p+o+RSTART-1, substr(h, RSTART, RLENGTH))
    else
      print substr(h, RSTART, RLENGTH)
    o += RSTART+RLENGTH-1
    h = substr(h, RSTART+RLENGTH)
  }
  p += length() + 1
}' "$@"

この strings.sh では、'-n 4', '-t d|o|x' オプションが指定でき、このように、オプション解析はシェルに任せ、Awk の '-v' オプションでそれを伝えると柔軟な awk スクリプトが書ける。

しかし、The One True Awk のような実装では動作しない。よって、機能消極的な実装では以下のようになる。

#!/bin/ksh
n=4
t=
while [ "$1" != "" ]; do
  case "$1" in
  -n)	shift; n="$1"   ;;
  -t)	shift; t="$1"   ;;
  *)	break   ;;
  esac
  shift
done
${AWK:-awk} -v n=$n -v t=$t '
BEGIN{
  for (i=0; i<n; i++)
    r = r "[\\\\\\f[:print:]]"
  if (t != "") {
    if (t == "x")
      fmt = "%x %s\n"
    else if (t == "o")
      fmt = "%o %s\n"
    else
      fmt = "%d %s\n"
  }
}
{
  if (FNR == 1) p = 0
  o = 0
  h = $0
  while (match(h, r "+")) {
    if (t != "")
      printf(fmt, p+o+RSTART-1, substr(h, RSTART, RLENGTH))
    else
      print substr(h, RSTART, RLENGTH)
    o += RSTART+RLENGTH-1
    h = substr(h, RSTART+RLENGTH)
  }
  p += length() + 1
}' "$@"

このように、strings -a と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`printenv`

さて、ここまでは入力ファイル駆動型のプログラムばかりであったが、そうではなく自律型のプログラムを書くには、Awk では 'BEGIN' ブロックのみにコードを書けばよい。

この例、環境変数をすべて表示する awk スクリプトを示す。

	awk 'BEGIN{ for (v in ENVIRON) print v "=" ENVIRON[v]}'

連想配列のキーをすべて取り出すには 'for (variable in array)' の構文を使う。

このように、printenv と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`yes [expletive]`

この例、プロセス停止まで永遠に "yes^J" を印字し続ける awk スクリプトを示す。但しこのコマンド、第一引数を指定した場合には "yes" の代わりにそれを印字する仕様となっている。この例では yes.sh という実行権のついたファイルに記述するものとする。

#!/bin/sh
${AWK:-awk} 'BEGIN{ $0 = ARGV[1] ? ARGV[1] : "yes"; while (!0) print }' "$@"

このように、yes と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`paste [-d delimiter] [-s] file ...`

この例、複数のファイルの内容を行毎に結合する awk スクリプトを示す。但しこのコマンド、'-s' オプションを指定した場合はファイル毎に改行を除去して結合する仕様となっており、特に難しくはない。この例では paste.sh という実行権のついたファイルに記述するものとする。

Awk ではファイル名を指定できる 'getline' 関数があるので、以下のように sed では困難な、複数のファイルの平行な入力に対応できる。

#!/bin/ksh
s=false
d='\t'
while [ "$1" != "" ]; do
  case "$1" in
  -s)	s=:	;;
  -d)	shift; d="$1"	;;
  *)	break	;;
  esac
  shift
done
if ! $s; then
  ${AWK:-awk} -v d="$d" '
BEGIN{
  c = ARGC - 1
  while (c) {
    s = ""
    for (a=1; a<ARGC; a++) {
      if (a != 1)
        s = s sprintf("%s", d)
      if (!ARGV[a]) continue
      if ((getline b < ARGV[a]) != 1) {
        if (ARGV[a] != "-") close(ARGV[a])
        delete ARGV[a]
        --c
        continue
      }
      s = s sprintf("%s", b)
    }
    if (c) print s
  }
}
' "$@"
else
  ${AWK:-awk} -v d="$d" '
{
  if (FNR == 1 && FILENAME != ARGV[1])
    printf("" ORS)
  if (FNR != 1)
    printf("%s", d)
  printf("%s", $0)
}
END{ if (NR) printf("" ORS) }
' "$@"
fi

このように、paste と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`comm [-1] [-2] [-3] file1 file2`

この例、複数のファイルの内容を行のキー毎に結合する awk スクリプトを示す。この例では comm.sh という実行権のついたファイルに記述するものとする。

入力ファイルは、行でソートされていることが前提となっているので、複数のファイルをキーの文字列の大小で並行して 'getline' で読み進めていけば良い。

#!/bin/ksh
s1=0
s2=0
s3=0
while [ "$1" != "" ]; do
  case "$1" in
  -1)	s1="$1"	;;
  -2)	s2="$1"	;;
  -3)	s3="$1"	;;
  *)	break	;;
  esac
  shift
done
${AWK:-awk} -v s1=$s1 -v s2=$s2 -v s3=$s3 '
BEGIN{
  for (a=1; a<ARGC; a++) {
    cf[a] = -1
    s[a] = 0
    sc[a] = 0
  }
  s[a] = 0
  sc[a] = 0
  s[1] = s1
  s[2] = s2
  s[3] = s3
  for (a=2; a<=ARGC; a++) {
    for (j=1; j<=a; j++) {
      if (s[j]) sc[a]++
    }
  }
  km = "\n"
  c = ARGC - 1
  {
    ceq = 0
    while (c) {
      for (a=1; a<ARGC; a++) {
        cf[a] = -1
        while (ARGV[a] && cf[a] == -1) {
          if ((getline b < ARGV[a]) != 1) {
            if (ARGV[a] != "-") close(ARGV[a])
            delete ARGV[a]
            --c
            if (!c && km != "\n") {
              if (ceq + 1 != ARGC-1) {
                if (!s[last_a]) {
                  for (j=0; j<last_a-1-sc[last_a]; j++) printf("\t")
                  printf("%s\n", km)
                }
              }
            }
            continue
          }
          if (km == "\n") {
            km = b
            cf[last_a=a] = -2
            ceq = 0
          }
          else {
            if (km < b) {
              if (ceq + 1 != ARGC-1) {
                if (!s[last_a]) {
                  for (j=0; j<last_a-1-sc[last_a]; j++) printf("\t")
                  printf("%s\n", km)
                }
              }
              km = b
              cf[last_a=a] = 1
              ceq = 0
            }
            else if (km == b) {
              if (ceq + 1 != ARGC-1) {
                if (!s[ARGC]) {
                  for (j=0; j<ARGC-1-sc[ARGC]; j++) printf("\t")
                  printf("%s\n", b)
                }
                ++ceq
              }
              else {
                ceq = 0
              }
              cf[last_a=a] = 0
            }
            else {
              if (!s[a]) {
                for (j=0; j<a-1-sc[a]; j++) printf("\t")
                printf("%s\n", b)
              }
              cf[a] = -1
              ceq = 0
            }
          }
        }
      }
    }
  }
}
' "$@"

このように、comm と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`join [-a file_number|-v file_number] ... [-t char] [-1 field] [-2 field] file1 file2`

この例、複数のファイルの内容を行のキー毎に結合する awk スクリプトを示す。この例では join.sh という実行権のついたファイルに記述するものとする。

入力ファイルは、キーとなるフィールドでソートされていることが前提となっているので、複数のファイルをキーの文字列の大小で並行して 'getline' で読み進めていけば良い。とは言え、以下のように多少繁雑になるだろう。

#!/bin/ksh
na=
nv=
t=' '
n1=1
n2=1
while [ "$1" != "" ]; do
  case "$1" in
  -a)	shift; [ "$na" = "" ] && na="$1" || na="$na,$1"	;;
  -v)	shift; [ "$nv" = "" ] && nv="$1" || nv="$nv,$1"	;;
  -t)	shift; t="$1"	;;
  -1)	shift; n1="$1"	;;
  -2)	shift; n2="$1"	;;
  *)	break	;;
  esac
  shift
done
${AWK:-awk} -F "$t" -v OFS="$t" -v a=$na -v v=$nv -v n1=$n1 -v n2=$n2 '
function find_index(a, v, i)
{
  for (i=1; i<=length(a); i++)
    if (a[i] == v) return i
  return 0
}
function printout(n, m, o, i)
{
  o = "\n"
  for (i=1; i<ARGC; i++) {
    if (va[i] == "\n") continue
    n = i
    m++
    if (o == "\n") o = va[i]
    else o = o OFS va[i]
    va[i] = "\n"
  }
  if (o != "\n" && (m == ARGC-1 || find_index(na, n))) print km, o
}
BEGIN{
  split(a, na, ",")
  split(v, nv, ",")
  for (a=1; a<ARGC; a++) {
    cf[a] = -1
    kn[a] = 1
    va[a] = "\n"
  }
  kn[1] = n1
  kn[2] = n2
  km = "\n"
  c = ARGC - 1
  {
    while (c) {
      for (a=1; a<ARGC; a++) {
        cf[a] = -1
        while (ARGV[a] && cf[a] == -1) {
          if ((getline b < ARGV[a]) != 1) {
            if (ARGV[a] != "-") close(ARGV[a])
            delete ARGV[a]
            --c
            if (!c) printout()
            continue
          }
          split(b, f)
          k = f[kn[a]]
          v = "\n"
          for (i=1; i<=length(f); i++) {
            if (i == kn[a]) continue
            if (v == "\n") v = f[i]
            else v = v OFS f[i]
          }
          if (km == "\n") {
            km = k
            va[a] = v
            cf[last_a = a] = 0
          }
          else {
            if (km < k) {
              if (find_index(nv, last_a)) {
                if (va[last_a] != "\n") print km, va[last_a]
                va[last_a] = "\n"
              }
              if (find_index(nv, a) && last_a == a) print k, v
              if (!length(nv)) printout()
              km = k
              if (!length(nv)) va[a] = v
              cf[last_a = a] = 1
            }
            else if (km == k) {
              if (!length(nv)) va[a] = v
              cf[last_a = a] = 0
            }
            else {
              if (find_index(na, a) || find_index(nv, a)) print k, v
              va[a] = "\n"
              cf[a] = -1
            }
          }
        }
      }
    }
  }
}
' "$@"

このように、join と同じ awk スクリプトは以上のようになる。但し、正確には単一ファイル内の重複するキーにおける挙動には対応していない。

[sed] [Awk] [Perl] [Ruby] [Python]

`split [-l line_count] [-a suffix_length] [file[name]]`

この例、単一のファイルの内容を行数で複数のファイルに分割する awk スクリプトを示す。この例では split-l.sh という実行権のついたファイルに記述するものとする。

Awk は '>' で出力ファイルの指定をできるので、`split` コマンドにおける '-b' オプションによるバイナリファイルとしての分割でなければ、Awk でも実装はさほど難しくはない。

#!/bin/ksh
k=0
s=!0
d=0
pre=x
suf=
n=2
lc=1000
f=
while [ "$1" != "" ]; do
  case "$1" in
  -k)	k=!0	;;
  --verbose)	s=0	;;
  -d)	d=!0	;;
  -f)	shift; pre="$1"	;;
  -x)	shift; suf="$1"	;;
  -a)	shift; n="$1"	;;
  -l)	shift; lc="$1"	;;
  *)	[ "$f" = "" ] && f="$1" || break	;;
  esac
  shift
done
[ $# != 1 ] || { pre="$1"; shift; }
${AWK:-awk} -v k=$k -v s=$s -v d=$d -v pre="$pre" -v suf="$suf" -v n=$n -v lc=$lc -v f="$f" '
function outputfilename_digit(nf)
{
  return sprintf("%s%0*d%s", pre, n, nf, suf)
}
function outputfilename_lower(nf, b, d, r, q)
{
  b = d = 26
  while (int(nf/d)) d *= b
  d /= b
  xxxxxx = ""
  do {
    r = int(nf / d)
    nf -= d*r
    xxxxxx = xxxxxx sprintf("%c", r + 97)
    d = int(d / b)
  } while (d)
  while (length(xxxxxx) < n)
    xxxxxx = sprintf("%c", 0 + 97) xxxxxx
  return sprintf("%s%s%s", pre, xxxxxx, suf)
}
function outputfilename(nf)
{
  return d ? outputfilename_digit(nf) : outputfilename_lower(nf)
}
BEGIN{
  nr = 0
  of = outputfilename(nf=0)
  while ((getline < f) == 1) {
    ++nr
    print $0 > of
    if (nr % lc == 0) {
      if (!s) print l; l = 0
      close(of); of = outputfilename(++nf)
    }
    l += length + 1
  }
  if (!s) print l
}
' "$@"

このように、split と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`csplit [-k] [-s] [-f prefix] [-n number] file arg1 ...argn`

この例、単一のファイルの内容を行のパターンや行番号で複数のファイルに分割する awk スクリプトを示す。この例では csplit.ksh という実行権のついたファイルに記述するものとする。

Awk では、'fseek' 等のファイルハンドル関数がないので、負のオフセット(マッチした行の指定行数分戻った行で区切る)に対応できない。よって、零か正のオフセットのみに対応した awk スクリプトの実現例を紹介する。

#!/bin/ksh
k=0
s=0
pre=xx
suf=
n=2
f=-
while [ "$1" != "" ]; do
  case "$1" in
  -k)	k=!0	;;
  -s)	s=!0	;;
  -f)	shift; pre="$1"	;;
  -x)	shift; suf="$1"	;;
  -n)	shift; n="$1"	;;
  *)	[ "$f" = "-" ] && f="$1" || break	;;
  esac
  shift
done
#AWK="$HOME"/import/local/devel/misc/others/gawk-4.1.0/gawk
${AWK:-awk} -v k=$k -v s=$s -v pre="$pre" -v suf="$suf" -v n=$n -v f="$f" '
function outputfilename(nf)
{
  return sprintf("%s%0*d%s", pre, n, nf, suf)
}
function touch(f, ors)
{
  ors = ORS
  ORS = ""
  print "" > f
  ORS = ors
}
function nextsplit()
{
  previous_stl = stl
  ope = (a<ARGC) ? ARGV[a++] : ""
  rep = stl = ln = 0
  if (ope && match(ARGV[a], /^\{[[:digit:]]+\}$/)) {
    rep = substr(ARGV[a], RSTART+1, RLENGTH-2)
    ++a
  }
  if (match(ope, /^[\/\%].*[\/\%]([-+]?[[:digit:]]+)?$/)) {
    match(ope, /^[\/\%].*[\/\%]/)
    reg = substr(ope, RSTART+1, RLENGTH-2)
    if (match(ope, /[-+]?[[:digit:]]+$/))
      ofs = int(substr(ope, RSTART, RLENGTH))
    else
      ofs = 0
    c = oc = 0
    stl = match(ope, /^\%.*\%([-+]?[[:digit:]]+)?$/)
    ope = 1
  }
  else if (match(ope, /^[[:digit:]]+$/)) {
    ln = int(substr(ope, RSTART, RLENGTH))
    ope = 2
  }
  else {
    ope = 0
  }
}
BEGIN{
  nf = 0
  of = outputfilename(nf++); touch(of)
  a = 1
  nextsplit()
  while ((getline < f) == 1) {
    nr++
    if (ope == 1) {
      if (match($0, reg)) {
        if (!(ofs < 0)) {
          for (i=0; i<ofs; i++) {
            if (!stl) {
              print > of
              ol += length + 1
            }
            if ((eof=((getline < f) != 1))) break
          }
        }
        if (!stl) {
          if (!s) print ol
          close(of); of = outputfilename(nf++); touch(of)
          ol = 0
        }
        if (!(rep--)) nextsplit()
      }
    }
    else if (ope == 2) {
      if (nr % ln == 0) {
        if (!s) print ol
        close(of); of = outputfilename(nf++); touch(of)
        ol = 0
        if (!(rep--)) nextsplit()
      }
    }
    if (!eof && (previous_stl || !stl)) {
      print > of
      ol += length + 1
    }
  }
  if (!s) print ol
  close(f)
  close(of)
}' "$@"

このように、csplit と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`seq [-f format] [-s separator] [low [increment]] hi`

この例、GNU coreutils seq コマンドのように、単調増加する数列を生成する awk スクリプト seq.sh を示す。

#!/bin/sh
${AWK:-awk} '
BEGIN{
  s = "\n"
  beg = dlt = 1
  for (a=1; a<ARGC; a++) {
    if (ARGV[a] ~ /^-f$/ && a+1<ARGC) f = ARGV[++a]
    else if (ARGV[a] ~ /^-s$/ && a+1<ARGC) s = ARGV[++a]
    else break
  }
  if (ARGC - a == 1)
    end = ARGV[a+0]
  else if (ARGC - a == 2) {
    beg = ARGV[a+0]
    end = ARGV[a+1]
  }
  else if (ARGC - a == 3) {
    beg = ARGV[a+0]
    dlt = ARGV[a+1]
    end = ARGV[a+2]
  }
  else {
    exit 1
  }
  if (!f) {
    f = "%d"
    if (beg ~ /\./ || end ~ /\./) f = "%g"
  }
  rep = (end - beg)/dlt
  if (rep) printf(f, beg)
  for (i=1; i<=rep; i++) {
    printf(s f, dlt*i + beg)
  }
  if (rep) printf("\n")
}
' "$@"

このように、GNU coreutils seq と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`jot [-c|-n|-r] [-b word] [-w word] [-s string] [reps [begin [end [s]]]]`

この例、BSD jot コマンドのように、単調増減する数列、乱数列、定数列を生成する awk スクリプト jot.sh を示す。

#!/bin/sh
${AWK:-awk} -v ORS='' '
function ord(c, o){
  for (o=0; o<256; o++)
    if (sprintf("%c", o) == c)
      break
  return o
}
BEGIN{
  s = "\n"
  beg = dlt = 1
  rep = end = 0
  undef_end = !0
  for (a=1; a<ARGC; a++) {
    if (ARGV[a] ~ /^-w$/ && a+1<ARGC) w = ARGV[++a]
    else if (ARGV[a] ~ /^-c$/) f = "%c"
    else if (ARGV[a] ~ /^-b$/ && a+1<ARGC) b = ARGV[++a]
    else if (ARGV[a] ~ /^-s$/ && a+1<ARGC) s = ARGV[++a]
    else if (ARGV[a] ~ /^-r$/) { r = !0; undef_dlt = !0 }
    else break
  }
  if (ARGC - a == 1)
    rep = ARGV[a+0]-1
  else if (ARGC - a == 2) {
    rep = ARGV[a+0]-1
    beg = ARGV[a+1]
  }
  else if (ARGC - a == 3) {
    if (ARGV[a+0] == "-") { rep = 0 } else { rep = ARGV[a+0]-1 }
    if (ARGV[a+1] == "-") { beg = 0; undef_beg = !0 } else { beg = ARGV[a+1]; undef_beg = 0 }
    if (ARGV[a+2] == "-") { end = 0; undef_end = !0 } else { end = ARGV[a+2]; undef_end = 0 }
  }
  else if (ARGC - a == 4) {
    if (ARGV[a+0] == "-") { rep = 0 } else { rep = ARGV[a+0]-1 }
    if (ARGV[a+1] == "-") { beg = 0; undef_beg = !0 } else { beg = ARGV[a+1]; undef_beg = 0 }
    if (ARGV[a+2] == "-") { end = 0; undef_end = !0 } else { end = ARGV[a+2]; undef_end = 0 }
    if (ARGV[a+3] == "-") { dlt = 0; undef_dlt = !0 } else { dlt = ARGV[a+3]; undef_dlt = 0 }
  }
  else {
    exit 1
  }
  if (!f) {
    f = "%d"
    if (beg ~ /\./ || end ~ /\./) f = "%g"
  }
  if (w) {
    if (w ~ /%/)
      f = w
    else
      f = w f
  }
  if (beg ~ /^[^[:digit:]]$/) { beg = ord(beg) }
  if (end ~ /^[^[:digit:]]$/) { end = ord(end) }
  if (r) {
    srand(dlt)
    dlt = 0; undef_dlt = !0
  }  
  if (rep == -1) {}
  else if (rep) {
    if (undef_beg) { beg = end - dlt*rep; undef_beg = 0 }
    if (undef_end) { end = beg + dlt*rep; undef_end = 0 }
    dlt = (end - beg)/rep; undef_dlt = 0
  }
  else {
    rep = (end - beg)/dlt
  }
  if (b) {
    if (rep == -1) {
      while (!0) print b s
    }
    else {
      if (rep) print b
      for (i=1; i<=rep; i++) print s b
      if (rep) print "\n"
    }
  }
  else if (r) {
    dlt = (end - beg)
    if (rep == -1) {
      while (!0) printf(f s, rand()*dlt + beg)
    }
    else {
      if (rep) printf(f, rand()*dlt + beg)
      for (i=1; i<=rep; i++) printf(s f, rand()*dlt + beg)
      if (rep) print "\n"
    }
  }
  else {
    if (rep == -1) {
      while (!0) printf(f s, dlt*i++ + beg)
    }
    else {
      if (rep) printf(f, beg)
      for (i=1; i<=rep; i++) printf(s f, dlt*i + beg)
      if (rep) print "\n"
    }
  }
}
' "$@"

オリジナルと微妙に既定の書式の扱いが異なるが、ほぼ等価な実装となっている。

このように、BSD jot と同じ awk スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

Awk での実装に向いていないその他のコマンド

GNU Awk

プラットフォームによっては The One True Awk を修正した awk しかインストールされておらず、別途 GNU Awk (gawk) をインストール必要があり、その価値は十分にある。GNU Awk 独自の拡張についてはマニュアル POSIX/GNU に詳しい。以下に有用な機能を列挙しよう。

参考文献

  1. The One True Awk
  2. POSIX Awk
  3. GNU Awk
Written by Taiji Yamada <taiji@aihara.co.jp>