package terminal // The pure half of the Windows support: the shapes Windows wants things in, // built here with no Windows API so that they can be tested on any platform. // pty_windows.go hands them to the API. import ( "strings" "unicode/utf16" ) // windowsFallbackShell is what runs when COMSPEC is unset, which it never is // on a working Windows: cmd.exe is found through PATH. const windowsFallbackShell = "cmd.exe" // windowsShell returns the shell Windows says is the user's — the value of // COMSPEC, which is cmd.exe on every Windows since NT — or the fallback when // the variable is empty. func windowsShell(comspec string) string { if comspec != "" { return comspec } return windowsFallbackShell } // environmentBlock encodes an environment the way CreateProcess wants it: each // entry as UTF-16, ended by a NUL, the whole block ended by a second NUL. // // An empty environment is two NULs, which CreateProcess reads as "no // variables" rather than as "inherit mine" — the latter is a nil block, which // this never returns, because the caller has always set TERM. func environmentBlock(env []string) []uint16 { if len(env) == 0 { return []uint16{0, 0} } block := make([]uint16, 0, 64*len(env)+2) for _, entry := range env { block = append(block, utf16.Encode([]rune(entry))...) block = append(block, 0) } return append(block, 0) } // windowsCommandLine composes the single command-line string Windows starts a // process from, since Windows has no argv: the program receives one string and // splits it by its own rules. // // cmd.exe's rules are its own and are not the C runtime's. Given `/S /C`, it // strips the first and last quote of what follows and hands the rest to its // parser verbatim, so the command is wrapped in exactly one pair of quotes and // nothing inside it is escaped — a `"` inside the command has to reach cmd.exe // as a `"`, and the C-runtime spelling `\"` would reach it as a backslash and // a quote. Every other program gets the C-runtime quoting that Go's own exec // uses, which is what a Go, Node or Python program on the other end expects. func windowsCommandLine(program string, args []string) string { if isCmdExe(program) && len(args) >= 1 && strings.EqualFold(args[0], "/S") { return cmdExeCommandLine(program, args) } parts := make([]string, 0, 1+len(args)) parts = append(parts, escapeArgument(program)) for _, arg := range args { parts = append(parts, escapeArgument(arg)) } return strings.Join(parts, " ") } // isCmdExe reports whether a program path names cmd.exe, whatever its case // and wherever it is. func isCmdExe(program string) bool { name := program if at := strings.LastIndexAny(name, `\/`); at >= 0 { name = name[at+1:] } return strings.EqualFold(name, "cmd.exe") || strings.EqualFold(name, "cmd") } // cmdExeCommandLine writes `"cmd.exe" /S /C "command"`: the switches verbatim, // and the command — the last argument — inside one pair of quotes that /S // tells cmd.exe to strip. func cmdExeCommandLine(program string, args []string) string { switches := args[:len(args)-1] command := args[len(args)-1] parts := []string{`"` + program + `"`} parts = append(parts, switches...) parts = append(parts, `"`+command+`"`) return strings.Join(parts, " ") } // escapeArgument quotes one argument the way the C runtime's argv parser // undoes it: quotes when it holds a space, a tab or a quote, `\"` for a quote, // and doubled backslashes only where they precede a quote or end the argument. // It is the rule syscall.EscapeArg follows, written here because that function // exists only on Windows and this one has to be testable everywhere. func escapeArgument(arg string) string { if arg == "" { return `""` } if !strings.ContainsAny(arg, " \t\"") { return arg } var b strings.Builder b.WriteByte('"') backslashes := 0 for i := 0; i < len(arg); i++ { switch arg[i] { case '\\': backslashes++ continue case '"': b.WriteString(strings.Repeat(`\`, 2*backslashes+1)) b.WriteByte('"') default: b.WriteString(strings.Repeat(`\`, backslashes)) b.WriteByte(arg[i]) } backslashes = 0 } b.WriteString(strings.Repeat(`\`, 2*backslashes)) b.WriteByte('"') return b.String() } // programName returns the last element of a Windows or Unix path, which is // what a window title shows for the shell. func programName(program string) string { if at := strings.LastIndexAny(program, `\/`); at >= 0 { return program[at+1:] } return program }