The Man Who Fell From The Wrong Side Of The Sky:2019年5月4日分

2019/5/4(Sat)

[Windows] PowerShellで生活するために - Get-Dateコマンドレット編(その1)

@惨めなUNIXシェルスクリプトプログラミング

いきなりタイトルに反してタッパーウェアの話をするけど、日付計算をシェルスクリプトでやろうとするとちょっと大変だ *1

おそらく大多数の人はdate(1)コマンドを使って計算しとるんじゃないか。

$ LANG=ja_JP.eucJP date -d "2019/05/01+114514 days" +"%Ec"
平成344年11月10日 00時00分00秒

西暦2019年5月1日から114514日後を計算した結果、まだ令和パッチ適用前なので平成344年で表示されている(一瞬334にみえるやつ)。

これは何かというと、*BSDやGNUのdate(1)実装は-dオプションの引数をparsedate(3)という関数に喰わせてtime_t型に変換している。

 68 static time_t tval;
 69 static int aflag, jflag, rflag, nflag;

 90         while ((ch = getopt(argc, argv, "ad:jnr:u")) != -1) {
 91                 switch (ch) {

 96                 case 'd':
 97                         rflag = 1;
 98                         tval = parsedate(optarg, NULL, NULL);
 99                         if (tval == -1)
100 badarg:                          errx(EXIT_FAILURE,
101                                     "Cannot parse `%s'", optarg);
102                         break;

この関数は マニュアルを読むと

-1 month
last friday
one week ago
this thursday
next sunday
+2 years

といった、ある日時からの相対日時を表現するさまざまなキーワードが実装されているのだ。 詳しい仕様についてはマニュアルもろくな内容じゃないので ソースを読め、なおオリジナルが1992年に書かれたyacc(1)による構文解析器なもんでタイムゾーンはハードコード国際化なにそれという酷い実装で、さらに伝播していった先々で独自に拡張されてるから移植性考えたらあまり使うべきではないのだ。

そうそう、移植性ハードコア勢であるならPOSIX縛りのドMコーディングなんだけど、その場合 そもそもPOSIX date(1)に-dオプションは無いから *2日付計算には使えないのだ、というかどうやってんですかあの辺の人種は。

だから言ってんだろーがdate(1)は時刻の設定と取得のコマンドなんだってば、日付計算に使うのは邪道なんだよ。

@豊かなPowerShellプログラミング

一方でUNIXなんぞより洗練され…洗練?…洗練ってなんだ?…まあいいPowerShellでは、日付計算はDateTimeオブジェクトを使うだけで簡単にできる。

文字列(String)から日時(DateTime)への変換もコマンドなんぞ介さず型キャストでおわり。

$date = [DateTime]"2019/05/01"

変数が型付きで宣言されてるなら型推論に任せてそのまま代入できる。

[DateTime] $date = "2019/05/01"

いちどDateTimeに変換してしまえばさまざまなメソッドで日付計算が可能だ。

$date = [DateTime]"2019/05/01"
$newdate = $date.AddDays(114514)

はい便利ですね。

そんでここからが本題、PowerShellには Get-Dateというコマンドレットが存在する、 Set-DateとあわせてUNIX date(1)と同等の働きを担うコマンドレットだ。

なんせ名前もGet-Date & Toughというくらいでひとりでは解けない現在日時刻を取得するコマンドなんだけれど、UNIX date(1)を真似したのか

  • -Dateオプションの引数で渡された文字列を解釈し
  • その日時を表すDateTimeオブジェクトを生成する

なんて機能もある。

$date = (Get-Date -Date "2019/05/01")

デフォルト引数は-Dateとして扱われるので

$date = (Get-Date "2019/05/01")

と書いてもよい。

ところがDA、恐ろしいことに

  • DateTimeへのキャストによる変換
  • Get-Timeを介しての変換

それぞれ動作が異なるのですわな、そしてそれが和暦表示と絡むとかなりの大災害が発生する。 特にUNIX使いがシェルスクリプトのノリで日付操作しようとするとdate(1)の代わりはっとでGet-Date使ってしまいがちなのでな…

@大災害

まずは論より証拠、PowerShellを開いて以下のコードを実行してみよう。

Write-Host ([DateTime]"2019/05/01").ToLongDateString()
Write-Host (Get-Date "2019/05/01").ToLongDateString()

この短いコードの出力結果は以下の通り。

2019年5月1日
2019年5月1日

はいどちらも同じ日をさしてますね、当たり前だよなぁ?

それでは以下の手順でカレンダーの種類を「和暦」に変更してみよう。

  • コントロールパネルから「地域と言語」ダイアログを開く
  • 「形式」タブの「追加の設定」ボタンを押して「形式のカスタマイズ」ダイアログを開く(ダイアログの天丼は最悪のUIだぞ)
  • 「日付」タブで「カレンダーの種類」を「西暦(日本語)」から「和暦」に変更する

さっきのコードを実行した結果は以下の通り。

令和元年5月1日
令和2019年5月1日

はいGet-Date使ってる方が2019年後に化けたよ!

改元のタイミングで設定変更して遊んでた人、その間に裏で動いてたPowerShellスクリプトがもしかしたら明日期限のタスクを2019年後にしてしまったかもね!

あ、そういえば先日ちらっと書いた 和暦表示が100年以上を扱えないバグ?の話だけど

  • PowerShellは令和2019年を扱える
  • にもかかわらずNew-SelfSignedCertificateコマンドレットで証明書を出力すると令和19年となってしまう

という現象も確認したので、証明書関連のAPIのどこかに

  • カレンダーが和暦の場合は西暦から変換してやらんとアカンやろうなぁ…
  • せや!CCyy形式(2019)からyy形式(19)に変換したで!

という国際化への造詣あふれるコードが存在してる可能性が高い、素晴らしいセキュリティエンジニア雇って書かせたコードなんやろうなぁ(しろめ)。

@次回予告

どうしてこうなった!を検証するよ。

*1:シェルスクリプトがshebang含めて3行以上になりそうならperlのようなもので書くワイには関係ない話なんだけどね…
*2:実際Solaris 9にはdate(1)に-d無かったりしたので使わないのは正義ではあった、今はLinux以外全滅したので縛りプレイは無意味でしょ。