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

実用的な awk プログラム

この章ではあなたが読むためのawkプログラムの寄せ集めを提供する。 この章には二つのセクションがある。最初の章は 幾つかの一般的なPOSIXユーティリティのawk バージョンを提供し、二番目の章は面白いプログラムの 福袋(grab-bag)である。

これらのプログラムの多くは セクション awkの関数ライブラリを参照. にあるライブラリ関数を使用している。

Re-inventing Wheels for Fun and Profit

このセクションでは、幾つかのawkによって実現したPOSIXユーティリテ ィがある。これらのプログラムをawkで作成しなおすということは、アル ゴリズムが非常に明確に説明されているのと一般的にプログラムコードが簡潔か つ単純であるので楽しいことである。これはawkがそれをするのに適して いるからである。

これらのプログラムが、今あなたの使っているシステムのそれを置き換えるため のものではないことに注意して欲しい。これらのプログラムの目的は、"現実世 界"の作業におけるawk言語プログラミングを説明することである。

プログラムはアルファベット順に並んでいる。

フィールドとカラムの切り出し

cutユーティリティは、標準入力からの入力中にあるキャラクタかフィー ルドに対して選択もしくは"カット"(cut)を行い、その結果を標準出力に送る。 cutはキャラクタのリストか、フィールドのリストを切り出すことができ る。デフォルトでは、フィールドはタブで区切られているとみなされるが、コマ ンドラインオプションで指定することによりフィールドデリミッタ(filed delimiter)、言い換えればフィールドを区切るキャラクタを指定することもできる。 cutの定義するフィールドはawk程には汎用性はない。

一般的なcutの使用はwhoの出力からログオンしているユーザーの ログイン名だけを切り出すといったものだろう。たとえば、次の例に挙げるパイ プラインは、ログオンしているユーザーのソートされ、重複が取り除かれたリス トを生成する。

who | cut -c1-8 | sort | uniq

cutのオプションには以下に挙げるのものがある

-c list
listを切り抜く(cut out)するキャラクタのリストとする。 リスト中の要素はカンマで区切ったり、キャラクタの間にダッシュを置いて 範囲を表わすこともできる。 `1-8,15,22-35'というリストは1番目から8番め、15番め、22番目から 35番めまでのキャラクタを指定するものである。
-f list
切り出すフィールドのリストとしてlistを使う。
-d delim
delimをタブキャラクタの代わりにフィールドを区切るキャラクタとする。
-s
フィールド区切りがない行を出力しないようにする。

awkによるcutは、getoptライブラリ関数 (セクション コマンドラインオプションの処理を参照), と joinライブラリ関数 (セクション Merging an Array Into a Stringを参照). を使っている。

プログラムは、プログラムのオプションと使用法を出力 (そのあとプログラムを終了させる)するuasgeという関数から 始まっている。 usageは不正な引数が渡されたときに呼び出される。

# cut.awk -- awkによるcut
# Arnold Robbins, arnold@gnu.org, Public Domain
# May 1993

# Options:
#    -f list        Cut fields切り取るフィールド
#    -d c           Field delimiter characterフィールド区切りのキャラクタ
#    -c list        Cut characters切り取るキャラクタ
#
#    -s        Suppress lines without the delimiter character
区切りキャラクタがない行の出力を抑制する

function usage(    e1, e2)
{
    e1 = "usage: cut [-f list] [-d c] [-s] [files...]"
    e2 = "usage: cut [-c list] [files...]"
    print e1 > "/dev/stderr"
    print e2 > "/dev/stderr"
    exit 1
}

変数e1e2は関数が ページ にフィットするために使われれている。

次にくるのはBEGINルールで、ここではコマンドラインオプションの解析 を行っている。FSにはcutのデフォルトのキャラクタである単一 のタブキャラクタをセットし、出力フィールドセパレータも入力セパレータと同 じにする。それからgetoptはコマンドラインオプションを処理するため に使われている。by_fieldsby_charsという変数のいずれかが セットされ、フィールドによって処理するのかキャラクタによって処理するのか を指示する。キャラクタによって切り出しを行うとき、出力のフィールドセパレ ータは空文字列である。

BEGIN    \
{
    FS = "\t"    # デフォルト
    OFS = FS
    while ((c = getopt(ARGC, ARGV, "sf:c:d:")) != -1) {
        if (c == "f") {
            by_fields = 1
            fieldlist = Optarg
        } else if (c == "c") {
            by_chars = 1
            fieldlist = Optarg
            OFS = ""
        } else if (c == "d") {
            if (length(Optarg) > 1) {
                printf("Using first character of %s" \
                " for delimiter\n", Optarg) > "/dev/stderr"
                Optarg = substr(Optarg, 1, 1)
            }
            FS = Optarg
            OFS = FS
            if (FS == " ")    # awkのsemanticsを上書きする
                FS = "[ ]"
        } else if (c == "s")
            suppress++
        else
            usage()
    }

    for (i = 1; i < Optind; i++)
        ARGV[i] = ""

特に注意するのはフィールドデリミッタがスペースであるときである。 " "(単一のスペース)をFSの値にするというのは正しくない。 awkはこれによってスペース、タブ、改行の連続(一つでも良い)を、フィールド を分割するものとして扱う。我々はここでは個々のスペースで分割を行うことを 望んでいる。また、getoptが処理した後では、awkがそれらのオ プションをファイル名として扱わないようにするためにARGVの一番目か らOptindまでの要素をクリアする必要がある ことにも注意すること。

コマンドラインオプションを処理した後で、プログラムはそれらのオプションが 正しいものであることを検査する。`-c'`-f'はいずれか一つだけが 使われ、かつどちらの場合もフィールドリストが必要である。 set_fieldlistset_charlistはそれぞれフィールドリストやキャ ラクタリストを取り出すために呼び出される。

    if (by_fields && by_chars)
        usage()

    if (by_fields == 0 && by_chars == 0)
        by_fields = 1    # デフォルト

    if (fieldlist == "") {
        print "cut: needs list for -c or -f" > "/dev/stderr"
        exit 1
    }

    if (by_fields)
        set_fieldlist()
    else
        set_charlist()
}

下にあるコードはset+fieldlistである。最初にフィールドリストを カンマで分割し、配列にしている。それから配列の各要素を それが範囲指定であるかをチェックして、範囲指定であれば それをさらに分割する。 続いて範囲指定が最初の数字が二番目の数字より小さいかどうかをチェックし、 リスト中の各数字はflistという配列に加えていって 出力するフィールドの単純なリストを作成する。 そして、プログラムはawkにフィールド分割を行わせる。

function set_fieldlist(        n, m, i, j, k, f, g)
{
    n = split(fieldlist, f, ",")
    j = 1    # index in flist
    for (i = 1; i <= n; i++) {
        if (index(f[i], "-") != 0) { # 範囲
            m = split(f[i], g, "-")
            if (m != 2 || g[1] >= g[2]) {
                printf("bad field list: %s\n",
                                  f[i]) > "/dev/stderr"
                exit 1
            }
            for (k = g[1]; k <= g[2]; k++)
                flist[j++] = k
        } else
            flist[j++] = f[i]
    }
    nfields = j - 1
}

seet_charlist関数はset_fieldlistよりも複雑である。ここでは 固定長の入力を記述するgawkFIELDWIDTHS (セクション 固定長データの読み込みを参照)を使っている。キャラク タリストを使うとき、我々はこれを使う。

FIELDWIDTHSの設定は、単純なフィールドの設定よりも 複雑である。出力するフィールドを記録しつつ、余計なキャラクタはスキップし なければならない。例えば、1番目から8番め、15番め、22番目から35番目までの キャラクタを取り出したいとする。これは`-c 1-8,15,22-35'という指定を すれば良い。このときFIELDWIDTHS"8 6 1 6 14"という値 になる。これには5つのフィールドがあり、出力すべきものは$1$3、 $code{$5}である。間にあるフィールドは"埋め草"(filler)であり、無 視されるデータである。

flistは、出力するフィールドのリストであり、 tは埋め草も含めた完全なフィールドのリストである。

function set_charlist(    field, i, j, f, g, t,
                          filler, last, len)
{
    field = 1   # count total fields フィールドの合計をカウントする
    n = split(fieldlist, f, ",")
    j = 1       # index in flist flistの添え字
    for (i = 1; i <= n; i++) {
        if (index(f[i], "-") != 0) { # 範囲
            m = split(f[i], g, "-")
            if (m != 2 || g[1] >= g[2]) {
                printf("bad character list: %s\n",
                               f[i]) > "/dev/stderr"
                exit 1
            }
            len = g[2] - g[1] + 1
            if (g[1] > 1)  # 埋め草の長さを計算する
                filler = g[1] - last - 1
            else
                filler = 0
            if (filler)
                t[field++] = filler
            t[field++] = len  # フィールドの長さ
            last = g[2]
            flist[j++] = field - 1
        } else {
            if (f[i] > 1)
                filler = f[i] - last - 1
            else
                filler = 0
            if (filler)
                t[field++] = filler
            t[field++] = 1
            last = f[i]
            flist[j++] = field - 1
        }
    }
    FIELDWIDTHS = join(t, 1, field - 1)
    nfields = j - 1
}

次にあるコード片は実際にデータを処理するルールである。`-s'オプショ ンが指定されている場合、suppressが真になる。最初のif文は入 力レコードにフィールドセパレータが含まれているかどうかをチェックしている。 cutがフィールドを処理し、suppressが真であり、フィールドセ パレータがレコードにない場合には、そのレコードはスキップされる。

レコードが正当なものであれば、gawkはこの時点でFSにあるキャ ラクタか、FIELDWIDTHSを使った固定長フィールドでのレコードのフィー ルドへの分割は完了している。ループは出力すべきフィールドのリストを参照し、 対応するフィールドにデータがあればそのフィールドが出力される。同様に次の フィールドがデータを持っていれば、間にセパレータキャラクタが出力される。

