| // Package shell supports splitting and joining of shell command strings. |
| // |
| // The Split function divides a string into whitespace-separated fields, |
| // respecting single and double quotation marks as defined by the Shell Command |
| // Language section of IEEE Std 1003.1 2013. The Quote function quotes |
| // characters that would otherwise be subject to shell evaluation, and the Join |
| // function concatenates quoted strings with spaces between them. |
| // |
| // The relationship between Split and Join is: |
| // |
| // Split(Join(ss)) == ss |
| // |
| package shell |
| |
| import ( |
| "bufio" |
| "bytes" |
| "io" |
| "strings" |
| ) |
| |
| // These characters must be quoted to escape special meaning. This list |
| // doesn't include the single quote. |
| const mustQuote = "|^;<>()$\\\"\t\n`" |
| |
| // These characters should be quoted to escape special meaning, since in some |
| // contexts they are special (e.g., "x=y" in command position, "*" for globs). |
| const shouldQuote = `*?[#~=%` |
| |
| // These are the separator characters in unquoted text. |
| const spaces = " \t\n" |
| |
| const allQuote = mustQuote + shouldQuote + spaces |
| |
| type state int |
| |
| const ( |
| stNone state = iota |
| stBreak |
| stWord |
| stWordQ |
| stSingle |
| stDouble |
| stDoubleQ |
| stEnd |
| ) |
| |
| type class int |
| |
| const ( |
| clBreak class = iota |
| clNewline |
| clQuote |
| clSingle |
| clDouble |
| clOther |
| clEnd |
| ) |
| |
| type action int |
| |
| const ( |
| drop action = iota |
| push |
| xpush |
| emit |
| ) |
| |
| var update = map[state]map[class]struct { |
| state |
| action |
| }{ |
| stBreak: { |
| clBreak: {stBreak, drop}, |
| clNewline: {stBreak, drop}, |
| clQuote: {stWordQ, drop}, |
| clSingle: {stSingle, drop}, |
| clDouble: {stDouble, drop}, |
| clOther: {stWord, push}, |
| }, |
| stWord: { |
| clBreak: {stBreak, emit}, |
| clNewline: {stBreak, emit}, |
| clQuote: {stWordQ, drop}, |
| clSingle: {stSingle, drop}, |
| clDouble: {stDouble, drop}, |
| clOther: {stWord, push}, |
| }, |
| stWordQ: { |
| clBreak: {stWord, push}, |
| clNewline: {stWord, drop}, |
| clQuote: {stWord, push}, |
| clSingle: {stWord, push}, |
| clDouble: {stWord, push}, |
| clOther: {stWord, push}, |
| }, |
| stSingle: { |
| clBreak: {stSingle, push}, |
| clNewline: {stSingle, push}, |
| clQuote: {stSingle, push}, |
| clSingle: {stWord, drop}, |
| clDouble: {stSingle, push}, |
| clOther: {stSingle, push}, |
| }, |
| stDouble: { |
| clBreak: {stDouble, push}, |
| clNewline: {stDouble, push}, |
| clQuote: {stDoubleQ, drop}, |
| clSingle: {stDouble, push}, |
| clDouble: {stWord, drop}, |
| clOther: {stDouble, push}, |
| }, |
| stDoubleQ: { |
| clBreak: {stDouble, xpush}, |
| clNewline: {stDouble, drop}, |
| clQuote: {stDouble, push}, |
| clSingle: {stDouble, xpush}, |
| clDouble: {stDouble, push}, |
| clOther: {stDouble, xpush}, |
| }, |
| } |
| |
| var byteClass = map[byte]class{ |
| ' ': clBreak, |
| '\t': clBreak, |
| '\n': clNewline, |
| '\\': clQuote, |
| '\'': clSingle, |
| '"': clDouble, |
| } |
| |
| func classOf(b byte) class { |
| if c, ok := byteClass[b]; ok { |
| return c |
| } |
| return clOther |
| } |
| |
| // Split partitions s into fields divided on space, tab, and newline |
| // characters. Single and double quotation marks will be handled as described |
| // in |
| // http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02. |
| func Split(s string) []string { |
| buf := bufio.NewReader(strings.NewReader(s)) |
| var cur bytes.Buffer |
| var ss []string |
| |
| pop := func() { |
| if cur.Len() != 0 { |
| ss = append(ss, cur.String()) |
| cur.Reset() |
| } |
| } |
| st := stBreak |
| for st != stEnd { |
| c, err := buf.ReadByte() |
| if err == io.EOF { |
| break |
| } else if err != nil { |
| panic(err) |
| } |
| |
| next := update[st][classOf(c)] |
| switch next.action { |
| case push: |
| cur.WriteByte(c) |
| case xpush: |
| cur.Write([]byte{'\\', c}) |
| case emit: |
| pop() |
| case drop: |
| break |
| default: |
| panic("unknown action") |
| } |
| st = next.state |
| } |
| pop() |
| return ss |
| } |
| |
| func quotable(s string) (hasQ, hasOther bool) { |
| for i := 0; i < len(s); i++ { |
| hasQ = hasQ || s[i] == '\'' |
| hasOther = hasOther || strings.IndexByte(allQuote, s[i]) >= 0 |
| } |
| return |
| } |
| |
| // Quote returns a copy of s in which shell metacharacters are quoted to |
| // protect them from evaluation. |
| func Quote(s string) string { |
| hasQ, hasOther := quotable(s) |
| if !hasQ && !hasOther { |
| return s // fast path: nothing needs quotation |
| } |
| |
| var buf bytes.Buffer |
| inq := false |
| for i := 0; i < len(s); i++ { |
| ch := s[i] |
| if ch == '\'' { |
| if inq { |
| buf.WriteByte('\'') |
| inq = false |
| } |
| buf.WriteByte('\\') |
| } else if !inq && hasOther { |
| buf.WriteByte('\'') |
| inq = true |
| } |
| buf.WriteByte(ch) |
| } |
| if inq { |
| buf.WriteByte('\'') |
| } |
| return buf.String() |
| } |
| |
| // Join quotes each element of ss with Quote and concatenates the resulting |
| // strings separated by spaces. |
| func Join(ss []string) string { |
| quoted := make([]string, len(ss)) |
| for i, s := range ss { |
| quoted[i] = Quote(s) |
| } |
| return strings.Join(quoted, " ") |
| } |