Files
kandev/apps/backend/internal/agentctl/server/process/cmdline.go
T
2026-02-18 18:24:24 +00:00

90 lines
2.2 KiB
Go

package process
import "strings"
// scanArgChars inspects each byte of s and returns whether any character
// requires backslash-escaping (double-quote or backslash) and whether
// the argument contains whitespace (space or tab).
func scanArgChars(s string) (needsBackslash, hasSpace bool) {
for i := 0; i < len(s); i++ {
switch s[i] {
case '"', '\\':
needsBackslash = true
case ' ', '\t':
hasSpace = true
}
}
return needsBackslash, hasSpace
}
// appendEscapedBytes appends the bytes of s to b applying MSDN
// CommandLineToArgvW backslash-doubling rules for double-quote characters.
// Returns the updated byte slice and the trailing slash count.
func appendEscapedBytes(b []byte, s string) ([]byte, int) {
slashes := 0
for i := 0; i < len(s); i++ {
c := s[i]
switch c {
default:
slashes = 0
case '\\':
slashes++
case '"':
for ; slashes > 0; slashes-- {
b = append(b, '\\')
}
b = append(b, '\\')
}
b = append(b, c)
}
return b, slashes
}
// escapeArg rewrites a command-line argument following the MSDN
// CommandLineToArgvW parsing rules (same algorithm as Go's
// syscall.EscapeArg on Windows):
//
// - every backslash (\) is doubled, but only if immediately
// followed by a double quote (");
// - every double quote (") is escaped with a backslash (\);
// - finally, the argument is wrapped in double quotes only if
// it contains spaces or tabs.
// - an empty string becomes "" (two double-quote characters).
func escapeArg(s string) string {
if len(s) == 0 {
return `""`
}
needsBackslash, hasSpace := scanArgChars(s)
if !needsBackslash && !hasSpace {
return s
}
if !needsBackslash {
return `"` + s + `"`
}
var b []byte
if hasSpace {
b = append(b, '"')
}
b, slashes := appendEscapedBytes(b, s)
if hasSpace {
for ; slashes > 0; slashes-- {
b = append(b, '\\')
}
b = append(b, '"')
}
return string(b)
}
// buildCmdLine joins arguments into a single command-line string
// with proper quoting for Windows CreateProcess.
func buildCmdLine(args []string) string {
escaped := make([]string, len(args))
for i, arg := range args {
escaped[i] = escapeArg(arg)
}
return strings.Join(escaped, " ")
}