{
    if (by_fields && suppress && $0 !~ FS)
        next

    for (i = 1; i <= nfields; i++) {
        if ($flist[i] != "") {
            printf "%s", $flist[i]
            if (i < nfields && $flist[i+1] != "")
                printf "%s", OFS
        }
    }
    print ""
}

このcutでは、gawkFIELDWIDTHS変数の キャラクタベースの取り出しに依存している。 他のawk処理系では、substrを使って同じことが実現できるが (セクション Built-in Functions for String Manipulationを参照)、 それは実行するには実際骨の折れる作業となるだろう。 FIELDWIDTHS変数は、キャラクタによって入力行を分けて 取り出すような問題の優雅な解決策を提供する。

ファイル中の正規表現を探す

egrepユーティリティはファイルからパターンを検索するが、 awkで使えるものとほぼ同じ正規表現を使っている (セクション 正規表現定数を参照)。 egrepは次のように使用する。

egrep [ options ] 'pattern' files ...

patterは正規表現である。典型的な使い方では、この正規表現はシェルが メタキャラクタをファイル名のワイルドカードと見なして展開してしまうのを防 ぐためにクォートされている。通常、egrepは正規表現がマッチした行を 出力する。コマンドラインには複数のファイル名を置くことができ、出力にはパ ターンが見つかったファイルの名前がコロンの前に置かれる。

オプションは以下の通り。

-c
パターンにマッチした行数を出力する。
-s
静かにさせる。何も出力せず、終了コードが パターンにマッチしたかどうかの値を表わす。
-v
テストの条件を逆にする。egrepは 与えられたパターンにマッチしなかったときに、その行を出力し、 パターンがマッチしなかったときに成功の終了コードを返して 終了する。
-i
パターンと、入力データ両方の大小文字の区別を無視する。
-l
マッチしたファイルの名前だけを出力し、マッチした行は出力しない。
-e pattern
patternをマッチを試みる正規表現として使用する。 これは、`-'で始まるパターンを許すためのものである。

このバージョンでは、getoptライブラリ関数と (セクション コマンドラインオプションの処理を参照)、 file transitionライブラリプログラム (セクション Noting Data File Boundariesを参照) を使っている。

このプログラムはコメントで始まり、次いで getoptを使ってコマンドライン引数を処理する BEGINルールがくる。 `-i'(太小文字の無視)オプションはgawkでは とても簡単である。ただ単にIGNORECASE変数を (セクション 組み込み変数を参照)使うだけで良い。

# egrep.awk -- awkを使ってegrepをシミュレートする
# Arnold Robbins, arnold@gnu.org, Public Domain
# May 1993

# Options:
#    -c    行を数える
#    -s    静かにする。終了コードを使用する。
#    -v    テストを逆にして、マッチしなかったときに成功
#    -i    大小文字の違いを無視する
#    -l    ファイル名だけを出力する
#    -e    引数がパターンであることを明示する

