Mac 端末のターミナルでミリ秒以下のタイムスタンプを取得したい

macOS に標準でインストールされた date コマンドを使用した際に、ミリ秒以下の数字が取得できないことに気がつきました。coreutils をインストールして GNU 版の date コマンドを利用することで対応しました。

コンバンハ、千葉(幸)です。

dateコマンドでミリ秒以下のタイムスタンプを取得したい時がありました。

具体的には、以下のような出力を期待していました。

$ date +'%H:%M:%S.%3N'
19:34:20.571 #3桁のミリ秒まで取得できている

普段使いしている Mac 端末で実際にコマンドを実行してみると、以下の結果になりました。

$ date +'%H:%M:%S.%3N'
19:34:20.3N #%3Nのフォーマットが効かずそのまま3Nが出力されている

原因と対策を調べるのに15分くらいかかったので、同じような方の15分を節約するためにブログに残しておきます。

まとめ

  • Linux のコマンドによっては BSD か GNU で挙動に差異がある
  • macOS でデフォルトでインストールされているものは基本的に BSD 版
  • date コマンドでミリ秒以下をフォーマット表示できるのは GNU 版
  • Homebrew でcoreutilsをインストールすることで macOS でも多くの GNU 版のユーティリティを利用可能に

実行環境

  • macOS Ventura 13.0.1
  • zsh

BSD date コマンドはミリ秒以下をフォーマットできない

(主に)Linux 系で使用するコマンドには、同じ名前でも実装が異なるものあります。BSD 版か GNU 版か、を区別する機会が多いかと思います。

GNU date ではミリ秒以下のフォーマットに対応しています。

FORMAT で出力を制御します。解釈される文字列は次の通りです。

…….

  • %N

    ナノ秒 (000000000..999999999)

%3Nのように記載することで桁数を指定できます。

一方、BSD date では「秒」までしか対応していません。

 If   an operand does not have a leading plus sign, it is interpreted as a
 value for setting the system's notion of the current date and time.  The
 canonical representation for setting the date and time is:

 cc      Century (either 19 or 20) prepended to the abbreviated
     year.
 yy      Year in abbreviated form (e.g., 89 for 1989, 06 for 2006).
 mm      Numeric month, a number from 1 to 12.
 dd      Day, a number from 1 to 31.
 HH      Hour, a number from 0 to 23.
 MM      Minutes, a number from 0 to 59.
 SS      Seconds, a number from 0 to 60 (59 plus a potential leap
     second).

macOS に標準でインストールされているコマンドは基本的に BSD 版であり、BSD date がミリ秒以下に対応していないことから冒頭の結果になったというわけでした。

coreutils のインストールにより GNU date を利用可能に

macOS でも GNU 版の date コマンドを使いたくなります。その場合、Homebrew でcoreutilsをインストールするのが一番手っ取り早いかと思います。

(Homebrew のインストール自体がまだの場合は、以下を実行してください。)

$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Homebrew を使用できる状態で、以下を実行します。

$ brew install coreutils

インストールが完了すれば、GNU 版の date コマンドをgdateとして利用可能になります。

$ gdate --version
date (GNU coreutils) 9.3
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by David MacKenzie.

$ gdate +'%H:%M:%S.%3N'
20:01:13.404

↑ミリ秒以下のフォーマットがきちんとできています。

参考:他にどんな GNU コマンドが使えるようになっているのか

coreutilsでインストールされたユーティリティはgdateだけではありません。

その内訳を見るには、インストール時のログが参考になりそうです。

……
==> Installing coreutils
==> Pouring coreutils--9.3.arm64_ventura.bottle.tar.gz
==> Caveats
Commands also provided by macOS and the commands dir, dircolors, vdir have been installed with the prefix "g".
If you need to use these commands with their normal names, you can add a "gnubin" directory to your PATH with:
  PATH="/opt/homebrew/opt/coreutils/libexec/gnubin:$PATH"
……

ログの中で「接頭辞gなしで GNU 版のコマンドを使用したい場合、追加してね」と表示されているパスを確認してみます。

