標準愚痴出力

個人的なIT作業ログです。もしかしたら一般的に参考になることが書いているかもしれません(弱気

Go 1.22 でも残る、"syscall" を使わないといけないケース(ソース付き)

Windows の検索コマンド find.exe は検索対象の文字列は二重引用符で囲んで与えてやらないといけないという困った仕様があります1

C:> find /?
ファイル (複数可) 内のテキスト文字列を検索します。

FIND [/V] [/C] [/N] [/I] [/OFF[LINE]] "文字列" [[ドライブ:][パス]ファイル名[...]]

  /V        指定した文字列を含まない行をすべて表示します。
  /C        指定した文字列を含む行の数だけを表示します。
  /N        行番号を表示します。
  /I        大文字と小文字の区別をしないで検索します。
  /OFF[LINE] オフライン属性が設定されたファイルをスキップしません。
  "文字列"  検索する文字列を指定します。
  [ドライブ:][パス]ファイル名
            検索するファイル (複数可) を指定します。

パスが指定されていないときは、プロンプトで入力されたテキストまた
は別のコマンドからパイプ処理で渡されたテキストを検索します。

C:>

この仕様が Go から子プロセスで find.exe を呼ぶ際の障害となります。通常の os.exec.Cmd でやるなら

nosyscall1.go

package main

import (
    "os"
    "os/exec"
)

func main() {
    cmdline := exec.Command("find.exe", `"main"`, "nosyscall1.go")
    cmdline.Stdin = os.Stdin
    cmdline.Stdout = os.Stdout
    cmdline.Stderr = os.Stderr
    cmdline.Run()
}

となりますが、これを実行すると

C:> go run nosyscall1.go
アクセスは拒否されました - \

---------- NOSYSCALL1.GO

とうまくいきません。内部的には find.exe \"main\" nosyscall.go というコマンドラインに直されているため、"main の前の \ がエラーになっているようです2。かといって、二重引用符を外して

nosyscall2.go

package main

import (
    "os"
    "os/exec"
)

func main() {
    cmdline := exec.Command("find.exe", "main", "nosyscall2.go")
    cmdline.Stdin = os.Stdin
    cmdline.Stdout = os.Stdout
    cmdline.Stderr = os.Stderr
    cmdline.Run()
}

として呼び出しても

C:> go run nosyscall2.go
FIND: パラメーターの書式が違います

となってしまいます。内部的には普通に find.exe main nosyscall2.go となってしまうので、コマンドが検索文字列がどれか分からないんですよね。

この問題は "syscall" を使って、直接コマンドライン全体を引き渡してやると、なんとかなります。

syscall.go

package main

import (
    "os"
    "os/exec"
    "syscall"
)

func main() {
    cmdline := exec.Command("find.exe")
    cmdline.Stdin = os.Stdin
    cmdline.Stdout = os.Stdout
    cmdline.Stderr = os.Stderr
    cmdline.SysProcAttr = &syscall.SysProcAttr{CmdLine: `find.exe "main" syscall.go`}
    cmdline.Run()
}
C:> go run syscall.go

---------- SYSCALL.GO
package main
func main() {
        cmdline.SysProcAttr = &syscall.SysProcAttr{CmdLine: `find.exe "main" syscall.go`}
C:>

ちゃんと、syscall.go ファイルの中の main が含まれた行が表示されました!


  1. 今日では findstr という find の上位互換のコマンドを使うので、find が使えなくて困るという事態はまずないのですが
  2. 子プロセス側がどういう生文字列を受け取るかを確認するで、nyaosorg/go-windows-commandline というライブラリを開発しまして、それでコマンド名だけ差し替えて確認しています。