はじめに
awk は srand
関数を使って Unix 時間を取得することができます。srand
関数は乱数のシードを初期化する関数です(乱数値を返す rand
関数と混同しないようにしてください)。これはどの環境のどの awk でも動く移植性が高い方法で、この書き方をすれば srand
関数は必ず Unix 時間を返します。
$ awk 'BEGIN{srand(); print srand()}'
1688282747
$ date +%s # 補足 date コマンドを使っても取得することができます
1688282747
本題の前に一つ注意点です。Unix時間を取得する方法として以下のコードを見たことがある人もいるかも知れませんが、これでは正しくUnix 時間を取得することはできません。有り体に言えばバグです。
$ awk 'BEGIN{print srand() + srand()}'; date +%s
1686750469 ← awk コマンドによる出力(1秒ズレている)
1686750468 ← date コマンドによる出力(date コマンドを使った Unix 時間の取得)
$ readlink -f $(which awk) # awk の実体は GAWK
/usr/bin/gawk
このコードは書籍『「シェル芸」に効くAWK処方箋』で紹介されているコードで、「これは強力! AWKとパイプの新しい関係 ~ 時刻を取得する関数、Socket通信、双方向パイプ」や「第2回 月刊『シェルスクリプトマガジン 2014 November (Vol.19)」にも掲載されているようです。一応 Solaris 10.3 / 11.4 の POSIX awk (/usr/xpg4/bin/awk
) に限れば正しく取得できるのですが、同書が対象としているのは GNU awk (gawk) なので間違いです。ちなみに間違っていることは報告済みです。本記事を読むとなぜこのコードで正しく取得できないかの理由も知ることができます。
POSIX (Issue 8) で規定される動作
srand
関数で Unix 時間を取得できるかもしれないけど、そんなのたまたま動く「裏技」のようなもので動かない実装もあるかもしれないし、そんなコード書いたらダメ!と思うかもしれませんが、この動作は POSIX Issue 8 で標準化される仕様です。
参照 0000983: awk's rand()/srand() spec not useful
確かにかつては裏技のようにみなされていましたが、私が調べた既存の全ての awk で期待したとおりに Unix 時間を取得することができる移植性が高い方法です。ちなみに私が調べた awk は GNU awk、オリジナルの新しい awk である One true awk (nawk)、mawk、busybox awk、Solaris の POSIX awk です。
Solaris 10.3 や Solaris 11.3 の /usr/bin/awk
では動きませんが、これは 1977年版の最初の awk でそもそもユーザー定義関数や多くの機能が使えず POSIX にも準拠しておらず、現実的には使い物にならないレベルの古い awk なので考慮する必要はありません。Solaris 10.3 や Solaris 11.3 でも /usr/xpg4/bin/awk
に POSIX awk があるのでこちらを使ってください。また最新の Solaris 11.4 では /usr/bin/awk
は POSIX awk に更新されているようです。
POSIX Issue 8 はまだ完成していませんが、標準化の流れの話から分かる通り、これは新機能ではなく「歴史的な動作を明文化しただけ」の「すでに使える機能」です、標準化される以上、今後この仕様が変わることは考えられないので、この方法で Unix 時間を取得しても問題ありません。もしかしたらマイナーな awk の実装で動かないものがあるかもしれませんが「POSIX に準拠するべきだ」と提案すれば修正されるでしょう。
なぜsrand関数でUnix時間が取得できるのか?
勘のいい人は「シード値として Unix 時間を使用するから」の一言で全てを理解できるでしょう。
srand
関数は awk の関数でランダム値を取得する rand
関数のためのシード値を設定する関数です。シード値とは(疑似)乱数を生成するために使用する値で、シード値が同じであれば awk を再実行しても同じ乱数列が生成されます。ただし異なる awk の実装でも同じ乱数列が生成されるというようなものではありません。
$ seq 3 | awk 'BEGIN { srand(42) } { print rand() }'
0.24632
0.396143
0.955075
$ seq 3 | awk 'BEGIN { srand(42) } { print rand() }'
0.24632
0.396143
0.955075
同じ乱数列が生成されるのであれば乱数として使えないと思うかもしれませんが、テストなどのために意図的に同じ乱数列を生成したい場合には便利です。ですが、一般的には異なる乱数列が欲しいでしょう。その場合はシード値を省略して次のような使い方で実現することができます。
$ seq 3 | awk 'BEGIN { srand() } { print rand() }'
0.379604
0.109919
0.0617382
$ sleep 1 # 再実行まで1秒以上間隔をあけること
$ seq 3 | awk 'BEGIN { srand() } { print rand() }'
0.684365
0.951084
0.119918
これはなぜかというと awk の srand
関数はシード値を省略すると Unix 時間をシード値として使うという仕様だからです。Unix 時間は一秒毎に自動的に変化する値とみなせるので、Unix 時間をシード値として使うと一秒ごとに別の乱数列が出力されるようになります。逆に言えば一秒以内に再実行すると同じ乱数列が出力されてしまうので注意が必要です。昔に比べてコンピュータの性能が大幅に向上しシェルスクリプトの書き方によっては一秒以内に awk を再実行してしまう可能性が考えられるので、そのような場合には別の工夫が必要になってきます。以下の方法で /dev/urandom
(または /dev/random
)から乱数を生成することができるので、この値を直接利用するか、どうしても awk を使う必要がある場合は srand
関数のシード値として使うことができます。
$ od -An -tu4 -N 4 /dev/urandom
273275427
# 乱数を複数生成したい場合(下記の例では 10 個)
$ od -An -tu4 -N $((4 * 10)) /dev/urandom
1372449868 2666820430 3972546570 1398767086
2131978540 2222568716 2805779795 3822953772
2755474640 685674344
# 複数の乱数を一つずつ行にしたい場合
$ od -An -tu4 /dev/urandom | tr -s ' ' '\n'
3396887147
1064438785
3015535112
3825612160
︙
/dev/urandom
や /dev/random
はほとんどの環境で使用できると思いますが、厳密な話をすると POSIX で標準化されていません。
おまけ FreeBSD 10.1までの古いawkは乱数の品質が低い
少々話が脱線しますが、先程「(シード値が変わらないため)1秒以内に再実行すると同じ乱数列が出力されてしまう」と書きましたが、1秒以上待ったとしても、古い FreeBSD(おそらく10.1 まで)では乱数の品質が低い(似たような乱数値を生成する)という問題がありました。
$ for i in $(seq 5); do awk 'BEGIN { srand(); print rand() }'; sleep 1; done
0.424013
0.424021
0.424028
0.424044
これは awk の過去の実装が品質の低い rand(3) を内部で使用していたためです。この問題に関する修正は2014年8月頃に検討され2014年10月頃に rand(3) から random(3) への修正が行われています。FreeBSD のバージョンではこの修正は 10.1 に含まれていませんが 10.2 には含まれています。
Linuxでは rand(3) は random(3) と同じアルゴリズムを使用しているため rand(3) のままでも問題ありませんが、移植性を考慮し「精度の高い乱数が必要な アプリケーションではこの関数は使用してはいけない」と警告しています。
rand() と srand() の Linux C Library 版は、 random(3) と srandom(3) の両関数と同じ乱数生成 アルゴリズムを使用している。そのため、下位のビットは上位のビットと 同じくらいにランダムである。 しかし、旧版の rand() の実装や、他のシステムの現在の実装では、下位のビットが上位のビットほど ランダムになっていない。移植性を高める場合でも、精度の高い乱数が必要な アプリケーションではこの関数は使用してはいけない (代わりに random(3) を使うこと)。
いずれにしても、FreeBSD 10.1 は 2016年12月31日 にサポートが終了しており、FreeBSD 版 awk の乱数の品質が低いという問題は(2023年7月現在時点で)9 年近く前に修正されているので、もはや気にするような話ではありません。
srand関数の戻り値はなに?
さて srand
関数が引数を省略するとシード値として Unix 時間を使うという話までしました。ではなぜ srand
関数を使用すると Unix 時間を取得することが出来るのでしょうか? そしてなぜ srand
関数をニ回呼び出しているのでしょうか?
$ awk 'BEGIN{ srand(); print srand() }'
1686749521
srand
関数の戻り値が Unix 時間となっている理由は srand
関数が「前回設定したシード値を返す」という仕様だからです。例えば srand(12345)
でシード値を 12345 に設定すれば、次の srand
関数の呼び出しは 12345 を返します。
$ awk 'BEGIN{ srand(12345); print srand() }'
12345
つまり最初の srand
関数の呼び出しで引数を省略して Unix 時間をシード値として設定し、次の print srand()
で前回設定したシード値を取得しているので Unix 時間が出力されるのです。
初回のsrand関数呼び出しの戻り値
srand
関数が戻り値として「前回設定したシード値を返す」仕様であることを説明しました。ここで一つの疑問が浮かびます、一度もシード値を設定しない場合の最初の呼び出しでは何を返すのか?です。
その答えは内部で初期化されたデフォルトのシード値です。実はこの値は awk の実装によって異なります。実装によって動作が異なるため POSIX では規定されていません(正確には「未指定」と標準化されています)。
$ gawk 'BEGIN{print srand()}'
1
$ mawk 'BEGIN{print srand()}'
1688291452
$ original-awk 'BEGIN{print srand()}'
1
$ busybox awk 'BEGIN{print srand()}'
1
$ awk 'BEGIN{print srand()}'
1
$ /usr/bin/awk 'BEGIN{print srand()}'
1688289906
$ /usr/xpg4/bin/awk 'BEGIN{print srand()}'
0
「正しくUnix時間を取得できないコード」は何が間違っていたのか?
さてここで冒頭の「正しく Unix 時間を取得できないコード」を思い出してみましょう。もうおわかりですね。1秒ズレる理由は srand
関数の最初の呼び出しで GNU awk は 1
を返すからです。つまり最初の srand
関数の戻り値は捨てなければいけないということです。
$ awk 'BEGIN{print srand() + srand()}'; date +%s
1686750469 ← awk コマンドによる出力(1秒ズレている)
1686750468 ← date コマンドによる出力(date コマンドを使った Unix 時間の取得)
一応 Debian 2.2 (2000年)でも試したのですが、1
を返していたので、昔の GNU awk は 0
を返していたという話でもないはずです。1 秒のズレは気づきにくいですし、もしかしたら Solaris 11.4 の /usr/xpg4/bin/awk
の挙動と勘違いしていたのかもしれません。またちょっと起動が遅れた程度と考えれば実害もあまりないでしょう。しかし移植性を考えた場合、Debian や Ubuntu でデフォルトで使われている mawk や Solaris 11.4 の /usr/bin/awk
では大幅にズレてしまうことになります。
GNU awkではsystimeでもUnix時間を取得できる
GNU awk には Unix 時間を取得するための専用の systime
関数 があるので、こちらを使っても構いません。
$ gawk 'BEGIN{srand(); print srand(); print systime();}'; date +%s
1688292598
1688292598
1688292598
また date +%s
でも Unix 時間を取得できるのでこちらを利用するという方法もあります。+%s
は POSIX Issue 7 では標準化されておらず、Solaris 10.3 では使えなかったりしますが、こちらも POSIX Issue 8 で標準化され、少なくとも今も主要な OS では問題なく使用することができます。awk の中から呼び出すこともできます。
$ awk 'BEGIN{ system("date +%s") }'
1688293955
$ # 変数に入れる場合
$ awk 'BEGIN{ "date +%s" | getline ut; print ut }'
1688294020
$ # 厳密にはコマンド呼び出しは close しなければいけません
$ awk 'BEGIN{ cmd = "date +%s"; cmd | getline ut; print ut; close(cmd) }'
1688294135
さいごに
awk で Unix 時間を取得する裏技は POSIX で標準化されます。とは言ってもあまり unix 時間を取得したいということもないでしょう。シェルスクリプトからであれば date +%s
を使えばほぼ問題ないのでなおさらです。知っていてもあまり役に立たない雑学に近い話かなーとは思いつつも、今までの常識を更新するという意味でこれを書く意味はあるかなーと思っています。昔の話をいつまでもしていても意味はないですからね。
余談ですが他にもシェルスクリプトや awk で乱数やUnix時間を扱う方法は色々あったりします。興味がある方は参考にどうぞ。