エクセルって便利ですよね。
CSV貼り付けて加工して保存して。
・・・ 気をつけないと 0 が消えるけど。
PowerShell よ。お前もか!
経緯
XD.GROWTH の開発用ツールを作成していたとき、コマンドライン引数で渡した値の先頭ゼロが消える事象に遭遇しました。
Go で書いたツールに対して PowerShell から次のように実行します:
.\mytool.exe -id 00123,45678
すると、ツールがログに出力する id の値は [123,45678] と、先頭の 00 が落ちていました。
最初に疑ったのはもちろん自前の Go コードです。引数のパース部分は次のように、ごく素直な実装でした。
ids := strings.Split(*idFlag, ",")
しかし strings.Split("00123,45678", ",") が先頭ゼロを落とす理由はありません。%v による配列出力も []string{"00123","45678"} を [00123 45678] と表示するだけで、間に変換が挟まる箇所もない。コードをいくらにらんでも怪しいところがない。
念のため WSL 上の bash から同じバイナリを実行すると、
$ ./mytool.exe -id 00123,45678
... id=[00123,45678]
と先頭ゼロが保持されます。となるとあやしいのは Go ではなく、引数を渡す側 = PowerShell です。
再現
PowerShell の echo を使うと素朴に再現できます。
PS> echo 00001
00001
PS> echo 00001,1234
1
1234
単独の 00001 は文字列として保持されるのに、00001,1234 とカンマで繋がっていると 1 と 1234 が出力される。
これは PowerShell のパーサーが 00001,1234 を 整数リテラルのカンマ区切りとして解釈し、@(00001, 1234) という Int 配列を作っているためです。Int 化される過程で先頭ゼロが落ち、@(1, 1234) になります。
ネイティブ実行ファイルへ引数を渡すときも同様で、PowerShell の式評価を一度通った値が 1,1234 という文字列に再フォーマットされて exe に渡ります。Go 側の引数パーサーが受け取る時点ですでに先頭ゼロは失われています。
解決
シングル/ダブルクォートで囲めば、PowerShell は中身を式として評価せずそのまま文字列として渡してくれます。
PS> echo '00001,1234'
00001,1234
PS> .\mytool.exe -id '00123,45678'
... id=[00123,45678]
これで意図通りの値を受け取れました。
なぜ罠なのか
- 単独値ではゼロが保持される ので、引数の中身次第で挙動が変わる。再現が安定しないと厄介。
- bash や cmd.exe では普通に文字列として渡せる。クロスシェルで使っていると、特定環境でだけ事故が起きる。
- ログ上の
[123,45678]という出力はクォートが付かないので、ぱっと見で 整数配列としてプログラムが受け取った ようにも読めてしまう。実際にはプログラムに来る前に PowerShell 側で値が変わっている。
ID やコード値で「先頭ゼロがあり得る文字列」をコマンド引数に渡す場合は、PowerShell では原則クォートする、と覚えておくのが安全です。
おまけ: SQL 側にも保険を
念のため、ID をクエリに渡すときに text[] などへ明示キャストしておくと、データベース側の型推論で数値扱いになる事故も防げます。
WHERE id = ANY((@id)::text[])
pgx のようなドライバの simple protocol だと、配列リテラルは '{123,45678}' のような形でクエリに展開されます。型は文脈依存なので、明示キャストしておくと意図が固まって安心です。
まとめ
- PowerShell では
-id 00001,1234のような引用符なしのカンマ区切り値は Int 配列として扱われ、先頭ゼロが落ちる。 - 文字列を期待するなら
-id '00001,1234'と引用符で囲む。 - 「自分のコードで意図しない型変換しているのでは」と疑う前に、シェルが引数をどう解釈しているか を確認しよう。