awk
における配列
配列は要素と呼ばれる値のテーブルである。配列の要素はその添字と区
別される。 添字は数字でも、文字列でも良い。配列は変数と同じようにそれ
ぞれ名前を持つが、 awk
プログラム中で使っている変数と同じ名前を使って
はいけない。
awk
には一次元の配列があり、文字列や数値を関係するグループ毎にま
とめることができる。
awk
の配列は名前を持たなければならない。配列の名前は変数名と同じ文法で
ある。つまり、変数名として正しい名前であれば配列名としても(文法的に)正しい。
しかし、プログラム中で同時に配列と変数で同じ名前を使うことはできない。
表面的にはawk
での配列は他のプログラミング言語の配列と似ているように見
える。しかし、基本的な違いがある。それは、awk
では配列の大きさを使う前
に特定する必要がなく、それに加えてどんな数字でも、文字列でも配列の添字として
使うことができると言うことである。
他の多くの言語では配列と、その要素数や要素の型を宣言しなければならない。 そういった言語では宣言によって、要求された要素数に見あうだけの連続しているメ モリブロックの確保が引き起こされる。配列の添字は正の整数でなければならない。 例えば、配列中で0で添字付けされる要素はメモリブロックの先頭にある配列の一番 目の要素であり、1で添字付けされる要素は二番目の要素であり一番目の要素の次に 格納されている。以下は同様である。この方法では、最初に宣言された分だけの場所 しかないので、配列に新たに要素を付け加えることはできない。
概念的には8
, "foo"
, ""
, 30
の四つの要素を持つ配列
は次のような感じだろう。
+---------+---------+--------+---------+ | 8 | "foo" | "" | 30 | 値 +---------+---------+--------+---------+ 0 1 2 3 添え字
値は単に格納されているだけであり、添字はその大きさの順に並んでいるという暗黙
のルールがある。 8
は0で添字付けされる要素の値である。なぜなら8
の前には一つの要素もないからである。
awk
における配列は違っていて、連想的である。つまり配列が、添字と
それが添字付けする配列要素のペアの集合であるということである。
要素 4 値 30 要素 2 値 "foo" 要素 1 値 8 要素 3 値 ""
ペアはごちゃごちゃの順番で並んでいるが、それは添字による配列要素の順番付けが (連想配列では)意味のないことだからである。
連想配列の有利な所は、新しいペアを任意に作り出すことができるということである。
例えば先の配列に"number ten"
という値を持つ十番目の要素を加えてみ
よう。結果はこうなる。
要素 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"
つまり、一つの配列の添字として、数字と文字列を同時に使うことができるというこ とである。
awk
がユーザーのために配列を作ったとき(例えば組み込み関数のsplit
を使った時)は配列の添字は1から始まる連続した整数である。
(セクション 組込みの文字列操作関数を参照.)
配列の使用において重要な点は、その要素の一つをどのように参照するかである。配 列の参照は次のような式で行う。
array[index]
ここでarrayは配列の名前であり、indexは、アクセスしたい要素を 配列中で添字付けする式である。
配列の参照によって得られる値はその配列要素のその時点での値である。例えば、
foo[4.3]
は4.3で添字付けされた配列foo
の要素を取り出す式である。
何も値の入っていない配列要素を参照すると、空文字列""
がその値であるとして
取り出される。このことは何も値を代入していない配列要素や削除された配列要素にも
言える(セクション delete
文を参照)。
このような参照を行うと自動的に空文字列を値とする配列要素が自動的に作られる
(これは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
の要素をスキャン
して入力から見付かった単語をすべて検索し、その単語が十文字よりも長ければ出力
する。最後に、そういった単語の合計数も出力する。組み込み関数length
の
詳しい説明は セクション 組込み関数を参照.にある。
# 最低一回は使われている単語に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, "words longer than 10 characters" }
詳しくはセクション サンプルプログラムを参照.。
この文を使ってアクセスされる配列中の要素の並びは awk
内部での配列要素
の配置よって決定され、それを制御したり変更することはできない。このことは、
bodyの中にある文で arrayに新しい要素を付け加えたときに問題を引き
起こす可能性がある。 for
ループが付け加えられた要素に到達するかどうか
も分からない。同様に、ループの中でvarを変更すると、何が起こるか分から
ない。最善の方法はそんなことをしないということである。
delete
文
delete
文を使って任意の要素を配列から削除することができる。
delete array[index]
要素を削除した後では、その要素に対する参照は行えない。そういった参照は何も値 を設定しておらず、一回も参照していない配列に対する参照と同じである。
次は配列の要素の削除の例である。
for (i in frequencies) delete frequencies[i]
この例はfrequencies
という配列中の要素を全て削除する。
要素を削除すると、for
文を使って配列をスキャンしても(削除された)要素は
存在しないと報告され、in
オペレータを使ってチェックしても 0が返ってく
る。
delete foo[4] if (4 in foo) print "これは決して出力されない"
存在しない要素を削除してもエラーにならない。
配列の扱いで重要なことは配列の添字が常に文字列であるということである。 もし、数字を添字に使ったとすると、それは実際に添字付けに使われる前に文字列に 変換される (セクション 文字列と数値の変換を参照).
これは、CONVFMT
の値が、プログラム中での配列要素に対するアクセスに影響
を及ぼす可能性があると言うことである。例えば、
a = b = 12.153 data[a] = 1 CONVFMT = "%2.2f" if (b in data) printf "%s is in data", b else printf "%s is not in data", b
これは`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"
は違うのでこのテストは失敗する。
変換ルールに従うと
(セクション 文字列と数値の変換を参照)、
整数値は常に整数を表わす文字列に変換され、CONVFMT
の
値は変換に対して影響を与えない。良くある例では、
for (i = 1; i <= maxsub; i++) do something with array[i]
CONVFMT
の値が意味を持たないのでこれはうまく動作する。
awk
での多くの事象のように、
大概の場合期待通りに動作する。しかし、これは実際のルールの
正確な知識を得るのに有益であり、時として微妙な効果をプログラムに
対して及ぼすことがある。
多次元配列とは、配列要素の指定を複数の添字の並びによって行う配列である。例え
ば二次元の配列は二つの添字を必要とする。一般的な(awk
も含めた大多数の
言語では) 二次元配列の要素に対する参照は grid[x,y]
このよ
うに行う。 (grid
は配列の名前)
awk
では、多次元配列をその添字を一つの文字列に連結して扱うということで
実現している。つまり、添字を文字列に変換し
(セクション 文字列と数値の変換を参照) 、
それをセパレータを間にはさんで一つに連結する。ということである。ここで一つに
まとめられた文字列が多次元配列の添字となり、この文字列は通常の一次元配列に対
する添字であるように扱われる。セパレータには組み込み変数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
文と
(セクション 配列の全要素の走査を参照) 、
組み込み関数split
を組み合わせて使うことによって配列のスキャンを
行うことができる
(セクション 組込みの文字列操作関数を参照)。
具体的にはこうする。
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"
がセット
される。こうすることによって、元々の分割された添字の並びを取り出すことができ
る。