BEGIN {
    while ((c = getopt(ARGC, ARGV, "ce:svil")) != -1) {
        if (c == "c")
            count_only++
        else if (c == "s")
            no_print++
        else if (c == "v")
            invert++
        else if (c == "i")
            IGNORECASE = 1
        else if (c == "l")
            filenames_only++
        else if (c == "e")
            pattern = Optarg
        else
            usage()
    }

以下のコード片はegrep特有の動作を扱うものである。`-e'を使っ てパターンが指定されていなかった場合、最初のオプションでないコマンドライ ン引数がパターンとして使用される。awkのコマンドライン引数は ARGV[Optind]までクリアされ、awkがそれらの引数をファイルと して扱わないようにする。ファイルが指定されていない場合には標準入力が使わ れ、複数のファイルが指定されている場合にはマッチした行を出力するときにフ ァイル名を前置するようにする。

最後の二行は、gawkでは不要なのでコメントアウトしている。 この二行は他のawkを使う場合にはコメントをはずす 必要がある。

    if (pattern == "")
        pattern = ARGV[Optind++]

    for (i = 1; i < Optind; i++)
        ARGV[i] = ""
    if (Optind >= ARGC) {
        ARGV[1] = "-"
        ARGC = 2
    } else if (ARGC - Optind > 1)
        do_filenames++

#    if (IGNORECASE)
#        pattern = tolower(pattern)
}

以下のコード片はgawkでは使わないのでコメントアウトしている 行である。 このルールは `-i'オプションが指定されたときに 入力された行を小文字に変換するものだが、 gawkでは必要ではないのでコメントアウトされている。

#{
#    if (IGNORECASE)
#        $0 = tolower($0)
#}

beginfileという関数は、新しいファイルが処理されたときに `ftran.awk'から呼び出される。この例は非常に単純で、fcountと いう変数を0にすることが全てである。fcountはカレントファイルで、 パターンがどれだけマッチしたかということを記録している。

function beginfile(junk)
{
    fcount = 0
}

endfileは各ファイルを処理し終えたときに呼び出される。これは、ユー ザーがマッチした行数を希望したときにだけ使用される。no_printとい う変数は終了コードが要求されたときにだけ真になる。count_onlyはマ ッチした行数だけを要求されたときに真となる。egrepはしたがって、出 力とカウントがイネーブルのときだけ行数を出力する。出力のフォーマットは処 理するファイルの数によって調整されなければならない。最後に、fcounttotalに足しこまる。これはパターンが全体でどれだけの行にマッチ したかということを知るためのものである。

function endfile(file)
{
    if (! no_print && count_only)
        if (do_filenames)
            print file ":" fcount
        else
            print fcount

    total += fcount
}

以下のルールは、行のマッチング作業のほとんどを行っている。matches という変数は行がパターンにマッチしたときに真となる。ユーザーがマッチしな かった行を望んだ場合には、`!'演算子を使ってmatchesの意味を逆 転している。fcountmatchesの数だけインクリメントされる。 インクリメントする値は0か1であり、これはマッチが成功したかしなかったかに よる。行がマッチしなければ、next文を使って次のレコードへ単に移行 させる。

幾つかの、パフォーマンスを向上させるための二、三行のコードがある。ユーザ ーが終了ステータスだけを必要としているとき(no_printが真のとき)に は行をカウントする必要はなく、ファイルには一行マッチすれば十分でありその 時点でnextfileを使って次のファイルへ処理を移すことができる。同様 に、ファイル名だけを出力すればいい場合にも行をカウントする必要はなく、フ ァイル名を出力してしまえばnextfileを使って次のファイルへ処理を移 すことができる。

最後に各行を出力し、必要であればファイル名とコロンを前置する。

{
    matches = ($0 ~ pattern)
    if (invert)
        matches = ! matches

    fcount += matches    # 1 か 0

    if (! matches)
        next

    if (no_print && ! count_only)
        nextfile

    if (filenames_only && ! count_only) {
        print FILENAME
        nextfile
    }

    if (do_filenames && ! count_only)
        print FILENAME ":" $0
    else if (! count_only)
        print
}

ENDルールは正しい終了ステータスを取り扱う。 マッチした行がなければ、終了ステータスは1、あれば0となる。

END    \
{
    if (total == 0)
        exit 1
    exit 0
}

usage関数は不正なオプションが渡されたときに使用法のメッセージを出 力し、実効を終了する。

function usage(    e)
{
    e = "Usage: egrep [-csvil] [-e pat] [files ...]"
    print e > "/dev/stderr"
    exit 1
}

eという変数は関数を印刷されるページに きちんと納めるために使われている。

プログラミングスタイルに関するのと同じように、ENDルールに バックスラッシュによる行継続を使って、開きブレースのみを行に置く ようなことをしたいと思うかもしれない。これは この章にある 多くの例で使っている関数の記述スタイルにより近づけるものである。 あなたはBEGINルールやENDルールを記述するときに このスタイルを使って書くかどうかを自由に決めることができる。

ユーザー情報を出力する

idユーティリティはあるユーザーの実ユーザーID、実効ユーザーID、実 グループID、実効グループID、そして(存在していれば)ユーザーのグループセッ トをリストアップするものである。idは本人以外の誰かを指定して実行 した場合には、実効IDと実効グループIDだけを出力するもし可能なら、id は対応するユーザー名とグループ名を出力する。idの出力は以下のよ うなものである。

$ id
-| uid=2076(arnold) gid=10(staff) groups=10(staff),4(tty)

この情報は実際にgawkの特殊ファイル`/dev/user' から得られたものである (セクション Special File Names in gawkを参照)。

しかし、idユーティリティは単なる数字の並びよりも よりわかりやすい出力を行う。

次に挙げるのはシンプルなバージョンの awkによるidである。 この例ではユーザーデータベースライブラリの関数 (セクション Reading the User Databaseを参照)と グループデータベースライブラリの関数 (セクション Reading the Group Databaseを参照)を 使っている。

プログラムは実に簡単なものであって、BEGINルール中で全てのことを行 っている。ユーザーID番号とグループID番号は`/dev/user'から取得してい る。もし`/dev/user'がサポートされていなければ、プログラムはその時点 でギブアップする。

プログラムコードは反復的である。実ユーザーID番号のためのユーザーデータベ ースのエントリは`:'を区切りとして分割される。名前は最初のフィールド にある。同様のコードが実効ユーザーID番号とグループ番号のために使われてい る。

# id.awk -- idのawkによる実装
# Arnold Robbins, arnold@gnu.org, Public Domain
# May 1993

# output is:
# uid=12(foo) euid=34(bar) gid=3(baz) \
#             egid=5(blat) groups=9(nine),2(two),1(one)

BEGIN    \
{
    if ((getline < "/dev/user") < 0) {
        err = "id: no /dev/user support - cannot run"
        print err > "/dev/stderr"
        exit 1
    }
    close("/dev/user")

    uid = $1
    euid = $2
    gid = $3
    egid = $4

    printf("uid=%d", uid)
    pw = getpwuid(uid)
    if (pw != "") {
        split(pw, a, ":")
        printf("(%s)", a[1])
    }

    if (euid != uid) {
        printf(" euid=%d", euid)
        pw = getpwuid(euid)
        if (pw != "") {
            split(pw, a, ":")
            printf("(%s)", a[1])
        }
    }

    printf(" gid=%d", gid)
    pw = getgrgid(gid)
    if (pw != "") {
        split(pw, a, ":")
        printf("(%s)", a[1])
    }

    if (egid != gid) {
        printf(" egid=%d", egid)
        pw = getgrgid(egid)
        if (pw != "") {
            split(pw, a, ":")
            printf("(%s)", a[1])
        }
    }

    if (NF > 4) {
        printf(" groups=");
        for (i = 5; i <= NF; i++) {
            printf("%d", $i)
            pw = getgrgid($i)
            if (pw != "") {
                split(pw, a, ":")
                printf("(%s)", a[1])
            }
            if (i < NF)
                printf(",")
        }
    }
    print ""
}

大きなファイルを分割する

splitユーティリティは大きなテキストファイルを小さな塊に分ける。デ フォルトでは出力ファイルの名前は`xaa', `xab', ... のようにつけ られる。分割された各々のファイルは1000行の長さ(最後のファイルを除く)とな る。分割したファイルの行数を変更するには、コマンドラインにマイナス記号を 前置した数字を置いて指定する。例えば、500行毎にファイルを分割するように するには`-500'とする。出力ファイルの名前を`myfileaa', `myfileab',...のように 変更するには、その様なファイル名を指定するコ マンドライン引数を置く。

以下に挙げたのはawkによるsplitである。 このプログラムは セクション Translating Between Characters and Numbersを参照. にあるord関数とchr関数を使っている。

プログラムは最初にデフォルトの設定を行い、次いでコマンドライン引数が多す ぎないかどうかをチェックする。それからそれぞれの引数のチェックを行う。最 初の引数はマイナス記号に続いた数字であってもよく、もしそういったものなら ば、それは負の数のように見えるので、符号を反転しそれを分割するときの行数 とする。データファイルの名前はスキップし、最後の引数を出力ファイルの prefixとして使用する。

# split.awk -- awkによるsplit
# Arnold Robbins, arnold@gnu.org, Public Domain
# May 1993

# usage: split [-num] [file] [outname]

BEGIN {
    outfile = "x"    # デフォルト
    count = 1000
    if (ARGC > 4)
        usage()

    i = 1
    if (ARGV[i] ~ /^-[0-9]+$/) {
        count = -ARGV[i]
        ARGV[i] = ""
        i++
    }
    # ファイルではなく標準入力から読み込むときにargvを検査する
    if (i in ARGV)
        i++    # データファイル名をスキップする
    if (i in ARGV) {
        outfile = ARGV[i]
        ARGV[i] = ""
    }

    s1 = s2 = "a"
    out = (outfile s1 s2)
}

次のルールで作業のほとんどを行っている。tcount(temporary count)は 出力ファイルにどれだけの行が出力したのかを記録している。もしこれが countより大くなったとき、カレントファイルをクローズし、新しいファイル を始めるときである。s1s2はファイル名につけるsuffixの現在 の値を保持している。これが両方とも`z'であるとき、処理対象のファイル が大きすぎるということである。そうでない場合にはs1は次のアルファ ベットに変わり、s2はまたaから再度始まる。

{
    if (++tcount > count) {
        close(out)
        if (s2 == "z") {
            if (s1 == "z") {
                printf("split: %s is too large to split\n", \
                       FILENAME) > "/dev/stderr"
                exit 1
            }
            s1 = chr(ord(s1) + 1)
            s2 = "a"
        } else
            s2 = chr(ord(s2) + 1)
        out = (outfile s1 s2)
        tcount = 1
    }
    print > out
}

usage関数は単純にエラーメッセージを出力して、実行を終了する。

function usage(   e)
{
    e = "usage: split [-num] [file] [outname]"
    print e > "/dev/stderr"
    exit 1
}

eという変数は関数を ページ にフィットするよために使われている。

このプログラムはちょっとばかりいい加減である。最後のファイルのクローズを ENDルール中で行うのではなく、awkが自動的に行うことに依存し ている。

出力を複数のファイルに複製する

teeプログラムは"pipe fitting" として知られている。teeは それに対する標準入力からの入力を標準出力にコピーするとともに、コマンドラ インで指定されたファイルに複製する。その使用方法は以下の通り、

tee [-a] file ...

`-a'オプションはteeに対して、開始時に指定したファイルを切り詰め るのではなくファイルに追加を行うように指示する。

BEGINルールは最初にすべてのコマンドライン引数のコピーをcopy という名前の配列に作成する。ARGV[0]は必要でないのでコピーされな い。teeは、awkARGVにあるファイル名を入力データと して扱おうとするのでARGVを直接は使わない。

最初の引数が`-a'であれば、appendという名前のフラグ変数を 真にセットし、ARGV[1]coppy[1]を削除する。 ARGCが2未満であれば、ファイル名が指定されていないという ことであり、teeは使用方のメッセージを出力し、 実行を終了する。最後に、awkARGV[1]"-"がセットされ、ARGCが2に セットされたことにより、標準入力から読み込みを行うことを強制される。

# tee.awk -- awkによるtee
# Arnold Robbins, arnold@gnu.org, Public Domain
# May 1993
# Revised December 1995

BEGIN    \
{
    for (i = 1; i < ARGC; i++)
        copy[i] = ARGV[i]

    if (ARGV[1] == "-a") {
        append = 1
        delete ARGV[1]
        delete copy[1]
        ARGC--
    }
    if (ARGC < 2) {
        print "usage: tee [-a] file ..." > "/dev/stderr"
        exit 1
    }
    ARGV[1] = "-"
    ARGC = 2
}

パターンがなく、すべての行に対して処理するので、一つのルールで作業の全て を行っている。ルールの本体は、単純にその行をコマンドラインで指定されたフ ァイルに出力してから、標準出力にも出力する。

{
    # if文をループの外に出せば高速化できる。
    if (append)
        for (i in copy)
            print >> copy[i]
    else
        for (i in copy)
            print > copy[i]
    print
}

このコードはループを次の様に書くこともできる。

for (i in copy)
    if (append)
        print >> copy[i]
    else
        print > copy[i]

これはより簡潔なものではあるが、効率は良くない。`if'はすべてのレコ ードのすべての出力ファイルでテストループの本体を複製することによって、 `if'は入力レコード一つにつき一度だけテストを行うようになる。N個 の入力レコードがあり、M個の入力ファイルがあった場合、最初のやり方 ではN`if'文が実行されるが、二番目のやり方ではN×M 回も`if'文が実行されてしまうことになる。

最後にENDルールですべての出力ファイルをクローズするという 後始末を行う。

END    \
{
    for (i in copy)
        close(copy[i])
}

テキストの重複のない行を出力する

uniqユーティリティは入力としてソートされた行を標準入力からとり、 (デフォルトでは)重複した行を取り除く。言い換えれば、ユニークな行だけが出 力される。この名前はそこから来ている。uniqは幾つかのオプションが あり、その使い方は以下の通り。

uniq [-udc [-n]] [+n] [ input file [ output file ]]

オプションの意味は以下の通り。

-d
繰り返している行だけを出力する。
-u
くり返しのない行だけを出力する。
-c
行をカウントする。このオプションは`-d'`-u'を オーバーライドする。繰り返しのある行もない行も共にカウントする。
-n
行の比較を行う前にmn個のフィールドをスキップする。フィールドの定 義はawkのデフォルトと同じであり、スペースやタブ、あるいは改行(の連なり、run)で 区切られた非空白キャラクタ(non-whitespace character)である。
+n
行の比較を行う前にnキャラクタをスキップする。`-n'で指定 されたすべてのフィールドが最初にスキップされる。
input file
データを標準入力からではなく、 コマンドラインで指定された入力ファイルから入力する。
output file
生成した出力を標準出力ではなく、指定された名前のファイルに送る。

通常のuniq`-d'オプションと`-u'オプションの両方が 指定されたかのように動作する。

以下に挙げるのはawkによるuniqの実装である。 このプログラムはライブラリ関数のgetopt (セクション コマンドラインオプションの処理を参照)と join (セクション Merging an Array Into a Stringを参照)を を使っている。

このプログラムはusage関数と オプションの一覧とその意味の説明から始まっている。

BEGINルールはコマンドライン上の引数とオプションを扱っている。 `-25'という形式のオプションを、`2'というオプションとそれに対する `5'という引数のように扱うために、getoptのトリックを使っている。 二文字以上の数字が与えられた(Optargが数字である)場合、Optarg はオプションの数字と連結され、0を加えることによってそれを数値にする。 もし一つの数字だけがオプションにあれば、Optargは必要ではなく、次 回のgetoptの処理のためにOptindをデクリメントせねばならない。 このコードは明らかにちょっとトリッキーである。

オプションが一つも与えられていなければデフォルトが採用され、 繰り返しのある行もない行も共に出力することになる。 出力ファイルが指定されていれば、それをoutputfileに代入する。 それ以前に、outputfileは標準出力`/dev/stdout'に 初期化されている。

# uniq.awk -- awkで uniq をする
# Arnold Robbins, arnold@gnu.org, Public Domain
# May 1993

function usage(    e)
{
    e = "Usage: uniq [-udc [-n]] [+n] [ in [ out ]]"
    print e > "/dev/stderr"
    exit 1
}

# -c    行をカウントする。-dと-uをオーバーライドするcount lines. overrides -d and -u
# -d    繰り返しのある行だけを出力するonly repeated lines
# -u    繰り返しのない行だけを出力するonly non-repeated lines
# -n    フィールドをn個スキップするskip n fields
# +n    n個のキャラクタをスキップskip n characters, skip fields first

BEGIN    \
{
    count = 1
    outputfile = "/dev/stdout"
    opts = "udc0:1:2:3:4:5:6:7:8:9:"
    while ((c = getopt(ARGC, ARGV, opts)) != -1) {
        if (c == "u")
            non_repeated_only++
        else if (c == "d")
            repeated_only++
        else if (c == "c")
            do_count++
        else if (index("0123456789", c) != 0) {
            # getoptはオプションに引数を要求している。
            # -5のようなものに対処するために干渉する
            if (Optarg ~ /^[0-9]+$/)
                fcount = (c Optarg) + 0
            else {
                fcount = c + 0
                Optind--
            }
        } else
            usage()
    }

    if (ARGV[Optind] ~ /^\+[0-9]+$/) {
        charcount = substr(ARGV[Optind], 2) + 0
        Optind++
    }

    for (i = 1; i < Optind; i++)
        ARGV[i] = ""

    if (repeated_only == 0 && non_repeated_only == 0)
        repeated_only = non_repeated_only = 1

    if (ARGC - Optind == 2) {
        outputfile = ARGV[ARGC - 1]
        ARGV[ARGC - 1] = ""
    }
}

以下にあるare_equalという関数は、現在行の$0と一つ前の行 lastとの比較を行っている。この関数はまたフィールドやキャラクタのスキ ップも処理している。

フィールドカウントやキャラクタカウントが指定されていなければ、 are_equalは単純にlast$0を文字列として 比較し、その結果を1または0として返す。そうでない場合には複雑になる。

フィールドをスキップしなければならない場合、各行をsplit (セクション Built-in Functions for String Manipulationを参照) を使って配列に分割し、それからjoinを使って必要なフィールドを 組み立てる。連結された行はclastclineに格納される。 スキップするフィールドがなければ、clastclineには それぞれlast$0の内容がそのままセットされる。

最後に、キャラクタをスキップする必要がある場合に、substrclastclineの先頭のキャラクタcharcount個スキップする ために使われる。

function are_equal(    n, m, clast, cline, alast, aline)
{
    if (fcount == 0 && charcount == 0)
        return (last == $0)

    if (fcount > 0) {
        n = split(last, alast)
        m = split($0, aline)
        clast = join(alast, fcount+1, n)
        cline = join(aline, fcount+1, m)
    } else {
        clast = last
        cline = $0
    }
    if (charcount) {
        clast = substr(clast, charcount + 1)
        cline = substr(cline, charcount + 1)
    }

    return (clast == cline)
}

次に挙げる二つのルールはプログラムの本体である。最初のルールは データの最初の行だけに実行される。このルールでは、 次の行に比較すべきものがあるので、last$0をセットしている。

二番目のルールが仕事をする。 equalという変数の内容は、are_equalで行った比較の結果に よって、1か0のいずれかになる。uniqは行の重複を数え、 行が同じ内容であればcountという変数をインクリメントする。 内容が異なっていれば行の内容を出力し、countをリセットする

uniqが数えることをしていなければ、countは 二つの行の内容が等しかったときにインクリメントされる。 uniqが繰り返しの行をカウントしていて二回以上その同じ行が 出現したり、逆にuniqが繰り返しのない行をカウントしていて ある行が一度しか出現しなければ、その様な行は出力され、countが リセットされる。

同様のロジックが、ENDルールの中で 入力の最後の行を出力するために使われている。

NR == 1 {
    last = $0
    next
}
    
{
    equal = are_equal()

    if (do_count) {    # -d と-uをオーバーライド
        if (equal)
            count++
        else {
            printf("%4d %s\n", count, last) > outputfile
            last = $0
            count = 1    # カウンタをリセット
        }
        next
    }

    if (equal)
        count++
    else {
        if ((repeated_only && count > 1) ||
            (non_repeated_only && count == 1))
                print last > outputfile
        last = $0
        count = 1
    }
}

END {
    if (do_count)
        printf("%4d %s\n", count, last) > outputfile
    else if ((repeated_only && count > 1) ||
            (non_repeated_only && count == 1))
        print last > outputfile
}

数え上げ

wc(word count)ユーティリティは、一つ以上の入力ファイルに 対して、行、単語、キャラクタの数をカウントする。その使い方は以下の通り。

wc [-lwc] [ files ... ]

コマンドラインで入力ファイルが指定されなかった場合には、wcは標準 入力から読み込みを行う。複数のファイルが指定された場合、すべてのファイル でカウントした合計を同様に出力する。wcのオプションとその意味は以 下の通り。

-l
行だけを数える。
-w
単語だけを数える。"単語"とはスペースやタブで区切られた、非空白キャラク タの並びである。幸運にも、これはawkが通常行う入力データの分割方法 と同じである。
-c
キャラクタだけを数える。

awkを使ったwcの実装はawkが仕事のほとんどを、つまり 行の単語(つまりはフィールド)への切り分けと、行(レコード)のカウントを awk自身が行い、行の長さがどれくらいかということも簡単に知ることが できるので実にエレガントである。

このバージョンではライブラリ関数の getopt (セクション コマンドラインオプションの処理を参照)と transition 関数 (セクション Noting Data File Boundariesを参照) を使用している。

このバージョンと伝統的なwcでは大きな違いがある。我々のバージョン では、常に行、単語、キャラクタの順序でカウントした数を出力する。伝統的な wcでは`-l'`-w'`-c'といったオプションをコマンド ライン上で使用してして出力する順序を指定することができる。

BEGINルールは引数の処理を行う。コマンドラインで二つ以上のファイル が指定されたときには変数print_totalの値が真となる。

# wc.awk -- 行、単語、キャラクタを数える
# Arnold Robbins, arnold@gnu.org, Public Domain
# May 1993

# Options:
#    -l    行だけを数える
#    -w    単語だけを数える
#    -c    キャラクタだけを数える
#
# デフォルトでは、行、単語、キャラクタを数える

BEGIN {
    # getoptに不正なオプションについてはメッセージを出力するよう
    # にし、ここではそのようなオプションを無視するようにする
    while ((c = getopt(ARGC, ARGV, "lwc")) != -1) {
        if (c == "l")
            do_lines = 1
        else if (c == "w")
            do_words = 1
        else if (c == "c")
            do_chars = 1
    }
    for (i = 1; i < Optind; i++)
        ARGV[i] = ""

    # オプションが何もなければ、全てを行う
    if (! do_lines && ! do_words && ! do_chars)
        do_lines = do_words = do_chars = 1

    print_total = (ARGC - i > 2)
}

beginfileは単純で、行、単語、キャラクタのカウントを0にリセットし、 fnameに入っている処理対象のファイルの名前をセーブする。

関数endfileは、それまで処理していたファイルで見つかった行、単語、 キャラクタの数をそれまでのそれぞれの合計に加える。その後でそのファイルで のカウンタの値を出力する。このカウンタはbeginfileで処理するのを当 てにしている。

function beginfile(file)
{
    chars = lines = words = 0
    fname = FILENAME
}

function endfile(file)
{
    tchars += chars
    tlines += lines
    twords += words
    if (do_lines)
        printf "\t%d", lines
    if (do_words)
        printf "\t%d", words
    if (do_chars)
        printf "\t%d", chars
    printf "\t%s\n", fname
}

各行毎に実行される一つのルールがある。そのルールではcharsにレコー ドの長さを加えている。ここで、改行キャラクタがレコードを分割していて (RSの値)レコード自身には含まれていないためさらに1を足す必要がある linesは行が読まれる度にインクリメントされ、wordsはその行にある "単語"の数、NFの値だけ増加される。 (22)

最後に、ENDルールはすべてのファイルでの合計値を 出力する。

# do per line
{
    chars += length($0) + 1    # 改行の分も忘れずに
    lines++
    words += NF
}

END {
    if (print_total) {
        if (do_lines)
            printf "\t%d", tlines
        if (do_words)
            printf "\t%d", twords
        if (do_chars)
            printf "\t%d", tchars
        print "\ttotal"
    }
}

A Grab Bag of awk Programs

このセクションは雑多なプログラムの"福袋"である。 我々はあなたがそれらが共に面白くて、楽しいことを見いだすことを 希望する。

ドキュメント中の重複した語を探しだす

長い文章を書くときにありがちなエラーは 間違って単語を重複させてしまうというものである。 テキスト中では "the the program does the following ...." のようなものをしばしばみかけることだろう テキストがオンラインにある場合には、しばしば重複した単語は 行末と次の行の先頭とで発生し、それは非常に見つけにくい場所である。

このプログラム、`dupword.awk'はファイルを一度に一行読み、同じ単語が 隣接していないかをチェックする。同様に、その行の最後の単語を(prev という変数に)次の行の最初の単語と比較するためにセーブしておく。

最初の二つのステートメントは行の内容を、 "The"から"the"の比較の結果が等しいものになるように 全て小文字にしている。 二番目のステートメントでは アルファベット、数字、空白以外のすべてのキャラクタを 句読点が比較に影響しないように取り去っている。 これによりときとして、本当は違う単語を重複していると報告する ときがあるが、これは滅多にないことである。

# dupword -- テキスト中の重複した単語を探し出す
# Arnold Robbins, arnold@gnu.org, Public Domain
# December 1991

{
    $0 = tolower($0)
    gsub(/[^A-Za-z0-9 \t]/, "");
    if ($1 == prev)
        printf("%s:%d: duplicate %s\n",
            FILENAME, FNR, $1)
    for (i = 2; i <= NF; i++)
        if ($i == $(i-1))
            printf("%s:%d: duplicate %s\n",
                FILENAME, FNR, $i)
    prev = $NF
}

アラーム時計プログラム

次に挙げるプログラムはシンプルな"アラーム時計"プログラムである。プログ ラムには、時刻とメッセージ(こちらは省略可能)を渡す。指定した時刻になった ときに、このプログラムはメッセージを標準出力に出力する。それに加えて、メ ッセージを繰り返す回数も指定することができ、さらにその繰り返しの間隔も指 定できる。

このプログラムでは セクション Managing the Time of Dayを参照. にあるgettimeofday関数を使っている。

作業の全てはBEGINルールで行っている。最初の部分では引数のチェック と、ディレイ、カウント、出力メッセージのデフォルト値のセットを行っている。 ユーザーがメッセージを指定しているが、そこにASCIIのベルキャラクタ("警告 "キャラクタとして知られているもの、`\a')が含まれていない場合、それ をメッセージに追加する(多くのシステムでは、ASCIIのベルキャラクタを出力す ると警告の類の音を生成する。したがって、ユーザーがコンピュータやターミナ ルを見ていない場合にアラームが作動したときにシステムが注意を喚起するので ある)。

# alarm -- アラームをセットする
# Arnold Robbins, arnold@gnu.org, Public Domain
# May 1993

# usage: alarm time [ "message" [ count [ delay ] ] ]

BEGIN    \
{
    # Initial argument sanity checking
    usage1 = "usage: alarm time ['message' [count [delay]]]"
    usage2 = sprintf("\t(%s) time ::= hh:mm", ARGV[1])

    if (ARGC < 2) {
        print usage > "/dev/stderr"
        exit 1
    } else if (ARGC == 5) {
        delay = ARGV[4] + 0
        count = ARGV[3] + 0
        message = ARGV[2]
    } else if (ARGC == 4) {
        count = ARGV[3] + 0
        message = ARGV[2]
    } else if (ARGC == 3) {
        message = ARGV[2]
    } else if (ARGV[1] !~ /[0-9]?[0-9]:[0-9][0-9]/) {
        print usage1 > "/dev/stderr"
        print usage2 > "/dev/stderr"
        exit 1
    }

    # set defaults for once we reach the desired time
    if (delay == 0)
        delay = 180    # 3 分
    if (count == 0)
        count = 5
    if (message == "")
        message = sprintf("\aIt is now %s!\a", ARGV[1])
    else if (index(message, "\a") == 0)
        message = "\a" message "\a"

次のコードは、アラームの時刻を時間と分に変換し、必要ならば24時間制の時間 にする。そのとき、この時間は真夜中からの経過秒数に変換される。次いで真夜 中からその時点までの経過秒数を計算する。これら二つの間の差は、アラームを 起動するまでどのくらいの長さがあるかということになる。

    #目的時刻を分解する
    split(ARGV[1], atime, ":")
    hour = atime[1] + 0    # 強制的に数値に変換する
    minute = atime[2] + 0  # 強制的に数値に変換する

    #現在時刻を取得する
    gettimeofday(now)

    #与えられた時刻が12時間制の時刻で、現在より後、
    #例えば午前9時に`alarm 5:30'とされたときに
    #目的時刻が午後5:30になるように、12を足す。
    if (hour < 12 && now["hour"] > hour)
        hour += 12

    #目的時刻を深夜からの秒数で取得する
    target = (hour * 60 * 60) + (minute * 60)

    #現在時刻を深夜からの秒数で取得する
    current = (now["hour"] * 60 * 60) + \
               (now["minute"] * 60) + now["second"]

    #どのくらい寝るのか
    naptime = target - current
    if (naptime <= 0) {
        print "time is in the past!" > "/dev/stderr"
        exit 1
    }

最後にプログラムはsystem関数 (セクション Built-in Functions for Input/Outputを参照) を使ってsleepユーティリティを呼び出す。 sleepユーティリティは単純に与えられた秒数だけ停止する。もし sleepユーティリティの終了ステータスが0でなければ、このプログラム はsleepが中断されたとみなし、実行を終了する。sleepがOKのス テータス (0)を返した場合には、ループの中でメッセージを出力し、メッセージ の出力間隔に必要な秒数だけ待つためにsleepをもう一度使用する。

    # zzzzzz..... 中断されたら終わり
    if (system(sprintf("sleep %d", naptime)) != 0)
        exit 1

    # time to notify!
    command = sprintf("sleep %d", delay)
    for (i = 1; i <= count; i++) {
        print message
        # sleepコマンドが中断されたら終わり
        if (system(command) != 0)
            break
    }

    exit 0
}

キャラクタを変換する

システムのtrユーティリティはキャラクタの変換を行う。例えば、大文 字のキャラクタを小文字に変換するのによく次のように使われる。

generate data | tr '[A-Z]' '[a-z]' | process data ...

trユーティリティにはブラケットで囲まれた二つのキャラクタリストを 渡す。通常は、これらのリストはシェルがファイル名の拡張子として扱うことの 内容にクォートされている。(23)入力を処理するときに、最初のリストにあ る最初のキャラクタは二番目のリストにある最初のキャラクタに変換され、最初 のリストにある二番目のキャラクタは二番目のリストにある二番目のキャラクタ に変換され、以後同様な変換が行われる。もし、"from"リストが"to'リスト よりも長ければ、"to"リストの最後のキャラクタが、"from"の残りのキャラ クタの変換後のキャラクタとして扱われる。

ちょっと前に、 あるユーザーが、我々に gawkに変換を行う関数を追加するように提案をしてきた。 これは(追加することに)反対するような"crepping featurism"であり、 私はそれを証明するために、 キャラクタの変換をユーザーレベルの関数で行うことのできる 後述するようなプログラムを作成した。 このプログラムはシステムのtrユーティリティほどには 完璧ではないが、ほとんどの仕事はきちんと行う。

translateプログラムは標準のawkの持つ幾つかの弱点の 一つを浮き彫りにする。それは、個々のキャラクタを扱うのが非常に 苦手であり、組込み関数のsubstrindexgsubを くり返し使うことが要求されるということである (セクション Built-in Functions for String Manipulationを参照)。 (24)

二つの関数があり、最初の一つはstranslateで、 これは三つの引数をとる。

from
変換の対象となるキャラクタのリスト。
to
変換後のキャラクタのリスト。
target
変換対象の文字列

連想配列は変換部分を実に簡単にしている。t_arは"from"中のキャラ クタで添え字付けされる"to"キャラクタを保持している。そして、単純なルー プを通じてfromのキャラクタが一度に一文字ずつ変換されていく。 fromの各キャラクタは、そのキャラクタがtargetにあれば gsubを使って、対応するtoのキャラクタに変換される。

関数translateは単に$0をターゲットとしてstranslateを 呼び出すことだけをしている。メインプログラムは二つのグローバル変数、 FROMTOをコマンドラインからセットして、それからawk が標準入力から読み込みを行うようにするためにARGVを変更する。

Finally, the processing rule simply calls translate for each record. 最後に、単にtranslateを呼ぶだけのルールが各レコードの処理を行う。

# translate -- trと同じようなことをする
# Arnold Robbins, arnold@gnu.org, Public Domain
# August 1989

# bugs: does not handle things like: tr A-Z a-z, it has
# to be spelled out. However, if `to' is shorter than `from',
# the last character in `to' is used for the rest of `from'.

function stranslate(from, to, target,     lf, lt, t_ar, i, c)
{
    lf = length(from)
    lt = length(to)
    for (i = 1; i <= lt; i++)
        t_ar[substr(from, i, 1)] = substr(to, i, 1)
    if (lt < lf)
        for (; i <= lf; i++)
            t_ar[substr(from, i, 1)] = substr(to, lt, 1)
    for (i = 1; i <= lf; i++) {
        c = substr(from, i, 1)
        if (index(target, c) > 0)
            gsub(c, t_ar[c], target)
    }
    return target
}

function translate(from, to)
{
    return $0 = stranslate(from, to, $0)
}

# メインプログラム
BEGIN {
    if (ARGC < 3) {
        print "usage: translate from to" > "/dev/stderr"
        exit
    }
    FROM = ARGV[1]
    TO = ARGV[2]
    ARGC = 2
    ARGV[1] = "-"
}

{
    translate(FROM, TO)
    print
}

ユーザーレベルの関数でキャラクタの変換をすることは可能であるが、それは必 ずしも能率的ではない。そのため我々は組み込みの機能を追加することを検討し 始めた。しかしながら、このプログラムを書いた後で、我々はSystem V Release 4 のawktouppertolowerが追加されていることを学ん だ(?)。これらの関数は必要があればキャラクタの大小文字の変換をするような 関数であり、gawkにこれらの関数をそれのみ(gawkだけ)で十分な ことができるように追加することを決めた。

このプログラムの明らかな改良策は、配列t_arのセットアップを BEGINルール中で一度だけ行うということである。しかしながら、これは "from"と"to"の二つのリストがプログラムの実行中を通じて決して変更され ないということを仮定したものである。

郵便の宛て名を出力する

次に挙げるプログラムは"real world"(25)プログラムである。 このスクリプトは名前とアドレスのリストを入力として読み込み、宛て名ラベル を生成する。各ページは20のラベルがあり、10個ずつの二段組みにされる。アド レスは5行を越えないように保証される。各アドレスは、空行で区切られる。

基本的なアイデアは、ラベル20個分のデータを読むことである。各ラベルの各行 は、lineという配列に格納される。この単一のルールは、lineと いう配列を満たすことを行い、そして20個のラベルが読み込まれたときにそのペ ージを出力するということを行っている。

BEGINルールではawkがレコードを空行で分割するようにするため にRSに空文字列をセットする (セクション 入力をレコードへと分割をするやりかたを参照)。また、一ページの最大行 が(20×5=100)なので、MAXLINESに100をセットする。

作業のほとんどは関数printpageで行われている。ラベル行は順番に配列 lineに格納されるが、それは水平方向に印刷する必要がある。つまり、 line[1]の次はline[6]であり、line[2]の次は line[7]、という具合になる。二つのループがこれを達成するために使わ れている。 外側の、iで制御されるループはデータ10行毎に一段落し、 これはラベルの各列(row)である。内側のjで制御されるループは列(row) にある各行を処理する。jは0から4まで変化し、`i+j'はその列の j番目の行である。i+j+5は次に処理するエントリとなる。出力は 最終的に次のようなものになる。

line 1          line 6
line 2          line 7
line 3          line 8
line 4          line 9
line 5          line 10

21行目と61行目では、ラベルにきちんと印字されるようにするために空行が出力 される。これはプログラムが書かれたときに使っていたラベルに依存するもので ある。同様に、ページの一番上二行と、一番下二行が空行であることに注意する こと。

ENDルールはラベルの最終ページのフラッシュを行っている。 データが、20ラベルの倍数である場合には、フラッシュされるようなデータは ない。

# labels.awk
# Arnold Robbins, arnold@gnu.org, Public Domain
# June 1992

# Program to print labels.  Each label is 5 lines of data
# that may have blank lines.  The label sheets have 2
# blank lines at the top and 2 at the bottom.

BEGIN    { RS = "" ; MAXLINES = 100 }

function printpage(    i, j)
{
    if (Nlines <= 0)
        return

    printf "\n\n"        # ヘッダー

    for (i = 1; i <= Nlines; i += 10) {
        if (i == 21 || i == 61)
            print ""
        for (j = 0; j < 5; j++) {
            if (i + j > MAXLINES)
                break
            printf "   %-41s %s\n", line[i+j], line[i+j+5]
        }
        print ""
    }

    printf "\n\n"        # フッター

    for (i in line)
        line[i] = ""
}

# メインルール
{
    if (Count >= 20) {
        printpage()
        Count = 0
        Nlines = 0
    }
    n = split($0, a, "\n")
    for (i = 1; i <= n; i++)
        line[++Nlines] = a[i]
    for (; i <= 5; i++)
        line[++Nlines] = ""
    Count++
}

END    \
{
    printpage()
}

単語の使用数を調べる

次のawkプログラムは入力中にあった単語がどのくらい現れたの数を出力 するものである。このプログラムはawkの配列の性質が、添え字に文字列 を使った連想的なものであるということを示している。また、 `for @var {x' in array}という構成を例示したものでもある。結局 のところこれは、最少限度の努力で少々複雑な有用な仕事をさせるために、 awkをどのように他の有益なプログラムと共に使うことをできるかという ことを示している。若干の説明がプログラムリストの後にある。

awk '
#単語の出現頻度のリストを出力する
{
    for (i = 1; i <= NF; i++)
        freq[$i]++
}

END {
    for (word in freq)
        printf "%s\t%d\n", word, freq[word]
}'

このプログラムを理解するにあたっての最初の点は、二つのルールがあるという ことである。最初のルールは、パターンが空であるので入力のすべての行で実行 される。awkのフィールドアクセス機構 (セクション フィールドの検査を参照) を行の中にある個々の単語を取り出すた めに使っていて、どのくらいのフィールドがあるかを知るためにNF (セクション 組み込み変数を参照)という組込み変数を使っている。

入力中にある各単語は、その単語が現れる度に一つ値が増える freqという配列の要素のために使われる。

二番目のルールはENDというパターンを持っているので、入力がつきるま では実行されない。そこでは最初のアクションで構築したテーブル、 freqの内容を出力している。

このプログラムは実際のテキストファイルに使用するには幾つかの問題点がある。

これらの問題を解決する方法はより進んだawk言語の機能を使うことであ る。第一に、大小文字の区別を取り除くために、tolowerを使用する。次 いで、句読点キャラクタを取り除くためにgsubを使用する。最後に、 awkスクリプトの出力に対してsortユーティリティを使用する。 次に新しいバージョンのプログラムを挙げる。

# Print list of word frequencies
# 単語の出現頻度のリストを出力する。
{
    $0 = tolower($0)    # 大小文字の区別をなくす
    gsub(/[^a-z0-9_ \t]/, "", $0)  # 句読点をとる
    for (i = 1; i <= NF; i++)
        freq[$i]++
}

END {
    for (word in freq)
        printf "%s\t%d\n", word, freq[word]
}

このプログラムを`wordref.awk'というファイルにセーブし、データが `file1'というファイルにあったとすると、次のパイプライン

awk -f wordfreq.awk file1 | sort +1 -nr

これは、`file1'中にある単語を出現頻度順に並べた テーブルを出力する。

awkプログラムは適切にデータを扱って、きちんと並べられては いない単語テーブルを作り出す。

awkスクリプトの出力は、その後でsortユーティリティによって ソートされ、ターミナルに出力される。この例でsortに与えられている オプションは、ソートユーティリティが各行二番目のフィールド(最初のフィー ルドはスキップする)を数値と見なした(`15'`5'の前にくる)ソート キーにして使って降順(逆順)並べかえを行うように指示している。

sortで行っていることを、ENDアクションを変えることによって プログラムの中で行うこともできる。

END {
    sort = "sort +1 -nr"
    for (word in freq)
        printf "%s\t%d\n", word, freq[word] | sort
    close(sort)
}

本当のパイプ(true pipe)を持ってないシステム上で ソートをするにはこのやり方を使わなければならないだろう。

sortプログラムの使い方に関するより詳しい情報は、 一般的なオペレーティングシステムのドキュメントを参照のこと。

ソートされていないテキストから重複を取り除く

uniqプログラム (セクション テキストの重複のない行を出力するを参照) はソートされたデータの重複を取り除く。

さてここで、あるデータファイルから重複したデータを取り除く必要があるが、 その順番はそのままにして置きたいということを望むようなことがあるだろう か?これは、シェルのヒストリーファイルが良い例かもしれない。ヒストリーフ ァイルは、ユーザーが入力したすべてのコマンドのコピーを保持する。そして、 数回同じコマンドを繰り返すことは希なことではない。ユーザーはときどき、ヒ ストリーファイルから重複したエントリを削除して、ヒストリーファイルをコン パクトにしたいと思うかもしれない。その場合にも元のコマンドの順序はそのま まにして置くことが望ましい。

次に挙げる単純なプログラムは、この仕事をこなす。このプログラムは二つの配 列を使い、dataという配列は各行のテキストによって添え字付けされる。

ある行がそれまでに現れていないコマンドでああれば、data[$0]はゼロ である。この場合、その行のテキストはlines[count]に格納される。 linesの各要素はユニークものであり、また、[lines]は行の出現 順 序に添え字付けされる。ENDルールは、単に順番に[lines]の要 素を出力しているだけである。

# histsort.awk -- シェルのヒストリーファイルを小さくする
# Arnold Robbins, arnold@gnu.org, Public Domain
# May 1993

# Thanks to Byron Rakitzis for the general idea
{
    if (data[$0]++ == 0)
        lines[++count] = $0
}

END {
    for (i = 1; i <= count; i++)
        print lines[i]
}

このプログラムにはまた、他の便利な情報を生成するための土台を提供する。 例えば、次のようなprint文をENDルールで使うことによって ある特定のコマンドがどのくらいの回数使われたのかを示すようになる。

print data[lines[i]], lines[i]

これはdata[$0]が見つかる度にインクリメントされているので きちんと働く。

Texinfoのソースファイルからプログラムを取り出す

この章と一つ前の章 (セクション awkの関数ライブラリを参照) では、多くのawkプログラムを例示している。 もしこれらのプログラムを試してみたいのであれば、 手でこれらのプログラムをタイプしなければならないというのは退屈である。 そこで、Texinfoの入力ファイルの一部を引き抜いて個々のファイルに納める ことのできるプログラムを提供する。

このマニュアルはGNUプロジェクトのドキュメントフォーマッティング 言語であるTexinfoで記述されている。 TexinfoはFree Software Foundationから入手可能な Texinfo--The GNU Documentation Format で詳しく説明されている。

上述した目的のためには、Texinfoファイルに関して 三つの事柄を知っておけば十分である。

以下に挙げるプログラム、`extract.awk'はTexinfoソースファイルを 読み、特殊なコメントに基づいて二つの事柄を行う。 `@c system ...'というシーケンスを見つけたときには、 コマンドのテキストを制御行から抜き取り、それを system関数に渡すことによりコマンドを実行する (セクション Built-in Functions for Input/Outputを参照)。 `@c file filename'というシーケンスを見つけたときには、 `@c endfile'というシーケンスに出会うまで 各行をfilenameというファイルに出力する。 `extract.awk'のルールは、`oment'の部分をオプショナルに することによって`@c'`@comment'の両方にマッチするように なっている。`@group'`@end group'を含んでいる行は 単に削除される。`extract.awk'はライブラリ関数のjoin (セクション Merging an Array Into a Stringを参照)を 使用している。

オンラインTexinfoソース、Effective AWK Programming(`gawk.texi')にあるサ ンプルプログラムは全て`file'`endfile'に囲まれた中に置かれて いる。gawkの配布キットでは、サンプルプログラムを展開するためと、 それらをgawkが見つけることのできる標準的なディレクトリにインスト ールするために`extract.awk'を使っている。 Texiinfoファイルは以下のような形式である:

...
This program has a @code{BEGIN} block,
which prints a nice message:

@example
@c file examples/messages.awk
BEGIN @{ print "Don't panic!" @}
@c end file
@end example

これは最後に次のような出力をする:

@example
@c file examples/messages.awk
END @{ print "Always avoid bored archeologists!" @}
@c end file
@end example
...

`extract.awk'はディレクティブの大小文字の違いを気にしないために、 IGNORECASEに1をセットすることから始まっている。

最初のルールではsystemを扱い、与えられたコマンドをチェックし(少な くともNFが三つある)、そのコマンドが正しく実行されたことを表わす終 了ステータス0を返すことをチェックする。

# extract.awk -- extract files and run programs
#                 from texinfo files
# texinfoファイルからファイルを取り出し、プログラムを実行する
# Arnold Robbins, arnold@gnu.org, Public Domain
# May 1993

BEGIN    { IGNORECASE = 1 }

/^@c(omment)?[ \t]+system/    \
{
    if (NF < 3) {
        e = (FILENAME ":" FNR)
        e = (e  ": badly formed `system' line")
        print e > "/dev/stderr"
        next
    }
    $1 = ""
    $2 = ""
    stat = system($0)
    if (stat != 0) {
        e = (FILENAME ":" FNR)
        e = (e ": warning: system returned " stat)
        print e > "/dev/stderr"
    }
}

eという変数は関数を page. ページ に体裁よく納めるために使っている。

二番目のルールでは、データをファイルに取り出すことを行っている。ディレク ティブで与えられたファイル名の確認を行い、もしそれがそのときに処理してい るファイルでなければ、以前のファイルをクローズする。これは`@c endfile' ではファイル名を与えないでも良いということである(この場合、今行って はいないが診断メッセージを出すべきである)。

`for'ループ中でgetline (セクション getlineを使った入力を参照). を使って、行の読み込み作業を行っている。予期しないファイルの終了に対して は、unexpected_eofという関数を呼び出す。行が"endfile"の行に あった場合には、ループを脱出する。注目している行に`@group'`@end group'があった場合、それを無視し、次の行の処理に移る (これらTexinfo制御の行は一ページにまとまるコードのブロックを 保持している。残念ながら、TeX はこの作業を常に正しく行うほどには 賢くない。そのために、ちょっとしたアドバイスをしなければならない)。

作業のほとんどは、それに続く数行で行っている。もし、読み込んだ行に@samp {@}シンボルがなければ、それは直接出力することができる。`@'があっ て、それが先頭にある行は取り除かなければならない。

`@'を取り除くためにsplit関数 (セクション Built-in Functions for String Manipulationを参照). を使って、その行はaという配列の要素に分割される。aの空に なっている要素は二つの連続した`@'シンボルが元の行にあったことを示し ている。二つの空要素(元のファイルの`@@')毎に、`@'シンボル一 つを戻す必要がある。

配列の処理が終わったとき、joinSUBSEPの値を引数に 使って呼び出し、配列を単一の行に戻す。 この行は、その後出力先のファイルに出力される。

/^@c(omment)?[ \t]+file/    \
{
    if (NF != 3) {
        e = (FILENAME ":" FNR ": badly formed `file' line")
        print e > "/dev/stderr"
        next
    }
    if ($3 != curfile) {
        if (curfile != "")
            close(curfile)
        curfile = $3
    }

    for (;;) {
        if ((getline line) <= 0)
            unexpected_eof()
        if (line ~ /^@c(omment)?[ \t]+endfile/)
            break
        else if (line ~ /^@(end[ \t]+)?group/)
            continue
        if (index(line, "@") == 0) {
            print line > curfile
            continue
        }
        n = split(line, a, "@")
        # if a[1] == "", means leading @,
        # don't add one back in.
        for (i = 2; i <= n; i++) {
            if (a[i] == "") { # @@だった
                a[i] = "@"
                if (a[i+1] == "")
                    i++
            }
        }
        print join(a, 1, n, SUBSEP) > curfile
    }
}

