| package shellwords |
| |
| import ( |
| "errors" |
| "os" |
| "regexp" |
| "strings" |
| ) |
| |
| var ( |
| ParseEnv bool = false |
| ParseBacktick bool = false |
| ) |
| |
| 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(getenv func(string) string, s string) string { |
| if getenv == nil { |
| getenv = os.Getenv |
| } |
| |
| return envRe.ReplaceAllStringFunc(s, func(s string) string { |
| s = s[1:] |
| if s[0] == '{' { |
| s = s[1 : len(s)-1] |
| } |
| return getenv(s) |
| }) |
| } |
| |
| type Parser struct { |
| ParseEnv bool |
| ParseBacktick bool |
| Position int |
| |
| // 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, |
| } |
| } |
| |
| 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 { |
| if p.ParseEnv { |
| buf = replaceEnv(p.Getenv, buf) |
| } |
| args = append(args, buf) |
| buf = "" |
| got = false |
| } |
| continue |
| } |
| |
| switch r { |
| case '`': |
| if !singleQuoted && !doubleQuoted && !dollarQuote { |
| if p.ParseBacktick { |
| 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 p.ParseBacktick { |
| if dollarQuote { |
| out, err := shellRun(backtick) |
| if err != nil { |
| return nil, err |
| } |
| if r == ')' { |
| buf = buf[:len(buf)-len(backtick)-2] + out |
| } else { |
| buf = buf[:len(buf)-len(backtick)-1] + 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 { |
| doubleQuoted = !doubleQuoted |
| continue |
| } |
| case '\'': |
| if !doubleQuoted && !dollarQuote { |
| singleQuoted = !singleQuoted |
| continue |
| } |
| case ';', '&', '|', '<', '>': |
| if !(escaped || singleQuoted || doubleQuoted || backQuote) { |
| if r == '>' && len(buf) > 0 { |
| if c := buf[0]; '0' <= c && c <= '9' { |
| i -= 1 |
| got = false |
| } |
| } |
| pos = i |
| break loop |
| } |
| } |
| |
| got = true |
| buf += string(r) |
| if backQuote || dollarQuote { |
| backtick += string(r) |
| } |
| } |
| |
| if got { |
| if p.ParseEnv { |
| buf = replaceEnv(p.Getenv, 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 Parse(line string) ([]string, error) { |
| return NewParser().Parse(line) |
| } |