jesseduffield.lazygit/pkg/commands/oscommands/os_windows_test.go
Stefan Haller 6b311ccb62 Fix quoting of shell commands on Windows
lazygit builds a shell command by interpolating Quote'd arguments into a
template and running the result via `cmd /c`. Several things were wrong
on Windows:

- Quote emitted bash-style `\"…\"` quoting, which cmd.exe doesn't
  understand. Making it usable at all previously required a fragile
  round-trip through str.ToArgv and re-escaping.
- The assembled command line was handed to `cmd /c` without `/s`, so
  cmd's default rules stripped the wrong quotes once the line contained
  more than two of them (e.g. a quoted editor path at a location with
  spaces, plus a quoted filename that also contains spaces).
- Shell metacharacters were escaped with `^` (`&` → `^&`, etc.), which
  neutralised command chaining, pipes, redirection and `%VAR%` expansion
  in custom commands.

Quote now emits the standard Windows convention directly, and NewShell
hands cmd.exe the fully-assembled line verbatim via SysProcAttr.CmdLine,
wrapped as `cmd /s /c "<command>"`. The /s flag strips exactly the outer
quote pair we add, leaving each argument's own quoting intact. With the
`^` escaping gone, metacharacters in a custom command reach cmd as the
author intended; this also removes the spurious `^` reported in #3092.

Fixes #5560
Fixes #2427
Fixes #4147

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:11:00 +02:00

76 lines
1.7 KiB
Go

package oscommands
import (
"testing"
"github.com/go-errors/errors"
"github.com/stretchr/testify/assert"
)
// handling this in a separate file because str.ToArgv has different behaviour if we're on windows
func TestOSCommandOpenFileWindows(t *testing.T) {
type scenario struct {
filename string
runner *FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/s", "/c", `start "" "test"`}, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
},
{
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/s", "/c", `start "" "test"`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
filename: "filename with spaces",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/s", "/c", `start "" "filename with spaces"`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
filename: "let's_test_with_single_quote",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/s", "/c", `start "" "let's_test_with_single_quote"`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
filename: "$USER.txt",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/s", "/c", `start "" "$USER.txt"`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
oSCmd := NewDummyOSCommandWithRunner(s.runner)
platform := &Platform{
OS: "windows",
Shell: "cmd",
ShellArg: "/c",
}
oSCmd.Platform = platform
oSCmd.Cmd.platform = platform
oSCmd.UserConfig().OS.Open = `start "" {{filename}}`
s.test(oSCmd.OpenFile(s.filename))
}
}