注意すべきことは、`>'を使ったリダイレクトである。`>'を使った出 力はファイルを一度だけオープンし、ファイルをオープンし続けてその後の出力 はファイルに追加する (セクション Redirecting Output of print and printfを参照)。 このことは、プログラムのテキストと、それ対しての説明をする文(ここでやっ ているように!)を何の面倒もなしに、簡単に混ぜることができることを可能とす る。ファイルは新しいデータファイル名に出会ったときか、入力ファイルの最後 にきたときだけクローズされる。

最後にunexpected_eofという関数が適切な エラーメッセージを出力し、実行を終了する。

ENDルールは、オープンしたファイルをクローズする 最後のクリーンアップ作業を行っている。

function unexpected_eof()
{
    printf("%s:%d: unexpected EOF or error\n", \
        FILENAME, FNR) > "/dev/stderr"
    exit 1
}

END {
    if (curfile)
        close(curfile)
}

シンプルなストリームエディター

sedユーティリティはデータストリームを読み取り、変更を行い、変更し たデータを出力するプログラム、"ストリームエディタ"(stream editor)であ る。これはしばしば大きなファイルやパイプラインを通じて他のコマンドが生成 したデータストリームに対して全面的な変更を加えるときに用いられる

sedはそれ自身が複雑なプログラムでもあるが、 ほとんどの使用目的はパイプラインの真ん中で全面的な置換を行うことである。

