diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-13 10:23:00 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-13 10:23:00 +0300 |
| commit | d799f3f04da8067669c90a755e90cd723f9746e7 (patch) | |
| tree | 02c0c5dda7aaa0d79a230e043297ad822cf2b9f0 /internal/tui/eventstream/export_test.go | |
| parent | 5856360a696b958a4c5c57cf512c5a04f0cfd66f (diff) | |
fix: use shell-aware tokenizer in resolveEditorCommand to handle EDITOR paths with spaces
Replace strings.Fields with a new shellSplit function that implements
POSIX-like quoting rules (single-quotes, double-quotes, backslash escapes),
so EDITOR values such as '/My Editor/hx' or "/path/with spaces/hx" --wait
are correctly parsed rather than mangled into multiple path components.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/tui/eventstream/export_test.go')
| -rw-r--r-- | internal/tui/eventstream/export_test.go | 80 |
1 files changed, 80 insertions, 0 deletions
diff --git a/internal/tui/eventstream/export_test.go b/internal/tui/eventstream/export_test.go index 9bdb2d2..6bebe6b 100644 --- a/internal/tui/eventstream/export_test.go +++ b/internal/tui/eventstream/export_test.go @@ -3,6 +3,7 @@ package eventstream import ( "os" "path/filepath" + "reflect" "testing" ) @@ -81,3 +82,82 @@ func TestResolveEditorCommandFallsBackToViWhenHxMissing(t *testing.T) { t.Fatalf("expected vi fallback, got %#v", parts) } } + +// TestResolveEditorCommandSingleQuotedPath verifies that an EDITOR value with +// a single-quoted path containing spaces is not broken into multiple tokens. +func TestResolveEditorCommandSingleQuotedPath(t *testing.T) { + t.Setenv("SUDO_EDITOR", "") + t.Setenv("VISUAL", "") + t.Setenv("EDITOR", "'/My Editor/hx'") + + parts, source, err := resolveEditorCommand() + if err != nil { + t.Fatalf("resolve editor: %v", err) + } + if source != "EDITOR" { + t.Fatalf("expected EDITOR source, got %q", source) + } + want := []string{"/My Editor/hx"} + if !reflect.DeepEqual(parts, want) { + t.Fatalf("expected %#v, got %#v", want, parts) + } +} + +// TestResolveEditorCommandDoubleQuotedPathWithArgs verifies that double-quoted +// paths with trailing arguments are all preserved correctly. +func TestResolveEditorCommandDoubleQuotedPathWithArgs(t *testing.T) { + t.Setenv("SUDO_EDITOR", "") + t.Setenv("VISUAL", "") + t.Setenv("EDITOR", `"/path/with spaces/hx" --wait`) + + parts, source, err := resolveEditorCommand() + if err != nil { + t.Fatalf("resolve editor: %v", err) + } + if source != "EDITOR" { + t.Fatalf("expected EDITOR source, got %q", source) + } + want := []string{"/path/with spaces/hx", "--wait"} + if !reflect.DeepEqual(parts, want) { + t.Fatalf("expected %#v, got %#v", want, parts) + } +} + +// TestShellSplitVariousCases covers the tokenizer with a table-driven approach. +func TestShellSplitVariousCases(t *testing.T) { + cases := []struct { + input string + want []string + }{ + // Plain tokens — behaviour identical to strings.Fields. + {"vi", []string{"vi"}}, + {"nvim --wait", []string{"nvim", "--wait"}}, + // Single-quoted path with spaces — the whole quoted span is one token. + {"'/path/with spaces/hx'", []string{"/path/with spaces/hx"}}, + // Double-quoted path. + {`"/path/with spaces/hx"`, []string{"/path/with spaces/hx"}}, + // Double-quoted path with escaped double quote inside. + {`"/path/\"hx\""`, []string{`/path/"hx"`}}, + // Mixed: quoted binary + unquoted flag. + {`"/My Editor/hx" --wait`, []string{"/My Editor/hx", "--wait"}}, + // Backslash escaping a space outside quotes. + {`/path/with\ spaces/hx`, []string{"/path/with spaces/hx"}}, + // Empty string returns no tokens. + {"", nil}, + // Whitespace-only returns no tokens. + {" ", nil}, + // Unterminated single quote: treated as implicit close at end. + {"'unterminated", []string{"unterminated"}}, + } + + for _, tc := range cases { + got := shellSplit(tc.input) + // Treat nil and empty slice as equivalent for comparison purposes. + if len(got) == 0 && len(tc.want) == 0 { + continue + } + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("shellSplit(%q): got %#v, want %#v", tc.input, got, tc.want) + } + } +} |
