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

awkの関数ライブラリ

この章では、便利なawk関数のライブラリを紹介する。 これら関数を使ったサンプルプログラムはこの後 (セクション 実用的な awk プログラムを参照) にある。

セクション Texinfoのソースファイルからプログラムを取り出すを参照, では、このマニュアルのTexinfoのソースファイルから、ライブラリ関数の サンプルとプログラムを取り出して使うためのプログラムを提供している (これはgawkの配布キットの一部として既に済んでいる)。

あなたが便利な、汎用目的のawk関数を作成し、それを このマニュアルの一部として寄付したいのなら、著者に連絡して欲しい。 そのための情報はセクション 問題やバグの報告を参照にある。 コードを送るだけではなくて、あなたのコードをパブリックドメインにしたり、 GPL(セクション GNU GENERAL PUBLIC LICENSEを参照)の元で公表したり、あるい は著作権を Free Software Foundationに譲渡することが求められる。

Simulating 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) }

これにより、すべての正規表現の検査と、文字列の比較が 小文字を使ってのみ行われる。

Implementing 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の組込み機能としたのか?"ということである。 これは重要な質問である。 機能の追加はプログラムを大きく遅くして、保守を難しくしてしまい かねないからだ。

この質問に対する回答は、nextfilegawkに組み込んでしまうことで 効率が上がるから、ということである。もし、nextfileが大きなデータファイルの先 頭で実行されたとすると、awkはファイル全体を読み続けなければならず、 (必要がないのに)ファイルをレコードに分割して、それをスキップする。組込み のnextfileは、単純にファイルを即座にクローズ して次のファイルの 処理を開始できるので時間を大幅に節約できる。これは特にawkプログ ラムは一般的に入出力に律速されるので、重要なことである(つまり、処理時間 の大部分が計算ではなく、入力や出力に費やされるということ)。

Assertions

大きなプログラムを作るとき、ある条件が真であるかどうかを知ることができる と便利である。ある処理を行う前に、前提としている条件を文にして置く。この ような文は"表明"(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を参照)。 そして、プログラムは一見ハングアップしたかのようになり、 入力を待つ。

Rounding Numbers

printfsprintf (セクション 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) }

Translating Between Characters and Numbers

ある商用の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ではコメントアウトされている。

Merging an Array Into a String

文字列の処理を行っているときには、 配列に格納されている文字列を連結して一つの長い文字列にするような ことができると便利である。 次に挙げる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に連接のための代入演算子があれば良かったかもしれない。 連接を行うための演算子が存在していないということは、 文字列に関する操作を必要以上に難しいものとしている。

Turning Dates Into Timestamps

systimeawkに組込みの関数であり、 タイムスタンプをシステムが開始された時刻からの経過秒数として 返すものである。この時刻情報はstrftimeという 組込み関数を使用して、自由自在な書式で出力可能なデータに することができる(systimestrftimeに関する詳しい説明は セクション Functions for Dealing with Time Stampsを参照.)。

興味深いが難しい問題は、標準形に変換されている日付のデータを タイムスタンプに戻すということである。ANSI Cのライブラリでは mktimeという関数を、通常の形式の日付データをタイムスタンプに 変換する目的で提供している。

gawkは組込み関数として、 Cバージョンを単純に"hook"したmktimeを提供すべきであると 考えるかもしれない。しかし、mktimeawkで完全に実現すること ができるのである。

以下に挙げたのは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ルールをまとめることの利益 (セクション 特殊パターンBEGINENDを参照) は特にライブラリファイルを記述するときに顕著である。 ライブラリファイル中の関数は、関数のプライベートデータの初期化をきれいに 行うことができるようになり、同様にプライベートな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が要求する"標準形式"を生成することができる。

Managing the Time of Day

セクション Functions for Dealing with Time Stampsを参照, で説明されているsystimestrftimeという関数は 日付を人間が読みやすい形で扱うために最低限必要な機能を 提供している。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は先に挙げたようなものが提供されている。 この関数に対するより一般的なデザインとしては、カレントの時刻の 代わりに使われるようなオプションのタイムスタンプを取れるように するというものがあるだろう。

Noting Data File Boundaries

BEGINルールとENDルールはそれぞれawk プログラムの実行開始時と終了時に一回だけ実行される (セクション 特殊パターンBEGINENDを参照)。 以前、BEGINルールは各データファイルの先頭で実行され、ENDル ールは各データファイルの最後で実行されるものと間違って考えていたユーザー がいた。我々(gawkの作者)がそのユーザーに対してその間違いを知らせ たところ、その人は我々にBEGIN_FILEEND_FILEという名前の そこで期待してた動作をする特殊ルールを新たにgawkに追加してくれる ようリクエストしてきた。彼は、そのためのコードも送ってきた。

しかし、ちょっと考えた後で、私は後述するようなライブラリプログラムを 提供した。これは二つのユーザー定義関数beginfileendfileを それぞれ各データファイルの先頭と末尾で呼び出すようにアレンジした。 たったの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) }

