事の発端は、Windows 上で Emacs の M-x grep で platinum searcher を使ったときにディレクトリを指定しないと何も引っかからないのを調べてて、 cmd.exe ではディレクトリ指定しなくても検索結果が表示されるが、msys2 だとディレクトリ指定しないと応答なしになるのはなんでやろと思ったこと。
platinum searcher のソースを見て、 the_platinum_searcher/platinum_searcher.go at master · monochromegane/the_platinum_searcher · GitHub
if p.givenStdin() && p.noRootPathIn(args) { opts.SearchOption.SearchStream = true }
のところにあたりをつけて、givenStdin() で呼んでいる os.Stdin.Stat() 挙動に注目した。まず
package main import ( "fmt" "os" ) func main() { _, err := os.Stdin.Stat() if err != nil { fmt.Println(err.Error()) } else { fmt.Println("success") } }
というサンプルプログラムを書いてビルドし
% go version go version go1.7.1 darwin/amd64 % GOOS=windows GOARCH=amd64 go build stdin_stat.go % ls stdin_stat.exe stdin_stat.exe*
Windows7環境で実行すると msys2 では
$ ./stdin_stat.exe success $ echo hoge | ./stdin_stat.exe success
cmd.exe では
>stdin_stat.exe GetFileInformationHandle /dev/stdin: The handle is invalid. >echo hoge | stdin_stat.exe success
となった。次にもう少し詳細を見るため go/src/os/stat_windows.go の実装を参考にして
package main import ( "fmt" "syscall" ) func main() { ft, err := syscall.GetFileType(syscall.Stdin) if err != nil { fmt.Println(err.Error()) } else { fmt.Println(ft) } var d syscall.ByHandleFileInformation err = syscall.GetFileInformationByHandle(syscall.Stdin, &d) if err != nil { fmt.Printf(err.Error()) } else { fmt.Println("GetFileInformationByHandle success") } }
というサンプルプログラムを書いてビルドし、WIndows7 環境で実行すると、msys2 では
$ ./stdin.exe 3 GetFileInformationByHandle success $ echo hoge | ./stdin.exe 3 GetFileInformationByHandle success
cmd.exe では
>stdin.exe 2 The handle is invalid. >echo hoge | stdin.exe 3 GetFileInformationByHandle success
となった。 GetFileType function (Windows) によると 3 = FILE_TYPE_PIPE, 2 = FILE_TYPE_CHAR であり、 GetFileInformationByHandle function (Windows) によると pipe のハンドルを第一引数に指定するなとある。 ただし go の実装では os: fix Stdin.Stat() on windows · golang/go@ebd67ba · GitHub の対処により pipe なら GetFileInformationByHandle を呼ばないようになっている
したがって、上のサンプルで pipe に対して GetFileInformationByHandle を呼んでおり成功してしまっているがそこはどうでもよくて、気になる点は
- msys2 で pipe に繋いでないときに STDIN が pipe と判定されているのは何故か?
- cmd.exe で pipe に繋いでいないときに GetFileInformationByHandle が失敗するのは何故か?
の2点。ここで、C++ (Visual Studio 2015) で直接 Win32 API を呼び出すサンプルプログラム
#include "stdafx.h" #include <Windows.h> int main() { HANDLE hdl = GetStdHandle(STD_INPUT_HANDLE); BY_HANDLE_FILE_INFORMATION info; printf("type=%d\n", GetFileType(hdl)); BOOL result = GetFileInformationByHandle(hdl, &info); if (result == 0) { printf("failed %d\n", GetLastError()); return -1; } printf("success\n"); return 0; }
をビルドして実行すると、msys2 では
$ ./ConsoleApplication1.exe type=3 success $ echo hoge | ./ConsoleApplication1.exe type=3 success
なのに対し、cmd.exe では
>ConsoleApplication1.exe type=2 failed 6 >echo hoge | ConsoleApplication1.exe type=3 success
System Error Codes (0-499) (Windows) によると 6 = ERROR_INVALID_HANDLE なので go から呼んだ時の挙動と整合しているのが確認できた。
まとめ
に書かれている os.Stdin.Stat() の挙動について、go がその内部で呼び出している Win32 API GetFileType, GetFileInformationByHandle を C++ プログラムから直接呼んで確認した。
pipe を食わせていないときに cmd.exe で os.Stdin.Stat() が失敗することや、msys2 で pipe と判定されてしまうことは現状の go の制限事項と思う(アプリケーション側で windows かどうかで条件分岐しないといけないのは望ましい動作ではないと思う)が、go から Win32 API 呼び出ししてることに起因するのではなく、使っている Win32 API 側の動作に起因しており、改善するには呼ぶものを変えるとか呼び方を変えるとかが必要とわかった。どうしたら改善できるかはわかっていないけど。