command1 < orig.data | sed 's/old/new/g' | command2 > result

この例の`s/old/new/g'sedに対して 入力行から`old'という正規表現にマッチするものを検索し、 (見つかったら)それを`new'に 置換する作業を グローバル(つまり、その行のすべてのマッチするものに対して) に行えということを指示している。 これは、awkgsub関数と似ている (セクション Built-in Functions for String Manipulationを参照)。

次のプログラム、`awksed.awk'は、少なくとも二つのコマンドライン引数を 受け付ける。その二つは、テキストから検索するパターンと、それを置き換える パターンである。さらに引数がある場合には、それは処理する対象の ファイル名として扱われる。もしファイル名が一つも与えられなければ、 標準入力が入力元として扱われる。

# awksed.awk -- s/foo/bar/g をprintだけでやる
#    アイデアを出してくれたMichael Brennanに感謝

# Arnold Robbins, arnold@gnu.org, Public Domain
# August 1995

function usage()
{
    print "usage: awksed pat repl [files...]" > "/dev/stderr"
    exit 1
}

BEGIN {
    # 引数を検査する
    if (ARGC < 3)
        usage()

    RS = ARGV[1]
    ORS = ARGV[2]

    # 引数をファイルとして使用しない
    ARGV[1] = ARGV[2] = ""
}

# look ma, no hands!
{
    if (RT == "")
        printf "%s", $0
    else
        print
}

このプログラムは 正規表現であるRSを扱う能力と、実際にレコードを区切ったテキストが RTであるという動作に依存している (セクション 入力をレコードへと分割をするやりかたを参照)。

アイデアの基本は、RSを検索するパターンにするということである。 gawkは自動的にそのパターンにマッチしたものの間にある テキストを$0にセットする。これは我々がそのままにしておきたいと 考えているテキストである。そして、ORSに置換後のテキストを セットすることによって、print文はそのままにしておきたい テキストを(変換せずにそのまま)出力し、その後ろに 置換したテキストを出力するのである。

この手順に対して、 レコードがRSで終わっていない場合の手続きにうまい手段は ないものだろうか? print文を使ってしまうと、出力すべきでない置換テキストを 出力してしまい、結果が正しいものでなくなってしまう。

しかし、ファイルがRSでマッチしたテキストで終わっていない場合には、 RTは空文字列になる。この場合、$0printfを使って出力することが可能である。 (セクション Using printf Statements for Fancier Printingを参照)。

BEGINルールは、引数が適正な数かどうかのチェック、そこに問題があっ た場合にusageを呼び出すなどのセットアップ作業を扱っている。その 後で、RSORSにコマンドライン引数からの値をセットし、ファ イル名として扱われないようにARGV[1]ARGV[2]に空文字列をセ ットしている(セクション Using ARGC and ARGVを参照)。

usage関数はエラーメッセージを出力し、実行を終了する。

最後に、一つのルールで上述した手順の通り、 RTの値に従って適切に printprintfを使って出力を行う。

ライブラリを使う簡単な方法

awkでライブラリ関数を使うことができると非常に有益である。それはコ ードの再利用と汎用的な関数の作成を促進しする。プログラムはより小さくなり、 それにより(プログラムの)見通しがよくなる。しかしながら、ライブラリ関数の 使用は、awkプログラムを書くときにのみ簡単になるのであって、それを 実行するときは複数の`-f'オプションを必要とするので面倒である。もし gawkが利用できないのなら、AWKPATH環境変数が使えず、 awk数をライブラリディレクトリに置くという 機能(セクション コマンドラインオプションを参照). もないので、より面倒さが顕著になる。

