Windows で一部の Git コマンドが動かないと思ったら PowerShell のせいだった
◆ PowerShell でパイプするとバイナリデータでも改行を見つけて CRLF 化される
◆ 標準出力は一旦改行区切りで文字列の配列化される
◆ echo (Write-Output) の場合は文字列のままなので LF を送信できる
◆ ただし末尾に CRLF が挿入されるので受け取る側で追加の処理が必要になる
◆ ファイルへリダイレクトすると BOM 付き UTF-16LE として書き込まれる
◆ このファイルをパイプで渡すと BOM が消えて UTF-8 化される
◆ .NET 文字列化されるせいなので cmd /c の中でコマンドプロンプトのパイプを使うしかなさそう
◆ 標準出力は一旦改行区切りで文字列の配列化される
◆ echo (Write-Output) の場合は文字列のままなので LF を送信できる
◆ ただし末尾に CRLF が挿入されるので受け取る側で追加の処理が必要になる
◆ ファイルへリダイレクトすると BOM 付き UTF-16LE として書き込まれる
◆ このファイルをパイプで渡すと BOM が消えて UTF-8 化される
◆ .NET 文字列化されるせいなので cmd /c の中でコマンドプロンプトのパイプを使うしかなさそう
Windows の git コマンド
しばらく使っていなかったスクリプトを久々に使うと動きませんでした場所は Git で zip ファイルを作ってリモートにコピーしているところです
昔も今も Git for Windows を使って Windows 上で実行していたはずです
Git Bash を起動してそこで実行した覚えはないです
アップデートでなにか変わったのでしょうか
やっぱり Windows 側で git コマンドを動かすのはやめたほうがいいのでしょうか
とは言え基本的なことは問題なく動いてるのですよね
PS C:\dir> git init
Initialized empty Git repository in C:/dir/.git/
PS C:\dir> echo "a" > a
PS C:\dir> echo "b" > b
PS C:\dir> git add -A
PS C:\dir> git commit -m "commit"
[master (root-commit) 3a652c5] commit
2 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 a
create mode 100644 b
PS C:\dir> git log
commit 3a652c57759e58b7a81f82fc33f05db54fa0ca15 (HEAD -> master)
Author: Your Name <you@example.com>
Date: Fri Aug 5 23:43:30 2022 +0900
commit
archive ファイルが壊れる
しかしなぜか archive で zip を作るとファイルが壊れていますPS C:\dir> git archive --format zip HEAD > a.zip
Windows 標準のエクスプローラーの機能で開こうとすると壊れていますと言われます
7-Zip でもやっぱり不正ファイルとして扱われます
別フォーマットならできるのかなと思って tar にしてみましたが
PS C:\dir> git archive --format tar HEAD > a.tar
PS C:\dir> tar -tf a.tar
tar.exe: Error opening archive: Unrecognized archive format
tar は 7-Zip だと開けるものの pax なんとかというシステムファイルぽいのだけで中身が見れませんでした
とりあえずの対処
バグかなとググってみたのですが これといったバグ報告は見つかりませんGit for Windows を最新にしてみましたが変わらずです
とりあえず WSL 側の Git を呼び出してみると正常な zip や tar を作れました
しかし PowerShell で実行しているスクリプトから WSL を呼び出すのはなんか微妙です
それに WSL がインストールされていない PC で実行する可能性もありえます
WSL で動くなら Git Bash で動かせば動いたりするのかなと試してみました
すると正常な zip が作れました
Windows 側のコマンドプロンプトや PowerShell でも普通に使えていたので使ってましたが Git Bash 上で動かすのが正しい使い方なんでしょうか
とりあえず PowerShell からでもこういう感じで "" の中に Git Bash 上で動かしたいコマンドを書けば動かせます
& 'C:\Program Files\Git\bin\bash.exe' -c "echo 1"
でもなんか気持ち悪さが残ります
原因
見ているとなんか標準出力を通すとダメそうな気がしますarchive のオプションを見るとファイル指定もできたので試してみます
PS C:\dir> git archive --format tar -o a.tar HEAD
PS C:\dir> tar -tf .\a.tar
a
b
正常なファイルになってました
ファイルサイズを見比べてみると 壊れているファイルの方が大きいです
大きめのリポジトリでは 1 割くらい増しでしたが 小さめのリポジトリでは 2 倍以上になっています
もしかして git コマンドって Windows 上で直接実行すると内部的に Git Bash 上で処理してそれを呼び出し元の PowerShell などに転送するようなことをしてたりするのでしょうか
だから標準出力でなにかの変換が入ってたり?
それにこれって zip をファイルで保存したいときには困りませんが パイプで繋いで処理したいときに不便ですよね
一旦一時ファイルに書き出さないといけないってことですし
詳細
Windows だし CRLF とかそういうのかなと中身を見てみましたtar はバイナリなのでとりあえずバイナリ表示です
PS C:\dir> git archive --format tar -o a1.tar HEAD
PS C:\dir> git archive --format tar HEAD > a2.tar
PS C:\dir> format-hex .\a1.tar | select -First 16
Path: C:\dir\a1.tar
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000 70 61 78 5F 67 6C 6F 62 61 6C 5F 68 65 61 64 65 pax_global_heade
00000010 72 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r...............
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000060 00 00 00 00 30 30 30 30 36 36 36 00 30 30 30 30 ....0000666.0000
00000070 30 30 30 00 30 30 30 30 30 30 30 00 30 30 30 30 000.0000000.0000
00000080 30 30 30 30 30 36 34 00 31 34 32 37 33 32 32 36 0000064.14273226
00000090 32 32 32 00 30 30 31 34 35 31 34 00 67 00 00 00 222.0014514.g...
000000A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
PS C:\dir> format-hex .\a2.tar | select -First 16
Path: C:\dir\a2.tar
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000 FF FE 70 00 61 00 78 00 5F 00 67 00 6C 00 6F 00 .þp.a.x._.g.l.o.
00000010 62 00 61 00 6C 00 5F 00 68 00 65 00 61 00 64 00 b.a.l._.h.e.a.d.
00000020 65 00 72 00 00 00 00 00 00 00 00 00 00 00 00 00 e.r.............
00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000C0 00 00 00 00 00 00 00 00 00 00 30 00 30 00 30 00 ..........0.0.0.
000000D0 30 00 36 00 36 00 36 00 00 00 30 00 30 00 30 00 0.6.6.6...0.0.0.
000000E0 30 00 30 00 30 00 30 00 00 00 30 00 30 00 30 00 0.0.0.0...0.0.0.
000000F0 30 00 30 00 30 00 30 00 00 00 30 00 30 00 30 00 0.0.0.0...0.0.0.
ぱっと見では結構違ってます
a2 (標準出力リダイレクトで生成した方) の方は FF FE から始まってます
それに最初の文字列部分 pax_global_header の各文字の間に . が入ってます
バイナリデータ的には 00 が挟まってます
UTF-16LE 形式のテキストファイルとして保存されてるみたいです
気になっていた CRLF は?と思ってわかりやすく
abcdef[LF]
abcdef[LF]
[EOF]
というファイルをリポジトリに追加して tar ファイルに出力してテキストエディタで表示させてみました
すると予想通り標準出力をリダイレクトした方では f のあとの改行が CRLF 化していました
-o オプションでファイル出力した方では LF のままです
PowerShell のせい?
Windows で git 使うのは問題の元だなぁなんて思ってたら ふと以前は動いてたはずと思い出しましたもしや PowerShell がダメ?
PowerShell って標準出力を配列で扱えたりとか特殊なことをしています
確認のためコマンドプロンプトと比較してみました
C:\dir>git archive HEAD > cmd.tar
PS C:\dir> git archive HEAD > ps.tar
PS C:\dir> dir
Directory: C:\dir
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 8/6/2022 12:13 AM 10240 cmd.tar
-a---- 8/6/2022 12:13 AM 20492 ps.tar
PS C:\dir> tar -tf .\cmd.tar
a.txt
ファイルサイズが違っていますし コマンドプロンプトの方では問題なく中身が読み取れています
最近はもうコマンドプロンプトは使ってなくて PowerShell で実行したのが問題だったようです
まさかそんなところに原因があったなんて……
PowerShell の挙動
適当にいろいろ試してみましたが PowerShell ではパイプやリダイレクトを使うのはやめたほうが良さそうですecho をパイプすると BOM や CRLF 化はされていませんが echo の末尾に自動挿入される改行は CRLF です
PS C:\dir> echo "foo`nbar" | node -p "fs.readFileSync(0)"
<Buffer 66 6f 6f 0a 62 61 72 0d 0a>
ファイルに出力して中身を見てみます
PS C:\dir> echo "foo`nbar" > 0.txt
PS C:\dir> node -p "fs.readFileSync('0.txt')"
<Buffer ff fe 66 00 6f 00 6f 00 0a 00 62 00 61 00 72 00 0d 00 0a 00>
BOM 付き UTF-16LE になってます
改行はパイプのときと同じで LF のままで echo の末尾の改行が CRLF です
cat で出力してパイプしてみます
PS C:\dir> node -p "fs.readFileSync('0.txt')"
<Buffer ff fe 66 00 6f 00 6f 00 0a 00 62 00 61 00 72 00 0d 00 0a 00>
PS C:\dir> cat .\0.txt | node -p "fs.readFileSync(0)"
<Buffer 66 6f 6f 0d 0a 62 61 72 0d 0a>
BOM がなくなって UTF-8 化されていますが foo のあとの改行が CRLF 化されています
これは cat の挙動なのかパイプの挙動なのかわからないので Node.js でファイルを読み取り標準出力に流してみます
PS C:\dir> node -p "fs.readFileSync('0.txt')"
<Buffer ff fe 66 00 6f 00 6f 00 0a 00 62 00 61 00 72 00 0d 00 0a 00>
PS C:\dir> node -e "fs.createReadStream('0.txt').pipe(process.stdout)" | node -p "fs.readFileSync(0)"
<Buffer 66 6f 6f 0d 0a 62 61 72 0d 0a>
同じく UTF-8 / CRLF 化されています
PowerShell のパイプはバイナリデータをそのまま通さず文字列として解釈して色々変換したあとに後続プログラムに渡されるようです
ですが 最初の echo では LF 改行はそのままでした
Node.js 側の問題の可能性もあるので念のため他ツールでも改行を含む文字列を出力してみます
PS C:\dir> wsl python3 -c "`"print('foo\nbar')`"" > 1.txt
PS C:\dir> node -p "fs.readFileSync('1.txt')"
<Buffer ff fe 66 00 6f 00 6f 00 0d 00 0a 00 62 00 61 00 72 00 0d 00 0a 00>
同じく CRLF 化されています
プログラムの実行と PowerShell 組み込み機能だと扱いが違いそうです
PS C:\dir> $foo=(node -e "process.stdout.write('foo\nbar')")
PS C:\dir> $foo.gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
PS C:\dir> $foo[0]
foo
PS C:\dir> $foo[1]
bar
PS C:\dir> $foo[2]
PS C:\dir> $foo[0].gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
PS C:\dir> $bar=(echo "foo`nbar")
PS C:\dir> $bar.gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
PS C:\dir> $bar[0]
f
PS C:\dir> $bar[1]
o
PS C:\dir> $bar[2]
o
Node.js の方は改行区切りの string 型の配列です
しかし echo の方は配列化されていない string 型です
呼び出したプログラムの出力が改行区切りで配列化されるなら改行コードの保持は無理そうです
設定でできるものかとドキュメントを見てみましたが .NET 文字列化されるのでデータが破損する可能性ありとか書かれています
そして対策には cmd /c を使って生のパイプで処理する必要があるようです
cmd /c の中だとコマンドプロンプトのわかりづらい構文のことも考えないといけなくなりますしあまり気が進みません
PowerShell なんだし .NET の機能から呼び出せばいいかなと思いましたが かなり面倒でした
$p = new-object System.Diagnostics.Process
$p.StartInfo.FileName = "node"
$p.StartInfo.Arguments = "-e `"process.stdout.write('foo\nbar')`""
$p.StartInfo.UseShellExecute = $false
$p.StartInfo.RedirectStandardOutput = $true
$p.StartInfo.RedirectStandardError = $true
$p.Start() > $null
$p.WaitForExit()
$result = $p.StandardOutput.ReadToEnd()
[System.IO.File]::WriteAllText("output.txt", $result)
関数化して文字列で標準出力を返せばパイプの影響もないかと思いましたが末尾 CRLF は確実に入ります
PowerShell の標準入出力は PowerShell 組み込み機能以外では避けたほうがいいですね