awk
の関数ライブラリ
この章では、便利なawk
関数のライブラリを紹介する。
これら関数を使ったサンプルプログラムはこの後
(セクション 実用的な awk
プログラムを参照)
にある。
セクション Texinfoのソースファイルからプログラムを取り出すを参照,
では、このマニュアルのTexinfoのソースファイルから、ライブラリ関数の
サンプルとプログラムを取り出して使うためのプログラムを提供している
(これはgawk
の配布キットの一部として既に済んでいる)。
あなたが便利な、汎用目的のawk
関数を作成し、それを
このマニュアルの一部として寄付したいのなら、著者に連絡して欲しい。
そのための情報はセクション 問題やバグの報告を参照にある。
コードを送るだけではなくて、あなたのコードをパブリックドメインにしたり、
GPL(セクション GNU GENERAL PUBLIC LICENSEを参照)の元で公表したり、あるい
は著作権を Free Software Foundationに譲渡することが求められる。
gawk
-specific Features
この章とセクション 実用的な awk
プログラムを参照にあるプロ
グラムは、gawk
特有の機能を自由に使っている。このセクションでは、
他のawk
処理系のためにこれらのプログラムを修正する方法について簡単
に説明する。
エラーメッセージを`/dev/stderr'に送っていないかチェックする。
もしあなたが使っているシステムに`/dev/stderr'がなかったり、
gawk
が使えない場合には、`> "/dev/stderr"'の代わりに
`| "cat 1>&2"' を使う。
幾つかのプログラムではnextfile
(セクション The nextfile
Statementを参照)
を入力ファイルの残りをスキップするために使っている。
セクション Implementing nextfile
as a Functionを参照
に、同じことを行う関数をどのように記述するかの説明がある。
さらに一部のプログラムでは、入力の大文字と小文字の違いを無視していて、
それをIGNORECASE
に1を設定することで行っている。
同様の効果を、プログラムの先頭に以下に挙げるようなルールを 追加することによって得ることができる。
# 大小文字の違いを無視する { $0 = tolower($0) }
これにより、すべての正規表現の検査と、文字列の比較が 小文字を使ってのみ行われる。
nextfile
as a Function
セクション The nextfile
Statementを参照.で
説明されているnextfile
文は、gawk
特有の拡張であり、
これは他のawk
処理系では使うことができない。
このセクションでは、gawk
が使えない場合にnextfile
関数を
シミュレートしたものを二つ例示する。
次の例は、nextfile
を記述しようとする最初の試みである。
# nextfile -- カレントファイルの残りレコードをスキップする。 # これはawkの "main" programの前に置く必要がある。 function nextfile() { _abandon_ = FILENAME; next } _abandon_ == FILENAME { next }
このファイルはメインプログラムの前に置くようにする必要がある。なぜなら、
このルールは一番最初に実行されなければならないからである。このルールはカ
レントデータファイルの名前(FILENAME
という変数で参照できる)を@code
{_abandon_}という名前の変数の内容と比較していて、ファイル名とマッチした
ときにこのルールのアクションパートのnext
文を実行し、次のレコード
へ移る(変数名に`_'を使うのは規約であるセクション Naming Library Function Global Variablesを参照.で詳しく説明されている)。
nexta
文の使用は、カレントデータファイルからすべてのレコードを読む
ようなループの効率を挙げるのに効果的である。最終的にファイルの終端に達し、
そして新しいファイルがオープンされ、FILENAME
の値が変更される。こ
れが一回おこると、_abandon_
とFILENAME
の比較は失敗し、"本
当の"プログラムの最初のルールから実行が継続される。
nextfile
関数それ自体は、単に_abandon_
という変数に
値をセットして、next
文を実行してループを継続している。
(19)
このバージョンでは、ちょっとした問題がある。 コマンドライン上で同じファイルが二度連続して指定されていたら どうなるだろうか?
この場合、このプログラムは最初のファイルは正しくスキップするが、
二番目のファイルは、スキップすべきでないにもかかわらず
最初のファイルの最後からそのままスキップされ続けてしまう。
次にこの点を改善したnextfile
の二番目のバージョンを例に挙げる。
# nextfile -- 現在処理しているファイルの残りレコードをスキップする # this should be read in before the "main" awk program # correctly handle successive occurrences of the same file # Arnold Robbins, arnold@gnu.org, Public Domain # May, 1993 # このライブラリは“本当の”awkプログラムよりも前に読まれるようにすること function nextfile() { _abandon_ = FILENAME; next } _abandon_ == FILENAME { if (FNR == 1) _abandon_ = "" else next }
nextfile
関数には変更がなく、_abandon_
にカレント
ファイル名をセットし、next
文を実行するだけである。next
文は
次のレコードを読み込み、FNR
をインクリメントし、同様にFNR
は
最低でも2という値であることが保証される。しかしながら、nextfile
が
ファイルの最後のレコードで呼び出された場合、awk
はカレントデータフ
ァイルをクローズし、次のファイルへ処理を移す。これによって、FILENAME
は新しいファイルの名前がセットされ、FNR
は1にリセットされる。
次のファイルがその直前のファイルと同じ名前であった場合、_abandon_
は(ファイルが変わったのに)まだFILENAME
と等しいままである。
この場合は、_abandon_
は空文字列にリセットされ、この後のこのル
ールの実行は(次にnextfile
が呼ばれるまで)失敗するようになる。
FNR
が1でないのなら、まだ元のデータファイルを処理しているのであり、
プログラムはファイルのスキップをするためにnext
文を実行する。
この点に関する重大な疑問とは、
"nextfile
の機能はライブラリファイルとして提供することも
できる。なぜ、gawk
の組込み機能としたのか?"ということである。
これは重要な質問である。
機能の追加はプログラムを大きく遅くして、保守を難しくしてしまい
かねないからだ。
この質問に対する回答は、nextfile
をgawk
に組み込んでしまうことで
効率が上がるから、ということである。もし、nextfile
が大きなデータファイルの先
頭で実行されたとすると、awk
はファイル全体を読み続けなければならず、
(必要がないのに)ファイルをレコードに分割して、それをスキップする。組込み
のnextfile
は、単純にファイルを即座にクローズ して次のファイルの
処理を開始できるので時間を大幅に節約できる。これは特にawk
プログ
ラムは一般的に入出力に律速されるので、重要なことである(つまり、処理時間
の大部分が計算ではなく、入力や出力に費やされるということ)。
大きなプログラムを作るとき、ある条件が真であるかどうかを知ることができる
と便利である。ある処理を行う前に、前提としている条件を文にして置く。この
ような文は"表明"(assertion)として知られている。C言語では、これは
<assert.h>
というヘッダファイルで提供されており、assrt
マク
ロをプログラマーが表明したいことに一致するようにして使う。表明が失敗した
場合、assert
マクロは、真であるべきもなのに偽であった条件を説明する
メッセージを出力するように変換される。Cでは、assert
は次のように使う。
#include <assert.h> int myfunc(int a, double b) { assert(a <= 5 && b >= 17); ... }
表明が失敗すると、このプログラムは以下のようなメッセージを出力する。
prog.c:5: assertion failed: a <= 5 && b >= 17
ANSI Cでは、条件を文字列に変更して検査メッセージを出力することを
可能にしている。これはawk
では不可能であり、そのため
assert
関数は検査する条件を文字列にしたものも
同時に要求する。
# assert -- 条件が真であることを表明し、そうでない場合にはexitする。 # Arnold Robbins, arnold@gnu.org, Public Domain # May, 1993 function assert(condition, string) { if (! condition) { printf("%s:%d: assertion failed: %s\n", FILENAME, FNR, string) > "/dev/stderr" _assert_exit = 1 exit 1 } } END { if (_assert_exit) exit 1 }
assrt
関数はcondition
パラメータを検査する。もしこれが偽であ
れば、string
パラメータを失敗した条件を説明するのに使用して、メッ
セージを標準出力に出力する。そして、_assrt_exit_
という変数に1をセ
ットしてexit
文を実行する。exit
文によってEND
ルールへ
ジャンプする。END
ルールでは、_asert_exit_
が真であれば、即
座に終了する。
END
ルールがあるのと、そこでテストをする目的はEND
ルールが他
にあるとき(ユーザーが作成したスクリプトにもとからあったなど)のためである。
表明が失敗していなければ、END
ルールが実行されたときに
_assrt_exit_
は偽のままであり、プログラム中の残りのEND
ルールは
実行されることになる。これらの動作をきちんと行うために、`assert.awk'は
awk
が読み込む最初のソースファイルでなければならない。
プログラムでこの関数を使うには次のようにする。
function myfunc(a, b) { assert(a <= 5 && b >= 17, "a <= 5 && b >= 17") ... }
表明が失敗すると、次のようなメッセージを目にすることになる。
mydata:1357: assertion failed: a <= 5 && b >= 17
このバージョンのassert
には問題があり、標準のawk
では
うまく働かないことがありえる。通常、プログラムがBEGIN
ルールだけであった場合には、入力ファイ
ルや標準入力からの読み込みは行われない。しかし(assertを使ったことにより)、
今やEND
ルールがプログラムにあり、このためawk
は入力データを
ファイル、あるいは標準入力から読み込もうとするのである
(セクション Startup and Cleanup Actionsを参照)。
そして、プログラムは一見ハングアップしたかのようになり、
入力を待つ。
printf
やsprintf
(セクション Using printf
Statements for Fancier Printingを参照)
が丸めを行う方法は、しばしば使用するシステムのCライブラリにある
sprintf
に依存する。多くのマシンでは、spritnf
の
丸めは`.5'を常に切り上げるのではなく、正反対の例外がある
"unbiased"なものである。
"unbiased"な丸めでは、`.5'は偶数方向へ丸めが行なわれる。
このため、1.5は2へ丸められるのに対して4.5は4へ丸められることとなる。
あなたが丸めを行うような書式指定(".0f"
など)を使った
場合には、システムがどのように丸めをしたのかを確かめておく
べきである。以下に示す関数は、伝統的な丸めを行う関数である。
これはあなたの使うawkのprintf
がunbiasedどな
丸めをするような場合に便利でしょう。
# round -- do normal rounding # # Arnold Robbins, arnold@gnu.org, August, 1996 # Public Domain function round(x, ival, aval, fraction) { ival = int(x) # 整数部を取り出す # 小数部があれば処理する if (ival == x) # 小数部がない return x if (x < 0) { aval = -x # 絶対値 ival = int(aval) fraction = aval - ival if (fraction >= .5) return int(x) - 1 # -2.5 --> -3 else return int(x) # -2.3 --> -2 } else { fraction = x - ival if (fraction >= .5) return ival + 1 else return ival } } # test harness { print $0, round($0) }
ある商用のawk
処理系では、
キャラクタを引数にとり、そのキャラクタを表わすキャラクタコードの値を
返す組込み関数ord
を提供している。
二文字以上の長さの文字列がord
に渡された場合には
その最初のキャラクタが対象となる。
その反対の関数が数値を引数にとり、それが表わすキャラクタを返す
chr
(Pascalの同名の関数から来ている)である。
この二つの関数はawk
で立派に記述できる。
これらの関数をawk
インタープリターの組込みにすべき
客観的な理由はない。
# ord.awk -- do ord and chr # # Global identifiers: # _ord_: キャラクタで添え字付けされる数値 # _ord_init: _ord_を初期化する関数 # # Arnold Robbins # arnold@gnu.org # Public Domain # 16 January, 1992 # 20 July, 1992, revised BEGIN { _ord_init() } function _ord_init( low, high, i, t) { low = sprintf("%c", 7) # BEL は ASCIIコードの 7 if (low == "\a") { # 通常のASCII low = 0 high = 127 } else if (sprintf("%c", 128 + 7) == "\a") { # ascii, mark parity low = 128 high = 255 } else { # EBCDIC(!) low = 0 high = 255 } for (i = low; i <= high; i++) { t = sprintf("%c", i) _ord_[t] = i } }
chr
が使っている数値についてはもう少し説明する価値があるだろう。
今日使われているキャラクタセットで最も名が知られているものは、ASCIIで
ある。8ビット バイトでは256(0から255)の異なった値を区別できるが、ASCII
では0から127までの値を使ったキャラクタだけを定義している。
(20)
私達が知っている manufacturerの少なくとも一つはASCIIを使っているが、mark
parityを伴っていて、バイトの一番左のビットが常に1である。これは、その様
なキャラクタコードを使っているシステムではキャラクタコードの値は128から
255までになるということである。さらに、大きなメインフレームシステムでは、
EBCDICキャラクタセットを使っていて、これは256すべての値を使用する。一部
の古いシステムでは、また別のキャラクタセットを使っているものがあるが、
心配する必要はない。
function ord(str, c) { # 最初のキャラクタだけに注目する。 c = substr(str, 1, 1) return _ord_[c] } function chr(c) { # 0を足すことによって強制的に数値にする return sprintf("%c", c + 0) } #### test code #### # BEGIN \ # { # for (;;) { # printf("文字を入力してください: ") # if (getline var <= 0) # break # printf("ord(%s) = %d\n", var, ord(var)) # } # }
これらの関数の明白な改良は、_ord_init
のコードを
BEGIN
ルールの本体に移すことである。
上記のものはこの方法で記述されている。
BEGIN
ルールには関数をテストするための"テストプログラム"がある。
これはproduction useではコメントアウトされている。
文字列の処理を行っているときには、
配列に格納されている文字列を連結して一つの長い文字列にするような
ことができると便利である。
次に挙げるjoin
という関数はこれを行うものであり、
後に挙げるアプリケーションプログラムの幾つかでも使用している
(セクション 実用的な awk
プログラムを参照)。
関数の良いデザインというものは重要である。この関数は一般的である
必要があるが、デフォルトの振る舞いが妥当なものであるべきものである。
この関数は配列と、連結する配列要素の下限と上限を引数として呼び出される。
ここで、配列の添え字は数値であることを仮定しているが、これは
split
が作り出す配列が同様なものであるので、妥当な仮定である
(セクション Built-in Functions for String Manipulationを参照)。
# join.awk -- 配列をつなげて文字列にする # Arnold Robbins, arnold@gnu.org, Public Domain # May 1993 function join(array, start, end, sep, result, i) { if (sep == "") sep = " " else if (sep == SUBSEP) # 魔法の値 sep = "" result = array[start] for (i = start + 1; i <= end; i++) result = result sep array[i] return result }
省略可能な引数は文字列を連結するときに間に挟むセパレータである。呼び出し
時にこれが空以外の値であれば、join
はそれを使う。引数が省略されて
いれば、これは空文字列となる。このケースでは、join
は一つのスペー
スをデフォルトのセパレータとする。渡された値がSUBSEP
と等しい場合、
join
はセパレータなしで文字列の連結を行う。SUBSEP
は文字列の
連結をセパレータなしで行うことを指示する"magic value"として扱われる。
awk
に連接のための代入演算子があれば良かったかもしれない。
連接を行うための演算子が存在していないということは、
文字列に関する操作を必要以上に難しいものとしている。
systime
はawk
に組込みの関数であり、
タイムスタンプをシステムが開始された時刻からの経過秒数として
返すものである。この時刻情報はstrftime
という
組込み関数を使用して、自由自在な書式で出力可能なデータに
することができる(systime
とstrftime
に関する詳しい説明は
セクション Functions for Dealing with Time Stampsを参照.)。
興味深いが難しい問題は、標準形に変換されている日付のデータを
タイムスタンプに戻すということである。ANSI Cのライブラリでは
mktime
という関数を、通常の形式の日付データをタイムスタンプに
変換する目的で提供している。
gawk
は組込み関数として、
Cバージョンを単純に"hook"したmktime
を提供すべきであると
考えるかもしれない。しかし、mktime
はawk
で完全に実現すること
ができるのである。
以下に挙げたのはawk
によるmktime
である。
これは、単純な日付と時刻を受け取り、それをタイムスタンプに
変換するものである。
このプログラムはプログラムの説明といっしょになっている。 セクション Texinfoのソースファイルからプログラムを取り出すを参照, でこのマニュアルのTexinfoソースファイルから プログラムコードを抜き取ってファイルにするやり方を説明している。
このプログラムは説明のためのコメントで始まり、
_tm_months_
というテーブルを初期化するBEGIN
ルールが続く。
このテーブルは二次元の配列であり、次の長さを保持する。
最初の添え字は0か1であり、0は通常の年に、1は閏年に対して使われる。
この値は二月を除いては同じ月の値は同じ値となるので、
多重代入を使っている。
# mktime.awk -- 標準形式の日付データをタイムスタンプに変換する # Arnold Robbins, arnold@gnu.org, Public Domain # May 1993 BEGIN \ { # Initialize table of month lengths # 各月の日数のテーブルを初期化する。 _tm_months[0,1] = _tm_months[1,1] = 31 _tm_months[0,2] = 28; _tm_months[1,2] = 29 _tm_months[0,3] = _tm_months[1,3] = 31 _tm_months[0,4] = _tm_months[1,4] = 30 _tm_months[0,5] = _tm_months[1,5] = 31 _tm_months[0,6] = _tm_months[1,6] = 30 _tm_months[0,7] = _tm_months[1,7] = 31 _tm_months[0,8] = _tm_months[1,8] = 31 _tm_months[0,9] = _tm_months[1,9] = 30 _tm_months[0,10] = _tm_months[1,10] = 31 _tm_months[0,11] = _tm_months[1,11] = 30 _tm_months[0,12] = _tm_months[1,12] = 31 }
複数のBEGIN
ルールをまとめることの利益
(セクション 特殊パターンBEGIN
とEND
を参照)
は特にライブラリファイルを記述するときに顕著である。
ライブラリファイル中の関数は、関数のプライベートデータの初期化をきれいに
行うことができるようになり、同様にプライベートなEND
ルールは個々の
関数のクリーンアップ作業を提供することができる。
次の関数は与えられた年に対して単純な閏年判定を行うものである。 与えられた年が4で割り切れて、100では割り切れないか、400で 割り切れる年であればそれは閏年である。 したがって、1904は閏年だったが、1900年は閏年ではなかった。 そして2000年は閏年である。
# 閏年かどうかを判定する function _tm_isleap(year, ret) { ret = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) return ret }
この関数はこのファイルの中でほんの数回しか使われず、 その計算はインライン(in-line。使う場所に記述すること)に 記述することもできる。 独立した関数にすることによって、オリジナルの開発を容易にし、 関数の中身を何ヶ所にも記述するときにタイプエラーが起きてしまうことを 防止するのである。
次の関数はもっと興味深い。この関数は日付・時刻のデータをタイムスタンプに
変換するときの作業のほとんどを行ってしまう。呼び出し側は六つの値を配列
(a
という名前で定義されている)で渡す。この六つの内訳は、年(世紀も含
めたもの)、月、日、時、分、秒である。
この関数は幾つかのローカル変数を使って、時間を秒で表わした値、日を秒で表 わした値、年を秒で表わした値といったものを前計算している。しばしばCでは このような式はインラインで書きくだされ、コンパイラが定数の畳み込み (constant folding)を行っている。例えば、ほとんどのCコンパイラはコンパイ ル時に`60 * 60'を`3600'に変換し、実行時にいちいちそれを計算す るようなことにはならない。これらの値の前計算は関数をより効率的にするので ある。
# 日付を秒に変換する function _tm_addup(a, total, yearsecs, daysecs, hoursecs, i, j) { hoursecs = 60 * 60 # 一時間あたりの秒数 daysecs = 24 * hoursecs # 一日あたりの秒数 yearsecs = 365 * daysecs # 一年あたりの秒数 total = (a[1] - 1970) * yearsecs # 閏年の処理 for (i = 1970; i < a[1]; i++) if (_tm_isleap(i)) total += daysecs j = _tm_isleap(a[1]) for (i = 1; i < a[2]; i++) total += _tm_months[j, i] * daysecs total += (a[3] - 1) * daysecs total += a[4] * hoursecs total += a[5] * 60 total += a[6] return total }
この関数はまず1970年1月1日 零時 (21) から始まって、指定された年の初めまでの経過時間の近似値を秒で求め、 その後で1970年から、指定された年までにあった閏年の補正分を加えている。
j
という変数は閏年であれば1、そうでなければ0という値を保持している。
指定された年での指定された月の直前の月までは
_tm_months
という配列にある対応する月毎の秒数を加えていく。
最後に、指定された日の経過時間の秒数を加える。
結果は、1970年1月1日からの経過秒数である。 この値はまだ必要とするものではない。その理由を簡単に説明する。
mktime
関数は一つの文字列を引数としてとる。
この文字列は日付・時刻を"標準"形式(canonical form)で表わしたものであり、
"year month day hour minute second"
のような形である。
# mktime -- 日付を秒に変換し、タイムゾーンの補正を行う。 function mktime(str, res1, res2, a, b, i, j, t, diff) { i = split(str, a, " ") # FSをあてにしてはいけない if (i != 6) return -1 # 強制的に数値にする for (j in a) a[j] += 0 # 確認 if (a[1] < 1970 || a[2] < 1 || a[2] > 12 || a[3] < 1 || a[3] > 31 || a[4] < 0 || a[4] > 23 || a[5] < 0 || a[5] > 59 || a[6] < 0 || a[6] > 60 ) return -1 res1 = _tm_addup(a) t = strftime("%Y %m %d %H %M %S", res1) if (_tm_debug) printf("(%s) -> (%s)\n", str, t) > "/dev/stderr" split(t, b, " ") res2 = _tm_addup(b) diff = res1 - res2 if (_tm_debug) printf("diff = %d seconds\n", diff) > "/dev/stderr" res1 += diff return res1 }
この関数は最初に文字列をスペース、タブをセパレータとして配列に分割する。
ここで配列に要素が6個なければ、エラーとして、-1を返す。次に、配列
の各要素を0を加えることにより強制的に数値にする。その後にあるif
文
は、各要素がそれぞれの範囲内に収まっているかどうかをチェックしている(こ
のチェックはもっと厳しくできる。つまり、月によって許される日付は違うので、
そこまでチェックするということ)。これらは、欠くことのできない準備作業と
エラーチェックである。
_tm_addup
を再度呼び出して1970年1月1日 零時からの経過秒数を作り出
す。この値はまだ必要としているものではない。それは、ローカルタイムゾーン
を考慮した計算をしていないからである。言い換えれば、この値はUTC(Universal
Coordinated Time)での経過秒数を表わしたものである。ローカルタイムゾーン
がUTCの東、あるいは西にあるのであれば、この値にある数を加えるか減じるか
した値が求めるタイムスタンプとなる。
例えばアメリカのジョージア州アトランタで午後6時23分だったとしてみよう。
アトランタは UTCから西へ五時間のところにある。夏時間の場合には UTCとの差は
四時間となる。アトランタで
mktime
に"1993 5 23 18 23 12"
という引数を渡して呼び出し
たとすると、_tm_addup
からの結果はUTCでの午後6時23分、アトランタで
は午後2時23分という値になる。そこで、その結果にさらに四時間分の秒数(5月23
日は夏時間を使っている期間のため)を加える必要がある。
どのようにすればmktime
はUTCからどれくらい離れているかを決められるだ
ろうか? これはびっくりするほど簡単である。mktime
に日付をUTC
であるかのように渡したときに返ってきたタイムスタンプを使えば良い、この
タイムスタンプはstrftime
の書式にローカルタイムを使うことで元に戻
すことができる。つまり、すでにUTCとの差が補正されたものにする。これは、
"%Y %m %d %H %M %S"
という書式をstrftime
に渡せば良い。こ
れによって返されるのはオリジナルの書式文字列の元で計算されたタイムスタン
プである。ここで返ってきた値はUTCのと差を考慮したものである。新しい時間
を再度タイムスタンプに変換すると、二つのタイムスタンプの差は、その場所の
タイムゾーンとUTCとの(秒で表わした)差である。この差をオリジナルの結果に
加算する。後で実例を挙げる。
最後は、関数をテストするための"メイン"プログラムである。
BEGIN { if (_tm_test) { printf "yyyy mm dd hh mm ss の形式で日付を入力してください: " getline _tm_test_date t = mktime(_tm_test_date) r = strftime("%Y %m %d %H %M %S", t) printf "Got back (%s)\n", r } }
プログラム全体は、
デバッグ出力と最後のBEGIN
ルールのテストを有効にするための制御を、
二つの変数をコマンドライン上でセットすることで行っている。
次の例はテスト実行の結果である(デバッグ出力は標準エラー出力に、
テストの出力は標準出力に出力されていることに注意)。
$ gawk -f mktime.awk -v _tm_test=1 -v _tm_debug=1 -| Enter date as yyyy mm dd hh mm ss: 1993 5 23 15 35 10 error--> (1993 5 23 15 35 10) -> (1993 05 23 11 35 10) error--> diff = 14400 seconds -| Got back (1993 05 23 15 35 10)
入力された時刻は1993年の5月23日の午後3時35分(24時間制では15時35分)である。 デバッグ出力の最初の行はUTCとしての時間の結果、ローカルタイムゾーンから4 時間進んだ時刻である。二番目の行はその差である14440秒、時間で表わして4時 間である(差が4時間なのは、夏時間だからである)。最後の行の出力はタイムゾ ーンの補正が正しく働いていることを示していて、入力した時刻と同じである。
このプログラムは
任意の書式で表わされた日付をタイムスタンプに変換するときの
一般的な問題を解決してはいない。
この問題は非常に複雑である。しかしながら、mktime
関数は
構築するための基礎を提供している。他のソフトウェアは
月を名前で指定することができたり、AM/PMを使った時刻を24時間制に
変換して、mktime
が要求する"標準形式"を生成することができる。
セクション Functions for Dealing with Time Stampsを参照,
で説明されているsystime
とstrftime
という関数は
日付を人間が読みやすい形で扱うために最低限必要な機能を
提供している。strftime
は大規模なもので、制御書式は必然的に
プログラムを読むときに簡単に思い出したり、ぱっとわかるような
ものではない。
以下に挙げた関数、gettimeofday
は、ユーザーから
渡されたあらかじめフォーマットされた時間情報の入った配列
をpopulateするものである。この関数は現在時刻をdate
ユーティリティ
と同じやり方でフォーマットされた時刻情報の文字列を返す。
# gettimeofday -- わかりやすい書式の日付情報を得る # Arnold Robbins, arnold@gnu.org, Public Domain, May 1993 # # Returns a string in the format of output of date(1) # Populates the array argument time with individual values: # time["second"] -- 秒 (0 - 59) # time["minute"] -- 分 (0 - 59) # time["hour"] -- 時 (0 - 23) # time["althour"] -- 時 (0 - 12) # time["monthday"] -- 日 (1 - 31) # time["month"] -- 月 (1 - 12) # time["monthname"] -- 月の名前 # time["shortmonth"] -- 省略表記の月 # time["year"] -- 世紀中の年 (0 - 99) # time["fullyear"] -- 世紀つきの年 (19xx or 20xx) # time["weekday"] -- 曜日 (日曜日が0) # time["altweekday"] -- 曜日 (月曜日が0) # time["weeknum"] -- 日曜を始まりとする曜日番号 # time["altweeknum"] -- 月曜を始まるとする曜日番号 # time["dayname"] -- 曜日の名前 # time["shortdayname"] -- 曜日の短い名前 # time["yearday"] -- 一年の中での日数 (0 - 365) # time["timezone"] -- タイムゾーンの略称 # time["ampm"] -- AM or PM designation function gettimeofday(time, ret, now, i) { # 不必要なシステムコールを行わないために一度だけ時刻を取得する。 now = systime() # date(1)スタイルの出力を返す。 ret = strftime("%a %b %d %H:%M:%S %Z %Y", now) # 結果を格納する配列をクリアする。 for (i in time) delete time[i] # 値を埋める。このとき強制的に数値にするため # 0を足す time["second"] = strftime("%S", now) + 0 time["minute"] = strftime("%M", now) + 0 time["hour"] = strftime("%H", now) + 0 time["althour"] = strftime("%I", now) + 0 time["monthday"] = strftime("%d", now) + 0 time["month"] = strftime("%m", now) + 0 time["monthname"] = strftime("%B", now) time["shortmonth"] = strftime("%b", now) time["year"] = strftime("%y", now) + 0 time["fullyear"] = strftime("%Y", now) + 0 time["weekday"] = strftime("%w", now) + 0 time["altweekday"] = strftime("%u", now) + 0 time["dayname"] = strftime("%A", now) time["shortdayname"] = strftime("%a", now) time["yearday"] = strftime("%j", now) + 0 time["timezone"] = strftime("%Z", now) time["ampm"] = strftime("%p", now) time["weeknum"] = strftime("%U", now) + 0 time["altweeknum"] = strftime("%W", now) + 0 return ret }
strftime
の書式で要求されるものを、より簡単で読みやすい
文字列で添え字付けしている。
この関数を使ったalarm
プログラムが
セクション アラーム時計プログラムを参照,
にある。
関数 gettimeofday
は先に挙げたようなものが提供されている。
この関数に対するより一般的なデザインとしては、カレントの時刻の
代わりに使われるようなオプションのタイムスタンプを取れるように
するというものがあるだろう。
BEGIN
ルールとEND
ルールはそれぞれawk
プログラムの実行開始時と終了時に一回だけ実行される
(セクション 特殊パターンBEGIN
とEND
を参照)。
以前、BEGIN
ルールは各データファイルの先頭で実行され、END
ル
ールは各データファイルの最後で実行されるものと間違って考えていたユーザー
がいた。我々(gawk
の作者)がそのユーザーに対してその間違いを知らせ
たところ、その人は我々にBEGIN_FILE
と END_FILE
という名前の
そこで期待してた動作をする特殊ルールを新たにgawk
に追加してくれる
ようリクエストしてきた。彼は、そのためのコードも送ってきた。
しかし、ちょっと考えた後で、私は後述するようなライブラリプログラムを
提供した。これは二つのユーザー定義関数beginfile
とendfile
を
それぞれ各データファイルの先頭と末尾で呼び出すようにアレンジした。
たったの9行のコードで問題を解決できた! しかもこれは
ポータブルで、すべてのawk
処理系で正しく動作する。
# transfile.awk # # Give the user a hook for filename transitions # # ユーザーはbeginfile()、endfile()という # それぞれファイルを処理し始めるとき、処理し終わったとき # に適用される関数を用意しなければならない。 # # Arnold Robbins, arnold@gnu.org, January 1992 # Public Domain FILENAME != _oldfilename \ { if (_oldfilename != "") endfile(_oldfilename) _oldfilename = FILENAME beginfile(FILENAME) } END { endfile(FILENAME) }
このファイルは、その中にあるルールを最初に実行させるために ユーザーの"メイン"プログラムの前にロードしなければならない。
このルールはawk
がFILENAME
という変数を、新しいファイルに
処理が移ったときに自動的に変更するということに依存している。
カレントのファイル名はプライベート変数の_oldfilename
に
格納されている。FILENAME
が_oldfilename
と等しくないとき、
新しいデータファイルが処理され始め、このとき古いファイルのために
endfile
を呼ぶ必要がある。endfile
はファイルが処理された後で
呼び出されるべきものなので、プログラムでは_oldfilename
が
空文字列でないことを最初にチェックしている。それから、
カレントファイルの名前を_oldfilename
にセットして、
新しいデータファイルのためにbeginfile
を呼び出している。
すべてのawk
の変数と同様に、_oldfilename
は空文字列で
初期化されているので、このルールは最初のデータファイルのときでも
正しく実行されるのである。
このプログラムは同様にEND
ルールがあり、これは
最後のファイルを処理したときの最終処理を行うためのものである。
このEND
ルールは"メイン"プログラムにあるどのEND
ルール
よりも前にあるので、endfile
は一番最初に呼び出される。
もう一度複数のBEGIN
ルールとEND
ルールの価値を明確に
しておこう。
このバージョンには、最初のバージョンのnextfile
と同じ問題がある
(セクション Implementing nextfile
as a Functionを参照)。
同じデータファイルが二度コマンドラインで連続して指定された場合、
endfile
とbeginfile
はそれぞれ
最初のパスの最後と、二番目のパスの先頭では実行されないという
ことになってしまう。
以下のものは、この問題に対処したものである
# ftrans.awk -- handle data file transitions # # ユーザーが beginfile() と endfile() という関数を用意する # # Arnold Robbins, arnold@gnu.org, November 1992 # Public Domain FNR == 1 { if (_filename_ != "") endfile(_filename_) _filename_ = FILENAME beginfile(FILENAME) } END { endfile(_filename_) }
セクション 数え上げを参照,では、このライブラリ関数を 使って、メインプログラムを単純にする方法がある。
POSIX互換システムのほとんどのユーティリティは、プログラムの振る舞いを変
えるために使われるコマンドライン上のオプション、あるいは"スイッチ'をと
る。awk
はその様なプログラムの一例である(セクション コマンドラインオプションを参照)。しばしばオプションは、コマンドラインオプションに正しく従
うのに必要なデータである引数(argument)をとる。例えばawk
の
`-F'オプションはフィールドセパレータとして使用する文字列を要求する。
コマンドライン上で最初に現れる`--'あるいは、`-'で始まらない文
字列は、オプションの終端を示す。
ほとんどのUNIXシステムではgetopt
という名前のコマンドライン引数の
処理を行うC関数が提供されている。プログラマーは一文字のオプションを記述
する文字列を渡す。オプションが引数を必要とする場合には、コロンをその文字
の直後に続ける。getopt
はコマンドライン引数の数と、値が渡され、ル
ープ中で呼び出される。getopt
はコマンドライン引数を渡されたオプシ
ョン文字列に従って処理を行う。ループ中で呼ばれるごとに、(オプション文字
列中にある)オプションが見つかれば見つかったオプションを表わすキャラクタ
一文字を、不正なオプションが見つかったときには`?'を返す。コマンドラ
イン上にもうオプション指定が残っていないときには-1を返す。
getopt
を使ったとき、引数を伴わないオプションは一つのグループにま
とめることができる。それから、引数を伴うオプションは引数があることを要求
する。この引数はオプション文字の直後に続けてもいいし、次のコマンドライン
引数であっても良い。
三つのコマンドラインオプション、`-a', `-b', `-c'をとり、 このうち`-b'は引数を要求するようなプログラムを仮定すると、以下のよ うなプログラムの起動は全て正しいものである。
prog -a -b foo -c data1 data2 data3 prog -ac -bfoo -- data1 data2 data3 prog -acbfoo data1 data2 data3
あるオプションの引数がそのオプションとまとめられているとき、そのコマンド ライン引数の残りは、そのオプションの引数として扱われることに注意すること。 上記の例では、`-acbfoo'は`-a', `-b', `-c' のすべての オプションがあり、`foo'が`-b'オプションの引数であるように指示 している。
getopt
はプログラマーが使うことのできる
四つの大域変数を提供している。
optind
argv
)上でのインデックス。
optarg
opterr
getopt
は通常、不正なオプションが渡されたときにはエラーメッセージ
を出力する。opterr
に0をセットすることによってこの機能を抑制するこ
とができる(アプリケーション自身でエラーメッセージを出したいときがあるか
もしれない)。
optopt
次のCのプログラム片は、awk
に対するコマンドライン引数を
処理するgetopt
の使い方の例である。
int main(int argc, char *argv[]) { ... /* 自分のメッセージを出力する */ opterr = 0; while ((c = getopt(argc, argv, "v:f:F:W:")) != -1) { switch (c) { case 'f': /* ファイル */ ... break; case 'F': /* フィールドセパレータ */ ... break; case 'v': /* 変数代入 */ ... break; case 'W': /* 拡張 */ ... break; case '?': default: usage(); break; } } ... }
余談ながら、gawk
は通常スタイルのオプションとGNUスタイルのオプショ
ンの両方を扱うために、GNUの getopt_long
関数を使用している。
(セクション コマンドラインオプションを参照).
getopt
がもたらす作用は非常に便利であり、awk
プログラム
同様に使い易いものである。
次に挙げるのはawk
で記述したgetopt
である。
この関数はawk
の最大の弱点、単一のキャラクタの扱いが非常に
貧弱であるということを浮き彫りにしている。
個々のキャラクタにアクセスするために、substr
をくり返し呼ぶ必要がある
(セクション Built-in Functions for String Manipulationを参照)。
The discussion walks through the code a bit at a time.
# getopt -- awkによる Cライブラリの getopt(3) 関数の実装 # # arnold@gnu.org # Public domain # # Initial version: March, 1991 # Revised: May, 1993 # 外部変数: # Optind -- 最初の非引数のARGVでの添え字 # Optarg -- カレントオプションに対する引数の文字列 # Opterr -- if non-zero, print our own diagnostic # Optopt -- カレントのオプション文字 # 戻り値 # -1 オプションの終端に達した # ? 認識できなかったオプション # <c> カレントオプションを示すキャラクタ # Private Data # _opti -abcのようなマルチフラグオプションにおけるインデックス
関数はだれが、いつ書いたのか、使用している大域変数のリスト、戻り値とその 意味、このライブラリ関数で使っている"プライベートな"大域変数などを説明 する多少のドキュメントから始まっている。このようなドキュメントは、どんな プログラムでも、とくにライブラリ関数では重要な事である。
function getopt(argc, argv, options, optl, thisopt, i) { optl = length(options) if (optl == 0) # オプションなし return -1 if (argv[Optind] == "--") { # 全て終わった Optind++ _opti = 0 return -1 } else if (argv[Optind] !~ /^-[^: \t\n\f\r\v\b]/) { _opti = 0 return -1 }
関数では、呼び出されたときに最初にオプション文字列(パラメータoptions
)を
チェックする。options
の長さが0であれば、getopt
は
即座に-1を返す。
次にオプションの終了をチェックする。`--'は、
`-'で始まっていないコマンドライン引数と同じように
コマンドラインオプションを終了させる。
Optind
はコマンドライン引数配列を通じての段階で使用される。
これは、グローバル変数であるので、
getopt
が呼ばれる間も値を保ち続ける。
使用している正規表現/^-[^: \t\n\f\r\v\b]/
は、
おそらくは少々おおげさだろう。これは-
の後に、
ホワイトスペース(whitespace)やコロン以外のキャラクタが続いていないかどうかを
チェックしている。カレントのコマンドライン引数がこのパターンに
マッチしないのなら、その引数はオプションではなく、そこで
オプション処理は終わる。
if (_opti == 0) _opti = 2 thisopt = substr(argv[Optind], _opti, 1) Optopt = thisopt i = index(options, thisopt) if (i == 0) { if (Opterr) printf("%c -- invalid option\n", thisopt) > "/dev/stderr" if (_opti >= length(argv[Optind])) { Optind++ _opti = 0 } else _opti++ return "?" }
_opti
という変数はカレントのコマンドライン引数の位置
(argv[Optind]
)を保持している。
複数のオプションが一つの`-'に続いている(たとえば`-abx'の
ように)場合、ユーザーに対してはそれらのオプションを一度に
一つずつ返す必要がある。
_opti
が0であった場合には文字列中で注目すべきキャラクタの位置(1の
位置には`-'があり、これはスキップする)である2がセットされる。
thisopt
という変数はsubstr
を使って得られたキャラクタを保持し
ている。Optopt
はメインプログラムでそれを参照するために同じ値を保存
する。
thisopt
が文字列options
の中になかった場合、それは不正なオプ
ションである。Opterr
が非0であれば、getopt
はCバージョンの
getopt
と同じようなエラーメッセージを標準エラー出力に出力する。
オプションが不正なものであったので、それをスキップして
次のオプションキャラクタに移動する必要がある。
_opti
がカレントのコマンドライン引数の長さ以上であれば、
次の引数に移動する必要があり、同様にOptind
はインクリメントされ、
_opti
は0にリセットされる。それ以外の場合はOptind
はそのままで
_optie
は単にインクリメントされる。
この場合、オプションが不正なものだったので、getopt
は`?'を
返す。メインプログラムは無効なオプション文字が何であったのかを
知る必要がある場合には、Optopt
を調べることができる。
if (substr(options, i + 1, 1) == ":") { # get option argument # オプションの引数を取得する if (length(substr(argv[Optind], _opti + 1)) > 0) Optarg = substr(argv[Optind], _opti + 1) else Optarg = argv[++Optind] _opti = 0 } else Optarg = ""
オプションが引数を必要とする場合、文字列option
にある
オプション文字列に続けてコロンを置く。カレントのコマンドライン引数
(argv[Optind]
))にキャラクタがまだ残っているのなら、
残りの文字列はOptarg
にセットされ、そうでなければ
次のコマンドライン引数が使われる(`-xFOO' と `-x FOO')。
これらのどちらの場合でも、_opti
は
カレントのコマンドライン引数でチェックすべきキャラクタがないので
0にリセットされる。
if (_opti == 0 || _opti >= length(argv[Optind])) { Optind++ _opti = 0 } else _opti++ return thisopt }
最後に、_opti
は0か、カレントのコマンドライン引数の長さよりも大き
い値となる。これはそのargv
の要素が処理されたということであり、
Optind
は次のargv
の要素を指すようにインクリメントされる。どち
らかの条件も真とならない場合には、_opti
だけがインクリメントされる。
これは次にgetopt
が呼ばれたときに次のオプション文字を処理できるよ
うにするためのものである。
BEGIN { Opterr = 1 # default is to diagnose Optind = 1 # ARGV[0]はスキップする # test program if (_getopt_test) { while ((_go_c = getopt(ARGC, ARGV, "ab:cd")) != -1) printf("c = <%c>, optarg = <%s>\n", _go_c, Optarg) printf("non-option arguments:\n") for (; Optind < ARGC; Optind++) printf("\tARGV[%d] = <%s>\n", Optind, ARGV[Optind]) } }
BEGIN
ルールはOpterr
とOptind
を1に初期化している。
getopt
のデフォルトの動作は不正なオプションを見つけたときには検査
メッセージを出力するというものなのでOpterr
に1をセットし、それは
ARGV[0]
にあるプログラム名を処理する必要がないのでOptind
に1をセ ットしている。
BEGIN
ルールの残りは単純なテストプログラムである。
次に、このテストプログラムを実行した二つの例を挙げる。
$ awk -f getopt.awk -v _getopt_test=1 -- -a -cbARG bax -x -| c = <a>, optarg = <> -| c = <c>, optarg = <> -| c = <b>, optarg = <ARG> -| non-option arguments: -| ARGV[3] = <bax> -| ARGV[4] = <-x> $ awk -f getopt.awk -v _getopt_test=1 -- -a -x -- xyz abc -| c = <a>, optarg = <> error--> x -- invalid option -| c = <?>, optarg = <> -| non-option arguments: -| ARGV[4] = <xyz> -| ARGV[5] = <abc>
最初の`--'はawk
に対する引数を終了させるためのものであり、
これにより`-a'などをawk
のオプションとして処理することを
防止する。
引数を処理するためにgetopt
を使った例は
セクション 実用的な awk
プログラムを参照.
に幾つか挙げられている。
`/dev/user'という特殊ファイル
(セクション Special File Names in gawk
を参照)は、
カレントの実ユーザーID、実効ユーザーID、グループIDの番号に対する
アクセスを提供する。また、可能な場合にはユーザーの
supplementary グループもセットされる。
しかしながら、これらは数値であり、平均的なユーザーにとっては
便利な情報を提供していない。
ユーザーやグループ番号から導かれるユーザーの情報を見つけるための
何等かの手段を提供する必要がある。
このセクションではユーザーデータベースから情報を回復する一連の
関数を提供する。グループデータベースから情報を回復する
同様の関数群はセクション Reading the Group Databaseを参照.
POSIXの標準では、ユーザー情報を保持するファイルの場所を規定していない。
その代わり、ユーザー情報を取得するための<pwd.h>
というヘッダファイ
ルと幾つかのC言語のサブルーチンを提供している。基本的な関数はgetpwent
であり、これは"get password entry"から命名された。"`password"は
オリジナルのユーザーデータベースファイル`/etc/passwd'から来ており、
このファイルは暗号化されたパスワードも含めたユーザーの情報を保持している。
awk
プログラムは`/etc/passwd'ファイルを直接
(フォーマットはよく知られている)読むこともできる。
パスワードファイルはネットワークシステム上で扱われるために
このファイルはシステムのすべてのユーザーに関する情報を
保持していないかもしれない。
ユーザーデータベースの完全なバージョンを確実に読み出せるようにするために、
getpwent
を呼び出す小さなCプログラムを書く必要がある。getpwent
はstruct passwd
へのポインタを返すように定義され、呼びされるた
びにデータベースの次のエントリを返す。エントリがそれ以上ない場合には空ポ
インタNULL
を返す。これが起こった場合、Cプログラムはデータベースを
クローズするためにendpwent
を呼び出す必要がある。次のプログラムは
パスワードデータベースを"cat"するCプログラム、pwcat
である。
/* * pwcat.c * * パスワードデータベースの印字可能バージョンを生成する * * Arnold Robbins * arnold@gnu.org * May 1993 * Public Domain */ #include <stdio.h> #include <pwd.h> int main(argc, argv) int argc; char **argv; { struct passwd *p; while ((p = getpwent()) != NULL) printf("%s:%s:%d:%d:%s:%s:%s\n", p->pw_name, p->pw_passwd, p->pw_uid, p->pw_gid, p->pw_gecos, p->pw_dir, p->pw_shell); endpwent(); exit(0); }
もしCのことがわからなくても、心配することはない。pwcat
の出力はユ
ーザーデータベースであり、伝統的なコロンで区切られた`/etc/passwd'フ
ォーマットである。フィールドには以下のものがある。
$HOME
として知られている)。
以下にpwcat
の出力の一部を挙げる。
$ pwcat -| root:3Ov02d5VaUPB6:0:1:Operator:/:/bin/sh -| nobody:*:65534:65534::/: -| daemon:*:1:1::/: -| sys:*:2:2::/:/bin/csh -| bin:*:3:3::/bin: -| arnold:xyzzy:2076:10:Arnold Robbins:/home/arnold:/bin/sh -| miriam:yxaay:112:10:Miriam Robbins:/home/miriam:/bin/sh -| andy:abcca2:113:10:Andy Jacobs:/home/andy:/bin/sh ...
以下にユーザー情報を取得する関数のグループを挙げる。これらの関数は同名の Cの関数と同じ働きをするものである。
# passwd.awk -- パスワードファイルの情報にアクセスする # Arnold Robbins, arnold@gnu.org, Public Domain # May 1993 BEGIN { # tailor this to suit your system _pw_awklib = "/usr/local/libexec/awk/" } function _pw_init( oldfs, oldrs, olddol0, pwcat) { if (_pw_inited) return oldfs = FS oldrs = RS olddol0 = $0 FS = ":" RS = "\n" pwcat = _pw_awklib "pwcat" while ((pwcat | getline) > 0) { _pw_byname[$1] = $0 _pw_byuid[$3] = $0 _pw_bycount[++_pw_total] = $0 } close(pwcat) _pw_count = 0 _pw_inited = 1 FS = oldfs RS = oldrs $0 = olddol0 }
BEGIN
ルールではプライベート変数にpwcat
が格納されているディ
レクトリ名をセットする。このファイルはawk
ライブラリを補助するのに
使われるので、`/usr/local/libexec/awk'というディレクトリに置いてい
る。望むなら他のディレクトリに置くこともできる。
関数_pw_init
は三つの連想配列に三つのユーザー情報をコピーしている。
これらの配列はユーザー名によって(_pw_byname
)、ユーザーIDによって
(_pw_byuid
)、出現順序によって(_pw_bycount
)それぞれ添え字付
けされる。
変数_pw_inited
は効率のために使われている。_pw_init
は一度だ
け呼ばれれば良い。
この関数はgetline
を使ってpwcat
から情報を読み込んでいるので、
最初にFS
、RS
、$0
の値をセーブしている。これはこれら
の関数がユーザープログラムのどんなところからでも呼ばれる可能性があり、ユ
ーザーが独自のFS
やRS
を設定している可能性があるので、それ
に対処するために必要なことである。
関数のメイン部分ではループを使って、データベースの一行読み込み、読み込ん
だ一行のフィールドへの分割、必要に応じて読み込んだ行を配列要素にセットと
いう一連の手順を行っている。ループが完了したときに、_pw_init
は
パイプのクローズ、_pw_inited
への1のセット、FS
、RS
、
$0
の値の復元といったクリーンアップ作業を行っている。
function getpwnam(name) { _pw_init() if (name in _pw_byname) return _pw_byname[name] return "" }
getpwnam
という関数はユーザー名を表わす文字列を引数としてとる。そ
のようなユーザーがデータベース中にあれば、適切な行を返し、ユーザーがみつ
からなければ空文字列を返す。
function getpwuid(uid) { _pw_init() if (uid in _pw_byuid) return _pw_byuid[uid] return "" }
同じように、getpwid
という関数はユーザーID番号を引数にとる。データ
ベース中に該当するユーザー番号があれば適切な行を返し、みつからなければ空
文字列を返す。
function getpwent() { _pw_init() if (_pw_count < _pw_total) return _pw_bycount[++_pw_count] return "" }
関数getpwent
は単純にデータベース中にあるエントリを一度に一つのエ
ントリを走査する。_pw_count
は配列_pw_bycount
中の現在位置を
保持するのに使われている。
function endpwent() { _pw_count = 0 }
関数endpwent
は_pw_count
をリセットする。
これにより、getpwent
に対する呼び出しは最初から繰り返されることになる。
これら一連の関数をデザインするにあたって意識したことは、それらの関数それ
ぞれがデータベース配列の初期化のために_pw_init
を呼び出すとい
うことである。ユーザーデータベースを生成するために別のプロセスを実行する
オーバーヘッド、それを走査するための入出力のためのオーバーヘッド、といっ
たものはユーザーのメインプログラムが実際にこれらの関数を呼び出すときだけ
にかかる。このライブラリファイルがユーザープログラムにロードされても使わ
れなかった場合には、余計な実行時オーバーヘッドはかからない(もう一つの手
段として_pw_init
の本体をBEGIN
ルールに移動するというも
のがある。これはプログラムを単純にはするが、必要ではないかもしれない余計
なプロセスを実行することになる)。
_pw_init
の呼び出しは高価なものではない。それは_pw_inited
と
いう変数がデータの読み出しを二度以上行うことを防いでいるためである。もし、
あなたのawk
プログラムのすべての最後のサイクルを詰め込むことを心配
しているのなら(??)、_pw_inited
のチェックを_pw_init
からとっ
てしまい、それぞれの関数に複製することもできる。実際には、ほとんどの
awk
プログラムは入出力(の速度)に縛られており、気にする必要はなく、
行った場合にはプログラムをメンテナンスしにくくしてしまうことになる。
セクション ユーザー情報を出力するを参照,にあるid
プログラム
はこれらの関数を使用している。
グループデータベースに対する同様な操作のより詳しい議論は
セクション Reading the User Databaseを参照,
にある。
そのシステムに伝統的な、よく知られている`/etc/group'というファイル
がある場合にも、POSIXの標準は情報にアクセスするためのCライブラリルーチン
(<grp.h>
とgetgrent
)を提供している。このファイル
(`/etc/group')があった場合でも、それに完全な情報があるとはかぎらない。
このため、ユーザーデータベースを扱うのにその出力としてグループデータベース
を出力する小さなCプログラムを作る必要があるのである。
次に挙げるプログラムは、グループデータベースを"cat"するCプログラム
grcat
である。
/* * grcat.c * * 印字可能なグループデータベースを生成する * * Arnold Robbins, arnold@gnu.org * May 1993 * Public Domain */ #include <stdio.h> #include <grp.h> int main(argc, argv) int argc; char **argv; { struct group *g; int i; while ((g = getgrent()) != NULL) { printf("%s:%s:%d:", g->gr_name, g->gr_passwd, g->gr_gid); for (i = 0; g->gr_mem[i] != NULL; i++) { printf("%s", g->gr_mem[i]); if (g->gr_mem[i+1] != NULL) putchar(','); } putchar('\n'); } endgrent(); exit(0); }
グループデータベースの各行は、一つのグループである。そこにあるフィールド はコロンで区切られ、個々のフィールドは以下に挙げるような情報を表わしてい る。
$5
から$NF
までのフィールドに返ってくる
(`/dev/user'はgawk
の拡張であることに注意。
セクション Special File Names in gawk
を参照.)。
次にgrcat
を実行したときの出力例を挙げる。
$ grcat -| wheel:*:0:arnold -| nogroup:*:65534: -| daemon:*:1: -| kmem:*:2: -| staff:*:10:arnold,miriam,andy -| other:*:20: ...
以下に挙げるのは、グループデータベースから情報を得るための関数群である。 このうち幾つかは同名のCライブラリ関数から来ている。
# group.awk -- グループファイルを扱うための関数 # Arnold Robbins, arnold@gnu.org, Public Domain # May 1993 BEGIN \ { # あなたのシステムにあわせて変更すること _gr_awklib = "/usr/local/libexec/awk/" } function _gr_init( oldfs, oldrs, olddol0, grcat, n, a, i) { if (_gr_inited) return oldfs = FS oldrs = RS olddol0 = $0 FS = ":" RS = "\n" grcat = _gr_awklib "grcat" while ((grcat | getline) > 0) { if ($1 in _gr_byname) _gr_byname[$1] = _gr_byname[$1] "," $4 else _gr_byname[$1] = $0 if ($3 in _gr_bygid) _gr_bygid[$3] = _gr_bygid[$3] "," $4 else _gr_bygid[$3] = $0 n = split($4, a, "[ \t]*,[ \t]*") for (i = 1; i <= n; i++) if (a[i] in _gr_groupsbyuser) _gr_groupsbyuser[a[i]] = \ _gr_groupsbyuser[a[i]] " " $1 else _gr_groupsbyuser[a[i]] = $1 _gr_bycount[++_gr_count] = $0 } close(grcat) _gr_count = 0 _gr_inited++ FS = oldfs RS = oldrs $0 = olddol0 }
BEGIN
ルールではプライベート変数にgrcat
が置かれているディレ
クトリをセットしている。これはawk
ライブラリルーチンを手助けするの
に使われるので、我々はこれを`/usr/local/libexec/awk'.に置くことを選
択した。使用するシステムによっては、異なったディレクトリに置きたくなるか
もしれない。
これらのルーチンはユーザーデータベースルーチンと同じようなアウトラインに
従っている(セクション Reading the User Databaseを参照)。
_gr_inited
という変数はグループデータベースを二度以上走査しな
いようにすることを保証するためのものである_gr_init
という関数
は最初に、FS
、RS
、$0
の値をセーブし、その後で
FS
とRS
にグループ情報を走査するための値をセットしている。
グループ情報は幾つかの連想配列に格納されている。これらの配列はグループ名
(_gr_byname
)、グループID番号(_gr_bygid
)、データベ
ース中の位置(_gr_bycount
)で添え字付けされる。さらにユーザー名
(_gr_groupsbyuser
)で添え字付けされる配列があり、その内容は各
ユーザーが所属しているグループがスペースで区切られているリストである。
ユーザーデータベースとは異なり、同じグループのために複数のレコードが使わ れる可能性がある。これはあるグループに多数のメンバーが所属しているときに は一般的なことである。そのようなエントリのペアは次のような形式である。
tvpeople:*:101:johny,jay,arsenio tvpeople:*:101:david,conan,tom,joan
このため、_gr_init
はグループ名やグループID番号が既にでてきたもの
かどうかをチェックしている。もし以前にでてきたものであれば、ユーザー名は
単にそれまでのユーザーのリストに連結されるだけである(先に述べたように、
このプログラムには微妙な問題がある。最初に呼び出されたときには名前がない
という仮定をしている。このプログラムでは追加するときに、(追加する)名前の
前にカンマを置いている。これは、$4
がすでにあるかどうかをチェック
していない)。
最後に_gr_init
はgrcat
に対するパイプをクローズし、FS
、
RS
、$0
の値を元に戻し、_gr_count
を0で初期化(後で使用
する)し、_gr_inited
を非0にしている
function getgrnam(group) { _gr_init() if (group in _gr_byname) return _gr_byname[group] return "" }
getgrnam
関数はグループ名を引数にとり、そのグループが存在していれば
それを返し、なければ空文字列を返す。
function getgrgid(gid) { _gr_init() if (gid in _gr_bygid) return _gr_bygid[gid] return "" }
同様にgetgrgid
関数は引数にグループID番号をとり、そのグループIDを
元に情報を検索する。
function getgruser(user) { _gr_init() if (user in _gr_groupsbyuser) return _gr_groupsbyuser[user] return "" }
getgruser
関数は対応するCのライブラリ関数はない。
この関数はユーザー名を引数にとり、そのユーザーがメンバーとなっている
グループのリストを返す。
function getgrent() { _gr_init() if (++_gr_count in _gr_bycount) return _gr_bycount[_gr_count] return "" }
getgrent
関数はデータベースのエントリを一度に一エントリずつ
走査していく。gr_count
という変数はリスト中のポジションを
保持するのに使われている。
function endgrent() { _gr_count = 0 }
endgrent
は_gr_count
を0にリセットし、
getgrent
が再実行できるようにする。
ユーザーデータベースを扱ったルーチンと同じように、各関数は_gr_int
を配列の初期化のために呼び出している。その呼び出しは、これらの関数が使っ
たときにgrcat
を実行する余計なオーバーヘッドを生じるだけである
(_gr_init
の本体をBEGIN
ルールに移動したときと同じように)。
作業のほとんどはデータベースの走査と、様々な連想配列の
構築である。ユーザーの呼び出す関数はそれ自身は非常に単純であり、
行う作業はawk
の連想配列に頼っている。
これらの関数を使ったid
プログラムが
セクション ユーザー情報を出力するを参照,
にある。
て、変数はグローバル(global、プログラム全体から使用可能である)か、 ローカル(local、特定の関数でだけ使用可能である)のいずれかであり、C でのスタティック(static)変数にあたるものはない。
ライブラリ関数はしばしば、複数回の関数呼び出しの間に以前の情報を保存でき
るようにするためのグロバール変数を必要とする。例えば、getopt
の_opti
という変数(セクション コマンドラインオプションの処理を参照)や、
mktime
で使われている_tm_months
という配列
(@pxref {Mktime Function, ,Turning Dates Into Timestamps})がある。
このような変数は、ライブラリ中の一つの関数だけが使用するのでプライベート
(privat e)であると呼ばれる。
ライブラリ関数を記述するとき、プライベート変数の名前には 他のライブラリ関数や、ユーザーのメインプログラムで使われている変数と 同じ名前をつけないようにすべきである。たとえば、`i'とか `j'といった名前はよい選択ではない。なぜなら、これは ユーザープログラム中で、しばしば別の目的で使用されるような 変数名であるからである。
この章で挙げたサンプルプログラムでは、プライベート変数は その名前がすべてアンダースコア(`_')から始まっていた。 ユーザーは通常、変数名の先頭にはアンダースコアを使用せず、 そのためこの命名規則はユーザープログラムと同じ名前の変数を 使ってしまうという事故にあう機会を減らすのである。
それに加えて、幾つかのライブラリ関数ではその変数を使用している関数をわか
りやすくするために、プリフィックス(prefix)を使っている。例えば、
mktime
では_tm_months
を使っており(セクション Turning Dates Into Timestampsを参照)、データベースルーチンでは_pw_byname
という
ものを使っていた
(セクション Reading the User Databaseを参照)。
この命名規則は変数名の衝突が起こる可能性をさらに低くでき、使うことを推奨
する。この命名規則は、変数名と、プライベート関数の名前の両方に等しく適用
できることに注意すること。
私はこの命名規則を使ってライブラリルーチンの全てを書き直すこともできたが、
私(著者)自身のawk
プログラミングのスタイルがどのように進歩してきた
かを説明するため、またこの議論についての若干のとっかかりを提供するために、
あえて書き直しは行わなかった。
変数の名前付けに関しての最後の注意事項として、関数がメインプログラムから
使用できるようなグローバル変数を作った場合、そういった変数の名前を大文字
から始めるのがよい命名規則である。例えば、getopt
のOpterr
や
Optind
といった変数がその例である
(セクション コマンドラインオプションの処理を参照)。
先頭の大文字はグローバルであることを示し、また全てが大文字ではない変数名
なので、FS
のような組込み変数の一つではないことを示している。
ライブラリ関数中の、状態を保持し続ける必要がないすべての変数をロ ーカルとして宣言することも同様に重要なことであるもしこれが行われていなけ れば、そのような変数がユーザープログラムでたまたま使われる可能性があり、 それによって追いかけるのが非常に難しいバグを導きかねないのである。
function lib_func(x, y, l1, l2) { ... use variable some_var # some_var could be local ... # but is not by oversight }
異なる命名規則として、Tclコミュニティで一般的に使われている
ライブラリ関数や"パッケージ"が必要としている値を一つの連想配列を使って
保持するというものがある。これは、実際に使用するグローバルな名前を
劇的に減らすものである。例えば、
セクション Reading the User Databaseを参照.
で説明されている関数では、
_pw_inited
, _pw_awklib
, _pw_total
,
_pw_count
といった変数の代わりに
PW_data["inited"]
, PW_data["total"]
,
PW_data["count"]
, PW_data["awklib"]
を使うのである。
このセクションで説明された命名規則は実際には協定といったものであり、 プログラムを書くときにはこれに 必ず従わなければならないというものではないが、 我々はこのような方法をとることをお勧めする。