次のようにプログラムが書ければとても良いだろう。

# ライブラリ関数
@include getopt.awk
@include join.awk
...

# メインプログラム
BEGIN {
    while ((c = getopt(ARGC, ARGV, "a:b:cde")) != -1)
        ...
    ...
}

以下に挙げるプログラム`igawk.sh'は今述べたサービスを提供する。 gawkAWKPATH環境変数を使ったファイルの検索をシミュレートし、 ネストしたインクルード、つまり@includeでインクルードされたフ ァイルがさらに`@include'を含んでいることを許す。igawkはファ イルのインクルードを一度だけ行うようになっている。それは、ネストしてイン クルードすることによってライブラリ関数を二度インクルードすることがないよ うにするためである。

igawkは外面的にはgawkと同じように振る舞うべきである。これ は、複数の`-f'を使ったソースファイルの指定や、コマンドライン上のプ ログラムとライブラリファイルとを混ぜてつかるということも含めて、 gawkのコマンドライン引数の全てを受け付けるべきであるということである。

このプログラムはPOSIXのシェル(sh)言語を使って記述された。 プログラムの動作の方法は以下の通り。

  1. 最初から最後まで通して引数を眺め、awkのソースコードを表わしていな い引数を後で展開したプログラムを実行するときに使用するためにセーブする。
  2. awkテキストである引数では、それを展開するテンポラリファイルに出力 する。ここで二つの場合がある。
    1. `--source' or `--source='で指定されたリテラルテキスト。 このテキストはそのまま出力される。echoプログラムが 自動的に行末の改行を付け加える。
    2. `-f'で指定されたファイル名。ここで巧妙なトリックを使って `@include filename' をテンポラリファイルに出力している。 ファイル取り込みプログラムがgawkが行うやり方で作業するので、 正しい箇所でファイルの内容がプログラムにインクルードされる
  3. `@include'文を展開するために、テンポラリファイルに対して (自然に)awkプログラムを実行する。 展開されたプログラムは二番目のテンポラリファイルに出力される。
  4. 展開したプログラムをgawkにユーザーが元々指定したコマンドライン引数を一緒に 渡して実行させる。

プログラムの最初の部分では、最初の引数が`debug'であったときにシェル のトレースをオンにするということをしている。そうでない場合にはシェルの trap文を使ってプログラムが終了したり、割り込みを受けた場合にテンポ ラリファイルを消去するようにする。

