diff --git a/pkg/editutil/editorcmd/editorcmd.go b/pkg/editutil/editorcmd/editorcmd.go index 796f38dd..3388f046 100644 --- a/pkg/editutil/editorcmd/editorcmd.go +++ b/pkg/editutil/editorcmd/editorcmd.go @@ -23,11 +23,15 @@ package editorcmd import ( "os" "os/exec" + + "github.com/mattn/go-shellwords" ) // Detect detects a text editor command. +// Returns the editor exec path with arguments if provided. +// // Returns an empty string when no editor is found. -func Detect() string { +func Detect() (editor string, args []string) { candidates := []string{ os.Getenv("VISUAL"), os.Getenv("EDITOR"), @@ -40,10 +44,23 @@ func Detect() string { if f == "" { continue } - x, err := exec.LookPath(f) - if err == nil { - return x + + if path, err := exec.LookPath(f); err == nil { + return path, nil } + + parsedArgs, err := shellwords.Parse(f) + if err != nil || len(parsedArgs) == 0 { + continue + } + + path, err := exec.LookPath(parsedArgs[0]) + if err != nil { + continue + } + + return path, parsedArgs[1:] } - return "" + + return "", nil } diff --git a/pkg/editutil/editorcmd/editorcmd_test.go b/pkg/editutil/editorcmd/editorcmd_test.go new file mode 100644 index 00000000..f30bac97 --- /dev/null +++ b/pkg/editutil/editorcmd/editorcmd_test.go @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package editorcmd + +import ( + "os/exec" + "testing" + + "gotest.tools/v3/assert" +) + +func TestDetectEditorWithArguments(t *testing.T) { + t.Setenv("VISUAL", "") + t.Setenv("EDITOR", "go --wait") + + expectedPath, err := exec.LookPath("go") + assert.NilError(t, err) + + path, args := Detect() + + assert.Equal(t, path, expectedPath) + assert.DeepEqual(t, args, []string{"--wait"}) +} + +func TestDetectQuotedEditor(t *testing.T) { + t.Setenv("VISUAL", "") + t.Setenv("EDITOR", `"go"`) + + expectedPath, err := exec.LookPath("go") + assert.NilError(t, err) + + path, args := Detect() + + assert.Equal(t, path, expectedPath) + assert.Equal(t, len(args), 0) +} + +func TestDetectQuotedEditorArgument(t *testing.T) { + t.Setenv("VISUAL", "") + t.Setenv("EDITOR", `go -ldflags "-s -w"`) + + expectedPath, err := exec.LookPath("go") + assert.NilError(t, err) + + path, args := Detect() + + assert.Equal(t, path, expectedPath) + assert.DeepEqual(t, args, []string{"-ldflags", "-s -w"}) +} + +func TestDetectSkipsInvalidEditor(t *testing.T) { + t.Setenv("VISUAL", `go -c "unterminated`) + t.Setenv("EDITOR", "go --wait") + + expectedPath, err := exec.LookPath("go") + assert.NilError(t, err) + + path, args := Detect() + + assert.Equal(t, path, expectedPath) + assert.DeepEqual(t, args, []string{"--wait"}) +} + +func TestDetectSkipsEmptyEditor(t *testing.T) { + t.Setenv("VISUAL", " ") + t.Setenv("EDITOR", "go --wait") + + expectedPath, err := exec.LookPath("go") + assert.NilError(t, err) + + path, args := Detect() + + assert.Equal(t, path, expectedPath) + assert.DeepEqual(t, args, []string{"--wait"}) +} + +func TestDetectNoEditor(t *testing.T) { + t.Setenv("VISUAL", "") + t.Setenv("EDITOR", "") + t.Setenv("PATH", t.TempDir()) + + path, args := Detect() + + assert.Equal(t, path, "") + assert.Equal(t, len(args), 0) +} diff --git a/pkg/editutil/editorcmd/editorcmd_windows_test.go b/pkg/editutil/editorcmd/editorcmd_windows_test.go new file mode 100644 index 00000000..2570ecbb --- /dev/null +++ b/pkg/editutil/editorcmd/editorcmd_windows_test.go @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +//go:build windows + +package editorcmd + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "gotest.tools/v3/assert" +) + +func TestDetectWindowsPath(t *testing.T) { + editorPath := makeWindowsEditor(t) + t.Setenv("VISUAL", "") + t.Setenv("EDITOR", editorPath) + + path, args := Detect() + + assert.Equal(t, path, editorPath) + assert.Equal(t, len(args), 0) +} + +func TestDetectWindowsPathWithArguments(t *testing.T) { + editorPath := makeWindowsEditor(t) + t.Setenv("VISUAL", "") + t.Setenv("EDITOR", fmt.Sprintf(`'%s' --wait`, editorPath)) + + path, args := Detect() + + assert.Equal(t, path, editorPath) + assert.DeepEqual(t, args, []string{"--wait"}) +} + +func makeWindowsEditor(t *testing.T) string { + t.Helper() + + editorDir := filepath.Join(t.TempDir(), "editor with spaces") + assert.NilError(t, os.Mkdir(editorDir, 0o755)) + editorPath := filepath.Join(editorDir, "editor.exe") + assert.NilError(t, os.WriteFile(editorPath, nil, 0o755)) + return editorPath +} diff --git a/pkg/editutil/editutil.go b/pkg/editutil/editutil.go index a0711ce9..33ff6510 100644 --- a/pkg/editutil/editutil.go +++ b/pkg/editutil/editutil.go @@ -58,7 +58,7 @@ func GenerateEditorWarningHeader() string { // // OpenEditor returns nil when the file was saved as an empty file, optionally with whitespaces. func OpenEditor(ctx context.Context, content []byte, hdr string) ([]byte, error) { - editor := editorcmd.Detect() + editor, args := editorcmd.Detect() if editor == "" { return nil, errors.New("could not detect a text editor binary, try setting $EDITOR") } @@ -76,7 +76,7 @@ func OpenEditor(ctx context.Context, content []byte, hdr string) ([]byte, error) return nil, err } - editorCmd := exec.CommandContext(ctx, editor, tmpYAMLPath) + editorCmd := exec.CommandContext(ctx, editor, append(args, tmpYAMLPath)...) editorCmd.Env = os.Environ() editorCmd.Stdin = os.Stdin editorCmd.Stdout = os.Stdout