diff --git a/go.mod b/go.mod index 3d476595..551cce53 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ require ( github.com/blang/semver v3.5.1+incompatible github.com/dustin/go-humanize v1.0.0 github.com/go-errors/errors v1.0.1 + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/mattn/go-isatty v0.0.11 github.com/mattn/go-runewidth v0.0.7 github.com/mitchellh/go-homedir v1.1.0 diff --git a/go.sum b/go.sum index 420ee6b9..714b292b 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= diff --git a/internal/action/actions.go b/internal/action/actions.go index 925c97e8..f5e8441e 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -7,13 +7,13 @@ import ( "time" "unicode/utf8" + shellquote "github.com/kballard/go-shellquote" "github.com/zyedidia/clipboard" "github.com/zyedidia/micro/internal/buffer" "github.com/zyedidia/micro/internal/config" "github.com/zyedidia/micro/internal/screen" "github.com/zyedidia/micro/internal/shell" "github.com/zyedidia/micro/internal/util" - "github.com/zyedidia/micro/pkg/shellwords" "github.com/zyedidia/tcell" ) @@ -647,7 +647,7 @@ func (h *BufPane) SaveAs() bool { InfoBar.Prompt("Filename: ", "", "Save", nil, func(resp string, canceled bool) { if !canceled { // the filename might or might not be quoted, so unquote first then join the strings. - args, err := shellwords.Split(resp) + args, err := shellquote.Split(resp) filename := strings.Join(args, " ") if err != nil { InfoBar.Error("Error parsing arguments: ", err) diff --git a/internal/action/command.go b/internal/action/command.go index 8af9a515..5947a634 100644 --- a/internal/action/command.go +++ b/internal/action/command.go @@ -14,6 +14,7 @@ import ( luar "layeh.com/gopher-luar" + shellquote "github.com/kballard/go-shellquote" lua "github.com/yuin/gopher-lua" "github.com/zyedidia/micro/internal/buffer" "github.com/zyedidia/micro/internal/config" @@ -21,7 +22,6 @@ import ( "github.com/zyedidia/micro/internal/screen" "github.com/zyedidia/micro/internal/shell" "github.com/zyedidia/micro/internal/util" - "github.com/zyedidia/micro/pkg/shellwords" ) // A Command contains information about how to execute a command @@ -344,7 +344,7 @@ func (h *BufPane) OpenCmd(args []string) { if len(args) > 0 { filename := args[0] // the filename might or might not be quoted, so unquote first then join the strings. - args, err := shellwords.Split(filename) + args, err := shellquote.Split(filename) if err != nil { InfoBar.Error("Error parsing args ", err) return @@ -706,7 +706,7 @@ func (h *BufPane) UnbindCmd(args []string) { // RunCmd runs a shell command in the background func (h *BufPane) RunCmd(args []string) { - runf, err := shell.RunBackgroundShell(shellwords.Join(args...)) + runf, err := shell.RunBackgroundShell(shellquote.Join(args...)) if err != nil { InfoBar.Error(err) } else { @@ -953,7 +953,7 @@ func (h *BufPane) TermCmd(args []string) { // HandleCommand handles input from the user func (h *BufPane) HandleCommand(input string) { - args, err := shellwords.Split(input) + args, err := shellquote.Split(input) if err != nil { InfoBar.Error("Error parsing args ", err) return diff --git a/internal/action/terminal_supported.go b/internal/action/terminal_supported.go index 21a6de0e..b0b10632 100644 --- a/internal/action/terminal_supported.go +++ b/internal/action/terminal_supported.go @@ -3,14 +3,14 @@ package action import ( + shellquote "github.com/kballard/go-shellquote" "github.com/zyedidia/micro/internal/shell" - "github.com/zyedidia/micro/pkg/shellwords" ) const TermEmuSupported = true func RunTermEmulator(h *BufPane, input string, wait bool, getOutput bool, callback string, userargs []interface{}) error { - args, err := shellwords.Split(input) + args, err := shellquote.Split(input) if err != nil { return err } diff --git a/internal/shell/shell.go b/internal/shell/shell.go index 3a00a30f..24df587f 100644 --- a/internal/shell/shell.go +++ b/internal/shell/shell.go @@ -9,8 +9,8 @@ import ( "os/signal" "strings" + shellquote "github.com/kballard/go-shellquote" "github.com/zyedidia/micro/internal/screen" - "github.com/zyedidia/micro/pkg/shellwords" ) // ExecCommand executes a command using exec @@ -32,7 +32,7 @@ func ExecCommand(name string, arg ...string) (string, error) { // RunCommand executes a shell command and returns the output/error func RunCommand(input string) (string, error) { - args, err := shellwords.Split(input) + args, err := shellquote.Split(input) if err != nil { return "", err } @@ -45,7 +45,7 @@ func RunCommand(input string) (string, error) { // It returns a function which will run the command and returns a string // message result func RunBackgroundShell(input string) (func() string, error) { - args, err := shellwords.Split(input) + args, err := shellquote.Split(input) if err != nil { return nil, err } @@ -68,7 +68,7 @@ func RunBackgroundShell(input string) (func() string, error) { // RunInteractiveShell runs a shellcommand interactively func RunInteractiveShell(input string, wait bool, getOutput bool) (string, error) { - args, err := shellwords.Split(input) + args, err := shellquote.Split(input) if err != nil { return "", err } diff --git a/pkg/shellwords/LICENSE b/pkg/shellwords/LICENSE deleted file mode 100644 index 740fa931..00000000 --- a/pkg/shellwords/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -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. diff --git a/pkg/shellwords/README.md b/pkg/shellwords/README.md deleted file mode 100644 index d77ee695..00000000 --- a/pkg/shellwords/README.md +++ /dev/null @@ -1,49 +0,0 @@ -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) diff --git a/pkg/shellwords/shellwords.go b/pkg/shellwords/shellwords.go deleted file mode 100644 index 99a506c6..00000000 --- a/pkg/shellwords/shellwords.go +++ /dev/null @@ -1,180 +0,0 @@ -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 ' ', '\t', '\r', '\n': - buf.WriteByte('\\') - buf.WriteString(string(b)) - default: - buf.WriteString(string(b)) - } - } - } - return buf.String() -} diff --git a/pkg/shellwords/util_posix.go b/pkg/shellwords/util_posix.go deleted file mode 100644 index 31bdda5a..00000000 --- a/pkg/shellwords/util_posix.go +++ /dev/null @@ -1,22 +0,0 @@ -// +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 -} diff --git a/pkg/shellwords/util_windows.go b/pkg/shellwords/util_windows.go deleted file mode 100644 index 5e06565a..00000000 --- a/pkg/shellwords/util_windows.go +++ /dev/null @@ -1,20 +0,0 @@ -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 -} diff --git a/runtime/help/commands.md b/runtime/help/commands.md index 5e04dd34..ee0eb31d 100644 --- a/runtime/help/commands.md +++ b/runtime/help/commands.md @@ -5,10 +5,10 @@ meaning that all keybindings from a normal buffer are supported (as well as mouse and selection). 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. +running the command. To use an argument with a space in it, put it in +quotes. The command bar parser uses the same rules for parsing arguments that +`/bin/sh` would use (single quotes, double quotes, escaping). The command bar +does not look up environment variables. # Commands