次の部分にあるループでは、すべてのコマンドライン引数を処理する。 いくつかの重要な事例がある。

--
igawkに対する引数を終わらせる。この後の引数は評価されることなくユ ーザーのawkプログアムに渡される
-W
これは次のオプションがgawkに特有のものであることを示す。引数の処 理を早めに行わせるために、`-W'は残りの引数に付け足され、ループは継 続される(これはshプログラミングトリックである。もしshに不 慣れであっても気にすることはない)。
-v
-F
これらは保存され、gawkに渡される。
-f
--file
--file=
-Wfile=
ファイル名はテンポラリファイル`/tmp/ig.s.$$'`@include'文と共にセーブされる。 sedユーティリティが、ファイル名の前にある 部分(例えば`--file=')を取り去るために使われる。
--source
--source=
-Wsource=
ソーステキストは`/tmp/ig.s.$$'に保存される。
--version
--version
-Wversion
igawkのバージョンを出力し、また、 `gawk --version'を実行して使用する gawkの バージョン情報を出力して終了する。

`-f', `--file', `-Wfile', `--source',`-Wsource' のいずれもが指定されなかった場合、最初の非オプション引数が awkプログラムとなる。もしコマンドライン引数が 残っていなければ、igawk]はエラーメッセージを出力し、 実行を終了する。引数がある場合には、最初の引数が `/tmp/ig.s.$$'に出力される。

どのような場合でも、引数が処理された後では `/tmp/ig.s.$$'の内容は完全なオリジナルawkプログラム のテキストとなる。

