awkにおける配列
配列は要素と呼ばれる値のテーブルであり、配列の要素は、その添え
字によって区別される。添え字は数値でも文字列でも良い。
awkは変数に使える名前と、配列や関数
(セクション ユーザー定義関数を参照)
につかえる名前をひとまとめに管理している。
そのため、変数と配列で同じ名前を持つものを同時に使用することはできない。
awkには一次元の配列があり、文字列や数値を関係するグループ毎にま
とめることができる。
awkの配列は名前を持たなければならない。配列の名前は変数名と同じ文法で
ある。つまり、変数名として正しい名前であれば配列名としても(文法的に)正しい。
しかし、プログラム中で同時に配列と変数で同じ名前を使うことはできない。
表面的にはawkでの配列は他のプログラミング言語の配列と似ているように見
える。しかし、基本的な違いがある。それは、awkでは配列の大きさを使う前
に特定する必要がなく、それに加えて、
連続した整数だけでなく、どんな数字や文字列でも配列
の添字として使うことができると言うことである。
他の多くの言語では配列と、その要素数や要素の型を宣言しなければならない。 そういった言語では宣言によって、要求された要素数に見あうだけの連続しているメ モリブロックの確保が引き起こされる。配列の添字は正の整数でなければならない。 例えば、配列中で0で添字付けされる要素はメモリブロックの先頭にある配列の一番 目の要素であり、1で添字付けされる要素は二番目の要素であり一番目の要素の次に 格納されている。以下は同様である。この方法では、最初に宣言された分だけの場所 しかないので、配列に新たに要素を付け加えることはできない (一部の言語では、`15 .. 27'のように配列の下限と上限を指定することができ る。しかし、そのような言語でも配列の大きさはあらかじめ宣言した大きさに 固定である)。
概念的には8, "foo", "", 30という四つの要素を持つ
配列は次のようになるだろう。
値は単に格納されているだけであり、添字はその大きさの順に並んでいるという暗黙
のルールがある。 8は0で添字付けされる要素の値である。なぜなら8
の前には一つの要素もないからである。
awkにおける配列は違っていて、連想的である。つまり配列が、添字と
それが添字付けする配列要素のペアの集合であるということである。
要素 4 値 30 要素 2 値 "foo" 要素 1 値 8 要素 3 値 ""
ペアはごちゃごちゃの順番で並んでいるが、それは添字による配列要素の順番付けが (連想配列では)意味のないことだからである。
連想配列の有利な所は、新しいペアを任意に作り出すことができるということである。
例えば先の配列に"number ten"という値を持つ10番目の要素を加えてみ
よう。結果はこうなる。
要素 10 値 "number ten" 要素 4 値 30 要素 2 値 "foo" 要素 1 値 8 要素 3 値 ""
今この配列は疎(一部の要素がない)である。要素として1 から4までと10を持っているが、 5,6,7,8,9 の要素は持っていない。
連想配列でもう一つの重要な点は、添字が正の整数に限られない。ということである。 どんな数値でもよいし、文字列であってもよい。例えば、次の例はある単語を英語か らフランス語に翻訳する配列である。
要素 "dog" 値 "chien" 要素 "cat" 値 "chat" 要素 "one" 値 "un" 要素 1 値 "un"
ここで数字の1が"one"のように使われていても翻訳できるように している。つまり、同じ配列で添え字に数字と文字列を使うことができる ということである (事実、配列の添え字は常に文字列として扱われる。これに関しての詳細は セクション Using Numbers to Subscript Arraysを参照)
IGNORECASE の値は、配列の添え字に関しては何の影響も
及ぼさない。あなたは配列要素を取り出すためには、その要素に
格納するのに使ったのと全く同じ文字列を使わなければならない。
awkがユーザーのために配列を作ったとき(例えば組み込み関数のsplit
を使ったとき)は配列の添字は1から始まる連続した整数である
(セクション Built-in Functions for String Manipulationを参照.)。
配列の使用において重要な点は、その要素の一つをどのように参照するかである。配 列の参照は次のような式で行う。
array[index]
ここでarrayは配列の名前であり、indexは、アクセスしたい要素を 配列中で添字付けする式である。
配列の参照によって得られる値はその配列要素のその時点での値である。例えば、
foo[4.3]は4.3で添字付けされた配列fooの要素を取り出す式である。
何も値の入っていない配列要素を参照すると、空文字列""がその値であるとして
取り出される。このことは何も値を代入していない配列要素や削除された配列要素にも
言える(セクション The delete Statementを参照)。
このような参照を行うと自動的に空文字列を値とする配列要素が自動的に作られる
(これはawk内部でのメモリの浪費を招き、時として不運な結果となる)。
配列中に、ある添字で指定される要素があるかどうかを次のようにして調べることが できる。
index in array
この式は(存在しない添字の要素をアクセスしようとして新たな配列要素を作ってし
まう)副作用を引き起こすことなしに、配列中に特定の添字が存在するかどうかをテス
トする。式の値はarray[index]が存在すれば1(真)、存在しなけ
れば0(偽)となる。
例えばfrequenciesという配列に"2"で添字付けされるものがあるかど
うかは次のように書けばよい。
if (2 in frequencies)
print "Subscript 2 is present."
これはfrequenciesという配列の中に"2"という値を持った要
素があるかどうかを調べるのではなく(そういったことを調べるには配列全体をスキャ
ンする以外にない)、また、frequencies["2"] という要素を作るようなこと
もしない。完全には同じではないが、次の例と同じ事である。
if (frequencies[2] != "")
print "Subscript 2 is present."
配列要素は左辺値を持ち、awkの普通の変数と同じ様に代入することができる。
array[subscript] = value
ここでarrayは配列の名前であり、subscriptという式は代入先の配列要 素を指定するインデックスである。 valueという式は配列要素に代入する値で ある。
次に挙げたプログラムは、行番号で始まる行のリストを入力として受け取り、行 番号順にそれらを出力するものである。行番号は入力時には順番になっておらず、 ごちゃごちゃになっている。このプログラムは、それらの行を行番号を添字とし て配列を使うことによって行のソートを行っている。そのため、行の出力は行番 号の順番に行われるようになっている。これは非常に単純なプログラムであり、 同じ番号が出てきたり、番号に抜けがあったり、行が番号で始まっていなかった りすると混乱してしまう。
{
if ($1 > max)
max = $1
arr[$1] = $0
}
END {
for (x = 1; x <= max; x++)
print arr[x]
}
最初のルールではそれまでに見つかった最大の行番号を記録している。
同時に、各行をarrという配列に行番号を添え字に使って記録している。
二番目のルールはすべての入力が読み込まれたあとで、 すべての行を出力するために実行される。
このプログラムに次のような入力を与えたとすると
5 I am the Five man 2 Who are you? The new number two! 4 . . . And four on the floor 1 Who is number one? 3 I three you.
出力はこうなる。
1 Who is number one? 2 Who are you? The new number two! 3 I three you. 4 . . . And four on the floor 5 I am the Five man
番号が繰り返して現れた場合、最後の行が(同じ番号の)他のものを 上書きする。
行番号に抜けが合った場合も、プログラム中のENDルールの
簡単な修正によって対処できる。
END {
for (x = 1; x <= max; x++)
if (x in arr)
print arr[x]
}
プログラムで配列を使っていると、配列の各要素に対して何等かの処理を行うような
ループを実行する必要にせまられる。そういった操作は、他の言語では配列はその添
字が正の整数に限定されるので簡単である。つまり、配列の最大の添字はその配列の
長さよりも短く、ゼロから添字を数え上げていけば全ての要素を参照できる。しかし
この技法はawkでは使えない。なぜなら配列の添字が整数に限定されず、文字
列の場合もあるからである。そのため、awkは配列のスキャンのために特別な
for文を備えている。
for (var in array) body
このループはbodyをプログラム中で、それまでにarrayで添字付けする のに使ったそれぞれの値に対して一度ずつ実行される。ここで変数varにはそ のインデックスがセットされる。
次のプログラムは、今説明したようなfor文を使用している。最初のルールで
は入力レコードをスキャンし、そこで(少なくとも一度)見付かった単語を添字とした
usedの要素に1を代入する。二番目のルールでusedの要素をスキャン
して入力から見付かった単語をすべて検索し、その単語が10文字よりも長ければ出力
し、最後にそういった単語の合計数も出力する。組み込み関数lengthの
詳しい説明は セクション Built-in Functions for String Manipulationを参照.
にある。
# 少なくとも一度は使われた単語に1をセットして記録する
{
for (i = 1; i <= NF; i++)
used[$i] = 1
}
# 十文字以上の長さの単語が幾つあるか調べる
END {
for (x in used)
if (length(x) > 10) {
++num_long_words
print x
}
print num_long_words, "個の単語が十文字を越えていました"
}
セクション 単語の使用数を調べるを参照, for a more detailed example of this type.
この文を使ってアクセスされる配列中の要素の並びは awk内部での配列要素
の配置よって決定され、それを制御したり変更することはできない。このことは、
bodyの中にある文で arrayに新しい要素を付け加えたときに問題を引き
起こす可能性がある。 forループが付け加えられた要素に到達するかどうか
も分からない。同様に、ループの中でvarを変更すると、何が起こるか分から
ない。最善の方法はそんなことをしないということである。
delete Statement
delete文を使って任意の要素を配列から削除することができる。
delete array[index]
要素を削除した後では、その要素に対する参照は行えない。そういった参照は何も値 を設定しておらず、一回も参照していない配列に対する参照と同じである。
次は配列の要素の削除の例である。
for (i in frequencies) delete frequencies[i]
この例はfrequenciesという配列中の要素を全て削除する。
要素を削除すると、for文を使って配列をスキャンしても(削除された)
要素は存在しないと報告され、in 演算子を使ってチェックしても 0
(偽、false)が返ってくる。
delete foo[4]
if (4 in foo)
print "これは決して出力されない"
配列の要素を削除することとnull(空文字列, "")を代入
することが違うということは重要である。
foo[4] = "" if (4 in foo) print "これはfoo[4]が空であっても出力される"
存在しない要素を削除してもエラーにならない。
delete文で添え字付けを行わないことによって、
ある配列のすべての要素を一つの文で削除することができる。
delete array
この機能はgawkの拡張機能であり、互換モード
(セクション コマンドラインオプションを参照)では
使うことができない。
この形式のdelete文を使うのは、ループを使って配列の要素の
削除を行うものより三倍以上効率的である。
次の例は、ポータブルなやり方であるが、配列をクリアするのにわかりやすい やり方ではない。
# これを教えてくれた Michael Brennan に感謝
split("", array)
split関数
(セクション Built-in Functions for String Manipulationを参照)
は目的の配列を最初にクリアする。
この呼び出しは空文字列を分割するということであり、したがって
分割すべきデータはなく、関数は単に配列をクリアして
呼び出し元に復帰するのである。
警告: 配列の削除はその型を変更することはない。 配列を削除した後、その名前を持つスカラー変数を使うことはできない。 以下に挙げる例は正しく動作しない:
a[1] = 3; delete a; a = 3
配列について重要なのは、配列の添え字は常に 文字列として扱われるということである。 配列の添え字に数字を使った場合、それは添え字付けに使われる前に 文字列に変換される (セクション Conversion of Strings and Numbersを参照)。
これは、組込み変数CONVFMTの値が、プログラム中での配列要素に対する
アクセスに影響を及ぼす可能性があると言うことである。例えば、
xyz = 12.153
data[xyz] = 1
CONVFMT = "%2.2f"
if (xyz in data)
printf "%s は dataにあります\n", xyz
else
printf "%s は dataにありません\n", xyz
これは`12.15 is not in data'を出力する。最初の文ではa と
bの両方に同じ数値データを代入している。 data[a]に対する代入は、
aが文字列"12.153" (CONVFMTで指定されるデフォルトの変換
ルール"%.6g"による)を用いて行われ、結果としてdata["12.153"]に
1が代入される。プログラムはその後でCONVFMTを変更している。 `(b
in data)'というテストはbの文字列への変換を行うがこのときの結果は
"12.15"となる。なぜならCONVFMTに設定された値によって小数点以下
二桁までしか許されていないからである。したがって"12.15"と
"12.153"は違うのでこのテストは失敗する。
変換ルールに従うと(セクション Conversion of Strings and Numbersを参照)、
整数値は常に整数を表わす文字列に変換され、CONVFMTの値は変換に対し
て影響を与えない。良くある例では、
for (i = 1; i <= maxsub; i++)
do something with array[i]
CONVFMTの値が意味を持たないのでこれはうまく動作する。
awkでの多くの事象のように、大抵の場合は期待通りに動作する。
しかし、これは実際のルールの正確な知識を得るのに有益であり、時として
微妙な効果をプログラムに対して及ぼすことがある。
ここで入力されたデータを逆順に出力したいと考えたとしよう。 これを行う合理的な方法は次のようなものになるだろう (サンプルデータも付いている)。
$ echo 'line 1
> line 2
> line 3' | awk '{ l[lines] = $0; ++lines }
> END {
> for (i = lines-1; i >= 0; --i)
> print l[i]
> }'
-| line 3
-| line 2
残念なことに、入力データの中で一番最初の行は出力されていない!
一見するとこのプログラムはちゃんと動作するようにみえる。linesは
初期化されておらず、初期化されていない変数は数値 0の値を持っている。だ
から、l[0]の値は出力されるはずだ。
結論をいうと、awkの配列で使う添え字は常に文字列として扱
う。また、初期化されていない変数が文字列として扱われたときの値は""
であって、0ではない。そのため、`line 1'は最終的にはl[""]に格
納されたのだ。
次の例はちゃんと働くようにしたプログラムである。
{ l[lines++] = $0 }
END {
for (i = lines - 1; i >= 0; --i)
print l[i]
}
ここで、`++'はlineを強制的に数値にする。
したがって、"古い値"(old value)を数値の 0にする。これは
配列の添え字として(文字列の)"0"に変換する。
今見たように、それが幾分一般的でないものであったとしても、
空文字列("") は配列の添え字として正当なものなのである(d.c.)。
コマンドラインで`--lint'が指定されていた場合
(セクション コマンドラインオプションを参照)、
gawkは配列の添え字に空文字列を使うと警告を出す。
多次元配列とは、配列要素の指定を複数の添字の並びによって行う配列である。例え
ば二次元の配列は二つの添字を必要とする。一般的な(awkも含めた大多数の
言語では) 二次元配列の要素に対する参照は grid[x,y]このよ
うに行う。 (gridは配列の名前)
awkでは、多次元配列をその添字を一つの文字列に連結して扱うということで
実現している。つまり、添字を文字列に変換し
(セクション Conversion of Strings and Numbersを参照)、
それをセパレータを間にはさんで一つに連結する。ということである。ここで一つに
まとめられた文字列が多次元配列の添字となり、この文字列は通常の一次元配列に対
する添字であるように扱われる。セパレータには組み込み変数SUBSEPに格納
されている値が使われる。
例えば、SUBSEPの値が"@"であるとき foo[5,12]="value"と
いう式を評価するとしよう。 5と12という数値は文字列に変換され、そしてその間に
`@'をはさんで連結する。その結果は"5@12"となる。したがって、
foo["5@12"]という要素に"value"がセットされるという結果になる
のである。
配列要素に格納してしまうと、awkはその添字が一つのインデックスなのか、
複数のインデックスの並びなのかの区別がつけられない。このため、
foo[5,12] と foo[5 SUBSEP 12] の二つの式は常に同じ値となる。
SUBSEPのデフォルトの値は"\034"という文字列である。このキャラク
タは印字可能キャラクタでなく、awkプログラムや入力データ中に現れること
はまずないであろうキャラクタである。
ありそうもないキャラクタを選択するということは、 SUBSEPの値によっては
区別を付けられないような添字文字列を作り出してしまうことがあるので、それを避
けるために有効である。たとえばSUBSEP が"@"だったとすると、
foo["a@b", "c"] と foo["a","b@c"]は両方とも結果的に
foo["a@b@c"]となるので区別することができない。 SUBSEP(のデ
フォルト)が"\034"であるので、このような混乱を引き起こすことはASCIIコ
ードの034というキャラクタを使わない限りは起きないのでめったに発生しない。
ある添字の並びが"多次元"配列中に存在するかどうかは一次元配列の時と同じ様に
inオペレータを使ってできる。具体的には、inオペレータの左辺に、
全体が括弧でくくられているカンマで各添字を区切った添字の並びを置けばよい。
(subscript1, subscript2, ...) in array
次に挙げる例では入力を二次元配列のフィールドであるかのように扱い、配列を90度 時計まわりに回転させてその結果を出力する。この例では全ての行において要素の数 が同じであると仮定している。
awk '{
if (max_nf < NF)
max_nf = NF
max_nr = NR
for (x = 1; x <= NF; x++)
vector[x, NR] = $x
}
END {
for (x = 1; x <= max_nf; x++) {
for (y = max_nr; y >= 1; --y)
printf("%s ", vector[x, y])
printf("\n")
}
}'
入力として次のデータを与える
1 2 3 4 5 6 2 3 4 5 6 1 3 4 5 6 1 2 4 5 6 1 2 3
結果は次のようになる。
4 3 2 1 5 4 3 2 6 5 4 3 1 6 5 4 2 1 6 5 3 2 1 6
"多次元"配列を走査するための特殊なfor文はない。というのは、多次元の
配列やその要素というものはなくて、配列に対する多次元的なアクセス方法
しかないからである。
しかし、プログラム中でその(検査を行いたい)配列を常に多次元配列として
アクセスをしているのであれば、for文と
(セクション Scanning All Elements of an Arrayを参照) 、
組み込み関数splitを組み合わせて使うことによって配列のスキャンを
行うことができる
(セクション Built-in Functions for String Manipulationを参照)。
具体的にはこうする。
for (combined in array) {
split(combined, separate, SUBSEP)
...
}
これは、連結された状態の配列の添字をcombinedに取りだし、
それをSUBSEP
の値がある場所を基準にして独立した添字に分割している。分割された添字は配列
separateの要素となる。
したがって、以前にarray[1,"foo"]に値を格納したとすると、実際
にはarrayの中では"1\034foo"で添字付けされる要素に対して行なわれ
ている。(SUBSEPのデフォルトの値が034というキャラクタコードであったこ
とを思い出そう) 遅かれ早かれfor文は繰り返しの中で"1\034foo"と
いう連結された添字を見つけ出す。そしてsplit関数が次のように呼ばれる。
split("1\034foo", separate, "\034")
この結果、separate[1]に1が、separate[2]に"foo"がセット
される。こうすることによって、元々の分割された添字の並びを取り出すことができ
る。