移動先 先頭, 前, 次, 末尾 セクション, 目次.

awkにおける配列

配列は要素と呼ばれる値のテーブルである。配列の要素はその添字と区 別される。 添字は数字でも、文字列でも良い。配列は変数と同じようにそれ ぞれ名前を持つが、 awkプログラム中で使っている変数と同じ名前を使って はいけない。

配列のIntroduction

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"がセット される。こうすることによって、元々の分割された添字の並びを取り出すことができ る。


移動先 先頭, 前, 次, 末尾 セクション, 目次.