'^$'`'^$'`'^.+'`'^.+'`':' -f 1,6`':' -f 1,6 -s`'A-Za-z' 'N-ZA-Mn-za-m'`
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 について述べる。
パターンは 'BEGIN', 'END' のような最初のレコードの前と、最後のレコードの後を表す特殊パターンの他に、真偽値を得る「式」も書けて、アクションは、パターンもしくはパターンの範囲「式, 式」にマッチするときに処理される。これは sed の正規表現や行番号、もしくはそれらの範囲を表す「アドレス」を拡張したものとなっている。
例えば以下は、HTML の 'pre' タグを含むそれに囲まれた行を表示する Awk スクリプトである。
awk '/^<pre>/,/<\/pre>$/'
「パターンとアクション」のパターンを省略した場合、すべてのレコードでアクションが処理される。一方、アクションを省略した場合、'print' 文が省略されたものとして処理される。'print' は 'print $0' と等価である。
例えば以下は、ソースコードを HTML にペーストできるように「<」から「<」への変換等を行なう Awk スクリプトである。
awk '{ gsub(/&/, "\\&"); gsub(/</, "\\<" ); gsub(/>/, "\\>"); print }'
アクションは文のリストであり、文は代入式、制御構文、関数呼び出し、'{' 文のリスト '}' である。文の区切りは改行か ';' で区切る。シェルや C/C++ の文と似ているが '}' 直前の ';' が省略できることが異なる。
利用者は、Awk スクリプトでは型なしの変数をいくつも使用することが出来るが、すべては大域変数である。局所変数となり得るのは、ユーザ関数定義での引数のみであり、呼び出し側が使っていない余分な引数が局所変数としてよく使用される。
以下の制御構文が使用できる。
if () ... [else ...]while () ...do... while ()for (;;) ...continuebreakreturn ...nextexitfor (variable in array)nextfile (2012 POSIX)delete array (2012 POSIX)
'switch' は POSIX Awk ではサポートされないが GNU Awk では可能。'goto' は POSIX Awk でも GNU Awk でもサポートされない。
さらに以下の出力構文がある。
print - 自動改行 ORS、自動区切り OFS 出力。引数が省略された場合は、$0 が指定されたものとなる。printf - 通常の書式付き出力Awk の演算子は C/C++ のそれとおよそ同じだが、その型は基本的に、論理演算と浮動小数点の数値演算、そして文字列の正規表現検査のみである。よって、整数型にあるビット演算に関しては未サポートであり、GNU Awk では組み込み関数でサポートされる。特に注意すべき演算子を以下にあげる。
| Awk | C/C++ | 備考 |
expr1 expr2 | string(expr1) + string(expr2) | 文字列の連結 |
expr1 ^ expr2 | pow(expr1, expr2) | C/C++ の XOR ではなく、指数関数 |
lvalue ^= expr | lvalue = pow(lvalue, expr) | C/C++ の XOR 代入ではなく、指数関数の代入 |
expr1 ~ expr2 | regex_search(expr1, , regex(expr2, ...)) | C/C++ の ビット否定ではなく、正規表現のマッチ |
expr1 !~ expr2 | !regex_search(expr1, , regex(expr2, ...)) | C/C++ の ビット否定ではなく、正規表現のマッチの否定 |
POSIX Awk における特殊変数は以下の通りである。
ARGC - コマンドライン引数の数ARGV - コマンドライン引数の配列 [0,ARGC-1]ENVIRON - 環境変数の連想配列 ["名前"]FILENAME - 処理中のファイル名RS - 入力のレコードセパレータ。既定値は "\n"NR - 入力のレコード数FNR - 入力のレコード数(ファイル毎)FS - 入力のフィールドセパレータ。既定値は " "NF - 入力のフィールド数ORS - 出力のレコードセパレータ。既定値は "\n"OFS - 出力のフィールドセパレータ。既定値は " "RSTART - 'match' 組み込み関数におけるマッチした文字列の位置 [1,]RLENGTH - 'match' 組み込み関数におけるマッチした文字列の長さOFMT - 浮動少数点の出力書式。既定値は "%.6g"CONVFMT - 浮動少数点の変換書式。既定値は "%.6g"SUBSEP - 疑似的な多次元配列の添字の区切り。既定値は "\x1c,\034,28,^\,IS4(FS)"POSIX Awk におけるエスケープ文字は以下の通りである。
\" - ダブルクォートそのもの\/ - スラッシュそのもの\OOO - 8進数の文字コード\xHH - 16進数の文字コード(POSIX にはない)\\ - バックスラッシュそのもの\a - "^G,BEL,アラート(alert)"、nawk では未対応\b - "^H,BS,バックスペース(backspace)"\t - "^I,HT,水平タブ(horizontal tab)"\n - "^J,LF,改行(new-line)"\v - "^K,VT,垂直タブ(vertical tab)"、nawk では未対応\f - "^L,FF,改頁(form feed)"\r - "^M,CR,行頭復帰(carriage return)"\C - C は上記以外の任意の文字
例えば '(' そのものを表したい場合、'match(, "")' 等のダブルクォート内では、'\\(' としなければならない。さもなくば '\(' も '(' と同じくグループを表すことになってしまう。一方、'/' で括られた正規表現のパターン内では '\(' とすればよい。
atan2(y, x) - y/x についての逆正接関数cos(x) - x についての余弦関数sin(x) - x についての正弦関数exp(x) - x についての指数関数log(x) - x についての対数関数sqrt(x) - x の平方根int(x) - x の小数点以下切捨てrand() - [0, 1) の乱数srand([s]) - s で rand() のシード設定(省略した場合は自動設定)or(v1, v2) - v1 と v2 の論理和and(v1, v2) - v1 と v2 の論理積xor(v1, v2) - v1 と v2 の排他的論理和compl(v) - v のビット否定lshift(v, c) - v の c ビット左シフトrshift(v, c) - v の c ビット右シフト
Awk の配列は C/C++ のようなリニアなアドレッシングの配列ではなく、ハッシュ辞書いわゆる連想配列であるので、添字が 0 から始まろうが 1 から始まろうが関係ないし、添字が文字列でも構わない。さらには、添字をカンマで区切ることによって疑似的に多次元配列も扱える。そのとき添字に SUBSEP である '^\' を使うと意図した指示にはならないので注意。
tolower(s) - 文字列 s を小文字化した文字列を返す。s を省略できる実装があり、省略した場合 $0。toupper(s) - 文字列 s を大文字化した文字列を返す。s を省略できる実装があり、省略した場合 $0。length[([s])] - 文字列 s の長さを返す。s を省略できる伝統があり、省略した場合 $0。substr(s, i[, n]) - 文字列 s の部分文字列 [i,i+n] を返す。i は [1,]。n を省略した場合、または n>length(s)-i+1 の場合、n=length(s)-i+1 となる。index(s, t) - 文字列 s の部分文字列 t を検索して位置 [1,] を返す。見つからなかった場合は 0 を返す。split(s, a[, fs]) - フィールドセパレータ FS で文字列 s を分割して配列 a に格納、フィールド数を返す。fs を指定した場合、正規表現で分割がなされる。match(s, ere) - 文字列 s を正規表現 ere で検索してマッチした位置 [1,] を返す。見つからなかった場合は 0 を返す。さらに RSTART には返り値と同値が入り、RLENGTH にはマッチした文字列長かマッチしなかった場合 -1 が入る。sprintf(fmt, expr[,...]) - 書式付き出力された文字列を返す。書式は printf と同じ。sub(ere, repl[, in]) - sed の 's/ere/repl/' と同じ正規表現による1回のみ置換し置換回数を返す。in を省略した場合 $0。しかし、置換先にて、後方参照 '\1', '\2' … を使うことができず、マッチ全体をあらわす '&' のみである。GNU Awk ではそれが可能な gensub がある。gsub(ere, repl[, in]) - sed の 's/ere/repl/g' と同じ正規表現による繰返し置換し置換回数を返す。in を省略した場合 $0。しかし、置換先にて、後方参照 '\1', '\2' … を使うことができず、マッチ全体をあらわす '&' のみである。GNU Awk ではそれが可能な gensub がある。getline - 次のレコード入力を $0 に入れて、NF, NR, FNR を更新。成功: 1, EOF: 0, -1: 失敗を返す、以下同様。getline var - 次のレコード入力を var に入れて、NR, FNR を更新。getline < expression - パスを表す文字列 expression からのレコード入力を $0 に入れて、NF を更新。getline var < expression - パスを表す文字列 expression からのレコード入力を var に入れる。expression | getline - 出力構文 expression からのレコード入力を $0 に入れて、NF を更新。expression | getline var - 出力構文 expression からのレコード入力を var に入れる。close(expression) - パスを表す文字列 expression をファイルクローズfflush(expression) - パスを表す文字列 expression をフラッシュ(2012 POSIX)system(expression) - 外部コマンドの実行
このように、また、組み込み関数には、ファイル関係の関数が存在しない。よって、ファイルを操作するには system で外部コマンドを呼び出すか、Perl のような別の言語に移行するか、GNU Awk の拡張機能を組み込む必要がある。
代表的な Unix コマンドに相当する Awk スクリプトを以下にあげる。ちなみに、特に断らない限り以下すべては GNU Awk (gawk) でも動作する。
awk '{ print }'
このように、cat と同じ awk スクリプトはパターンを省略した以上のようになるが、「{ print $0 }」でもよいし、アクションを省略した「!0」でもよい。
awk 'NR == 1'
このように、head -n 1 と同じ awk スクリプトはアクション「{ print }」を省略した「NR == 1」となる。
awk 'END{ print }'
このように、tail -n 1 と同じ awk スクリプトは「END{ print }」となる。
awk '!(NR > 8)'
このように、head -n 8 と同じ awk スクリプトは以上のようになるが、「NR <= 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 スクリプトは以上のようになる。
awk 'END{ print NR }'
このように、wc -l と同じ awk スクリプトは「END{ print NR }」となる。
sed では大変面倒になる `wc -c` は awk だと算術演算があるので簡単である。
awk '{ l += length()+1 } END{ print l }'
ちなみに '+1' は省かれた改行コードの分である。
このように、wc -c と同じ awk スクリプトは以上のようになる。
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 スクリプトは以上のようになる。
'^$'`
awk '/^$/'
このように、基本正規表現の grep '^$' と同じ awk スクリプトは「/^$/」となるが、Awk では基本正規表現はサポートされないので、他のパターンでは拡張正規表現に書き直す必要がある。
'^$'`
awk '!/^$/'
このように、マッチの否定、基本正規表現の grep -v '^$' と同じ awk スクリプトは「!/^$/」となるが、Awk では基本正規表現はサポートされないので、他のパターンでは拡張正規表現に書き直す必要がある。
'^.+'`
awk '/^.+/'
このように、拡張正規表現の grep -E '^.+' と同じ awk スクリプトは「/^.+/」となる。
'^.+'`
awk '!/^.+/'
このように、拡張正規表現のマッチの否定、grep -E -v '^.+' と同じ awk スクリプトは「!/^.+/」となる。
':' -f 1,6`awk -F':'-v OFS=':''{ if (NF >= 6) print $1, $6; else print; }'
ここで "-F ':'" は "-v FS=':'" と等価である。
このように、区切り ':' のフィールド切り取り、cut -d ':' -f 1,6 但しフィールド数不足の行はそのまま出力と同じ awk スクリプトは以上のようになる。
':' -f 1,6 -s`awk -F':'-v OFS=':''NF >= 6{ print $1, $6 }'
ここで "-F ':'" は "-v FS=':'" と等価である。
このように、区切り ':' のフィールド切り取り、cut -d ':' -f 1,6 但しフィールド数不足の行は出力しないと同じ awk スクリプトは以上のようになる。
この例、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 スクリプトは以上のようになる。
awk '{ print; print > "filename.out" }'
このように、標準入力を標準出力とファイルに書き出し、tee と同じ awk スクリプトは以上のようになる。
'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 ではいささか面倒になる `cat -n` は awk だと極めて簡単である。
awk '{ printf("%6d\t%s" ORS, NR, $0) }'
このように、cat -n と同じ awk スクリプトは以上のようになる。
sed では大変面倒になる `cat -b` は awk だと極めて簡単である。以下の「;」は空の文であることに注意。
awk '/^$/;!/^$/{ i++; printf("%6d\t%s" ORS, i, $0) }'
このように、cat -b と同じ awk スクリプトは以上のようになる。
ところで、これらは GNU sed や 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 スクリプトは以上のようになる。
このタブを複数の空白に置換するコマンドを 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 スクリプトは以上のようになる。
この複数の空白をタブに置換するコマンドを 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 スクリプトは以上のようになる。
行毎に文字列を反転する BSD rev(1) コマンド。実用したことはないが、sed で実現するには秀逸な技法が必要であった。しかし、awk では極めて単純に、文字列を逆順に取り出して出力すればよい。
awk '
{
for (i=length(); i>0; i--)
printf("%c", substr($0, i, 1))
print ""
}'
このように、BSD rev(1) と同じ awk スクリプトは以上のようになる。
最終行から先頭行まで逆順で出力する 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 等の他の言語を採用すべきだ。
この例、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 スクリプトは以上のようになる。
この例、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 スクリプトは以上のようになる。
この例、ファイルの連続した印字可能な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 スクリプトは以上のようになる。
さて、ここまでは入力ファイル駆動型のプログラムばかりであったが、そうではなく自律型のプログラムを書くには、Awk では 'BEGIN' ブロックのみにコードを書けばよい。
この例、環境変数をすべて表示する awk スクリプトを示す。
awk 'BEGIN{ for (v in ENVIRON) print v "=" ENVIRON[v]}'
連想配列のキーをすべて取り出すには 'for (variable in array)' の構文を使う。
このように、printenv と同じ awk スクリプトは以上のようになる。
この例、プロセス停止まで永遠に "yes^J" を印字し続ける awk スクリプトを示す。但しこのコマンド、第一引数を指定した場合には "yes" の代わりにそれを印字する仕様となっている。この例では yes.sh という実行権のついたファイルに記述するものとする。
#!/bin/sh
${AWK:-awk} 'BEGIN{ $0 = ARGV[1] ? ARGV[1] : "yes"; while (!0) print }' "$@"
このように、yes と同じ awk スクリプトは以上のようになる。
この例、複数のファイルの内容を行毎に結合する 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 スクリプトは以上のようになる。
この例、複数のファイルの内容を行のキー毎に結合する 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 スクリプトは以上のようになる。
この例、複数のファイルの内容を行のキー毎に結合する 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 スクリプトは以上のようになる。但し、正確には単一ファイル内の重複するキーにおける挙動には対応していない。
この例、単一のファイルの内容を行数で複数のファイルに分割する 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 スクリプトは以上のようになる。
この例、単一のファイルの内容を行のパターンや行番号で複数のファイルに分割する 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 スクリプトは以上のようになる。
この例、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 スクリプトは以上のようになる。
この例、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 スクリプトは以上のようになる。
'fseek' 等がないと面倒'fseek' 等がないと非効率'fseek' 等がないと非効率
プラットフォームによっては The One True Awk を修正した awk しかインストールされておらず、別途 GNU Awk (gawk) をインストール必要があり、その価値は十分にある。GNU Awk 独自の拡張についてはマニュアル POSIX/GNU に詳しい。以下に有用な機能を列挙しよう。
'--traditional' オプション - GNU Awk 拡張を抑止し、オリジナルの awk のように動作させる。'--posix' オプション - GNU Awk 拡張を抑止し、POSIX Awk のように動作させる。gawk '{ print gensub(/<tt>([^<]*)<\/tt>/, "<code>\\1</code>", "g") }' のような、置換先における後方参照が可能。'asort' 他 - ソート関係のサポート'-M' オプション - GNU MPFR, MP ライブラリを用いた任意精度の浮動少数点および整数演算(但し、ビルド時に対応させておく必要がある)。'@load "filefuncs"' - ファイル関係の関数の組み込みに代表される拡張機能の読み込み。