dateコマンドと移植性についてのアレコレ 〜 BSD dateに日付計算の-vオプションなんて無いよ

## はじめに

よく `date` コマンドは GNU 版と BSD 版があって、例えば明日の日付を出力する場合に、GNU 版では `-d` オプションを、BSD 版では `-v` オプションを使うとして、以下のようなコードと説明をよく見かけるのですが、

```sh
# GNU 版 date コマンドで明日の日付を出力する
date -d tomorrow
date -d +1day

# BSD 版 date コマンドで明日の日付を出力する（macOS含む）
date -v +1d
```

**BSD 版 date には -v オプションはありません！**

## -vオプションはどこから来た？

`-v` オプションは **FreeBSD 版の拡張機能**で、元々の BSD（4.4BSD など）はもちろんのこと、少なくとも現時点の **NetBSD や OpenBSD には `-v` オプションは実装されていません**。macOS の `date` コマンドは FreeBSD 版なので macOS でも `-v` オプションが使えますが、**`-v` オプションを使うシェルスクリプトが NetBSD や OpenBSD で動くわけではありません。**

FreeBSD では [1997年8月に `-v` オプションが追加され](https://cgit.freebsd.org/src/commit/bin/date/date.c?id=698f86e401d193b01b09cf1ae7d1a26a5652deac)、1997年10月の FreeBSD 2.2.5 から利用可能になったようです。余談ですが、最初は `-D`, `-W`, `-M`, `-Y` オプションという年月日毎に異なるオプションで追加され、その 5日後に `-v` オプションに変更されたようです。

ということで、**`-v` オプションの話をするのであれば、BSD 版ではなく FreeBSD 版と呼びましょう**。でないと BSD 系 Unix はすべて `-v` オプションが使えるという間違いが広まってしまいます。

## dateは現在日時の取得と設定を行うためのシステム管理コマンド

 `date` コマンドは歴史的な Unix では「現在」日時の取得と設定のみを行うコマンドで、どちらかと言えばコンピュータの設定を変更するためのシステム管理コマンドです。歴史的な `date` コマンドの機能が少ないのは指定した日時を出力したり日時の計算処理を行うのは本来の`date` コマンドの役目ではないのでしょう。[Solaris 11 版 date コマンド](https://docs.oracle.com/cd/E26924_01/html/E29112/date-1.html)と [AIX 7.3 版 date コマンド](https://www.ibm.com/docs/ja/aix/7.3?topic=d-date-command)は今もなお現在日時の取得と設定ぐらいしかできません。`date` コマンドで共通して使える機能が `-u` オプションと `+` で始まる出力フォーマットしかなかっため、POSIX でもこれらの機能しか標準化されませんでした。

ちなみに **[POSIX date](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/date.html)では日時の取得のみが必須機能**で、日時の設定はオプションの XSI 拡張機能となっています。POSIX は UNIX を作るための仕様でもシステムを管理するためのものでもなく、アプリケーション（シェルスクリプト）の移植性を高めるためのガイドラインです。root 権限が必要になるようなシステム管理機能は元々の POSIX (POSIX.2-1992) の対象範囲ではなく、日時の設定も標準化されていませんでした。日時の設定は Single UNIX Specification (SUS) で拡張機能として標準化されており POSIX.1-2001 で SUS が POSIX に組み込まれたときに SUS の拡張機能は XSI 拡張機能として追加されたようです。XSI 拡張機能はオプションの仕様で POSIX に準拠するだけなら XSI 拡張機能に準拠する必要はありません。これは XSI 拡張機能を実装しなくて良いという意味だけではなく、XSI 拡張機能と矛盾する仕様で実装しても良いこと意味しています。POSIX で標準化されている仕様のうち XSI 拡張機能の部分は、特に BSD 系 Unix との互換性が低いように感じます。

## macOSはFreeBSD版だが一部挙動が異なる

macOS は FreeBSD 版の date コマンドを使用していますが、macOS が XSI 準拠なのにたいして、FreeBSD を含む BSD 系 Unix は XSI 準拠を目指しているわけではありません。POSIX で標準化されている仕様のうち XSI 拡張機能の部分は、BSD 系 Unix との互換性が低く、そのことは `date` コマンドでシステム日時を設定する時の書式の違いにも現れています。

1. `[[[mm]dd]HH]MM[[cc]yy][.ss]]`（月日時分年.秒） **macOS**、Solaris、AIX
2. `[[[[[[cc]yy]mm]dd]HH]MM[.ss]]`（年月日時分.秒） 2.9BSD、**FreeBSD**、NetBSD、OpenBSD 等

1 の分かりづらい書式は [Version 6 Unix](http://man.cat-v.org/unix-6th/1/date) でも使われていた歴史的な書式で Solaris や AIX でも使われており、POSIX の XSI 拡張機能として標準化されています。macOS では西暦上二桁 (cc)と秒 (ss) が追加されています。BSD では（日時の並び順が分かりづらいから？）書式を変更したようですが、System V 系 Unix はそのままです。macOS では FreeBSD 版の `date` コマンドを使用していますが、XSI に準拠させるために書式を変更しているようです。環境変数 `COMMAND_MODE` を `legacy` に設定すると 2 の書式に変わります。

```console:macOS 版の日時の書式
$ COMMAND_MODE=unix2003 date - # 標準は XSI 準拠の書式
date: illegal time format
usage: date [-jnRu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS]] ...
            [-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format]

$ COMMAND_MODE=legacy date - # legacy を設定すると FreeBSD と同じ書式
date: illegal time format
usage: date [-jnRu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS]] ...
            [-f fmt date | [[[[[cc]yy]mm]dd]HH]MM[.ss]] [+format]
```

## -rオプションはどこから来た？

「BSD 版」には昔から日時を指定する機能として `-r` オプションがありました。これは UNIX 時間で日時を指定するオプションです。`-r` オプションは少なくとも 1991 年にリリースされた [4.3BSD Net/2で実装](https://man.freebsd.org/cgi/man.cgi?query=date&manpath=4.3BSD+NET%2f2)されていることがわかります。NetBSD は 1993 年に、OpenBSD は 1996 年にリリースされており、フォーク元の 4.3BSD の `-r` オプションは実装されている一方で、フォーク後に実装された FreeBSD の `-v` オプションが実装されていなくても不思議ではありません。

GNU 版にも `-r` オプションがありますが、これは指定したファイルの更新日時で指定するオプションで BSD 版の `-r` オプションとは異なる機能です。この機能は [1995 年に実装](http://git.savannah.gnu.org/gitweb/?p=coreutils.git;a=commit;h=013277d331871930d21c48cbea00889401ed99e0)されました。FreeBSD 版（macOS 含む）の `-r` オプションは少し特殊で、BSD 版の UNIX 時間で指定する機能と GNU 版のファイルの更新日時で指定する機能の両方を持っています。GNU 版相当の機能は [2015 年に実装](https://cgit.freebsd.org/src/commit/bin/date/date.c?id=8b45db8ee2a9476c41bb5fd666a31dfc6a2546c2)がされました。

```console
$ date -r 1686144089 # 数字を指定した場合は UNIX 時間を指定
2023年 6月 7日 水曜日 22時21分29秒 JST

$ date -r /etc/hosts # 数字以外だと指定したファイルの更新日時を指定
2023年 4月 7日 金曜日 13時10分13秒 JST
```

ファイル名を指定する機能は GNU 版の `-r` オプションの機能との互換性を持たせた機能なわけですが、実に無理矢理感のある拡張ですね。しかしこの方法であれば後方互換性を維持しながら GNU 版との互換性を実現できるので面白い仕様です。ちなみに GNU 版にこの仕様を取り入れて UNIX 時間を解釈できるようにすることはできません。なぜならすでに数字のみのファイル名を指定している場合に後方互換性が保てなくなってしまうためです。もし数字のみのファイル名を指定する場合は `-r ./1686144089` のような形で書くようにしていれば、GNU 版と FreeBSD 版の両方で安全にファイルの更新日時を指定することができます。

## -dオプションはどこから来た？

GNU 版の `-d` オプションは、[昔の Changelog](https://github.com/coreutils/coreutils/blob/v8.26/old/sh-utils/ChangeLog.0) より 1992年3月に追加されたようです。Coreutils に組み込まれる前の sh-utils の頃に実装されています。[POSIX.2-1992](https://standards.ieee.org/ieee/1003.2/1408/) が承認されたのが 1992年9月なわけで標準化作業には間に合わなかったでしょう。そして更に都合が悪いことに、[2.10BSD では `-d` オプションは DST（夏時間）関連の別の意味のオプション](https://man.freebsd.org/cgi/man.cgi?query=date&manpath=2.10+BSD)でした。違う意味の `-d` オプションは 4.4BSD を経て 1997 年の NetBSD 1.2.1、2019 年の OpenBSD 6.5、2022 年の FreeBSD 12.4 まで使われていました。最近の BSD 系 Unix では DST 用の `-d` オプションはもはや不要になったとして（すべてかどうかは不明ですが）削除されているようですが、環境によって互換性がない `-d` オプションを使うと移植性が実現できなくなるのは明らかなわけで、このようなオプションが POSIX で移植性があるものとして標準化されることはありません。

（GNU 版のサブセット風の）BusyBox date にも `-d` オプションがあります。ただしこちらは現時点では `now` のような指定はできず `2023-06-03 01:23:45` みたいな形式だけです。

## -jと-fオプションはどこから来た？

`date` コマンドは日時を指定するとシステム日時を変更しようとしますが、BSD 系の `date` コマンドは `-j` オプションを指定すると、システム日時を設定せずに指定した日時を出力することができます。`-j` オプションは [2000 年の FreeBSD 4.0](https://man.freebsd.org/cgi/man.cgi?query=date&manpath=FreeBSD+4.0-RELEASE)、[2006 年の OpenBSD 3.9](https://man.freebsd.org/cgi/man.cgi?query=date&manpath=OpenBSD+3.9)、[2007 年の NetBSD 4.0](https://man.freebsd.org/cgi/man.cgi?query=date&manpath=NetBSD+4.0) から使えるようです。`-j` オプションは BSD date にはありませんでしたが、現在の BSD 系 date にはあると言って良さそうです。

標準の日時形式は数字を繋げて指定するので読みづらく XSI 準拠の書式は（日本人にとっては？）分かりづらい並び順ですが、FreeBSD（macOS 版含む）と OpenBSD は日時を指定するときのフォーマットを変更する `-f` オプションを持っています。`-f` オプションは [1997 年の FreeBSD 2.2.5](https://man.freebsd.org/cgi/man.cgi?query=date&manpath=FreeBSD+2.2.5-RELEASE)、[2019 年の OpenBSD 6.5](https://man.freebsd.org/cgi/man.cgi?query=date&manpath=OpenBSD+6.5) で実装されています。FreeBSD では古くから使えた機能ですが、OpenBSD で実装されたのが比較的最近なのと現時点の NetBSD では使えないことから、`-j` と `-f` を組み合わせた日時の指定を「BSD 版の書き方」とするのは少し抵抗があります。

```console:FreeBSD 版と OpenBSD 版の機能
$ date -j -f "%Y-%m-%d" "2023-06-19"
2023年 6月19日 月曜日 22時33分48秒 JST
```

GNU date も `-f` オプションを持っていますが、FreeBSD や OpenBSD とは別の機能で、指定したファイル（または標準入力）から複数の日時を読み取って解釈する機能です。解釈する文字列は `-d` オプションと同じものです。Busybox date には現時点で `-f` オプションはありません。

```console:GNU 版の機能
$ date -d 'now + 10days'
2023年  6月 17日 土曜日 22:02:20 JST

$ echo 'now + 10days' | date -f -
2023年  6月 17日 土曜日 22:02:56 JST

$ seq -f 'now + %.fdays' 3 | date -f - # 複数の日時を処理できる
2023年  6月  8日 木曜日 22:09:05 JST
2023年  6月  9日 金曜日 22:09:05 JST
2023年  6月 10日 土曜日 22:09:05 JST
```

## NetBSD 版は-vがないが-dがある

NetBSD 版の `date` コマンドには `-v` オプションは実装されていないと書きましたが、2007 年にリリースされた [NetBSD 4.0 の `date` コマンドは GNU 風の `-d` オプションが実装](https://man.freebsd.org/cgi/man.cgi?query=date&manpath=NetBSD+4.0)されています。NetBSD の「BSD 版」は GNU 版と同じように、`-v` オプションはなく `-d` オプションがあるわけで、「BSD date は `-d` ではなく `-v` オプションを使う」という説明はやっぱり間違っているわけです。

余談ですが NetBSD 版の `date` コマンドは、つい先週に [GNU date の `-R` オプションが実装](https://github.com/NetBSD/src/commits/trunk/bin/date/date.c)されています。`-R` オプションは RFC 5322 形式（RFC 822 → RFC 2822 → RFC 5322）で出力するオプションです。[`-R` オプションは FreeBSD 版 date では 2014 年に追加](https://cgit.freebsd.org/src/commit/bin/date/date.c?id=b69ced8d8c13d96fc35a9d0153db4fa26ae5541d)されています。

## DragonFly BSD版は-vと-dの両方がある

`-v` オプションは FreeBSD の拡張機能と書きましたが、実は（私が知っている中で）もう一つ実装されている BSD 系 Unix があって DragonFly BSD です。DragonFly BSD 自体が 2003 年の FreeBSD 4.8 からフォークしたものなので当然なのですが、[最近コミット](https://gitweb.dragonflybsd.org/dragonfly.git/commit/4238ce6f0c6df33ce677ae298b245c62cd60fb43)（現時点ではまだ未リリース）で GNU の `-d` オプションのサポートを開始したようです。ただし GNU の `-d` オプションをそのまま実装するわけではなく `-v` のエイリアスとして実装し、どうやら `-v` オプションを拡張して一部の `-d` オプションの引数をサポートするようなのです。この方法なら後方互換性を維持しながら使いやすくする事が可能です。

もしこの作業がうまくいき他の BSD や GNU にも取り込まれたら、将来は「`-d` オプションに近い機能を備えた `-v` オプション」が POSIX で標準化される可能性もあるのではないかと思っています。DragonFly BSD はそれを狙っているような気がします。

## まとめ

`-v` オプションは BSD date（4.3BSDなど）にあったものではなく FreeBSD date の拡張機能。

| BSD系         |  -d   |  -v   |   -r    |  -f   |  -j   |
| ------------- | :---: | :---: | :-----: | :---: | :---: |
| 4.3BSD        |   -   |   -   |   BSD   |   -   |   -   |
| FreeBSD       |   -   |   ✓  | BSD+GNU |  BSD  |   ✓   |
| NetBSD        |   ✓  |   -   |   BSD   |   -   |   ✓   |
| OpenBSD       |   -   |   -   |   BSD   |  BSD  |   ✓   |
| DragonFly BSD |   →   |   ✓  |   BSD   |  BSD  |   ✓   |

| その他        |  -d   |  -v   |  -r   |  -f   |
| ------------- | :---: | :---: | :---: | :---: |
| GNU           |   ✓   |   -   |  GNU  |  GNU  |
| BusyBox       |   ✓   |   -   |   -   |   -   |
| Solaris       |   -   |   -   |   -   |   -   |
| AIX           |   -   |   -   |   -   |   -   |
| HP-UX         |   -   |   -   |   -   |   -   |

#
* BSD 系 date
  * `-r` (BSD) は UNIX 時間で指定する機能
  * `-f` (BSD) は引数で日時を指定する機能
    * システム日時を変更しない場合は `-j` オプションと共に使用する
  * DragonFly BSD date の `-d` オプションは `-v` オプションのエイリアス
* その他の date
  * `-r` (GNU) は指定したファイルの更新日時で指定する機能
  * `-f` (GNU) は指定したファイル（または標準入力）から日時を読み取る機能
  * BusyBox date の `-d` オプションは低機能（`now` などの指定ができない）

BSD 系 Unix だからと言ってみんな同じ動きをするわけではありません。GNU コマンドの場合は、GNU プロジェクト一つで開発されているので（ディストリ固有のパッチが当てられていることもありますが）基本的には同一のソースコードを利用しています。しかし BSD 系 Unix は BSD からフォークしてそれぞれが独立して開発しているという違いがあります。BSD 系 Unix はフォークしてからすでに 30 年前後経っているわけで、その違いは小さいとは言えません。

今も少しずつ改良されており、GNU date との互換性を意識しているように感じられます。あと何年かすれば互換性が高まり、新たな機能が POSIX で標準化され、標準インストールされている `date` コマンドでも最低限の日付計算が移植性がある形で利用できる日が来るかもしれません。しかしそれまでの間は日付計算はどうしたら良いでしょうか？ そんなことにいちいち悩むぐらいなら、私は `date` コマンドよりももっと便利で簡単に使える [DateUtils](http://www.fresse.org/dateutils/) のようなツールをインストールして使うべきだと考えます。DateUtils の使い方については「[シェルスクリプトで日付処理ならdateコマンドは投げ捨ててDateutilsを使おう！](https://qiita.com/ko1nksm/items/46f74707693213a0da3d)」を参照してください。

**シェル言語は他のコマンドを組み合わせるのが得意な言語だと言うのに、使うコマンドを sed や awk だけに制限してしまっては本末転倒**です。いろんなコマンドをインストールして組み合わせて簡単なことをより簡單に実現するのがシェルスクリプトの得意分野です。そうすれば他の言語をいちいちインストールして面倒なコードを書くよりも簡單に目的を達成することができます。