Fix autocomplete behavior for empty args

This also adds a modified version of go-shellwords as a dependency
and removes the dependency on the original go-shellwords.
This commit is contained in:
Zachary Yedidia
2017-11-23 23:04:32 -05:00
parent 36d72c4cab
commit 7f287b62fb
12 changed files with 541 additions and 63 deletions

3
.gitmodules vendored
View File

@@ -55,6 +55,3 @@
[submodule "cmd/micro/vendor/github.com/flynn/json5"] [submodule "cmd/micro/vendor/github.com/flynn/json5"]
path = cmd/micro/vendor/github.com/flynn/json5 path = cmd/micro/vendor/github.com/flynn/json5
url = https://github.com/flynn/json5 url = https://github.com/flynn/json5
[submodule "cmd/micro/vendor/github.com/mattn/go-shellwords"]
path = cmd/micro/vendor/github.com/mattn/go-shellwords
url = https://github.com/mattn/go-shellwords

View File

@@ -10,6 +10,7 @@ import (
"github.com/yuin/gopher-lua" "github.com/yuin/gopher-lua"
"github.com/zyedidia/clipboard" "github.com/zyedidia/clipboard"
"github.com/zyedidia/micro/cmd/micro/shellwords"
"github.com/zyedidia/tcell" "github.com/zyedidia/tcell"
) )
@@ -997,7 +998,7 @@ func (v *View) SaveAs(usePlugin bool) bool {
filename, canceled := messenger.Prompt("Filename: ", "", "Save", NoCompletion) filename, canceled := messenger.Prompt("Filename: ", "", "Save", NoCompletion)
if !canceled { if !canceled {
// the filename might or might not be quoted, so unquote first then join the strings. // the filename might or might not be quoted, so unquote first then join the strings.
args, err := SplitCommandArgs(filename) args, err := shellwords.Split(filename)
filename = strings.Join(args, " ") filename = strings.Join(args, " ")
if err != nil { if err != nil {
messenger.Error("Error parsing arguments: ", err) messenger.Error("Error parsing arguments: ", err)

View File

@@ -13,6 +13,7 @@ import (
"strings" "strings"
humanize "github.com/dustin/go-humanize" humanize "github.com/dustin/go-humanize"
"github.com/zyedidia/micro/cmd/micro/shellwords"
) )
// A Command contains a action (a function to call) as well as information about how to autocomplete the command // A Command contains a action (a function to call) as well as information about how to autocomplete the command
@@ -285,7 +286,7 @@ func Open(args []string) {
if len(args) > 0 { if len(args) > 0 {
filename := args[0] filename := args[0]
// the filename might or might not be quoted, so unquote first then join the strings. // the filename might or might not be quoted, so unquote first then join the strings.
args, err := SplitCommandArgs(filename) args, err := shellwords.Split(filename)
if err != nil { if err != nil {
messenger.Error("Error parsing args ", err) messenger.Error("Error parsing args ", err)
return return
@@ -500,7 +501,7 @@ func Bind(args []string) {
// Run runs a shell command in the background // Run runs a shell command in the background
func Run(args []string) { func Run(args []string) {
// Run a shell command in the background (openTerm is false) // Run a shell command in the background (openTerm is false)
HandleShellCommand(JoinCommandArgs(args...), false, true) HandleShellCommand(shellwords.Join(args...), false, true)
} }
// Quit closes the main view // Quit closes the main view
@@ -645,7 +646,7 @@ func ReplaceAll(args []string) {
// RunShellCommand executes a shell command and returns the output/error // RunShellCommand executes a shell command and returns the output/error
func RunShellCommand(input string) (string, error) { func RunShellCommand(input string) (string, error) {
args, err := SplitCommandArgs(input) args, err := shellwords.Split(input)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -665,7 +666,7 @@ func RunShellCommand(input string) (string, error) {
// The openTerm argument specifies whether a terminal should be opened (for viewing output // The openTerm argument specifies whether a terminal should be opened (for viewing output
// or interacting with stdin) // or interacting with stdin)
func HandleShellCommand(input string, openTerm bool, waitToFinish bool) string { func HandleShellCommand(input string, openTerm bool, waitToFinish bool) string {
args, err := SplitCommandArgs(input) args, err := shellwords.Split(input)
if err != nil { if err != nil {
return "" return ""
} }
@@ -735,7 +736,7 @@ func HandleShellCommand(input string, openTerm bool, waitToFinish bool) string {
// HandleCommand handles input from the user // HandleCommand handles input from the user
func HandleCommand(input string) { func HandleCommand(input string) {
args, err := SplitCommandArgs(input) args, err := shellwords.Split(input)
if err != nil { if err != nil {
messenger.Error("Error parsing args ", err) messenger.Error("Error parsing args ", err)
return return

View File

@@ -10,6 +10,7 @@ import (
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"github.com/zyedidia/clipboard" "github.com/zyedidia/clipboard"
"github.com/zyedidia/micro/cmd/micro/shellwords"
"github.com/zyedidia/tcell" "github.com/zyedidia/tcell"
) )
@@ -272,7 +273,7 @@ func (m *Messenger) Prompt(prompt, placeholder, historyType string, completionTy
response, canceled = m.response, false response, canceled = m.response, false
m.history[historyType][len(m.history[historyType])-1] = response m.history[historyType][len(m.history[historyType])-1] = response
case tcell.KeyTab: case tcell.KeyTab:
args, err := SplitCommandArgs(m.response) args, err := shellwords.Split(m.response)
if err != nil { if err != nil {
break break
} }
@@ -322,7 +323,7 @@ func (m *Messenger) Prompt(prompt, placeholder, historyType string, completionTy
} }
if chosen != "" { if chosen != "" {
m.response = JoinCommandArgs(append(args[:len(args)-1], chosen)...) m.response = shellwords.Join(append(args[:len(args)-1], chosen)...)
m.cursorx = Count(m.response) m.cursorx = Count(m.response)
} }
} }

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2017 Yasuhiro Matsumoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,49 @@
This is a modified version of `go-shellwords` for the micro editor.
# go-shellwords
[![Coverage Status](https://coveralls.io/repos/mattn/go-shellwords/badge.png?branch=master)](https://coveralls.io/r/mattn/go-shellwords?branch=master)
[![Build Status](https://travis-ci.org/mattn/go-shellwords.svg?branch=master)](https://travis-ci.org/mattn/go-shellwords)
Parse line as shell words.
## Usage
```go
args, err := shellwords.Parse("./foo --bar=baz")
// args should be ["./foo", "--bar=baz"]
```
```go
os.Setenv("FOO", "bar")
p := shellwords.NewParser()
p.ParseEnv = true
args, err := p.Parse("./foo $FOO")
// args should be ["./foo", "bar"]
```
```go
p := shellwords.NewParser()
p.ParseBacktick = true
args, err := p.Parse("./foo `echo $SHELL`")
// args should be ["./foo", "/bin/bash"]
```
```go
shellwords.ParseBacktick = true
p := shellwords.NewParser()
args, err := p.Parse("./foo `echo $SHELL`")
// args should be ["./foo", "/bin/bash"]
```
# Thanks
This is based on cpan module [Parse::CommandLine](https://metacpan.org/pod/Parse::CommandLine).
# License
under the MIT License: http://mattn.mit-license.org/2017
# Author
Yasuhiro Matsumoto (a.k.a mattn)

View File

@@ -0,0 +1,189 @@
package shellwords
import (
"bytes"
"errors"
"os"
"regexp"
)
var envRe = regexp.MustCompile(`\$({[a-zA-Z0-9_]+}|[a-zA-Z0-9_]+)`)
func isSpace(r rune) bool {
switch r {
case ' ', '\t', '\r', '\n':
return true
}
return false
}
func replaceEnv(s string) string {
return envRe.ReplaceAllStringFunc(s, func(s string) string {
s = s[1:]
if s[0] == '{' {
s = s[1 : len(s)-1]
}
return os.Getenv(s)
})
}
type Parser struct {
Position int
}
func NewParser() *Parser {
return &Parser{0}
}
func (p *Parser) Parse(line string) ([]string, error) {
args := []string{}
buf := ""
var escaped, doubleQuoted, singleQuoted, backQuote, dollarQuote bool
backtick := ""
pos := -1
got := false
loop:
for i, r := range line {
if escaped {
buf += string(r)
escaped = false
continue
}
if r == '\\' {
if singleQuoted {
buf += string(r)
} else {
escaped = true
}
continue
}
if isSpace(r) {
if singleQuoted || doubleQuoted || backQuote || dollarQuote {
buf += string(r)
backtick += string(r)
} else if got {
buf = replaceEnv(buf)
args = append(args, buf)
buf = ""
got = false
}
continue
}
switch r {
case '`':
if !singleQuoted && !doubleQuoted && !dollarQuote {
if backQuote {
out, err := shellRun(backtick)
if err != nil {
return nil, err
}
buf = out
}
backtick = ""
backQuote = !backQuote
continue
backtick = ""
backQuote = !backQuote
}
case ')':
if !singleQuoted && !doubleQuoted && !backQuote {
if dollarQuote {
out, err := shellRun(backtick)
if err != nil {
return nil, err
}
buf = out
}
backtick = ""
dollarQuote = !dollarQuote
continue
backtick = ""
dollarQuote = !dollarQuote
}
case '(':
if !singleQuoted && !doubleQuoted && !backQuote {
if !dollarQuote && len(buf) > 0 && buf == "$" {
dollarQuote = true
buf += "("
continue
} else {
return nil, errors.New("invalid command line string")
}
}
case '"':
if !singleQuoted && !dollarQuote {
doubleQuoted = !doubleQuoted
continue
}
case '\'':
if !doubleQuoted && !dollarQuote {
singleQuoted = !singleQuoted
continue
}
case ';', '&', '|', '<', '>':
if !(escaped || singleQuoted || doubleQuoted || backQuote) {
pos = i
break loop
}
}
got = true
buf += string(r)
if backQuote || dollarQuote {
backtick += string(r)
}
}
buf = replaceEnv(buf)
args = append(args, buf)
if escaped || singleQuoted || doubleQuoted || backQuote || dollarQuote {
return nil, errors.New("invalid command line string")
}
p.Position = pos
return args, nil
}
func Split(line string) ([]string, error) {
return NewParser().Parse(line)
}
func Join(args ...string) string {
var buf bytes.Buffer
for i, w := range args {
if i != 0 {
buf.WriteByte(' ')
}
if w == "" {
buf.WriteString("''")
continue
}
for _, b := range w {
switch b {
case 'a', 'b', 'c', 'd', 'e', 'f', 'g',
'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u',
'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U',
'V', 'W', 'X', 'Y', 'Z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'_', '-', '.', ',', ':', '/', '@':
buf.WriteString(string(b))
default:
buf.WriteByte('\\')
buf.WriteString(string(b))
}
}
}
return buf.String()
}

View File

@@ -0,0 +1,229 @@
package shellwords
import (
"os"
"reflect"
"testing"
)
var testcases = []struct {
line string
expected []string
}{
{`var --bar=baz`, []string{`var`, `--bar=baz`}},
{`var --bar="baz"`, []string{`var`, `--bar=baz`}},
{`var "--bar=baz"`, []string{`var`, `--bar=baz`}},
{`var "--bar='baz'"`, []string{`var`, `--bar='baz'`}},
{"var --bar=`baz`", []string{`var`, "--bar=`baz`"}},
{`var "--bar=\"baz'"`, []string{`var`, `--bar="baz'`}},
{`var "--bar=\'baz\'"`, []string{`var`, `--bar='baz'`}},
{`var --bar='\'`, []string{`var`, `--bar=\`}},
{`var "--bar baz"`, []string{`var`, `--bar baz`}},
{`var --"bar baz"`, []string{`var`, `--bar baz`}},
{`var --"bar baz"`, []string{`var`, `--bar baz`}},
}
func TestSimple(t *testing.T) {
for _, testcase := range testcases {
args, err := Parse(testcase.line)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(args, testcase.expected) {
t.Fatalf("Expected %#v, but %#v:", testcase.expected, args)
}
}
}
func TestError(t *testing.T) {
_, err := Parse("foo '")
if err == nil {
t.Fatal("Should be an error")
}
_, err = Parse(`foo "`)
if err == nil {
t.Fatal("Should be an error")
}
_, err = Parse("foo `")
if err == nil {
t.Fatal("Should be an error")
}
}
func TestLastSpace(t *testing.T) {
args, err := Parse("foo bar\\ ")
if err != nil {
t.Fatal(err)
}
if len(args) != 2 {
t.Fatal("Should have two elements")
}
if args[0] != "foo" {
t.Fatal("1st element should be `foo`")
}
if args[1] != "bar " {
t.Fatal("1st element should be `bar `")
}
}
func TestBacktick(t *testing.T) {
goversion, err := shellRun("go version")
if err != nil {
t.Fatal(err)
}
parser := NewParser()
parser.ParseBacktick = true
args, err := parser.Parse("echo `go version`")
if err != nil {
t.Fatal(err)
}
expected := []string{"echo", goversion}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
args, err = parser.Parse(`echo $(echo foo)`)
if err != nil {
t.Fatal(err)
}
expected = []string{"echo", "foo"}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
parser.ParseBacktick = false
args, err = parser.Parse(`echo $(echo "foo")`)
expected = []string{"echo", `$(echo "foo")`}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
args, err = parser.Parse("echo $(`echo1)")
if err != nil {
t.Fatal(err)
}
expected = []string{"echo", "$(`echo1)"}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
}
func TestBacktickError(t *testing.T) {
parser := NewParser()
parser.ParseBacktick = true
_, err := parser.Parse("echo `go Version`")
if err == nil {
t.Fatal("Should be an error")
}
expected := "exit status 2:go: unknown subcommand \"Version\"\nRun 'go help' for usage.\n"
if expected != err.Error() {
t.Fatalf("Expected %q, but %q", expected, err.Error())
}
_, err = parser.Parse(`echo $(echo1)`)
if err == nil {
t.Fatal("Should be an error")
}
_, err = parser.Parse(`echo $(echo1`)
if err == nil {
t.Fatal("Should be an error")
}
_, err = parser.Parse(`echo $ (echo1`)
if err == nil {
t.Fatal("Should be an error")
}
_, err = parser.Parse(`echo (echo1`)
if err == nil {
t.Fatal("Should be an error")
}
_, err = parser.Parse(`echo )echo1`)
if err == nil {
t.Fatal("Should be an error")
}
}
func TestEnv(t *testing.T) {
os.Setenv("FOO", "bar")
parser := NewParser()
parser.ParseEnv = true
args, err := parser.Parse("echo $FOO")
if err != nil {
t.Fatal(err)
}
expected := []string{"echo", "bar"}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
}
func TestNoEnv(t *testing.T) {
parser := NewParser()
parser.ParseEnv = true
args, err := parser.Parse("echo $BAR")
if err != nil {
t.Fatal(err)
}
expected := []string{"echo", ""}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
}
func TestDupEnv(t *testing.T) {
os.Setenv("FOO", "bar")
os.Setenv("FOO_BAR", "baz")
parser := NewParser()
parser.ParseEnv = true
args, err := parser.Parse("echo $$FOO$")
if err != nil {
t.Fatal(err)
}
expected := []string{"echo", "$bar$"}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
args, err = parser.Parse("echo $${FOO_BAR}$")
if err != nil {
t.Fatal(err)
}
expected = []string{"echo", "$baz$"}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
}
func TestHaveMore(t *testing.T) {
parser := NewParser()
parser.ParseEnv = true
line := "echo foo; seq 1 10"
args, err := parser.Parse(line)
if err != nil {
t.Fatalf(err.Error())
}
expected := []string{"echo", "foo"}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
if parser.Position == 0 {
t.Fatalf("Commands should be remaining")
}
line = string([]rune(line)[parser.Position+1:])
args, err = parser.Parse(line)
if err != nil {
t.Fatalf(err.Error())
}
expected = []string{"seq", "1", "10"}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
if parser.Position > 0 {
t.Fatalf("Commands should not be remaining")
}
}

View File

@@ -0,0 +1,22 @@
// +build !windows
package shellwords
import (
"errors"
"os"
"os/exec"
"strings"
)
func shellRun(line string) (string, error) {
shell := os.Getenv("SHELL")
b, err := exec.Command(shell, "-c", line).Output()
if err != nil {
if eerr, ok := err.(*exec.ExitError); ok {
b = eerr.Stderr
}
return "", errors.New(err.Error() + ":" + string(b))
}
return strings.TrimSpace(string(b)), nil
}

View File

@@ -0,0 +1,20 @@
package shellwords
import (
"errors"
"os"
"os/exec"
"strings"
)
func shellRun(line string) (string, error) {
shell := os.Getenv("COMSPEC")
b, err := exec.Command(shell, "/c", line).Output()
if err != nil {
if eerr, ok := err.(*exec.ExitError); ok {
b = eerr.Stderr
}
return "", errors.New(err.Error() + ":" + string(b))
}
return strings.TrimSpace(string(b)), nil
}

View File

@@ -1,7 +1,6 @@
package main package main
import ( import (
"bytes"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@@ -12,7 +11,6 @@ import (
"unicode/utf8" "unicode/utf8"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"github.com/mattn/go-shellwords"
homedir "github.com/mitchellh/go-homedir" homedir "github.com/mitchellh/go-homedir"
) )
@@ -277,55 +275,6 @@ func ShortFuncName(i interface{}) string {
return strings.TrimPrefix(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name(), "main.(*View).") return strings.TrimPrefix(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name(), "main.(*View).")
} }
// SplitCommandArgs separates multiple command arguments which may be quoted.
// The returned slice contains at least one string
func SplitCommandArgs(input string) ([]string, error) {
shellwords.ParseEnv = true
shellwords.ParseBacktick = true
return shellwords.Parse(input)
}
// JoinCommandArgs joins multiple command arguments and quote the strings if needed.
func JoinCommandArgs(args ...string) string {
var buf bytes.Buffer
for i, w := range args {
if i != 0 {
buf.WriteByte(' ')
}
if w == "" {
buf.WriteString("''")
continue
}
strBytes := []byte(w)
for _, b := range strBytes {
switch b {
case
'a', 'b', 'c', 'd', 'e', 'f', 'g',
'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u',
'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U',
'V', 'W', 'X', 'Y', 'Z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'_', '-', '.', ',', ':', '/', '@':
buf.WriteByte(b)
case '\n':
buf.WriteString("'\n'")
default:
buf.WriteByte('\\')
buf.WriteByte(b)
}
}
// return buf.String()
// buf.WriteString(w)
}
return buf.String()
}
// ReplaceHome takes a path as input and replaces ~ at the start of the path with the user's // ReplaceHome takes a path as input and replaces ~ at the start of the path with the user's
// home directory. Does nothing if the path does not start with '~'. // home directory. Does nothing if the path does not start with '~'.
func ReplaceHome(path string) string { func ReplaceHome(path string) string {