$ ls -go /opt/homebrew/opt/coreutils/libexec/gnubin #-gと-oはそれぞれグループとオーナーの表示を省略するオプション
total 0
lrwxr-xr-x  1   12  4 18 23:32 [ -> ../../bin/g[
lrwxr-xr-x  1   16  4 18 23:32 b2sum -> ../../bin/gb2sum
lrwxr-xr-x  1   17  4 18 23:32 base32 -> ../../bin/gbase32
lrwxr-xr-x  1   17  4 18 23:32 base64 -> ../../bin/gbase64
lrwxr-xr-x  1   19  4 18 23:32 basename -> ../../bin/gbasename
lrwxr-xr-x  1   17  4 18 23:32 basenc -> ../../bin/gbasenc
lrwxr-xr-x  1   14  4 18 23:32 cat -> ../../bin/gcat
lrwxr-xr-x  1   16  4 18 23:32 chcon -> ../../bin/gchcon
lrwxr-xr-x  1   16  4 18 23:32 chgrp -> ../../bin/gchgrp
lrwxr-xr-x  1   16  4 18 23:32 chmod -> ../../bin/gchmod
lrwxr-xr-x  1   16  4 18 23:32 chown -> ../../bin/gchown
lrwxr-xr-x  1   17  4 18 23:32 chroot -> ../../bin/gchroot
lrwxr-xr-x  1   16  4 18 23:32 cksum -> ../../bin/gcksum
lrwxr-xr-x  1   15  4 18 23:32 comm -> ../../bin/gcomm
lrwxr-xr-x  1   13  4 18 23:32 cp -> ../../bin/gcp
lrwxr-xr-x  1   17  4 18 23:32 csplit -> ../../bin/gcsplit
lrwxr-xr-x  1   14  4 18 23:32 cut -> ../../bin/gcut
lrwxr-xr-x  1   15  4 18 23:32 date -> ../../bin/gdate
lrwxr-xr-x  1   13  4 18 23:32 dd -> ../../bin/gdd
lrwxr-xr-x  1   13  4 18 23:32 df -> ../../bin/gdf
lrwxr-xr-x  1   14  4 18 23:32 dir -> ../../bin/gdir
lrwxr-xr-x  1   20  4 18 23:32 dircolors -> ../../bin/gdircolors
lrwxr-xr-x  1   18  4 18 23:32 dirname -> ../../bin/gdirname
lrwxr-xr-x  1   13  4 18 23:32 du -> ../../bin/gdu
lrwxr-xr-x  1   15  4 18 23:32 echo -> ../../bin/gecho
lrwxr-xr-x  1   14  4 18 23:32 env -> ../../bin/genv
lrwxr-xr-x  1   17  4 18 23:32 expand -> ../../bin/gexpand
lrwxr-xr-x  1   15  4 18 23:32 expr -> ../../bin/gexpr
lrwxr-xr-x  1   17  4 18 23:32 factor -> ../../bin/gfactor
lrwxr-xr-x  1   16  4 18 23:32 false -> ../../bin/gfalse
lrwxr-xr-x  1   14  4 18 23:32 fmt -> ../../bin/gfmt
lrwxr-xr-x  1   15  4 18 23:32 fold -> ../../bin/gfold
lrwxr-xr-x  1   17  4 18 23:32 groups -> ../../bin/ggroups
lrwxr-xr-x  1   15  4 18 23:32 head -> ../../bin/ghead
lrwxr-xr-x  1   17  4 18 23:32 hostid -> ../../bin/ghostid
lrwxr-xr-x  1   13  4 18 23:32 id -> ../../bin/gid
lrwxr-xr-x  1   18  4 18 23:32 install -> ../../bin/ginstall
lrwxr-xr-x  1   15  4 18 23:32 join -> ../../bin/gjoin
lrwxr-xr-x  1   15  4 18 23:32 kill -> ../../bin/gkill
lrwxr-xr-x  1   15  4 18 23:32 link -> ../../bin/glink
lrwxr-xr-x  1   13  4 18 23:32 ln -> ../../bin/gln
lrwxr-xr-x  1   18  4 18 23:32 logname -> ../../bin/glogname
lrwxr-xr-x  1   13  4 18 23:32 ls -> ../../bin/gls
lrwxr-xr-x  1   17  4 18 23:32 md5sum -> ../../bin/gmd5sum
lrwxr-xr-x  1   16  4 18 23:32 mkdir -> ../../bin/gmkdir
lrwxr-xr-x  1   17  4 18 23:32 mkfifo -> ../../bin/gmkfifo
lrwxr-xr-x  1   16  4 18 23:32 mknod -> ../../bin/gmknod
lrwxr-xr-x  1   17  4 18 23:32 mktemp -> ../../bin/gmktemp
lrwxr-xr-x  1   13  4 18 23:32 mv -> ../../bin/gmv
lrwxr-xr-x  1   15  4 18 23:32 nice -> ../../bin/gnice
lrwxr-xr-x  1   13  4 18 23:32 nl -> ../../bin/gnl
lrwxr-xr-x  1   16  4 18 23:32 nohup -> ../../bin/gnohup
lrwxr-xr-x  1   16  4 18 23:32 nproc -> ../../bin/gnproc
lrwxr-xr-x  1   17  4 18 23:32 numfmt -> ../../bin/gnumfmt
lrwxr-xr-x  1   13  4 18 23:32 od -> ../../bin/god
lrwxr-xr-x  1   16  4 18 23:32 paste -> ../../bin/gpaste
lrwxr-xr-x  1   18  4 18 23:32 pathchk -> ../../bin/gpathchk
lrwxr-xr-x  1   16  4 18 23:32 pinky -> ../../bin/gpinky
lrwxr-xr-x  1   13  4 18 23:32 pr -> ../../bin/gpr
lrwxr-xr-x  1   19  4 18 23:32 printenv -> ../../bin/gprintenv
lrwxr-xr-x  1   17  4 18 23:32 printf -> ../../bin/gprintf
lrwxr-xr-x  1   14  4 18 23:32 ptx -> ../../bin/gptx
lrwxr-xr-x  1   14  4 18 23:32 pwd -> ../../bin/gpwd
lrwxr-xr-x  1   19  4 18 23:32 readlink -> ../../bin/greadlink
lrwxr-xr-x  1   19  4 18 23:32 realpath -> ../../bin/grealpath
lrwxr-xr-x  1   13  4 18 23:32 rm -> ../../bin/grm
lrwxr-xr-x  1   16  4 18 23:32 rmdir -> ../../bin/grmdir
lrwxr-xr-x  1   17  4 18 23:32 runcon -> ../../bin/gruncon
lrwxr-xr-x  1   14  4 18 23:32 seq -> ../../bin/gseq
lrwxr-xr-x  1   18  4 18 23:32 sha1sum -> ../../bin/gsha1sum
lrwxr-xr-x  1   20  4 18 23:32 sha224sum -> ../../bin/gsha224sum
lrwxr-xr-x  1   20  4 18 23:32 sha256sum -> ../../bin/gsha256sum
lrwxr-xr-x  1   20  4 18 23:32 sha384sum -> ../../bin/gsha384sum
lrwxr-xr-x  1   20  4 18 23:32 sha512sum -> ../../bin/gsha512sum
lrwxr-xr-x  1   16  4 18 23:32 shred -> ../../bin/gshred
lrwxr-xr-x  1   15  4 18 23:32 shuf -> ../../bin/gshuf
lrwxr-xr-x  1   16  4 18 23:32 sleep -> ../../bin/gsleep
lrwxr-xr-x  1   15  4 18 23:32 sort -> ../../bin/gsort
lrwxr-xr-x  1   16  4 18 23:32 split -> ../../bin/gsplit
lrwxr-xr-x  1   15  4 18 23:32 stat -> ../../bin/gstat
lrwxr-xr-x  1   17  4 18 23:32 stdbuf -> ../../bin/gstdbuf
lrwxr-xr-x  1   15  4 18 23:32 stty -> ../../bin/gstty
lrwxr-xr-x  1   14  4 18 23:32 sum -> ../../bin/gsum
lrwxr-xr-x  1   15  4 18 23:32 sync -> ../../bin/gsync
lrwxr-xr-x  1   14  4 18 23:32 tac -> ../../bin/gtac
lrwxr-xr-x  1   15  4 18 23:32 tail -> ../../bin/gtail
lrwxr-xr-x  1   14  4 18 23:32 tee -> ../../bin/gtee
lrwxr-xr-x  1   15  4 18 23:32 test -> ../../bin/gtest
lrwxr-xr-x  1   18  4 18 23:32 timeout -> ../../bin/gtimeout
lrwxr-xr-x  1   16  4 18 23:32 touch -> ../../bin/gtouch
lrwxr-xr-x  1   13  4 18 23:32 tr -> ../../bin/gtr
lrwxr-xr-x  1   15  4 18 23:32 true -> ../../bin/gtrue
lrwxr-xr-x  1   19  4 18 23:32 truncate -> ../../bin/gtruncate
lrwxr-xr-x  1   16  4 18 23:32 tsort -> ../../bin/gtsort
lrwxr-xr-x  1   14  4 18 23:32 tty -> ../../bin/gtty
lrwxr-xr-x  1   16  4 18 23:32 uname -> ../../bin/guname
lrwxr-xr-x  1   19  4 18 23:32 unexpand -> ../../bin/gunexpand
lrwxr-xr-x  1   15  4 18 23:32 uniq -> ../../bin/guniq
lrwxr-xr-x  1   17  4 18 23:32 unlink -> ../../bin/gunlink
lrwxr-xr-x  1   17  4 18 23:32 uptime -> ../../bin/guptime
lrwxr-xr-x  1   16  4 18 23:32 users -> ../../bin/gusers
lrwxr-xr-x  1   15  4 18 23:32 vdir -> ../../bin/gvdir
lrwxr-xr-x  1   13  4 18 23:32 wc -> ../../bin/gwc
lrwxr-xr-x  1   14  4 18 23:32 who -> ../../bin/gwho
lrwxr-xr-x  1   17  4 18 23:32 whoami -> ../../bin/gwhoami
lrwxr-xr-x  1   14  4 18 23:32 yes -> ../../bin/gyes

↑例えばdate -> ../../bin/gdateのように、各コマンドのリンクが張られていることがわかります。

リンク先の階層を確認するとこんな感じ。多くのコマンド(ユーティリティ)がインストールされていることがわかりました。

似た結果なので折り畳み。クリックで展開します。
% ls -go /opt/homebrew/opt/coreutils/bin
total 24104
lrwxr-xr-x  1        6  4 18 23:32 b2sum -> gb2sum
lrwxr-xr-x  1        7  4 18 23:32 base32 -> gbase32
lrwxr-xr-x  1        7  4 18 23:32 basenc -> gbasenc
lrwxr-xr-x  1        6  4 18 23:32 chcon -> gchcon
lrwxr-xr-x  1        7  4 18 23:32 factor -> gfactor
-rwxr-xr-x  1    92318  4 18 23:32 g[
-rwxr-xr-x  1   110994  4 18 23:32 gb2sum
-rwxr-xr-x  1    93395  4 18 23:32 gbase32
-rwxr-xr-x  1    93379  4 18 23:32 gbase64
-rwxr-xr-x  1    92197  4 18 23:32 gbasename
-rwxr-xr-x  1   112611  4 18 23:32 gbasenc
-rwxr-xr-x  1    93424  4 18 23:32 gcat
-rwxr-xr-x  1   114402  4 18 23:32 gchcon
-rwxr-xr-x  1   131490  4 18 23:32 gchgrp
-rwxr-xr-x  1   113106  4 18 23:32 gchmod
-rwxr-xr-x  1   131650  4 18 23:32 gchown
-rwxr-xr-x  1   113411  4 18 23:32 gchroot
-rwxr-xr-x  1   182146  4 18 23:32 gcksum
-rwxr-xr-x  1    94241  4 18 23:32 gcomm
-rwxr-xr-x  1   173119  4 18 23:32 gcp
-rwxr-xr-x  1   166291  4 18 23:32 gcsplit
-rwxr-xr-x  1   110528  4 18 23:32 gcut
-rwxr-xr-x  1   146593  4 18 23:32 gdate
-rwxr-xr-x  1   130927  4 18 23:32 gdd
-rwxr-xr-x  1   133519  4 18 23:32 gdf
-rwxr-xr-x  1   210896  4 18 23:32 gdir
-rwxr-xr-x  1   111174  4 18 23:32 gdircolors
-rwxr-xr-x  1    92164  4 18 23:32 gdirname
-rwxr-xr-x  1   223631  4 18 23:32 gdu
-rwxr-xr-x  1    91201  4 18 23:32 gecho
-rwxr-xr-x  1   111136  4 18 23:32 genv
-rwxr-xr-x  1    94083  4 18 23:32 gexpand
-rwxr-xr-x  1   183344  6  4 17:42 gexpr
-rwxr-xr-x  1   147536  6  4 17:42 gfactor
-rwxr-xr-x  1    91090  4 18 23:32 gfalse
-rwxr-xr-x  1   110624  4 18 23:32 gfmt
-rwxr-xr-x  1    93217  4 18 23:32 gfold
-rwxr-xr-x  1    92819  4 18 23:32 ggroups
-rwxr-xr-x  1   109921  4 18 23:32 ghead
-rwxr-xr-x  1    92163  4 18 23:32 ghostid
-rwxr-xr-x  1    93903  4 18 23:32 gid
-rwxr-xr-x  1   175205  4 18 23:32 ginstall
-rwxr-xr-x  1   112609  4 18 23:32 gjoin
-rwxr-xr-x  1    92449  4 18 23:32 gkill
-rwxr-xr-x  1    92417  4 18 23:32 glink
-rwxr-xr-x  1   133295  4 18 23:32 gln
-rwxr-xr-x  1    92228  4 18 23:32 glogname
-rwxr-xr-x  1   210895  4 18 23:32 gls
-rwxr-xr-x  1   110275  4 18 23:32 gmd5sum
-rwxr-xr-x  1    94258  4 18 23:32 gmkdir
-rwxr-xr-x  1    92467  4 18 23:32 gmkfifo
-rwxr-xr-x  1    93010  4 18 23:32 gmknod
-rwxr-xr-x  1    93811  4 18 23:32 gmktemp
-rwxr-xr-x  1   174367  4 18 23:32 gmv
-rwxr-xr-x  1    92353  4 18 23:32 gnice
-rwxr-xr-x  1   165167  4 18 23:32 gnl
-rwxr-xr-x  1    92898  4 18 23:32 gnohup
-rwxr-xr-x  1    92530  4 18 23:32 gnproc
-rwxr-xr-x  1   128627  4 18 23:32 gnumfmt
-rwxr-xr-x  1   113055  4 18 23:32 god
-rwxr-xr-x  1    93458  4 18 23:32 gpaste
-rwxr-xr-x  1    92244  4 18 23:32 gpathchk
-rwxr-xr-x  1    94898  4 18 23:32 gpinky
-rwxr-xr-x  1   132111  4 18 23:32 gpr
-rwxr-xr-x  1    92005  4 18 23:32 gprintenv
-rwxr-xr-x  1    92659  4 18 23:32 gprintf
-rwxr-xr-x  1   167264  4 18 23:32 gptx
-rwxr-xr-x  1    93680  4 18 23:32 gpwd
-rwxr-xr-x  1   111845  4 18 23:32 greadlink
-rwxr-xr-x  1   112357  4 18 23:32 grealpath
-rwxr-xr-x  1   114399  4 18 23:32 grm
-rwxr-xr-x  1    92978  4 18 23:32 grmdir
-rwxr-xr-x  1    92163  4 18 23:32 gruncon
-rwxr-xr-x  1    93248  4 18 23:32 gseq
-rwxr-xr-x  1   110292  4 18 23:32 gsha1sum
-rwxr-xr-x  1   110598  4 18 23:32 gsha224sum
-rwxr-xr-x  1   110598  4 18 23:32 gsha256sum
-rwxr-xr-x  1   110598  4 18 23:32 gsha384sum
-rwxr-xr-x  1   110598  4 18 23:32 gsha512sum
-rwxr-xr-x  1   113474  4 18 23:32 gshred
-rwxr-xr-x  1   113361  4 18 23:32 gshuf
-rwxr-xr-x  1    92690  4 18 23:32 gsleep
-rwxr-xr-x  1   172545  4 18 23:32 gsort
-rwxr-xr-x  1   114034  4 18 23:32 gsplit
-rwxr-xr-x  1   132945  4 18 23:32 gstat
-rwxr-xr-x  1    93635  4 18 23:32 gstdbuf
-rwxr-xr-x  1   111409  4 18 23:32 gstty
-rwxr-xr-x  1    94048  4 18 23:32 gsum
-rwxr-xr-x  1    92481  4 18 23:32 gsync
-rwxr-xr-x  1   148080  4 18 23:32 gtac
-rwxr-xr-x  1   112769  4 18 23:32 gtail
-rwxr-xr-x  1    93984  4 18 23:32 gtee
-rwxr-xr-x  1    91953  4 18 23:32 gtest
-rwxr-xr-x  1    93604  4 18 23:32 gtimeout
-rwxr-xr-x  1   131778  4 18 23:32 gtouch
-rwxr-xr-x  1   110943  4 18 23:32 gtr
-rwxr-xr-x  1    91089  4 18 23:32 gtrue
-rwxr-xr-x  1    92933  4 18 23:32 gtruncate
-rwxr-xr-x  1    93234  4 18 23:32 gtsort
-rwxr-xr-x  1    92096  4 18 23:32 gtty
-rwxr-xr-x  1    92194  4 18 23:32 guname
-rwxr-xr-x  1    94085  4 18 23:32 gunexpand
-rwxr-xr-x  1   110881  4 18 23:32 guniq
-rwxr-xr-x  1    92387  4 18 23:32 gunlink
-rwxr-xr-x  1   110579  4 18 23:32 guptime
-rwxr-xr-x  1    92642  4 18 23:32 gusers
-rwxr-xr-x  1   210897  4 18 23:32 gvdir
-rwxr-xr-x  1   112911  4 18 23:32 gwc
-rwxr-xr-x  1    94864  4 18 23:32 gwho
-rwxr-xr-x  1    92275  4 18 23:32 gwhoami
-rwxr-xr-x  1    92240  4 18 23:32 gyes
lrwxr-xr-x  1        7  4 18 23:32 hostid -> ghostid
lrwxr-xr-x  1        7  4 18 23:32 md5sum -> gmd5sum
lrwxr-xr-x  1        6  4 18 23:32 nproc -> gnproc
lrwxr-xr-x  1        7  4 18 23:32 numfmt -> gnumfmt
lrwxr-xr-x  1        6  4 18 23:32 pinky -> gpinky
lrwxr-xr-x  1        4  4 18 23:32 ptx -> gptx
lrwxr-xr-x  1        7  4 18 23:32 runcon -> gruncon
lrwxr-xr-x  1        8  4 18 23:32 sha1sum -> gsha1sum
lrwxr-xr-x  1       10  4 18 23:32 sha224sum -> gsha224sum
lrwxr-xr-x  1       10  4 18 23:32 sha256sum -> gsha256sum
lrwxr-xr-x  1       10  4 18 23:32 sha384sum -> gsha384sum
lrwxr-xr-x  1       10  4 18 23:32 sha512sum -> gsha512sum
lrwxr-xr-x  1        6  4 18 23:32 shred -> gshred
lrwxr-xr-x  1        5  4 18 23:32 shuf -> gshuf
lrwxr-xr-x  1        7  4 18 23:32 stdbuf -> gstdbuf
lrwxr-xr-x  1        4  4 18 23:32 tac -> gtac
lrwxr-xr-x  1        8  4 18 23:32 timeout -> gtimeout

date コマンド以外でも、GNU 版を利用したいケースで活用できそうです。

終わりに

Mac 端末でミリ秒以下のタイムスタンプを取得したい、という話でした。

BSD と GNU の違いをぼんやりとしか把握していなかったので、改めて調べ直して少し理解が深まりました。「Mac は BSD と GNU どっちだっけ?」というのを毎回忘れてしまうので、今回の調査で定着していたらいいなと思います。

ミリ秒以下のタイムスタンプはそこそこ需要が大きそうな予感がしますが、標準でインストールされたものだけでは賄いきれないことが少し意外でした。gdateを利用するのとはまた別のアプローチをとった例もありますので、あわせてご参考ください。

以上、 チバユキ (@batchicchi) がお送りしました。

参考