【緊急特集】最新の gawk 4.0.0 を追え!
Gawk 4.0.0 Now Available にあるように gawk 4.0.0 がリリースされ、gawk も大きな変化を迎えています。 そこで、gawk 4.0.0 の変更点をできるだけサンプルを使って、おさらいしておきます。
/dev/pid, /dev/ppid, /dev/pgrpid, /dev/user の廃止
/dev/pid, /dev/ppid, /dev/pgrpid, /dev/user はそれぞれ、プロセス ID、親プロセス ID、プロセスグループ ID、ユーザー ID を示し、今までは以下のようにしても取得することができました。 例えば、プロセス ID を取得する場合には以下のようにできます。
#! /usr/local/bin/gawk -f
# pid_old.awk
BEGIN {
while (getline < "/dev/pid" > 0) {
print $0;
}
close("/dev/pid");
}
既に、gawk 3.1.8 でもワーニングを出力しているので、気づかれている方も多いと思います。
$ gawk3 -f pid_old.awk gawk: pid.awk:8: warning: use `PROCINFO["pid"]' instead of `/dev/pid' 21299
新しい gawk 4.0.0 では
$ gawk4 -f pid_old.awk
のように何も出力されなくなります。
新しい gawk では以下のようにして、それぞれの値を求めるようになります。
#! /usr/local/bin/gawk -f
# pid.awk
BEGIN {
print PROCINFO["pid"];
print PROCINFO["ppid"];
print PROCINFO["pgrpid"];
print PROCINFO["uid"];
}
実行してみましょう。
$ gawk4 -f pid.awk 25520 27565 25520 15060
これは前の gawk 3.1.8 などでも有効です。
sub(), gsub() の振る舞いの変化
awk で '\' (バックスラッシュ) と '&' の振る舞いには毎回悩まされる人も多いと思います。 今回 POSIX 2008 に合わせて挙動が変更されています。
以下のようなプログラムを使います。
#! /usr/local/bin/gawk -f
# sub.awk
BEGIN {
str = "abc";
sub(/a/, "&", str);
print str;
str = "abc";
sub(/a/, "\&", str);
print str;
str = "abc";
sub(/a/, "\\\&", str);
print str;
str = "abc";
sub(/a/, "\\\\\&", str);
print str;
str = "abc";
sub(/a/, "\\\\", str);
print str;
str = "abc";
sub(/a/, "\\q", str);
print str;
}
まず、以前の gawk で実行してみます。
$ gawk3 -f sub.awk abc &bc \abc \&bc \\bc \qbc
そもそも予想できましたか? というところは置いておいて、5 番目の挙動に注目して gawk 4.0.0 で実行してみます。
$ gawk4 -f sub.awk abc &bc \abc \&bc \bc \qbc
5 番目の挙動が異なっていることが分かります。 これは gawk.info にも新たに加わっている POSIX に合わせた sub(), gsub() の挙動の変化です。
正規表現として '\s' と '\S' が使えます
最近の言語を使っていると awk の正規表現は貧弱に思えますが、新たに便利な正規表現が加わりました。
- '\s' はスペースの文字に該当し、'[[:space:]]' と同義です。
- '\S' はスペースでない文字に該当し、'[^[:space:]]' と同義です。
これは使うと便利そうですね。
split() に 4 番目の引数が使えます
split() はある正規表現で文字列を分割 (split) できるので便利ですが、その区切を何で行ったかを保持してくれる配列を格納できます。
#! /usr/local/bin/gawk -f
# split.awk
BEGIN {
str = "a b c:d::e";
num = split(str, arr, /[ :]+/, arr_sep);
# 分割された配列
for (i = 1; i <= num; i++) {
print "arr[" i "] = " arr[i];
}
# 分割した区切の配列
for (i = 1; i < num; i++) {
print "arr_sep[" i "] = \"" arr_sep[i] "\"";
}
}
実行してみます。
$ gawk4 -f split.awk arr[1] = a arr[2] = b arr[3] = c arr[4] = d arr[5] = e arr_sep[1] = " " arr_sep[2] = " " arr_sep[3] = ":" arr_sep[4] = "::"
このような形で、分割の区切を保持することができます。
文字列をバイトとして扱う
gawk はマルチバイトに対応するため、バイト単位ではなく文字単位で処理を行う方法を取ってきました。 これをオプションでバイト単位に変更できます。
以下のようなプログラムで確認してみます。
#! /usr/local/bin/gawk -f
# char.awk
BEGIN {
str = "123 abc あいう イロハ";
print length(str);
print substr(str, 11);
num = split(str, arr, "");
for (i = 1; i <= num; i++) {
print i " : " arr[i];
}
}
環境は UTF-8 とします。
$ gawk4 -f char.awk 15 う イロハ 1 : 1 2 : 2 3 : 3 4 : 5 : a 6 : b 7 : c 8 : 9 : あ 10 : い 11 : う 12 : 13 : イ 14 : ロ 15 : ハ
characters-as-bytes を使ってみます。
$ gawk4 --characters-as-bytes -f char.awk 27 <以下は泣き別れによる文字化け>
個人的には起動オプションでの変更ではなく、変数によって切り替えができると 嬉しいんですけどね。
サンドボックスモード
awk を使ったウイルスはないとしても、スクリプトの中に悪意を持ったコードが書かれていることを知らずに実行してしまい、ファイルが消去されてしまうケースはゼロではないでしょう。 具体的には system() や print, printf() を使ったリダイレクションや getline を使ったリダイレクションが該当します。
#! /usr/local/bin/gawk -f
# sandbox.awk
BEGIN {
print "abc" > "test.txt";
close("test.txt");
while (getline < "test.txt" > 0) {
print $0;
}
system("rm test.txt");
}
このようなコードがあったとします。
普通に実行すると 'abc' という文字列を test.txt というファイルに破壊的に書き込み、これを表示します。
$ gawk4 -f sandbox.awk abc
test.txt というファイルがあると破壊してしまいます。
これを sandbox モードで実行してみます。
$ gawk4 --sandbox -f sandbox.awk gawk: sandbox.awk:5: fatal: redirection not allowed in sandbox mode
このようにファイルを書き込むことができなくなります。
Indirect 関数のサポート
変数の値を関数名として受け取ってくれたらと思ったことはありませんか? 特にオブジェクト指向でない awk では、そうしたケースがあります。
良い例ではありませんが、じゃんけんに勝つプログラムを作ってみました。 じゃんけんで出した値 (手) によって呼び出される関数を変えています。 Indirect 関数では変数名の前に '@' を付けると変数名の関数が呼び出されます。
#! /usr/local/bin/gawk -f
# indirect.awk
BEGIN {
for (i = 1; i < 10; i++) {
if (i % 3 == 0) {
var = "gu";
printf("%s -> ", var);
@var();
}
if (i % 3 == 1) {
var = "choki";
printf("%s -> ", var);
@var();
}
if (i % 3 == 2) {
var = "pa";
printf("%s -> ", var);
@var();
}
}
}
function gu() {
print "pa";
}
function choki() {
print "gu";
}
function pa() {
print "choki";
}
実行してみます。
$ gawk4 -f indirect.awk choki -> gu pa -> choki gu -> pa choki -> gu pa -> choki gu -> pa choki -> gu pa -> choki gu -> pa
gawk.info の中には再帰的に使った Quick Sort の例が出ていますので、参考に見てください。
インターバル正規表現が可能に
今までの gawk ではオプションに '--re-interval' を付けないと、ある文字列が特定の回数繰り返される正規表現がうまく記述できませんでした。 例えば、数字が 5 回繰り返される場合には、
[0-9][0-9][0-9][0-9][0-9]
と記述する必要がありましたが、
[0-9]{5}
で簡潔に記述できるようになりました。
例えば、1 から 100000 までの間に 5 が 4 回連続する数値を抜き出すには以下のようにします。
#! /usr/local/bin/gawk -f
# interval.awk
BEGIN {
for (i = 1; i <= 100000; i++) {
if (i ~ /5{4}/) {
print i;
}
}
}
実行してみます。
$ gawk4 -f interval.awk 5555 15555 25555 35555 45555 55550 55551 55552 55553 55554 55555 55556 55557 55558 55559 65555 75555 85555 95555
桁数の区切や IP アドレスの簡易的な認識にも使えそうですね。
gen-po から gen-pot へ
単にオプションの変更です。
以下のようなファイルを用意します。
#! /usr/local/bin/gawk -f
# gen-pot.awk
BEGIN {
TEXTDOMAIN = "gettext";
bindtextdomain(".");
print _"Hello, World!";
}
以下のようにして pot ファイルを作成します。
$ gawk4 --gen-pot -f gettext.awk > gettext.pot
これを編集して、mo ファイルを生成し、適切なディレクトリに格納します。
$ cat gettext.pot #: gen-pot.awk:8 msgid "Hello, World!" msgstr "こんにちは世界!" $ msgfmt gettext.pot $ mkdir -p ja_JP/LC_MESSAGES $ cp messages.mo ja_JP/LC_MESSAGES/gettext.mo
ロケールを ja_JP にして実行します。
$ LC_ALL=ja_JP ../gawk -f gen-pot.awk こんにちは世界!
switch/case が使えます
gawk では configure オプションで --enable-switch を行うと switch/case が使えましたが、Linux などではディストリビューションごとに configure オプションが異なるため、使えたり使えなかったりしていました。 gawk 4.0.0 ではデフォルトで使えます。
3 の倍数の時だけ "aho" にしてみます。
#! /usr/local/bin/gawk -f
# switch.awk
BEGIN {
for (i = 1; i <= 10; i++) {
remainder = i % 3;
switch (remainder) {
case 0:
print "aho";
break;
default:
print i;
}
}
}
実行してみます。
$ gawk4 -f switch.awk 1 2 aho 4 5 aho 7 8 aho 10
特にオプションの処理などでは効果がありそうです。
BEGINFILE と ENDFILE が使えます
あまり BEGINFILE と ENDFILE の活用する場所が見つからないと思えるかもしれませんが、BEGINFILE はファイルからフィールド分割する前に処理されます。 つまり、単純にオープンだけを試みた状態ですから、ファイルの有無を調べるような場合に使えます。
#! /usr/local/bin/gawk -f
# beginfile.awk
BEGINFILE {
if (ERRNO) {
print "File not exist.";
}
exit;
}
{
print $0;
}
ファイルがない場合について実行してみます。
$ gawk4 -f beginfile.awk file_not_exist File not exist.
つまり、awk でファイルの有無を調べてエラーを出力するような場合に活用できます。
ディレクトリに対してワーニングとなる
入力にファイルではなくディレクトリを指定した場合には、エラーからワーニングになりました。 ただし、--posix, --traditional をし指定すれば、エラーになります。
$ gawk4 '1' directory gawk: warning: command line argument `directory' is a directory: skipped $ gawk4 --posix '1' directory gawk: fatal: cannot open file `ja_JP' for reading (Is a directory) $ gawk4 --traditional '1' ja_JP gawk: fatal: cannot open file `ja_JP' for reading (Is a directory) $ gawk3 '1' directory gawk: cmd. line:2: fatal: file `ja_JP' is a directory
新しい組込変数 FPAT
セパレーター (FS) ではなく、フィールドそのものの正規表現を FPAT で実現します。
具体的には開発時にも議論された CSV ファイルを扱う場合です。
CSV ファイル (ただし途中での改行は含まない) の FPAT は以下のようになります。
FPAT = "([^,]+)|(\"[^\"]+\")";
以下のような CSV (ダブルクォートで括られたもの) があるとします。
1,"Is this a pen?","No, it isn't." 2,"Is this a pencil?","Yes, it is."
gawk 4.0.0 では以下のように処理します。
#! /usr/local/bin/gawk -f
# fpat.awk
BEGIN {
FPAT = "([^,]+)|(\"[^\"]+\")";
}
{
for (i = 1; i <= NF; i++) {
print i " -> " $i;
}
}
実行してみましょう。
$ gawk4 -f fpat.awk sample.csv 1 -> 1 2 -> "Is this a pen?" 3 -> "No, it isn't." 1 -> 2 2 -> "Is this a pencil?" 3 -> "Yes, it is."
かなり CSV の取り扱いが楽になったのではないでしょうか?
FPAT に合わせた patsplit()
同様に FPAT のように必要なパターン部の正規表現を使った split() として patsplit() が用意されました。
以下のものは、通常の split() でも処理ができますが、patsplit() で処理してみます。
#! /usr/local/bin/gawk -f
# patsplit.awk
BEGIN {
time = "2011/07/01 12:31:56";
num = patsplit(time, arr, "[^/ :]+");
year = arr[1];
month = arr[2];
day = arr[3];
hour = arr[4];
min = arr[5];
sec = arr[6];
print year " 年 " month " 月 " day " 日 " hour " 時 " min " 分 " sec " 秒";
}
実行してみましょう。
$ gawk4 -f patsplit.awk 2011 年 07 月 01 日 12 時 31 分 56 秒
簡単そうで複雑な区切の場合に使えそうです。
シェバング (shebang) の文字数制限に優しい
シェバング (#!) には文字数制限の厳しい OS もあることから全ての長いオプション (GNU long option) には短い POSIX オプションが設定されました。
$ gawk4
Usage: gawk [POSIX or GNU style options] -f progfile [--] file ...
Usage: gawk [POSIX or GNU style options] [--] 'program' file ...
POSIX options: GNU long options: (standard)
-f progfile --file=progfile
-F fs --field-separator=fs
-v var=val --assign=var=val
Short options: GNU long options: (extensions)
-b --characters-as-bytes
-c --traditional
-C --copyright
-d[file] --dump-variables[=file]
-e 'program-text' --source='program-text'
-E file --exec=file
-g --gen-pot
-h --help
-L [fatal] --lint[=fatal]
-n --non-decimal-data
-N --use-lc-numeric
-O --optimize
-p[file] --profile[=file]
-P --posix
-r --re-interval
-S --sandbox
-t --lint-old
-V --version
To report bugs, see node `Bugs' in `gawk.info', which is
section `Reporting Problems and Bugs' in the printed version.
gawk is a pattern scanning and processing language.
By default it reads standard input and writes standard output.
Examples:
gawk '{ sum += $1 }; END { print sum }' file
gawk -F: '{ print $1 }' /etc/passwd
IPv6 対応
ついこの前、日本での IPv4 のアドレスが枯渇しましたが、これに合わせるように gawk も IPv6 に対応しました。
今まで、/inet となっていた部分を /inet6 に変更するそうですが、環境が IPv4 のためにテストできませんでした。 なお、今までの /inet はシステムの標準に合わせて切り替わるそうです。
ミスにも優しい
良く間違えるネタとして [[:space:]] と書かずに [:space:] と書いてしまうものがあります。
以前の gawk ではミスしても良しなに判断していたようですが、gawk 4.0.0 ではワーニングになります。
$ echo 'a b c' | gawk4 '/[:space:]/' gawk: cmd. line:1: warning: regexp component `[:space:]' should probably be `[[:space:]]' a b c $ echo 'a b c' | gawk4 '/[[:space:]]/' a b c $ echo 'a b c' | gawk3 '/[:space:]/' a b c $ echo 'a b c' | gawk3 '/[[:space:]]/' a b c
デバッガー dgawk の搭載
awk スクリプトも大規模になるとデバッガーが必要になってきます。 今までは printf() などで出力していたと思いますが、デバッガーが使えるようになり、便利になりました。
例えば以下のようなプログラムがあるとします。
#! /usr/local/bin/gawk -f
# dgawk.awk
BEGIN {
a = 12;
b = 5;
c = a / b;
div = abs(c);
print div;
}
function abs(num) {
if (num >= 0) {
return num;
} else {
return -num;
}
}
デバッガーから起動してみます。
$ dgawk -f dgawk.awk dgawk>
まず、ブレークポイント (b) を abs() にしてみます。
dgawk> b abs Breakpoint 1 set at file `dgawk.awk', line 16
ブレークポイントまで実行 (r) します。
dgawk> r
Starting program:
Stopping in BEGIN ...
Breakpoint 1, abs(num) at `dgawk.awk':16
16 if (num >= 0) {
引数となっている num に入っている値を表示 (p) します。
dgawk> p num num = 2.3999999999999999
終了 (q) します。
dgawk> q The program is running. Exit anyway (y/n)? y
使い方としてはこんな感じでしょうか。
詳細は gawk.info を参照してみてください。
break, continue がループの外では無効
今までは break, continue はループの外で使った場合、break や continue に達するとエラーになっていましたが、gawk 4.0.0 からは即時エラーになります。
#! /usr/local/bin/gawk -f
# break.awk
BEGIN {
for (i = 1; i <= 100; i++) {
print i;
if (i % 10 == 0) {
break;
}
}
break;
}
今までであれば、
$ gawk3 -f break.awk 1 2 3 4 5 6 7 8 9 10 gawk: break.awk:14: fatal: `break' outside a loop is not allowed
のように表示されますが、gawk 4.0.0 では即時エラーになります。
$ gawk4 -f break.awk gawk: break.awk:13: error: `break' is not allowed outside a loop or switch
多次元配列のサポート
今までちゃんとした多次元配列は awk にはありませんでした。 単に "\034" で連接した 1 次元配列でしたが、gawk 4.0.0 では多次元配列が加わります。
#! /usr/local/bin/gawk -f
# array.awk
BEGIN {
arr[1, 2] = 2;
arr[1][3] = 3;
print "arr[1][2] = " arr[1][2];
print "arr[1, 2] = " arr[1, 2];
print "arr[1][3] = " arr[1][3];
print "arr[1, 3] = " arr[1, 3];
}
実行してみると分かりますが、今までの配列と異なることが分かります。
$ gawk4 -f array.awk arr[1][2] = arr[1, 2] = 2 arr[1][3] = 3 arr[1, 3] =
また、split() で配列の配列を作成することもできますが、最初に空の配列を作成する必要があり、少しだけ注意が必要です。
#! /usr/local/bin/gawk -f
# array2.awk
BEGIN {
arr[1][1] == ""; # 必要
num = split("a b c d e", arr[1]);
for (i = 1; i <= num; i++) {
print arr[1][i];
}
}
この空の配列を作成しないとエラーになります。
$ gawk4 -f array2.awk a b c d e
文字範囲を示す正規表現が変わります
[a-z] は小文字と思っていたら、大文字にもマッチしたという経験はありませんか?
少し変な気がしますが、[a-z] は小文字だけにマッチするようになります。
#! /usr/local/bin/gawk -f
# range.awk
BEGIN {
str1 = "abcde";
str2 = "ABCDE";
if (str1 ~ /[a-z]/) {
print str1 " is lower case.";
}
if (str2 ~ /[a-z]/) {
print str2 " is lower case.";
}
}
今までの gawk では以下のようになっていました。
$ gawk3 -f range.awk abcde is lower case. ABCDE is lower case.
gawk 4.0.0 では以下のようになります。
$ gawk4 -f range.awk abcde is lower case.
リリースノートにも書かれていますが、変だという質問が多かったのでしょう。
PROCINFO"strftime" には strftime() のフォーマットを保持します
awk は変数や配列に値を破壊的に代入できますので、以下のようなこともできます。
#! /usr/local/bin/gawk -f
# starftime.awk
BEGIN {
print PROCINFO["strftime"] " : " strftime();
PROCINFO["strftime"] = "%Y/%m/%d";
print PROCINFO["strftime"] " : " strftime();
}
実行してみましょう。
$ gawk4 -f strftime.awk %a %b %e %H:%M:%S %Z %Y : Mon Jul 4 09:54:37 JST 2011 %Y/%m/%d : 2011/07/04
for in arr の順序を指定できる
gawk の隠しコマンド的なもので環境変数 WHINY_USERS を 1 にしておくと for in arr の順序をソートしてくれるというものがありましたが、gawk 4.0.0 では順序を指定することができます。
#! /usr/local/bin/gawk -f
# sort_in.awk
BEGIN {
arr[1] = 10;
arr[2] = 2;
arr[3] = "awk";
arr["abc"] = 1;
arr["def"] = 100;
arr["ghi"] = "test";
# インデックスの文字列昇順
print "=========== ind_str_asc";
PROCINFO["sorted_in"] = "@ind_str_asc";
for (i in arr) {
print i " : " arr[i];
}
# インデックスの数値昇順
print "=========== ind_num_asc";
PROCINFO["sorted_in"] = "@ind_num_asc";
for (i in arr) {
print i " : " arr[i];
}
# 値の型昇順
print "=========== val_type_asc";
PROCINFO["sorted_in"] = "@val_type_asc";
for (i in arr) {
print i " : " arr[i];
}
# 値の文字列昇順
print "=========== val_str_asc";
PROCINFO["sorted_in"] = "@val_str_asc";
for (i in arr) {
print i " : " arr[i];
}
# 値の数値昇順
print "=========== val_num_asc";
PROCINFO["sorted_in"] = "@val_num_asc";
for (i in arr) {
print i " : " arr[i];
}
}
実行してみます。
$ gawk4 -f sorted_in.awk =========== ind_str_asc 1 : 10 2 : 2 3 : awk abc : 1 def : 100 ghi : test =========== ind_num_asc abc : 1 def : 100 ghi : test 1 : 10 2 : 2 3 : awk =========== val_type_asc abc : 1 2 : 2 1 : 10 def : 100 3 : awk ghi : test =========== val_str_asc abc : 1 1 : 10 def : 100 2 : 2 3 : awk ghi : test =========== val_num_asc 3 : awk ghi : test abc : 1 2 : 2 1 : 10 def : 100
これは便利そうですね。 もちろん、逆順も指定できますが、gawk.info を参照してください。
さらに比較関数を独自に作って比較することもできるようです。
配列かどうかを調べられます
配列かどうかを調べるというよりも、gawk 4.0.0 で加わった多次元配列を走査するような場合に役立つでしょう。
#! /usr/local/bin/gawk -f
# isarray.awk
BEGIN {
arr[1] = 1;
arr[2][2] = 2;
arr[2, 3] = 3;
arr[3][4, 5] = 4;
arr[4][5][6] = 5;
walk_array(arr, "arr");
}
function walk_array(arr, name, i) {
PROCINFO["sorted_in"] = "@ind_num_asc";
for (i in arr) {
if (isarray(arr[i])) {
walk_array(arr[i], (name "[" i "]"))
} else {
printf("%s[%s] = %s\n", name, i, arr[i])
}
}
}
実行してみます。
$ gawk4 -f isarray.awk arr[1] = 1 arr[2][2] = 2 arr[23] = 3 arr[3][45] = 4 arr[4][5][6] = 5
asort(), asorti() はどのようにソートするかを決められる
asort() と asorti() は gawk で導入されているソート関数ですが、決まった動作しかできないため、自前でソート関数を作る人も多かったのではないでしょうか?
先ほどの PROCINFO で指定した for in arr の順序指定と同じものが使えます。
#! /use/local/bin/gawk -f
# asort.awk
BEGIN {
for (i = 1; i <= 10; i++) {
arr[i] = sin(i);
}
print "========== val_num_asc"
asort(arr, new_arr, "@val_num_asc");
for (i = 1; i <= 10; i++) {
print i " : " new_arr[i];
}
print "========== val_num_desc"
asort(arr, new_arr, "@val_num_desc");
for (i = 1; i <= 10; i++) {
print i " : " new_arr[i];
}
}
実行してみます。
$ gawk4 -f asort.awk ========== val_num_asc 1 : -0.958924 2 : -0.756802 3 : -0.544021 4 : -0.279415 5 : 0.14112 6 : 0.412118 7 : 0.656987 8 : 0.841471 9 : 0.909297 10 : 0.989358 ========== val_num_desc 1 : 0.989358 2 : 0.909297 3 : 0.841471 4 : 0.656987 5 : 0.412118 6 : 0.14112 7 : -0.279415 8 : -0.544021 9 : -0.756802 10 : -0.958924
最後に
駆け足でしたが、gawk 4.0.0 の新機能を見てきました。 いかがでしたでしょうか?
確実に便利になっているのですが、今までの awk との互換性を考えるとなかなか使えない機能が多い気がします。 機会があればイベントなどで gawk 4.0.0 の機能を紹介したいと思います。

