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"] [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

@@ -997,7 +997,12 @@ 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.
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) v.saveToFile(filename)
} }

View File

@@ -277,7 +277,12 @@ 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.
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) CurView().Open(filename)
} else { } else {
@@ -508,24 +513,35 @@ func Save(args []string) {
// Replace runs search and replace // Replace runs search and replace
func Replace(args []string) { 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 // We need to find both a search and replace expression
messenger.Error("Invalid replace statement: " + strings.Join(args, " ")) messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
return return
} }
allAtOnce := false all := false
if len(args) == 3 { noRegex := false
// user added -a flag
if args[2] == "-a" { if len(args) > 2 {
allAtOnce = true for _, arg := range args[2:] {
} else { switch arg {
messenger.Error("Invalid replace flag: " + args[2]) case "-a":
return all = true
case "-l":
noRegex = true
default:
messenger.Error("Invalid flag: " + arg)
return
}
} }
} }
search := string(args[0]) search := string(args[0])
if noRegex {
search = regexp.QuoteMeta(search)
}
replace := string(args[1]) replace := string(args[1])
regex, err := regexp.Compile("(?m)" + search) regex, err := regexp.Compile("(?m)" + search)
@@ -561,7 +577,7 @@ func Replace(args []string) {
view.Buf.MultipleReplace(deltas) view.Buf.MultipleReplace(deltas)
} }
if allAtOnce { if all {
replaceAll() replaceAll()
} else { } else {
for { for {
@@ -621,15 +637,18 @@ 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) {
inputCmd := SplitCommandArgs(input)[0] args, err := SplitCommandArgs(input)
args := SplitCommandArgs(input)[1:] if err != nil {
return "", err
}
inputCmd := args[0]
cmd := exec.Command(inputCmd, args...) cmd := exec.Command(inputCmd, args[1:]...)
outputBytes := &bytes.Buffer{} outputBytes := &bytes.Buffer{}
cmd.Stdout = outputBytes cmd.Stdout = outputBytes
cmd.Stderr = outputBytes cmd.Stderr = outputBytes
cmd.Start() cmd.Start()
err := cmd.Wait() // wait for command to finish err = cmd.Wait() // wait for command to finish
outstring := outputBytes.String() outstring := outputBytes.String()
return outstring, err 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 // 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 {
inputCmd := SplitCommandArgs(input)[0] args, err := SplitCommandArgs(input)
if err != nil {
return ""
}
inputCmd := args[0]
if !openTerm { if !openTerm {
// Simply run the command in the background and notify the user when it's done // Simply run the command in the background and notify the user when it's done
messenger.Message("Running...") messenger.Message("Running...")
@@ -663,7 +686,7 @@ func HandleShellCommand(input string, openTerm bool, waitToFinish bool) string {
screen.Fini() screen.Fini()
screen = nil screen = nil
args := SplitCommandArgs(input)[1:] args := args[1:]
// Set up everything for the command // Set up everything for the command
var output string var output string
@@ -704,7 +727,12 @@ 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 := SplitCommandArgs(input) args, err := SplitCommandArgs(input)
if err != nil {
messenger.Error("Error parsing args ", err)
return
}
inputCmd := args[0] inputCmd := args[0]
if _, ok := commands[inputCmd]; !ok { 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 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 := SplitCommandArgs(m.response) args, err := SplitCommandArgs(m.response)
currentArgNum := len(args) - 1 if err != nil {
currentArg := args[currentArgNum] break
}
currentArg := ""
currentArgNum := 0
if len(args) > 0 {
currentArgNum = len(args) - 1
currentArg = args[currentArgNum]
}
var completionType Completion var completionType Completion
if completionTypes[0] == CommandCompletion && currentArgNum > 0 { 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" "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"
) )
@@ -278,92 +279,50 @@ func ShortFuncName(i interface{}) string {
// SplitCommandArgs separates multiple command arguments which may be quoted. // SplitCommandArgs separates multiple command arguments which may be quoted.
// The returned slice contains at least one string // The returned slice contains at least one string
func SplitCommandArgs(input string) []string { func SplitCommandArgs(input string) ([]string, error) {
var result []string shellwords.ParseEnv = true
var curQuote *bytes.Buffer shellwords.ParseBacktick = true
return shellwords.Parse(input)
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
} }
// JoinCommandArgs joins multiple command arguments and quote the strings if needed. // JoinCommandArgs joins multiple command arguments and quote the strings if needed.
func JoinCommandArgs(args ...string) string { func JoinCommandArgs(args ...string) string {
buf := new(bytes.Buffer) var buf bytes.Buffer
first := true for i, w := range args {
for _, arg := range args { if i != 0 {
if first { buf.WriteByte(' ')
first = false
} else {
buf.WriteRune(' ')
} }
quoted := strconv.Quote(arg) if w == "" {
if quoted[1:len(quoted)-1] != arg || strings.ContainsRune(arg, ' ') { buf.WriteString("''")
buf.WriteString(quoted) continue
} else {
buf.WriteString(arg)
} }
}
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() return buf.String()
} }

View File

@@ -1,7 +1,6 @@
package main package main
import ( import (
"reflect"
"testing" "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) { func TestStringWidth(t *testing.T) {
tabsize := 4 tabsize := 4
if w := StringWidth("1\t2", tabsize); w != 5 { if w := StringWidth("1\t2", tabsize); w != 5 {

View File

@@ -9,11 +9,12 @@ Here are the possible commands that you can use.
will 'save as' the filename. will 'save as' the filename.
* `replace "search" "value" flags`: This will replace `search` with `value`. * `replace "search" "value" flags`: This will replace `search` with `value`.
The `flags` are optional. At this point, there is only one flag: `-a`, which The `flags` are optional. Possible flags are:
replaces all occurrences at once. * `-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 Note that `search` must be a valid regex (unless `-l` is passed). If one
have any spaces in it, you may omit the quotes. 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 * `replaceall "search" "value"`: This will replace `search` with `value` without
user confirmation. user confirmation.
@@ -84,3 +85,11 @@ Here are the possible commands that you can use.
The following commands are provided by the default plugins: The following commands are provided by the default plugins:
* `lint`: Lint the current file for errors. * `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.