mirror of
https://github.com/Hopiu/micro.git
synced 2026-03-17 06:20:28 +00:00
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:
parent
2ee7adb196
commit
0360a2fcb5
9 changed files with 141 additions and 157 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
1
cmd/micro/vendor/github.com/mattn/go-shellwords
generated
vendored
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 95c860c1895b21b58903abdd1d9c591560b0601c
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue