Improve cmdbar parsing and add -l replace flag

The -l flag to the replace command means "literal" and will treat
the search term literally instead of as a regular expression.

The command bar also now supports expanding environment variables
and running expressions through the shell and using the result
in the command.
This commit is contained in:
Zachary Yedidia 2017-11-22 13:54:39 -05:00
parent 2ee7adb196
commit 0360a2fcb5
9 changed files with 141 additions and 157 deletions

3
.gitmodules vendored
View file

@ -55,3 +55,6 @@
[submodule "cmd/micro/vendor/github.com/flynn/json5"]
path = cmd/micro/vendor/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

@ -997,7 +997,12 @@ func (v *View) SaveAs(usePlugin bool) bool {
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), " ")
args, err := SplitCommandArgs(filename)
filename = strings.Join(args, " ")
if err != nil {
messenger.Error("Error parsing arguments: ", err)
return false
}
v.saveToFile(filename)
}

View file

@ -277,7 +277,12 @@ func Open(args []string) {
if len(args) > 0 {
filename := args[0]
// the filename might or might not be quoted, so unquote first then join the strings.
filename = strings.Join(SplitCommandArgs(filename), " ")
args, err := SplitCommandArgs(filename)
if err != nil {
messenger.Error("Error parsing args ", err)
return
}
filename = strings.Join(args, " ")
CurView().Open(filename)
} else {
@ -508,24 +513,35 @@ func Save(args []string) {
// Replace runs search and replace
func Replace(args []string) {
if len(args) < 2 || len(args) > 3 {
if len(args) < 2 || len(args) > 4 {
// We need to find both a search and replace expression
messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
return
}
allAtOnce := false
if len(args) == 3 {
// user added -a flag
if args[2] == "-a" {
allAtOnce = true
} else {
messenger.Error("Invalid replace flag: " + args[2])
return
all := false
noRegex := false
if len(args) > 2 {
for _, arg := range args[2:] {
switch arg {
case "-a":
all = true
case "-l":
noRegex = true
default:
messenger.Error("Invalid flag: " + arg)
return
}
}
}
search := string(args[0])
if noRegex {
search = regexp.QuoteMeta(search)
}
replace := string(args[1])
regex, err := regexp.Compile("(?m)" + search)
@ -561,7 +577,7 @@ func Replace(args []string) {
view.Buf.MultipleReplace(deltas)
}
if allAtOnce {
if all {
replaceAll()
} else {
for {
@ -621,15 +637,18 @@ func ReplaceAll(args []string) {
// RunShellCommand executes a shell command and returns the output/error
func RunShellCommand(input string) (string, error) {
inputCmd := SplitCommandArgs(input)[0]
args := SplitCommandArgs(input)[1:]
args, err := SplitCommandArgs(input)
if err != nil {
return "", err
}
inputCmd := args[0]
cmd := exec.Command(inputCmd, args...)
cmd := exec.Command(inputCmd, args[1:]...)
outputBytes := &bytes.Buffer{}
cmd.Stdout = outputBytes
cmd.Stderr = outputBytes
cmd.Start()
err := cmd.Wait() // wait for command to finish
err = cmd.Wait() // wait for command to finish
outstring := outputBytes.String()
return outstring, err
}
@ -638,7 +657,11 @@ 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, waitToFinish bool) string {
inputCmd := SplitCommandArgs(input)[0]
args, err := SplitCommandArgs(input)
if err != nil {
return ""
}
inputCmd := args[0]
if !openTerm {
// Simply run the command in the background and notify the user when it's done
messenger.Message("Running...")
@ -663,7 +686,7 @@ func HandleShellCommand(input string, openTerm bool, waitToFinish bool) string {
screen.Fini()
screen = nil
args := SplitCommandArgs(input)[1:]
args := args[1:]
// Set up everything for the command
var output string
@ -704,7 +727,12 @@ func HandleShellCommand(input string, openTerm bool, waitToFinish bool) string {
// HandleCommand handles input from the user
func HandleCommand(input string) {
args := SplitCommandArgs(input)
args, err := SplitCommandArgs(input)
if err != nil {
messenger.Error("Error parsing args ", err)
return
}
inputCmd := args[0]
if _, ok := commands[inputCmd]; !ok {

View file

@ -272,9 +272,16 @@ func (m *Messenger) Prompt(prompt, placeholder, historyType string, completionTy
response, canceled = m.response, false
m.history[historyType][len(m.history[historyType])-1] = response
case tcell.KeyTab:
args := SplitCommandArgs(m.response)
currentArgNum := len(args) - 1
currentArg := args[currentArgNum]
args, err := SplitCommandArgs(m.response)
if err != nil {
break
}
currentArg := ""
currentArgNum := 0
if len(args) > 0 {
currentArgNum = len(args) - 1
currentArg = args[currentArgNum]
}
var completionType Completion
if completionTypes[0] == CommandCompletion && currentArgNum > 0 {

File diff suppressed because one or more lines are too long

View file

@ -12,6 +12,7 @@ import (
"unicode/utf8"
"github.com/mattn/go-runewidth"
"github.com/mattn/go-shellwords"
homedir "github.com/mitchellh/go-homedir"
)
@ -278,92 +279,50 @@ func ShortFuncName(i interface{}) string {
// SplitCommandArgs separates multiple command arguments which may be quoted.
// The returned slice contains at least one string
func SplitCommandArgs(input string) []string {
var result []string
var curQuote *bytes.Buffer
curArg := new(bytes.Buffer)
escape := false
finishQuote := func() {
if curQuote == nil {
return
}
str := curQuote.String()
if unquoted, err := strconv.Unquote(str); err == nil {
str = unquoted
}
curArg.WriteString(str)
curQuote = nil
}
appendResult := func() {
finishQuote()
escape = false
str := curArg.String()
result = append(result, str)
curArg.Reset()
}
for _, r := range input {
if r == ' ' && curQuote == nil {
appendResult()
} else {
runeHandled := false
appendRuneToBuff := func() {
if curQuote != nil {
curQuote.WriteRune(r)
} else {
curArg.WriteRune(r)
}
runeHandled = true
}
if r == '"' && curQuote == nil {
curQuote = new(bytes.Buffer)
appendRuneToBuff()
} else {
if curQuote != nil && !escape {
if r == '"' {
appendRuneToBuff()
finishQuote()
} else if r == '\\' {
appendRuneToBuff()
escape = true
continue
}
}
}
if !runeHandled {
appendRuneToBuff()
}
}
escape = false
}
appendResult()
return result
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 {
buf := new(bytes.Buffer)
first := true
for _, arg := range args {
if first {
first = false
} else {
buf.WriteRune(' ')
var buf bytes.Buffer
for i, w := range args {
if i != 0 {
buf.WriteByte(' ')
}
quoted := strconv.Quote(arg)
if quoted[1:len(quoted)-1] != arg || strings.ContainsRune(arg, ' ') {
buf.WriteString(quoted)
} else {
buf.WriteString(arg)
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()
}

View file

@ -1,7 +1,6 @@
package main
import (
"reflect"
"testing"
)
@ -67,56 +66,6 @@ func TestIsWordChar(t *testing.T) {
}
}
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{``}, ``},
{[]string{`"`}, `"\""`},
{[]string{`a`, ``}, `a `},
{[]string{``, ``, ``, ``}, ` `},
{[]string{"\n"}, `"\n"`},
{[]string{"foo\tbar"}, `"foo\tbar"`},
}
for i, test := range tests {
if result := JoinCommandArgs(test.Query...); test.Wanted != 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: `%q`", i, result)
}
}
splitTests := []struct {
Query string
Wanted []string
}{
{`"hallo""Welt"`, []string{`halloWelt`}},
{`"hallo" "Welt"`, []string{`hallo`, `Welt`}},
{`\"`, []string{`\"`}},
{`"foo`, []string{`"foo`}},
{`"foo"`, []string{`foo`}},
{`"\"`, []string{`"\"`}},
{`"C:\\"foo.txt`, []string{`C:\foo.txt`}},
{`"\n"new"\n"line`, []string{"\nnew\nline"}},
}
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)
}
}
}
func TestStringWidth(t *testing.T) {
tabsize := 4
if w := StringWidth("1\t2", tabsize); w != 5 {

1
cmd/micro/vendor/github.com/mattn/go-shellwords generated vendored Submodule

@ -0,0 +1 @@
Subproject commit 95c860c1895b21b58903abdd1d9c591560b0601c

View file

@ -9,11 +9,12 @@ Here are the possible commands that you can use.
will 'save as' the filename.
* `replace "search" "value" flags`: This will replace `search` with `value`.
The `flags` are optional. At this point, there is only one flag: `-a`, which
replaces all occurrences at once.
The `flags` are optional. Possible flags are:
* `-a`: Replace all occurrences at once
* `-l`: Do a literal search instead of a regex search
Note that `search` must be a valid regex. If one of the arguments does not
have any spaces in it, you may omit the quotes.
Note that `search` must be a valid regex (unless `-l` is passed). If one
of the arguments does not have any spaces in it, you may omit the quotes.
* `replaceall "search" "value"`: This will replace `search` with `value` without
user confirmation.
@ -84,3 +85,11 @@ Here are the possible commands that you can use.
The following commands are provided by the default plugins:
* `lint`: Lint the current file for errors.
# Command Parsing
When running a command, you can use extra syntax that micro will expand before
running the command. To use an argument with a space in it, simply put it in
quotes. You can also use environment variables in the command bar and they
will be expanded to their value. Finally, you can put an expression in backticks
and it will be evaluated by the shell beforehand.