CMD.EXEの引数での引用符の扱いを完全に理解した

multipost →Qrunch

CMD /? で出てくるヘルプを一部引用する:

/C または /K が指定されている場合、スイッチの後の残りのコマンド ラインがコマンド ラインとして処理されます。次のルールが引用符 (") の処理に使われます:

  1. 次のすべての条件に一致する場合、コマンド ラインの引用符が有効になります:
    • /S スイッチがない
    • 引用符が 1 組ある
    • 引用符の中に特殊文字がない(特殊文字は &<>()@^| です)
    • 引用符の中に 1 つ以上のスペースがある
    • 引用符の中の文字列が、実行可能ファイルの名前である
  2. 最初の文字が引用符であるにも関わらず上の条件に一致しない場合は、最初の引用符とコマンド ラインの最後の引用符が削除され、最後の引用符の後のテキストが有効になります。

正直、まったく意味が分からんかったのだが、よくよく考えると、基本の動作は「2.」で、「1.」が特殊なケースということなのだろう。

では「1.」はどういうケースかというと:

CMD /C "C:\Program Files\HogeHoge\Aplication.exe"

といったケースのことだろう。これは、1. のような解釈をしないと「C:\Program」というコマンドに「Files\HogeHoge\Application.exe」という引数を渡しているとみなしてしまう。

ということで、通常、引用符が含まれているかもしれないコマンド列を CMD.EXE に渡すには /S を指定して強制的に「2.」を指定して:

CMD.EXE /S /C ""C:\Program Files\Hogehoge\Application.exe "ahaha""

とするのがよろしいようだ。さて、ここで注意すべき点がある。

  • 「二重引用符の中の二重引用符は \" に置換するなど特別扱いして書く」など配慮は無用機械的に " をコマンド列の前後にくっつけるだけでよい
  • ただし、CMD.EXE を呼び出すための、外部プロセスの呼び出しの各言語ライブラリが二重引用符を特別扱いすることが多い("\" 変換を勝手にする)。これを回避するテクが必要

Goの "os/exec" の場合

普通に "os/exec".Cmd.Args に引数パラメータを置くと、"\" が行われてしまう。Windows"os/exec" の隠しパラメータ?である SysProcAttr.CmdLine を使おう

package main

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

func main() {
    cmd := &exec.Cmd{
        Path:        os.Getenv("COMSPEC"),
        Stdin:       os.Stdin,
        Stdout:      os.Stdout,
        Stderr:      os.Stderr,
        SysProcAttr: &syscall.SysProcAttr{CmdLine: `/S /C "DIR /w "C:\Program Files""`},
    }
    cmd.Run()
}

以上