From ccfe08bc6063ae358fc61d51668ea5e20b8dddc3 Mon Sep 17 00:00:00 2001 From: Florian Sundermann Date: Fri, 2 Sep 2016 13:14:31 +0200 Subject: [PATCH 1/5] allow command parameters to be quoted this way FileCompletions could contain space runes without breaking the parameter. --- cmd/micro/actions.go | 5 ++++ cmd/micro/command.go | 16 +++++------ cmd/micro/messenger.go | 8 ++---- cmd/micro/util.go | 60 ++++++++++++++++++++++++++++++++++++++++++ cmd/micro/util_test.go | 30 ++++++++++++++++++++- 5 files changed, 104 insertions(+), 15 deletions(-) diff --git a/cmd/micro/actions.go b/cmd/micro/actions.go index e1b66c90..b2990a35 100644 --- a/cmd/micro/actions.go +++ b/cmd/micro/actions.go @@ -716,6 +716,8 @@ func (v *View) Save(usePlugin bool) bool { if v.Buf.Path == "" { filename, canceled := messenger.Prompt("Filename: ", "Save", NoCompletion) if !canceled { + // the filename might or might not be quoted, so unquote first then join the strings. + filename = strings.Join(SplitCommandArgs(filename), " ") v.Buf.Path = filename v.Buf.Name = filename } else { @@ -1003,6 +1005,9 @@ func (v *View) OpenFile(usePlugin bool) bool { if canceled { return false } + // the filename might or might not be quoted, so unquote first then join the strings. + filename = strings.Join(SplitCommandArgs(filename), " ") + home, _ := homedir.Dir() filename = strings.Replace(filename, "~", home, 1) file, err := ioutil.ReadFile(filename) diff --git a/cmd/micro/command.go b/cmd/micro/command.go index 30ce6c1d..2ebfc6c5 100644 --- a/cmd/micro/command.go +++ b/cmd/micro/command.go @@ -225,7 +225,7 @@ func Bind(args []string) { // Run runs a shell command in the background func Run(args []string) { // Run a shell command in the background (openTerm is false) - HandleShellCommand(strings.Join(args, " "), false) + HandleShellCommand(JoinCommandArgs(args...), false) } // Quit closes the main view @@ -340,8 +340,8 @@ func Replace(args []string) { // RunShellCommand executes a shell command and returns the output/error func RunShellCommand(input string) (string, error) { - inputCmd := strings.Split(input, " ")[0] - args := strings.Split(input, " ")[1:] + inputCmd := SplitCommandArgs(input)[0] + args := SplitCommandArgs(input)[1:] cmd := exec.Command(inputCmd, args...) outputBytes := &bytes.Buffer{} @@ -357,7 +357,7 @@ func RunShellCommand(input string) (string, error) { // The openTerm argument specifies whether a terminal should be opened (for viewing output // or interacting with stdin) func HandleShellCommand(input string, openTerm bool) { - inputCmd := strings.Split(input, " ")[0] + inputCmd := SplitCommandArgs(input)[0] if !openTerm { // Simply run the command in the background and notify the user when it's done messenger.Message("Running...") @@ -382,7 +382,7 @@ func HandleShellCommand(input string, openTerm bool) { screen.Fini() screen = nil - args := strings.Split(input, " ")[1:] + args := SplitCommandArgs(input)[1:] // Set up everything for the command cmd := exec.Command(inputCmd, args...) @@ -414,12 +414,12 @@ func HandleShellCommand(input string, openTerm bool) { // HandleCommand handles input from the user func HandleCommand(input string) { - inputCmd := strings.Split(input, " ")[0] - args := strings.Split(input, " ")[1:] + args := SplitCommandArgs(input) + inputCmd := args[0] if _, ok := commands[inputCmd]; !ok { messenger.Error("Unknown command ", inputCmd) } else { - commands[inputCmd].action(args) + commands[inputCmd].action(args[1:]) } } diff --git a/cmd/micro/messenger.go b/cmd/micro/messenger.go index b57d7ff1..33fb8608 100644 --- a/cmd/micro/messenger.go +++ b/cmd/micro/messenger.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "strconv" - "strings" "github.com/zyedidia/clipboard" "github.com/zyedidia/tcell" @@ -196,7 +195,7 @@ func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Comple response, canceled = m.response, false m.history[historyType][len(m.history[historyType])-1] = response case tcell.KeyTab: - args := strings.Split(m.response, " ") + args := SplitCommandArgs(m.response) currentArgNum := len(args) - 1 currentArg := args[currentArgNum] var completionType Completion @@ -229,10 +228,7 @@ func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Comple } if chosen != "" { - if len(args) > 1 { - chosen = " " + chosen - } - m.response = strings.Join(args[:len(args)-1], " ") + chosen + m.response = JoinCommandArgs(append(args[:len(args)-1], chosen)...) m.cursorx = Count(m.response) } } diff --git a/cmd/micro/util.go b/cmd/micro/util.go index 9bb019d1..c81fadbf 100644 --- a/cmd/micro/util.go +++ b/cmd/micro/util.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "os" "path/filepath" "strconv" @@ -217,3 +218,62 @@ func Abs(n int) int { } return n } + +// SplitCommandArgs seperates multiple command arguments which may be quoted. +// The returned slice contains at least one string +func SplitCommandArgs(input string) []string { + var result []string + + inQuote := false + escape := false + curArg := new(bytes.Buffer) + for _, r := range input { + if !escape { + switch { + case r == '\\' && inQuote: + escape = true + continue + case r == '"' && inQuote: + inQuote = false + continue + case r == '"' && !inQuote && curArg.Len() == 0: + inQuote = true + continue + case r == ' ' && !inQuote: + result = append(result, curArg.String()) + curArg.Reset() + continue + } + } + escape = false + curArg.WriteRune(r) + } + if curArg.Len() > 0 || len(result) == 0 { + result = append(result, curArg.String()) + } + return result +} + +// JoinCommandArgs joins multiple command arguments and quote the strings if needed. +func JoinCommandArgs(args ...string) string { + buf := new(bytes.Buffer) + for _, arg := range args { + if buf.Len() > 0 { + buf.WriteRune(' ') + } + if !strings.Contains(arg, " ") { + buf.WriteString(arg) + } else { + buf.WriteRune('"') + for _, r := range arg { + if r == '"' || r == '\\' { + buf.WriteRune('\\') + } + buf.WriteRune(r) + } + buf.WriteRune('"') + } + } + + return buf.String() +} diff --git a/cmd/micro/util_test.go b/cmd/micro/util_test.go index e37a3237..79ea5a4d 100644 --- a/cmd/micro/util_test.go +++ b/cmd/micro/util_test.go @@ -1,6 +1,9 @@ package main -import "testing" +import ( + "reflect" + "testing" +) func TestNumOccurences(t *testing.T) { var tests = []struct { @@ -63,3 +66,28 @@ func TestIsWordChar(t *testing.T) { t.Errorf("IsWordChar(\n)) = true") } } + +func TestJoinAndSplitCommandArgs(t *testing.T) { + tests := []struct { + Query []string + Wanted string + }{ + {[]string{`test case`}, `"test case"`}, + {[]string{`quote "test"`}, `"quote \"test\""`}, + {[]string{`slash\\\ test`}, `"slash\\\\\\ test"`}, + {[]string{`path 1`, `path\" 2`}, `"path 1" "path\\\" 2"`}, + {[]string{`foo`}, `foo`}, + {[]string{`foo\"bar`}, `foo\"bar`}, + {[]string{``}, ``}, + } + + for i, test := range tests { + if result := JoinCommandArgs(test.Query...); test.Wanted != result { + t.Errorf("JoinCommandArgs failed at Test %d\nGot: %v", i, result) + } + + if result := SplitCommandArgs(test.Wanted); !reflect.DeepEqual(test.Query, result) { + t.Errorf("SplitCommandArgs failed at Test %d\nGot: %v", i, result) + } + } +} From 8617ae5c1f869c06c669155fae99283a29f70d05 Mon Sep 17 00:00:00 2001 From: boombuler Date: Sat, 3 Sep 2016 08:16:18 +0200 Subject: [PATCH 2/5] keep trailing space at commandline --- cmd/micro/util.go | 11 +++++++---- cmd/micro/util_test.go | 7 +++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/cmd/micro/util.go b/cmd/micro/util.go index c81fadbf..3f2ecd1b 100644 --- a/cmd/micro/util.go +++ b/cmd/micro/util.go @@ -248,17 +248,20 @@ func SplitCommandArgs(input string) []string { escape = false curArg.WriteRune(r) } - if curArg.Len() > 0 || len(result) == 0 { - result = append(result, curArg.String()) - } + //if curArg.Len() > 0 || len(result) == 0 { + result = append(result, curArg.String()) + // } return result } // JoinCommandArgs joins multiple command arguments and quote the strings if needed. func JoinCommandArgs(args ...string) string { buf := new(bytes.Buffer) + first := true for _, arg := range args { - if buf.Len() > 0 { + if first { + first = false + } else { buf.WriteRune(' ') } if !strings.Contains(arg, " ") { diff --git a/cmd/micro/util_test.go b/cmd/micro/util_test.go index 79ea5a4d..f224b905 100644 --- a/cmd/micro/util_test.go +++ b/cmd/micro/util_test.go @@ -79,15 +79,18 @@ func TestJoinAndSplitCommandArgs(t *testing.T) { {[]string{`foo`}, `foo`}, {[]string{`foo\"bar`}, `foo\"bar`}, {[]string{``}, ``}, + {[]string{`a`, ``}, `a `}, + {[]string{``, ``, ``, ``}, ` `}, } for i, test := range tests { if result := JoinCommandArgs(test.Query...); test.Wanted != result { - t.Errorf("JoinCommandArgs failed at Test %d\nGot: %v", i, result) + t.Errorf("JoinCommandArgs failed at Test %d\nGot: %q", i, result) } if result := SplitCommandArgs(test.Wanted); !reflect.DeepEqual(test.Query, result) { - t.Errorf("SplitCommandArgs failed at Test %d\nGot: %v", i, result) + t.Errorf("SplitCommandArgs failed at Test %d\nGot: `%s`", i, result) } } + } From 699ad316e57ca13f54caf5bceb00c81af24a1cea Mon Sep 17 00:00:00 2001 From: boombuler Date: Sat, 3 Sep 2016 08:18:47 +0200 Subject: [PATCH 3/5] removed old code --- cmd/micro/util.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmd/micro/util.go b/cmd/micro/util.go index 3f2ecd1b..f81bc7ca 100644 --- a/cmd/micro/util.go +++ b/cmd/micro/util.go @@ -248,9 +248,7 @@ func SplitCommandArgs(input string) []string { escape = false curArg.WriteRune(r) } - //if curArg.Len() > 0 || len(result) == 0 { result = append(result, curArg.String()) - // } return result } From b2735d7b5b6b793e049350bce2e32517b3395b2a Mon Sep 17 00:00:00 2001 From: boombuler Date: Sat, 3 Sep 2016 12:02:49 +0200 Subject: [PATCH 4/5] use build-in functions to quote / unquote --- cmd/micro/util.go | 71 ++++++++++++++++++++++++------------------ cmd/micro/util_test.go | 21 +++++++++++-- 2 files changed, 59 insertions(+), 33 deletions(-) diff --git a/cmd/micro/util.go b/cmd/micro/util.go index f81bc7ca..95d409e2 100644 --- a/cmd/micro/util.go +++ b/cmd/micro/util.go @@ -223,32 +223,47 @@ func Abs(n int) int { // The returned slice contains at least one string func SplitCommandArgs(input string) []string { var result []string - + curArg := new(bytes.Buffer) inQuote := false escape := false - curArg := new(bytes.Buffer) - for _, r := range input { - if !escape { - switch { - case r == '\\' && inQuote: - escape = true - continue - case r == '"' && inQuote: - inQuote = false - continue - case r == '"' && !inQuote && curArg.Len() == 0: - inQuote = true - continue - case r == ' ' && !inQuote: - result = append(result, curArg.String()) - curArg.Reset() - continue + + appendResult := func() { + str := curArg.String() + inQuote = false + escape = false + if strings.HasPrefix(str, `"`) && strings.HasSuffix(str, `"`) { + if unquoted, err := strconv.Unquote(str); err == nil { + str = unquoted } } - escape = false - curArg.WriteRune(r) + result = append(result, str) + curArg.Reset() } - result = append(result, curArg.String()) + + for _, r := range input { + if r == ' ' && !inQuote { + appendResult() + } else { + curArg.WriteRune(r) + + if r == '"' && !inQuote { + inQuote = true + } else { + if inQuote && !escape { + if r == '"' { + inQuote = false + } + if r == '\\' { + escape = true + continue + } + } + } + } + + escape = false + } + appendResult() return result } @@ -262,17 +277,11 @@ func JoinCommandArgs(args ...string) string { } else { buf.WriteRune(' ') } - if !strings.Contains(arg, " ") { - buf.WriteString(arg) + quoted := strconv.Quote(arg) + if quoted[1:len(quoted)-1] != arg || strings.ContainsRune(arg, ' ') { + buf.WriteString(quoted) } else { - buf.WriteRune('"') - for _, r := range arg { - if r == '"' || r == '\\' { - buf.WriteRune('\\') - } - buf.WriteRune(r) - } - buf.WriteRune('"') + buf.WriteString(arg) } } diff --git a/cmd/micro/util_test.go b/cmd/micro/util_test.go index f224b905..e3d417d5 100644 --- a/cmd/micro/util_test.go +++ b/cmd/micro/util_test.go @@ -77,10 +77,13 @@ func TestJoinAndSplitCommandArgs(t *testing.T) { {[]string{`slash\\\ test`}, `"slash\\\\\\ test"`}, {[]string{`path 1`, `path\" 2`}, `"path 1" "path\\\" 2"`}, {[]string{`foo`}, `foo`}, - {[]string{`foo\"bar`}, `foo\"bar`}, + {[]string{`foo\"bar`}, `"foo\\\"bar"`}, {[]string{``}, ``}, + {[]string{`"`}, `"\""`}, {[]string{`a`, ``}, `a `}, {[]string{``, ``, ``, ``}, ` `}, + {[]string{"\n"}, `"\n"`}, + {[]string{"foo\tbar"}, `"foo\tbar"`}, } for i, test := range tests { @@ -89,8 +92,22 @@ func TestJoinAndSplitCommandArgs(t *testing.T) { } if result := SplitCommandArgs(test.Wanted); !reflect.DeepEqual(test.Query, result) { - t.Errorf("SplitCommandArgs failed at Test %d\nGot: `%s`", i, result) + t.Errorf("SplitCommandArgs failed at Test %d\nGot: `%q`", i, result) } } + splitTests := []struct { + Query string + Wanted []string + }{ + {`"hallo""Welt"`, []string{`"hallo""Welt"`}}, + {`\"`, []string{`\"`}}, + {`"\"`, []string{`"\"`}}, + } + + for i, test := range splitTests { + if result := SplitCommandArgs(test.Query); !reflect.DeepEqual(test.Wanted, result) { + t.Errorf("SplitCommandArgs failed at Split-Test %d\nGot: `%q`", i, result) + } + } } From 403a99d2ea743c9b162ad4f502eec07a7c05aaea Mon Sep 17 00:00:00 2001 From: boombuler Date: Sat, 3 Sep 2016 12:13:25 +0200 Subject: [PATCH 5/5] removed obsolete replace command preparations --- cmd/micro/command.go | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/cmd/micro/command.go b/cmd/micro/command.go index 2ebfc6c5..62d82f5b 100644 --- a/cmd/micro/command.go +++ b/cmd/micro/command.go @@ -242,40 +242,20 @@ func Save(args []string) { // Replace runs search and replace func Replace(args []string) { - // This is a regex to parse the replace expression - // We allow no quotes if there are no spaces, but if you want to search - // for or replace an expression with spaces, you can add double quotes - r := regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"|[^\s]*`) - replaceCmd := r.FindAllString(strings.Join(args, " "), -1) - if len(replaceCmd) < 2 { + if len(args) < 2 { // We need to find both a search and replace expression messenger.Error("Invalid replace statement: " + strings.Join(args, " ")) return } var flags string - if len(replaceCmd) == 3 { + if len(args) == 3 { // The user included some flags - flags = replaceCmd[2] + flags = args[2] } - search := string(replaceCmd[0]) - replace := string(replaceCmd[1]) - - // If the search and replace expressions have quotes, we need to remove those - if strings.HasPrefix(search, `"`) && strings.HasSuffix(search, `"`) { - search = search[1 : len(search)-1] - } - if strings.HasPrefix(replace, `"`) && strings.HasSuffix(replace, `"`) { - replace = replace[1 : len(replace)-1] - } - - // We replace all escaped double quotes to real double quotes - search = strings.Replace(search, `\"`, `"`, -1) - replace = strings.Replace(replace, `\"`, `"`, -1) - // Replace some things so users can actually insert newlines and tabs in replacements - replace = strings.Replace(replace, "\\n", "\n", -1) - replace = strings.Replace(replace, "\\t", "\t", -1) + search := string(args[0]) + replace := string(args[1]) regex, err := regexp.Compile(search) if err != nil {