Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Windows CMD.EXE /C quoting now handles quoted commands with extra arguments (e.g., npm.cmd paths with spaces).

### Changed

## 0.9.0 - 2026-01-27
Expand Down
47 changes: 46 additions & 1 deletion mshell/Pathbin_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,51 @@ func EscapeForCmd(allArgs []string) string {
return sb.String()
}

func BuildCmdExeLine(allArgs []string) string {
if len(allArgs) == 0 {
return ""
}
if len(allArgs) == 1 {
return allArgs[0]
}

// From `help cmd`
// If /C or /K is specified, then the remainder of the command line after
// the switch is processed as a command line, where the following logic is
// used to process quote (") characters:

// 1. If all of the following conditions are met, then quote characters
// on the command line are preserved:

// - no /S switch
// - exactly two quote characters
// - no special characters between the two quote characters,
// where special is one of: &<>()@^|
// - there are one or more whitespace characters between the
// two quote characters
// - the string between the two quote characters is the name
// of an executable file.

// 2. Otherwise, old behavior is to see if the first character is
// a quote character and if so, strip the leading character and
// remove the last quote character on the command line, preserving
// any text after the last quote character.

if strings.EqualFold(allArgs[1], "/C") || strings.EqualFold(allArgs[1], "/K") {
cmdStr := EscapeForCmd(allArgs[2:])
if len(allArgs) > 3 && strings.HasPrefix(cmdStr, "\"") {
// CMD.EXE needs an extra quote pair when the command starts with quotes and has extra args.
cmdStr = "\"" + cmdStr + "\""
}
if cmdStr == "" {
return allArgs[0] + " " + allArgs[1]
}
return allArgs[0] + " " + allArgs[1] + " " + cmdStr
}

return EscapeForCmd(allArgs)
}

func (pbm *PathBinManager) SetupCommand(allArgs []string) (*exec.Cmd) {
// Get basename of the command
if len(allArgs) == 0 {
Expand All @@ -254,7 +299,7 @@ func (pbm *PathBinManager) SetupCommand(allArgs []string) (*exec.Cmd) {
execName := filepath.Base(allArgs[0])
if strings.ToUpper(execName) == "CMD.EXE" {
// If the command is CMD.EXE, we need to escape the arguments properly
cmdStr := EscapeForCmd(allArgs)
cmdStr := BuildCmdExeLine(allArgs)

cmd := exec.Command("CMD.EXE")
cmd.SysProcAttr = &syscall.SysProcAttr{
Expand Down