このファイルは、その中にあるルールを最初に実行させるために ユーザーの"メイン"プログラムの前にロードしなければならない。

このルールはawkFILENAMEという変数を、新しいファイルに 処理が移ったときに自動的に変更するということに依存している。 カレントのファイル名はプライベート変数の_oldfilenameに 格納されている。FILENAME_oldfilenameと等しくないとき、 新しいデータファイルが処理され始め、このとき古いファイルのために endfileを呼ぶ必要がある。endfileはファイルが処理された後で 呼び出されるべきものなので、プログラムでは_oldfilenameが 空文字列でないことを最初にチェックしている。それから、 カレントファイルの名前を_oldfilenameにセットして、 新しいデータファイルのためにbeginfileを呼び出している。 すべてのawkの変数と同様に、_oldfilenameは空文字列で 初期化されているので、このルールは最初のデータファイルのときでも 正しく実行されるのである。

このプログラムは同様にENDルールがあり、これは 最後のファイルを処理したときの最終処理を行うためのものである。 このENDルールは"メイン"プログラムにあるどのENDルール よりも前にあるので、endfileは一番最初に呼び出される。 もう一度複数のBEGINルールとENDルールの価値を明確に しておこう。

このバージョンには、最初のバージョンのnextfileと同じ問題がある (セクション Implementing nextfile as a Functionを参照)。 同じデータファイルが二度コマンドラインで連続して指定された場合、 endfilebeginfileはそれぞれ 最初のパスの最後と、二番目のパスの先頭では実行されないという ことになってしまう。 以下のものは、この問題に対処したものである

# 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ルールはOpterrOptindを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 プログラムを参照. に幾つか挙げられている。

Reading the User Database

`/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プログラムを書く必要がある。getpwentstruct 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'フ ォーマットである。フィールドには以下のものがある。

Login name
ユーザーのログイン名。
Encrypted password
ユーザーの、暗号化されたパスワード。これは一部のシステムでは使用できない。
User-ID
ユーザーの数字で表されたユーザーID。
Group-ID
ユーザーの数字で表されたグループID。
Full name
ユーザーのフルネームと、おそらくはユーザーに関するその他の情報。
Home directory
ユーザーのログインする、もしくは"ホームディレクトリ" (シェルプログラマには$HOMEとして知られている)。
Login shell
ユーザーがログインしたときに実行されるプログラム。 これは通常Bash(GNU Bourne-Again shell)のようなシェルである。

以下に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から情報を読み込んでいるので、 最初にFSRS$0の値をセーブしている。これはこれら の関数がユーザープログラムのどんなところからでも呼ばれる可能性があり、ユ ーザーが独自のFSRSを設定している可能性があるので、それ に対処するために必要なことである。

関数のメイン部分ではループを使って、データベースの一行読み込み、読み込ん だ一行のフィールドへの分割、必要に応じて読み込んだ行を配列要素にセットと いう一連の手順を行っている。ループが完了したときに、_pw_initは パイプのクローズ、_pw_initedへの1のセット、FSRS$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 Group Database

グループデータベースに対する同様な操作のより詳しい議論は セクション 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);
}

グループデータベースの各行は、一つのグループである。そこにあるフィールド はコロンで区切られ、個々のフィールドは以下に挙げるような情報を表わしてい る。

Group Name
グループ名。
Group Password
暗号化されたグループパスワード。実際にはこのフィールドは使われない。 通常はこのフィールドは空であるか、`*'がセットされている。
Group ID Number
グループID番号を表わす数値。この数字はファイルの中で ユニークなもので必要がある。
Group Member List
カンマで区切られたユーザー名のリスト。これらのユーザーはそのグループのメ ンバーである。ほとんどのUNIXシステムは、ユーザーが同時に幾つかのグループ に所属することを許している。それができるシステムであれば、`/dev/user' を読んだときに所属しているグループIDの番号が、$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という関数 は最初に、FSRS$0の値をセーブし、その後で FSRSにグループ情報を走査するための値をセットしている。

グループ情報は幾つかの連想配列に格納されている。これらの配列はグループ名 (_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_initgrcatに対するパイプをクローズし、FSRS$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プログラムが セクション ユーザー情報を出力するを参照, にある。

Naming Library Function Global Variables

て、変数はグローバル(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プログラミングのスタイルがどのように進歩してきた かを説明するため、またこの議論についての若干のとっかかりを提供するために、 あえて書き直しは行わなかった。

変数の名前付けに関しての最後の注意事項として、関数がメインプログラムから 使用できるようなグローバル変数を作った場合、そういった変数の名前を大文字 から始めるのがよい命名規則である。例えば、getoptOpterrOptindといった変数がその例である (セクション コマンドラインオプションの処理を参照)。 先頭の大文字はグローバルであることを示し、また全てが大文字ではない変数名 なので、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"] を使うのである。

このセクションで説明された命名規則は実際には協定といったものであり、 プログラムを書くときにはこれに 必ず従わなければならないというものではないが、 我々はこのような方法をとることをお勧めする。


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