`$$'shではカレントプロセスID番号となる。これは、ユニークな テンポラリファイル名を生成するためにシェルプログラミングでは良く使われて いる。これによりigawkを複数のユーザーがテンポラリファイルの重複を 気にすることなしに実行することができるようになる。

以下がプログラムである。

#! /bin/sh

# igawk -- gawkに似ているが、@includeを処理する
# Arnold Robbins, arnold@gnu.org, Public Domain
# July 1993

if [ "$1" = debug ]
then
    set -x
    shift
else
    # exit、ハングアップ、割り込み、quit、終了のときにクリーンアップをする
    trap 'rm -f /tmp/ig.[se].$$' 0 1 2 3 15
fi

while [ $# -ne 0 ] # loop over arguments
do
    case $1 in
    --)     shift; break;;

    -W)     shift
            set -- -W"$@"
            continue;;

    -[vF])  opts="$opts $1 '$2'"
            shift;;

    -[vF]*) opts="$opts '$1'" ;;

    -f)     echo @include "$2" >> /tmp/ig.s.$$
            shift;;

    -f*)    f=`echo "$1" | sed 's/-f//'`
            echo @include "$f" >> /tmp/ig.s.$$ ;;

    -?file=*)    # -Wfile or --file
            f=`echo "$1" | sed 's/-.file=//'`
            echo @include "$f" >> /tmp/ig.s.$$ ;;

    -?file)    # get arg, $2
            echo @include "$2" >> /tmp/ig.s.$$
            shift;;

    -?source=*)    # -Wsource or --source
            t=`echo "$1" | sed 's/-.source=//'`
            echo "$t" >> /tmp/ig.s.$$ ;;

    -?source)  # get arg, $2
            echo "$2" >> /tmp/ig.s.$$
            shift;;

    -?version)
            echo igawk: version 1.0 1>&2
            gawk --version
            exit 0 ;;

    -[W-]*)    opts="$opts '$1'" ;;

    *)      break;;
    esac
    shift
done

if [ ! -s /tmp/ig.s.$$ ]
then
    if [ -z "$1" ]
    then
         echo igawk: no program! 1>&2
         exit 1
    else
        echo "$1" > /tmp/ig.s.$$
        shift
    fi
fi

# この時点で/tmp/ig.s.$$ の内容は最終的に実行するプログラムとなる

awkプログラムは`@include'ディレクティブを処理するために、 getlineを使って一行ずつプログラムを通読する (セクション getlineを使った入力を参照)。 入力ファイル名と`@include'文はスタックを使って管理される。 `@include'に出会う度に、カレントファイル名が スタックに"プッシュ"され、`@include'ディレクティブで指定された ファイル名がカレントファイル名になる。 各ファイルを処理し終える度に、スタックから"ポップ"して 直前の入力ファイルを再びカレントファイルにする。 このプロセスはオリジナルのファイルをスタックの最初に 置くことから開始する。

pathto関数はファイルのフルパス名を探す作業を行っている。 これはgawkAWKPATH環境変数を使って検索するときの 動作のシミュレートである (セクション AWKPATH環境変数を参照)。 もしファイル名に`/'が含まれていれば、検索にパスは使用されない。 `/'がなければ、ファイル名はパスに含まれているディレクトリの 名前と連結され、その結果作られたファイル名によってファイルのオープンが できるかどうかを試す。awkでファイルから読み込みが できるかどうかをテストできる手段は一つだけしかなく、それは getlineでそのファイルから読み込みができるか、ということであり これはpathtoが行っていることである(26) もしファイルから読み込むことができればそのファイルをクローズし、 その時のファイル名を関数の戻り値として返す。

gawk -- '
# process @include directives
# @include指令を処理する

function pathto(file,    i, t, junk)
{
    if (index(file, "/") != 0)
        return file

    for (i = 1; i <= ndirs; i++) {
        t = (pathlist[i] "/" file)
        if ((getline junk < t) > 0) {
            # found it
            close(t)
            return t
        }
    }
    return ""
}

メインプログラムはBEGINルール一つからなる。最初に行うことはpathto で使用するpathlistという配列をセットアップすることである。パスを`:' で分割した後で、空の要素はカレントディレクトリを表わす"."に置き換えられる。

BEGIN {
    path = ENVIRON["AWKPATH"]
    ndirs = split(path, pathlist, ":")
    for (i = 1; i <= ndirs; i++) {
        if (pathlist[i] == "")
            pathlist[i] = "."
    }

スタックは`/tmp/ig.s.$$'となるARGV[1]で初期化される。 メインループがそれに続く。入力行は継続して読み続けられる。 `@include'で始まっていない行はそのまま出力される。

行が`@include'で始まっている場合、ファイル名は$2にある。 pathtoはフルパスを生成するために呼び出される。 もしここで生成できなければ、エラーメッセージを出力し、 処理を継続する。

次に行うことは、ファイルがすでにインクルードされたものでないかどうか のチェックである。もしそのファイルがすでに出現したものであれば、 警告メッセージを出力する。初めてのものであれば、 新しいファイル名をスタックにプッシュし、処理を継続する。

最後に、getlineが入力ファイルの終端にぶつかったとき、 そのファイルはクローズされ、スタックからポップされる。 stackptrが0を下回ったとき、プログラムは終了する。

    stackptr = 0
    input[stackptr] = ARGV[1] # ARGV[1] が最初のファイル

    for (; stackptr >= 0; stackptr--) {
        while ((getline < input[stackptr]) > 0) {
            if (tolower($1) != "@include") {
                print
                continue
            }
            fpath = pathto($2)
            if (fpath == "") {
                printf("igawk:%s:%d: cannot find %s\n", \
                    input[stackptr], FNR, $2) > "/dev/stderr"
                continue
            }
            if (! (fpath in processed)) {
                processed[fpath] = input[stackptr]
                input[++stackptr] = fpath
            } else
                print $2, "included in", input[stackptr], \
                    "already included in", \
                    processed[fpath] > "/dev/stderr"
        }
        close(input[stackptr])
    }
}' /tmp/ig.s.$$ > /tmp/ig.e.$$

最後のステップは展開したプログラムと、ユーザーが指定した オリジナルのオプションと コマンド引数を渡してgawkを呼び出すことである。 gawkの終了ステータスは呼び出し元の igawkに返される。

eval gawk -f /tmp/ig.e.$$ $opts -- "$@"

exit $?

このバージョンのigawkはこのプログラムに対する私の三つめの試みを表 わしている。プログラムをより良く作業させる三つの重要な単純化がそこにある。

  1. `-f'で指定されたファイルに対しても`@include'を使うことによって awkプログラムの組み立てが単純になる。 すべての`@include'の処理は一度だけ実行できる。
  2. pathto関数はファイルにアクセスできるかどうかをテストするために getlineを使って読んだ行をセーブしようとしない。 この行をメインプログラムで使うためにセーブしようとすると、 かなり複雑なものになってしまう。
  3. getlineのループをBEGINルールの中で使うことによって、 全てを一ヶ所で行う。これはネストした`include'文の処理の ために別のループを呼び出す必要がないということである。

同様に、このプログラムはshawkを組み合わせてプログラミン グすることはしばしば価値があることだということを示すものである。あなたは 通常、CやC++の低レベルなプログラミングに頼らずに多くのことを達成すること ができるし、ある種の文字列や引数の取り扱いはawkよりもシェルを使う ことによってしばしば簡単になる。

また、igawkはプログラムに新しい機能を付け加えることが常に必要なわ けではないということを証明している。igawkがあるので、 `@include'を処理する能力をgawk自身に追加すべき大きな理由はない。

この追加の例として、検索パスに含まれるディレクトリに 二つのファイルを置いておくことを考えてみよう。

`default.awk'
このファイルはgetoptassertのような デフォルトのライブラリ関数の集合からなる。
`site.awk'
このファイルはサイトやインストールに特有な ライブラリ関数から構成される。 つまり、それぞれの場所で開発された関数である。 別のファイルにすることによって、 新しいgawkがリリースされたときに `default.awk'を システム管理者がいちいちローカル関数を付け加なくとも 更新できるようになる。

あるユーザー が、gawkをその起動時に自動的にこれらのファイルを読み込む様に 修正して欲しいという要望を寄せてきた。そうする代わりに、 igawkをそのような動作をするように変更することは とても簡単だった。igawkはネストした@includeディレクティブを 扱うことができるので、`default.awk'は必要なライブラリ関数を指示する `@include'文を含むことができた。


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