You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
318 lines
5.8 KiB
Go
318 lines
5.8 KiB
Go
package shellwords
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"os"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
var (
|
|
ParseEnv bool = false
|
|
ParseBacktick bool = false
|
|
)
|
|
|
|
func isSpace(r rune) bool {
|
|
switch r {
|
|
case ' ', '\t', '\r', '\n':
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func replaceEnv(getenv func(string) string, s string) string {
|
|
if getenv == nil {
|
|
getenv = os.Getenv
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
rs := []rune(s)
|
|
for i := 0; i < len(rs); i++ {
|
|
r := rs[i]
|
|
if r == '\\' {
|
|
i++
|
|
if i == len(rs) {
|
|
break
|
|
}
|
|
buf.WriteRune(rs[i])
|
|
continue
|
|
} else if r == '$' {
|
|
i++
|
|
if i == len(rs) {
|
|
buf.WriteRune(r)
|
|
break
|
|
}
|
|
if rs[i] == 0x7b {
|
|
i++
|
|
p := i
|
|
for ; i < len(rs); i++ {
|
|
r = rs[i]
|
|
if r == '\\' {
|
|
i++
|
|
if i == len(rs) {
|
|
return s
|
|
}
|
|
continue
|
|
}
|
|
if r == 0x7d || (!unicode.IsLetter(r) && r != '_' && !unicode.IsDigit(r)) {
|
|
break
|
|
}
|
|
}
|
|
if r != 0x7d {
|
|
return s
|
|
}
|
|
if i > p {
|
|
buf.WriteString(getenv(s[p:i]))
|
|
}
|
|
} else {
|
|
p := i
|
|
for ; i < len(rs); i++ {
|
|
r := rs[i]
|
|
if r == '\\' {
|
|
i++
|
|
if i == len(rs) {
|
|
return s
|
|
}
|
|
continue
|
|
}
|
|
if !unicode.IsLetter(r) && r != '_' && !unicode.IsDigit(r) {
|
|
break
|
|
}
|
|
}
|
|
if i > p {
|
|
buf.WriteString(getenv(s[p:i]))
|
|
i--
|
|
} else {
|
|
buf.WriteString(s[p:])
|
|
}
|
|
}
|
|
} else {
|
|
buf.WriteRune(r)
|
|
}
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
type Parser struct {
|
|
ParseEnv bool
|
|
ParseBacktick bool
|
|
Position int
|
|
Dir string
|
|
|
|
// If ParseEnv is true, use this for getenv.
|
|
// If nil, use os.Getenv.
|
|
Getenv func(string) string
|
|
}
|
|
|
|
func NewParser() *Parser {
|
|
return &Parser{
|
|
ParseEnv: ParseEnv,
|
|
ParseBacktick: ParseBacktick,
|
|
Position: 0,
|
|
Dir: "",
|
|
}
|
|
}
|
|
|
|
type argType int
|
|
|
|
const (
|
|
argNo argType = iota
|
|
argSingle
|
|
argQuoted
|
|
)
|
|
|
|
func (p *Parser) Parse(line string) ([]string, error) {
|
|
args := []string{}
|
|
buf := ""
|
|
var escaped, doubleQuoted, singleQuoted, backQuote, dollarQuote bool
|
|
backtick := ""
|
|
|
|
pos := -1
|
|
got := argNo
|
|
|
|
i := -1
|
|
loop:
|
|
for _, r := range line {
|
|
i++
|
|
if escaped {
|
|
buf += string(r)
|
|
escaped = false
|
|
got = argSingle
|
|
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 != argNo {
|
|
if p.ParseEnv {
|
|
if got == argSingle {
|
|
parser := &Parser{ParseEnv: false, ParseBacktick: false, Position: 0, Dir: p.Dir}
|
|
strs, err := parser.Parse(replaceEnv(p.Getenv, buf))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
args = append(args, strs...)
|
|
} else {
|
|
args = append(args, replaceEnv(p.Getenv, buf))
|
|
}
|
|
} else {
|
|
args = append(args, buf)
|
|
}
|
|
buf = ""
|
|
got = argNo
|
|
}
|
|
continue
|
|
}
|
|
|
|
switch r {
|
|
case '`':
|
|
if !singleQuoted && !doubleQuoted && !dollarQuote {
|
|
if p.ParseBacktick {
|
|
if backQuote {
|
|
out, err := shellRun(backtick, p.Dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
buf = buf[:len(buf)-len(backtick)] + out
|
|
}
|
|
backtick = ""
|
|
backQuote = !backQuote
|
|
continue
|
|
}
|
|
backtick = ""
|
|
backQuote = !backQuote
|
|
}
|
|
case ')':
|
|
if !singleQuoted && !doubleQuoted && !backQuote {
|
|
if p.ParseBacktick {
|
|
if dollarQuote {
|
|
out, err := shellRun(backtick, p.Dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
buf = buf[:len(buf)-len(backtick)-2] + out
|
|
}
|
|
backtick = ""
|
|
dollarQuote = !dollarQuote
|
|
continue
|
|
}
|
|
backtick = ""
|
|
dollarQuote = !dollarQuote
|
|
}
|
|
case '(':
|
|
if !singleQuoted && !doubleQuoted && !backQuote {
|
|
if !dollarQuote && strings.HasSuffix(buf, "$") {
|
|
dollarQuote = true
|
|
buf += "("
|
|
continue
|
|
} else {
|
|
return nil, errors.New("invalid command line string")
|
|
}
|
|
}
|
|
case '"':
|
|
if !singleQuoted && !dollarQuote {
|
|
if doubleQuoted {
|
|
got = argQuoted
|
|
}
|
|
doubleQuoted = !doubleQuoted
|
|
continue
|
|
}
|
|
case '\'':
|
|
if !doubleQuoted && !dollarQuote {
|
|
if singleQuoted {
|
|
got = argQuoted
|
|
}
|
|
singleQuoted = !singleQuoted
|
|
continue
|
|
}
|
|
case ';', '&', '|', '<', '>':
|
|
if !(escaped || singleQuoted || doubleQuoted || backQuote || dollarQuote) {
|
|
if r == '>' && len(buf) > 0 {
|
|
if c := buf[0]; '0' <= c && c <= '9' {
|
|
i -= 1
|
|
got = argNo
|
|
}
|
|
}
|
|
pos = i
|
|
break loop
|
|
}
|
|
}
|
|
|
|
got = argSingle
|
|
buf += string(r)
|
|
if backQuote || dollarQuote {
|
|
backtick += string(r)
|
|
}
|
|
}
|
|
|
|
if got != argNo {
|
|
if p.ParseEnv {
|
|
if got == argSingle {
|
|
parser := &Parser{ParseEnv: false, ParseBacktick: false, Position: 0, Dir: p.Dir}
|
|
strs, err := parser.Parse(replaceEnv(p.Getenv, buf))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
args = append(args, strs...)
|
|
} else {
|
|
args = append(args, replaceEnv(p.Getenv, buf))
|
|
}
|
|
} else {
|
|
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 (p *Parser) ParseWithEnvs(line string) (envs []string, args []string, err error) {
|
|
_args, err := p.Parse(line)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
envs = []string{}
|
|
args = []string{}
|
|
parsingEnv := true
|
|
for _, arg := range _args {
|
|
if parsingEnv && isEnv(arg) {
|
|
envs = append(envs, arg)
|
|
} else {
|
|
if parsingEnv {
|
|
parsingEnv = false
|
|
}
|
|
args = append(args, arg)
|
|
}
|
|
}
|
|
return envs, args, nil
|
|
}
|
|
|
|
func isEnv(arg string) bool {
|
|
return len(strings.Split(arg, "=")) == 2
|
|
}
|
|
|
|
func Parse(line string) ([]string, error) {
|
|
return NewParser().Parse(line)
|
|
}
|
|
|
|
func ParseWithEnvs(line string) (envs []string, args []string, err error) {
|
|
return NewParser().ParseWithEnvs(line)
|
|
}
|