ようへいの日々精進XP

よかろうもん

jq の底力を目の当たりにしたのでここに記す 〜 関数 strflocaltime や strftime を試す 〜

追記 (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 の使い方は、以下の記事がとても解りやすかったです。

rat.cis.k.hosei.ac.jp

さすが、大学の講義で使われるであろう資料だと思います。

はじめに

jq にはいつもお世話になっていますが、今日ほどお世話になった日はありませんでした。ということで、jq に用意されている strflocaltimestrftime という関数が感動したのでメモっておきます。

stedolan.github.io

尚、本記事で扱う環境は 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 における strflocaltimestrftime 関数は、以下のように解説されています。(ドキュメントを抜粋しました)

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 のレスポンスについて、namehead_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

ここで出力されている 16113125341611312488 等が 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 言語で実装されていました。

github.com

今回、話題に上がっている strflocaltimestrftime については、以下に実装されていました。

github.com

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 という関数は、おそらく、時刻のローカルのタイムゾーンを取得して、引数として与えられた書式で時刻を返しているということが「何となく」解りました。