Skip to content
Open
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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
charm.land/bubbles/v2 v2.1.0
charm.land/bubbletea/v2 v2.0.6
charm.land/lipgloss/v2 v2.0.3
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
github.com/charmbracelet/glamour v1.0.0
github.com/fsnotify/fsnotify v1.10.1
github.com/gertd/go-pluralize v0.2.1
Expand Down
9 changes: 7 additions & 2 deletions internal/tui/components/split/commit_editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package split
import (
"fmt"
"os"
"os/exec"
"strings"

"charm.land/bubbles/v2/help"
Expand Down Expand Up @@ -178,7 +177,13 @@ func (m *CommitEditorModel) openEditor() tea.Cmd {
}

// Create the command
c := exec.Command(editor, m.tempFile) //nolint:gosec
c, err := utils.BuildEditorCommand(editor, m.tempFile)
if err != nil {
_ = os.Remove(m.tempFile)
return func() tea.Msg {
return editorFinishedMsg{err: err}
}
}
c.Stdin = os.Stdin
c.Stdout = os.Stdout
c.Stderr = os.Stderr
Expand Down
7 changes: 6 additions & 1 deletion internal/tui/editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"os"
"os/exec"
"strings"

"github.com/getstackit/stackit/internal/utils"
)

// OpenEditor opens the user's preferred editor with the given initial content.
Expand Down Expand Up @@ -52,7 +54,10 @@ func OpenEditor(initialContent, filenamePattern string) (string, error) {
}

// Open editor
cmd := exec.Command("sh", "-c", fmt.Sprintf("%s %s", editor, tmpFile.Name()))
cmd, err := utils.BuildEditorCommand(editor, tmpFile.Name())
if err != nil {
return "", fmt.Errorf("failed to build editor command: %w", err)
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
Expand Down
30 changes: 30 additions & 0 deletions internal/utils/editor_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package utils

import (
"fmt"
"os/exec"
"strings"

shlex "github.com/anmitsu/go-shlex"
)

// BuildEditorCommand parses an editor command and appends the target file path.
func BuildEditorCommand(editor, filePath string) (*exec.Cmd, error) {
editor = strings.TrimSpace(editor)
if editor == "" {
return nil, fmt.Errorf("editor command is empty")
}

parts, err := shlex.Split(editor, true)
if err != nil {
return nil, fmt.Errorf("parse editor command: %w", err)
}
if len(parts) == 0 {
return nil, fmt.Errorf("editor command is empty")
}

args := make([]string, 0, len(parts))
args = append(args, parts[1:]...)
args = append(args, filePath)
return exec.Command(parts[0], args...), nil //nolint:gosec
}
62 changes: 62 additions & 0 deletions internal/utils/editor_command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package utils

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestBuildEditorCommand(t *testing.T) {
tests := []struct {
name string
editor string
file string
want []string
wantErr string
}{
{
name: "binary only",
editor: "vim",
file: "/tmp/message.txt",
want: []string{"vim", "/tmp/message.txt"},
},
{
name: "editor with flags",
editor: "code --wait --reuse-window",
file: "/tmp/message.txt",
want: []string{"code", "--wait", "--reuse-window", "/tmp/message.txt"},
},
{
name: "quoted argument",
editor: `sed -i ''`,
file: "/tmp/message.txt",
want: []string{"sed", "-i", "/tmp/message.txt"},
},
{
name: "empty command",
editor: " ",
file: "/tmp/message.txt",
wantErr: "editor command is empty",
},
{
name: "unterminated quote",
editor: `"code --wait`,
file: "/tmp/message.txt",
wantErr: "parse editor command",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd, err := BuildEditorCommand(tt.editor, tt.file)
if tt.wantErr != "" {
require.Error(t, err)
require.ErrorContains(t, err, tt.wantErr)
return
}

require.NoError(t, err)
require.Equal(t, tt.want, cmd.Args)
})
}
}
Loading