複雑なawkプログラムは、しばしばユーザー定義関数を使うことで
単純にすることが可能である。ユーザー定義関数は組込みの関数
(セクション Function Callsを参照)と同じように使うことができるが、
(ユーザー定義関数は)awkに対して関数が行なうことを
教えるために、ユーザーが定義しなければならない。
関数定義はawkプログラムのルールの中ならどこにでも記述できる。だから、
awkプログラムの一般的な形というのは、ルールとユーザー定義の並びと言え
る。awkでは、関数は使用する前に定義する必要はない
これは、awkがプログラム全体を、実行に移る前に全て
読み込むからである。
nameと言う名前の関数を定義するには次のようにする。
function name(parameter-list)
{
body-of-function
}
nameは定義する関数の名前である。関数名として許されるのは変数名と同じで
ある。文字、数字またはアンダースコアの並びで、ただし先頭に数字が来てはいけな
い。一つのawkプログラム中では、ある名前は
関数名、は配列名、変数名のどれか一つでのみ使用できる。
parameter-listは、カンマで区切られた関数の引数と (関数内の)ローカル変 数の名前のリストである。関数が呼ばれるときに引数の名前は、対応する呼びだし側 の変数の値を得る為に使われる。ローカル変数は空文字列に初期化される。 関数は同じ名前を持った二つのパラメータを持つことはできない
body-of-functionはawkの文で構成されている。ここは関数定義で一番
重要な部分である。なぜなら、関数がどのように動作するかということを記述する部
分であるからである。引数名は関数本体で(その関数に対する)引数を扱うためにあ
る。ローカル変数は関数本体で一時的な値を扱うための場所を確保する。
仮引数名は文法的にはローカル変数名となんら変わる所はない。その代り、実引数の 値が三つ与えられたとすると、parameter-listの最初の三つが仮引数となり、 後の残りはローカル変数となる。
(関数定義部での)引数の数は、その関数が呼ばれるときの引数とは必ずしも同じ数で はなく、(そういった関数では)parameter-listの名前は本当の引数と、ロー カル変数がリスト中にある、あるいは省略された引数はデフォルトの値(空文字列) がセットされている状態で関数呼び出しが行われていると考えられる。
普通、関数を記述しようとするときには、引数としていくつ。ローカル変数としてい くつ名前を使うか分かっている。 引数とローカル変数の間には余計にスペースを入れ るという取り決めになっている。そうすることによって自分以外の人が、記述 された関数をどのように使うのか理解できる。
関数本体を実行している間、引数とローカル変数は(プログラム中に存在していれば)
同じ名前の変数を関数本体から見えないようにする。見えなくなった変数は関数の中
でアクセスすることはできない。なぜならローカル変数によって、名前によってその
(隠された)変数にアクセスすることができなくなっているからである。
awkプログラム中の他の(隠されていない)変数は、関数の中でも通常通りに参
照したり変更したりすることができる
引数とローカル変数は関数の本体を実行している間だけ存在している。関数本体から 抜けると、隠されていた変数に再びアクセスできるようになる。
関数本体には関数を呼び出す式を含めることができる。 そこでは直接、あるいは他の関数を通して自分自身を 呼び出すことができる。このような場合、関数が再帰的であるという。
多くのawk処理系ではfunctionというキーワード
を funcと省略して使うことができる。しかし、POSIX では function
の使用しか規定していない。これはある程度実用的な implications である。
gawkは POSIX 互換モードで動作しているときには
(セクション コマンドラインオプションを参照)、
次のような文で関数定義を行うことができない。
func foo() { a = sqrt($1) ; print a }
ルールとして定義され、個々のレコードで関数`foo'の戻り値と共に変数func(の
内容)を連接し、その結果を基として対応するアクションを実行する。このことはお
そらく望んだところではない。 (awkはこの入力を文法的に正しいものとして
受け入れる。したがって、関数はawkプログラムの中で定義される前に使われ
ることになるだろう)
作成したawkプログラムの移植性を保証するためには、
関数を定義するときには常にfunctionキーワードを使用する。
次にユーザー定義関数の例を挙げる。数値を引数に取りそれを特定の書式で出力す
るmyprintという関数を呼びだす。
function myprint(num)
{
printf "%6.3g\n", num
}
myprintを使ったルールの例を挙げよう。
$3 > 0 { myprint($3) }
このプログラムは、先ほど定義した書式にしたがって、入力されたレコードの三番目 のフィールドが正の数であるときに(そのフィールドを) 出力する。というものであ る。だから入力として次のデータを与えると、
1.2 3.4 5.6 7.8 9.10 11.12 -13.14 15.16 17.18 19.20 21.22 23.24
プログラムの出力はこうなる。
5.6 21.2
次の関数はある配列のすべての要素を削除する。
function delarray(a, i)
{
for (i in a)
delete a[i]
}
配列を使っているときには、ある配列のすべての要素を削除して
新しいリストの要素で上書きする必要がしばしばある
(セクション The delete Statementを参照)。
それを行うループを、操作を行う場所全てで記述する代わりに、
配列をクリアする必要があるところでdelarrayを呼び出せば良い。
次の例は、再帰関数のサンプルである。これは入力として文字列を受け取り、そ の文字列を逆順にならべた文字列を返す。
function rev(str, start)
{
if (start == 0)
return ""
return (substr(str, start, 1) rev(str, start - 1))
}
この関数が`rev.awk'という名前のファイルにあったとすると、 このようにテストすることができる。
$ echo "Don't Panic!" |
> gawk --source '{ print rev($0, length($0)) }' -f rev.awk
-| !cinaP t'noD
次に挙げるのは組込み関数のstrftimeを使った例である
(strftimeの詳細については
セクション Functions for Dealing with Time Stampsを参照.)。
Cライブラリのctime関数はタイムスタンプを引数にとり、
それを良く知られている書式の文字列にして返す。
awkによるバージョンは次のようになる。
# ctime.awk
#
# awk による ctime(3) 関数
function ctime(ts, format)
{
format = "%a %b %d %H:%M:%S %Z %Y"
if (ts == 0)
ts = systime() # デフォルトとして現在時刻を使用
return strftime(format, ts)
}
関数呼び出しは関数の実行を引き起こす。関数呼び出しは一つの式で あり、式としての値は関数が返す値である。
関数呼び出しは関数名とそれに続く(括弧に囲まれた)引数である。引数として、式を
書くこともできる。そのような式は呼び出しが実行されたときに評価が行われてその
値が実引数として扱われる。例えば、次のfooという関数の呼び出しでは引数
が三つある (最初の一つは文字列連接)。
foo(x y, "lose", 4 * z)
警告: 空白(スペースまたはタブ)を関数名と引数リストを囲んでいる左括弧
との間に入れてはいけない。もし間違って空白を入れてしまった場合、awkは
変数と、(括弧に囲まれた) 式との連接であると認識する。しかし、書かれている名
前は変数名ではなく関数名であるから、エラーが報告される結果となる。
関数が呼ばれるときに、関数に対してその実引数の値のコピーが渡される。これを 値呼び出し(call by value)と言う。呼びだし側は実引数に式として変数を 使うこともできる。しかし、呼び出される関数はそれを関知せず、単に引数の値があ るとだけ認識する。例えば次のようなコードでは、
foo = "bar" z = myfunc(foo)
myfuncの引数として"変数 foo"を渡すのではなく、(fooの
値である)文字列"bar"を渡していると考えた方が良い。
関数myfunc が関数内のローカル変数の値を変更したとしてもそれは(対応し
ている引数も含めて)他の変数には一切影響しない。 myfunc が次のような
ものであったとしよう、
function myfunc(str)
{
print str
str = "zzz"
print str
}
最初の引数のwinを変更しても、呼びだし側にあるfooには 影
響しない。myfuncを呼び出すときのfooの役割はその値である
"bar"を計算したときに終わっている。 winがmyfuncの外にも
あったとしても、関数の外にある winの値を関数の中で変更することはでき
ない。これは、myfuncを実行している間は外にあるwinは隠されてい
るので見ることも変更することもできないからである。
しかし、関数の引数として配列を渡すときは、(配列は)コピーされない。配 列の場合、関数内で直接操作できるように配列そのものが渡される。このような呼び 出し形式は、通常参照呼び出し(call by reference)と呼ばれる。関数内部で( 引数として渡された)配列を変更すると、それは関数の外にも 影響する。 これは、自分が何をしているか、ということがきちんと解っていなければ とても危ないといえるだろう。 たとえば、
function changeit(array, ind, nvalue)
{
array[ind] = nvalue
}
BEGIN {
a[1] = 1; a[2] = 2; a[3] = 3
changeit(a, 2, "two")
printf "a[1] = %s, a[2] = %s, a[3] = %s\n",
a[1], a[2], a[3]
}
このプログラムは
`a[1] = 1, a[2] = two, a[3] = 3'を出力する。なぜなら changeit の
呼び出しで、aの二番目の要素に "two" を格納しているからである。
一部のawk処理系では定義されていない関数を呼び出すことが
許されており、そしてその問題は
実行時に未定義の関数が呼び出されたときにだけ
報告されるという動作をしていた。
例えば、
BEGIN {
if (0)
foo()
else
bar()
}
function bar() { ... }
# `foo' が定義されていないことに注意
`if'文が決して真にならないため、fooが定義されていない
という問題は表面にでない。通常は、
プログラムが未定義関数を呼び出すときに問題となる。
`--lint'(セクション コマンドラインオプションを参照)が
指定されているとき、gawkは未定義関数の呼び出しを
報告する。
一部のawk処理系では、next文
(セクション The next Statementを参照)
をユーザー定義関数の中で使うと実行時エラーとなる。
gawkにはこのような問題はない。
return Statement
関数本体中にreturn文を記述することができる。この文は関数を呼び出した
ところへの復帰を行う。
このとき次のようにして
その値を伴って(呼びだし元に)復帰することも可能である。
return [expression]
expressionは省略可能である。省略された場合の戻り値は定義されておらず、し たがって何が返るか分からない。
全ての関数定義の終わりには戻り値が書かれていないreturn文があるとみなさ
れる。制御が関数の終わりに達したときに、その関数は予期できない値を返す。
awkはそのような関数の戻り値を使ったとしても、警告はしないだろう。単に、
予期できない結果となるだけだろう。
値を返すことはしないが、何事かを行なうような関数を書きたいと
思うこともあるだろう。そのような関数はCでいうところのvoid関数
であり、Pascalでいうところの手続き(procedure)である。
したがって、何の値も返さないのが妥当であるかもしれない。
そのような関数の返す値を使うのならば、それにはリスクがあるという
ことを認識しておくのが良いだろう。
次の例は、与えられた配列の要素の中で最大の数値を 返すユーザー定義関数である。
function maxelt(vec, i, ret)
{
for (i in vec) {
if (ret == "" || vec[i] > ret)
ret = vec[i]
}
return ret
}
maxeltの呼び出しは、配列の名前をただ一つの引数として行う。
ローカル変数である iとretは引数としては扱われない。
maxeltに二つ、もしくは三つの引数を渡すことを止めることは
できないが、それをやった場合の結果は奇妙なものとなるうだろう。関
数引数リストのiの前にある余計なスペースは、iと
@code[ret}が引数としては扱われないということを示すものである。こ
れはあなたが自分のユーザー定義関数を定義するときにも従ったほうが
良い約束ごとである。
以下に挙げた例はこのmaxelt関数を使ったもので、配列をロー
ドしてmaxeltを呼び出す。その後で配列にあった中で最大の数
値を報告する。
awk '
function maxelt(vec, i, ret)
{
for (i in vec) {
if (ret == "" || vec[i] > ret)
ret = vec[i]
}
return ret
}
# 各レコードのすべてのフィールドをnumにロードする
{
for(i = 1; i <= NF; i++)
nums[NR, i] = $i
}
END {
print maxelt(nums)
}'
入力として以下のデータを与える。
1 5 23 8 16 44 3 5 2 8 26 256 291 1396 2962 100 -6 467 998 1101 99385 11 0 225
このプログラムは、配列にある最大値として99385
を報告する。