追記 (2021/01/23)
gdb で strflocaltime 関数の処理を追う
ソースコードを github から取得して、手元の環境で jq をビルドし、gdb で strflocaltime 関数の処理を追ってみました。
$ gdb /usr/local/bin/jq ... 略 ... (gdb) break f_strflocaltime Breakpoint 1 at 0x4fca0: file src/builtin.c, line 1567. (gdb) -n '1611312534|strflocaltime("%Y-%m-%dT%H:%M:%S %Z")' Undefined command: "-n". Try "help". (gdb) run -n '1611312534|strflocaltime("%Y-%m-%dT%H:%M:%S %Z")' Starting program: /usr/local/bin/jq -n '1611312534|strflocaltime("%Y-%m-%dT%H:%M:%S %Z")' [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Breakpoint 1, f_strflocaltime (jq=0x555555638530, a=..., b=...) at src/builtin.c:1567 1567 if (jv_get_kind(a) == JV_KIND_NUMBER) { (gdb) n 1568 a = f_localtime(jq, a); ... 略 ... 1578 size_t alloced = strlen(fmt) + 100; (gdb) n 1579 char *buf = alloca(alloced); (gdb) n 1580 size_t n = strftime(buf, alloced, fmt, &tm); (gdb) n 1581 jv_free(b); ... 略 ... process (jq=0x555555638530, value=..., flags=<optimized out>, dumpopts=645) at src/main.c:181 181 if ((options & RAW_OUTPUT) && jv_get_kind(result) == JV_KIND_STRING) { (gdb) n 190 if (jv_get_kind(result) == JV_KIND_FALSE || jv_get_kind(result) == JV_KIND_NULL) (gdb) n 194 if (options & SEQ) (gdb) n 196 jv_dump(result, dumpopts); (gdb) n 198 if (!(options & RAW_NO_LF)) (gdb) n 199 priv_fwrite("\n", 1, stdout, dumpopts & JV_PRINT_ISATTY); (gdb) n "2021-01-22T10:48:54 UTC" 200 if (options & RAW_NUL)
strftime(3) あたりが呼ばれていることが解りました。
ちなみに、gdb の使い方は、以下の記事がとても解りやすかったです。
さすが、大学の講義で使われるであろう資料だと思います。
はじめに
jq にはいつもお世話になっていますが、今日ほどお世話になった日はありませんでした。ということで、jq に用意されている strflocaltime
や strftime
という関数が感動したのでメモっておきます。
尚、本記事で扱う環境は jq のバージョンは以下の通りです。
$ sw_vers ProductName: Mac OS X ProductVersion: 10.15.7 BuildVersion: 19H114 $ cat /etc/os-release PRETTY_NAME="Debian GNU/Linux 10 (buster)" NAME="Debian GNU/Linux" VERSION_ID="10" VERSION="10 (buster)" VERSION_CODENAME=buster ID=debian HOME_URL="https://www.debian.org/" SUPPORT_URL="https://www.debian.org/support" BUG_REPORT_URL="https://bugs.debian.org/" $ jq --version jq-1.6
strflocaltime 関数や strftime 関数
ドキュメントより
jq における strflocaltime
や strftime
関数は、以下のように解説されています。(ドキュメントを抜粋しました)
Low-level jq interfaces to the C-library time functions are also provided: strptime, strftime, strflocaltime, mktime, gmtime, and localtime. Refer to your host operating system's documentation for the format strings used by strptime and strftime. Note: these are not necessarily stable interfaces in jq, particularly as to their localization functionality. 〜 略 〜 The strftime(fmt) builtin formats a time (GMT) with the given format. The strflocaltime does the same, but using the local timezone setting.
C 言語の関数として提供されている strftime
等へのインターフェースとして、同名の関数が jq にも提供されているという理解です。注意書きに「これらは必ずしも jq の安定したインターフェースではありません」とあることから、関数が実行される OS の制約を受ける可能性があるから、このような書き方がされているのかなと考えています。
尚、strftime
は、GMT の時刻を返しますが、strflocaltime
はローカルのタイムゾーン設定を使用するとのことです。
UNIX TIME だと、ぱっと見わからない
JSON データに UNIX TIME が記録されていて、誰かに見せる時に何時何分なのか解らなくて、なんか良い方法がないかなーと調べていたら jq の strflocaltime
に出会いました。
以下は、RabbitMQ サーバーが提供している REST API のレスポンスについて、name
と head_message_timestamp
というキーを抽出した結果です。(唐突に RabbitMQ が登場してきてすいません。今回は単純に UNIX TIME を返す REST API サーバーと思って下さい)
$ curl -s -uxxxx:xxxx localhost:15672/api/queues | jq -r '.[]|[.name, .head_message_timestamp]|@sh' 'examplequeue1' 1611312534 'examplequeue2' 1611312488 'examplequeue3' 1611312502
ここで出力されている 1611312534
や 1611312488
等が UNIX TIME ですが、パッと見、何時何分なのか解りません。
これを、先述の strflocaltime
関数を利用してローカル時間に変換してみます。
# on macOS $ curl -s -uxxxx:xxxx rabbitmq-server:15672/api/queues | jq -r '.[]|[.name, (.head_message_timestamp|strflocaltime("%Y-%m-%dT%H:%M:%S %Z"))]|@sh' 'examplequeue1' '2021-01-22T19:48:54 JST' 'examplequeue2' '2021-01-22T19:48:08 JST' 'examplequeue3' '2021-01-22T19:48:22 JST' # on Debian $ export TZ=Asia/Tokyo $ curl -s -uxxxx:xxxx rabbitmq-server:15672/api/queues | jq -r '.[]|[.name, (.head_message_timestamp|strflocaltime("%Y-%m-%dT%H:%M:%S %Z"))]|@sh' 'examplequeue1' '2021-01-22T19:48:54 JST' 'examplequeue2' '2021-01-22T19:48:08 JST' 'examplequeue3' '2021-01-22T19:48:22 JST'
なんということでしょうか。ちゃんと日本時間で出力されているではないですか!
以上
ドキュメントを読みましょう案件ですが、jq の奥深さを実感した一日でした。
おまけ 〜 jq ソースコードリーディング 〜
jq って Go で実装されているもんだと思っていました (全く根拠の無い思い込みでした) が、実は C 言語で実装されていました。
今回、話題に上がっている strflocaltime
や strftime
については、以下に実装されていました。
strflocaltime
に関与している部分を抜粋すると、以下のような実装になっていました。
#ifdef HAVE_STRFTIME static jv f_strflocaltime(jq_state *jq, jv a, jv b) { if (jv_get_kind(a) == JV_KIND_NUMBER) { a = f_localtime(jq, a); } else if (jv_get_kind(a) != JV_KIND_ARRAY) { return ret_error2(a, b, jv_string("strflocaltime/1 requires parsed datetime inputs")); } else if (jv_get_kind(b) != JV_KIND_STRING) { return ret_error2(a, b, jv_string("strflocaltime/1 requires a string format")); } struct tm tm; if (!jv2tm(a, &tm)) return jv_invalid_with_msg(jv_string("strflocaltime/1 requires parsed datetime inputs")); const char *fmt = jv_string_value(b); size_t alloced = strlen(fmt) + 100; char *buf = alloca(alloced); size_t n = strftime(buf, alloced, fmt, &tm); jv_free(b); /* POSIX doesn't provide errno values for strftime() failures; weird */ if (n == 0 || n > alloced) return jv_invalid_with_msg(jv_string("strflocaltime/1: unknown system failure")); return jv_string(buf); } #else static jv f_strflocaltime(jq_state *jq, jv a, jv b) { jv_free(a); jv_free(b); return jv_invalid_with_msg(jv_string("strflocaltime/1 not implemented on this platform")); } #endif
色々と気になる (けど、理解は出来ない) 部分はありますが、内部的に C の API コールである strftime(3) がコールされていることが解ります。strftime(3) は、
strftime() 関数 は、要素別の時刻 tm の内容を format で指定された書式指定にしたがって変換し、 長さ max の文字列 s に書き込む。
とあります。strflocaltime
という関数は、おそらく、時刻のローカルのタイムゾーンを取得して、引数として与えられた書式で時刻を返しているということが「何となく」解りました。