diff --git a/.gitignore b/.gitignore index d82698c..654b1df 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ tests/ .DS_Store testing/ createfiles.sh -.vscode \ No newline at end of file +.vscode +run.sh diff --git a/main.go b/main.go index 4ed21f7..c607f2e 100644 --- a/main.go +++ b/main.go @@ -168,6 +168,68 @@ func guessEditorCommand() (string, error) { return "", errors.New("could not guess editor command") } +// Returns the executable path and arguments +func parseEditorCommand(editorCmd string) (string, []string, error) { + var args []string + state := "start" + current := "" + quote := "\"" + for i := 0; i < len(editorCmd); i++ { + c := editorCmd[i] + + if state == "quotes" { + if string(c) != quote { + current += string(c) + } else { + args = append(args, current) + current = "" + state = "start" + } + continue + } + + if c == '"' || c == '\'' { + state = "quotes" + quote = string(c) + continue + } + + if state == "arg" { + if c == ' ' || c == '\t' { + args = append(args, current) + current = "" + state = "start" + } else { + current += string(c) + } + continue + } + + if c != ' ' && c != '\t' { + state = "arg" + current += string(c) + } + } + + if state == "quotes" { + return "", []string{}, errors.New(fmt.Sprintf("Unclosed quote in command line: %s", editorCmd)) + } + + if current != "" { + args = append(args, current) + } + + if len(args) <= 0 { + return "", []string{}, errors.New("Empty command line") + } + + if len(args) == 1 { + return args[0], []string{}, nil + } + + return args[0], args[1:], nil +} + func editFile(filePath string) error { var err error editorCmd := config_.String("editor") @@ -181,9 +243,14 @@ func editFile(filePath string) error { } } - pieces := strings.Split(editorCmd, " ") - pieces = append(pieces, filePath) - cmd := exec.Command(pieces[0], pieces[1:]...) + commandString, args, err := parseEditorCommand(editorCmd) + if err != nil { + return err + } + + args = append(args, filePath) + // Run the properly formed command + cmd := exec.Command(commandString, args[0:]...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout err = cmd.Run() diff --git a/main_test.go b/main_test.go index 33d0c3f..80e29ba 100644 --- a/main_test.go +++ b/main_test.go @@ -266,6 +266,91 @@ ijkl } } +func Test_parseEditorCommand(t *testing.T) { + type TestCase struct { + editorCmd string + executable string + args []string + hasError bool + } + + var testCases []TestCase + + testCases = append(testCases, TestCase{ + editorCmd: "subl", + executable: "subl", + args: []string{}, + hasError: false, + }) + + testCases = append(testCases, TestCase{ + editorCmd: "/usr/bin/vim -f", + executable: "/usr/bin/vim", + args: []string{ "-f" }, + hasError: false, + }) + + testCases = append(testCases, TestCase{ + editorCmd: "\"F:\\Sublime Text 3\\sublime_text.exe\" /n /w", + executable: "F:\\Sublime Text 3\\sublime_text.exe", + args: []string{ "/n", "/w" }, + hasError: false, + }) + + testCases = append(testCases, TestCase{ + editorCmd: "subl -w --command \"something with spaces\"", + executable: "subl", + args: []string{ "-w", "--command", "something with spaces" }, + hasError: false, + }) + + testCases = append(testCases, TestCase{ + editorCmd: "notepad.exe /PT", + executable: "notepad.exe", + args: []string{ "/PT" }, + hasError: false, + }) + + testCases = append(testCases, TestCase{ + editorCmd: "vim -e \"unclosed quote", + executable: "", + args: []string{}, + hasError: true, + }) + + testCases = append(testCases, TestCase{ + editorCmd: "subl -e 'unclosed single-quote", + executable: "", + args: []string{}, + hasError: true, + }) + + testCases = append(testCases, TestCase{ + editorCmd: "", + executable: "", + args: []string{}, + hasError: true, + }) + + for _, testCase := range testCases { + executable, args, err := parseEditorCommand(testCase.editorCmd) + if (err != nil && !testCase.hasError) || (err == nil && testCase.hasError) { + t.Errorf("Error status did not match: %b: %s", testCase.hasError, err) + } + if executable != testCase.executable { + t.Errorf("Expected '%s', got '%s'", testCase.executable, executable) + } + if len(args) != len(testCase.args) { + t.Errorf("Expected and result args don't have the same length: [%s], [%s]", strings.Join(testCase.args, ", "), strings.Join(args, ", ")) + } + for i, arg := range testCase.args { + if arg != args[i] { + t.Errorf("Expected and result args differ: [%s], [%s]", strings.Join(testCase.args, ", "), strings.Join(args, ", ")) + } + } + } +} + func Test_processFileActions(t *testing.T) { setup(t) defer teardown(t) diff --git a/version.go b/version.go index a5f0ec0..582e32c 100644 --- a/version.go +++ b/version.go @@ -2,7 +2,7 @@ package main import "fmt" -const VERSION = "1.4.0" +const VERSION = "1.5.0" func handleVersionCommand(opts *CommandLineOptions, args []string) error { fmt.Println(VERSION)