[822] | 1 | package shellquote
|
---|
| 2 |
|
---|
| 3 | import (
|
---|
| 4 | "bytes"
|
---|
| 5 | "errors"
|
---|
| 6 | "strings"
|
---|
| 7 | "unicode/utf8"
|
---|
| 8 | )
|
---|
| 9 |
|
---|
| 10 | var (
|
---|
| 11 | UnterminatedSingleQuoteError = errors.New("Unterminated single-quoted string")
|
---|
| 12 | UnterminatedDoubleQuoteError = errors.New("Unterminated double-quoted string")
|
---|
| 13 | UnterminatedEscapeError = errors.New("Unterminated backslash-escape")
|
---|
| 14 | )
|
---|
| 15 |
|
---|
| 16 | var (
|
---|
| 17 | splitChars = " \n\t"
|
---|
| 18 | singleChar = '\''
|
---|
| 19 | doubleChar = '"'
|
---|
| 20 | escapeChar = '\\'
|
---|
| 21 | doubleEscapeChars = "$`\"\n\\"
|
---|
| 22 | )
|
---|
| 23 |
|
---|
| 24 | // Split splits a string according to /bin/sh's word-splitting rules. It
|
---|
| 25 | // supports backslash-escapes, single-quotes, and double-quotes. Notably it does
|
---|
| 26 | // not support the $'' style of quoting. It also doesn't attempt to perform any
|
---|
| 27 | // other sort of expansion, including brace expansion, shell expansion, or
|
---|
| 28 | // pathname expansion.
|
---|
| 29 | //
|
---|
| 30 | // If the given input has an unterminated quoted string or ends in a
|
---|
| 31 | // backslash-escape, one of UnterminatedSingleQuoteError,
|
---|
| 32 | // UnterminatedDoubleQuoteError, or UnterminatedEscapeError is returned.
|
---|
| 33 | func Split(input string) (words []string, err error) {
|
---|
| 34 | var buf bytes.Buffer
|
---|
| 35 | words = make([]string, 0)
|
---|
| 36 |
|
---|
| 37 | for len(input) > 0 {
|
---|
| 38 | // skip any splitChars at the start
|
---|
| 39 | c, l := utf8.DecodeRuneInString(input)
|
---|
| 40 | if strings.ContainsRune(splitChars, c) {
|
---|
| 41 | input = input[l:]
|
---|
| 42 | continue
|
---|
| 43 | } else if c == escapeChar {
|
---|
| 44 | // Look ahead for escaped newline so we can skip over it
|
---|
| 45 | next := input[l:]
|
---|
| 46 | if len(next) == 0 {
|
---|
| 47 | err = UnterminatedEscapeError
|
---|
| 48 | return
|
---|
| 49 | }
|
---|
| 50 | c2, l2 := utf8.DecodeRuneInString(next)
|
---|
| 51 | if c2 == '\n' {
|
---|
| 52 | input = next[l2:]
|
---|
| 53 | continue
|
---|
| 54 | }
|
---|
| 55 | }
|
---|
| 56 |
|
---|
| 57 | var word string
|
---|
| 58 | word, input, err = splitWord(input, &buf)
|
---|
| 59 | if err != nil {
|
---|
| 60 | return
|
---|
| 61 | }
|
---|
| 62 | words = append(words, word)
|
---|
| 63 | }
|
---|
| 64 | return
|
---|
| 65 | }
|
---|
| 66 |
|
---|
| 67 | func splitWord(input string, buf *bytes.Buffer) (word string, remainder string, err error) {
|
---|
| 68 | buf.Reset()
|
---|
| 69 |
|
---|
| 70 | raw:
|
---|
| 71 | {
|
---|
| 72 | cur := input
|
---|
| 73 | for len(cur) > 0 {
|
---|
| 74 | c, l := utf8.DecodeRuneInString(cur)
|
---|
| 75 | cur = cur[l:]
|
---|
| 76 | if c == singleChar {
|
---|
| 77 | buf.WriteString(input[0 : len(input)-len(cur)-l])
|
---|
| 78 | input = cur
|
---|
| 79 | goto single
|
---|
| 80 | } else if c == doubleChar {
|
---|
| 81 | buf.WriteString(input[0 : len(input)-len(cur)-l])
|
---|
| 82 | input = cur
|
---|
| 83 | goto double
|
---|
| 84 | } else if c == escapeChar {
|
---|
| 85 | buf.WriteString(input[0 : len(input)-len(cur)-l])
|
---|
| 86 | input = cur
|
---|
| 87 | goto escape
|
---|
| 88 | } else if strings.ContainsRune(splitChars, c) {
|
---|
| 89 | buf.WriteString(input[0 : len(input)-len(cur)-l])
|
---|
| 90 | return buf.String(), cur, nil
|
---|
| 91 | }
|
---|
| 92 | }
|
---|
| 93 | if len(input) > 0 {
|
---|
| 94 | buf.WriteString(input)
|
---|
| 95 | input = ""
|
---|
| 96 | }
|
---|
| 97 | goto done
|
---|
| 98 | }
|
---|
| 99 |
|
---|
| 100 | escape:
|
---|
| 101 | {
|
---|
| 102 | if len(input) == 0 {
|
---|
| 103 | return "", "", UnterminatedEscapeError
|
---|
| 104 | }
|
---|
| 105 | c, l := utf8.DecodeRuneInString(input)
|
---|
| 106 | if c == '\n' {
|
---|
| 107 | // a backslash-escaped newline is elided from the output entirely
|
---|
| 108 | } else {
|
---|
| 109 | buf.WriteString(input[:l])
|
---|
| 110 | }
|
---|
| 111 | input = input[l:]
|
---|
| 112 | }
|
---|
| 113 | goto raw
|
---|
| 114 |
|
---|
| 115 | single:
|
---|
| 116 | {
|
---|
| 117 | i := strings.IndexRune(input, singleChar)
|
---|
| 118 | if i == -1 {
|
---|
| 119 | return "", "", UnterminatedSingleQuoteError
|
---|
| 120 | }
|
---|
| 121 | buf.WriteString(input[0:i])
|
---|
| 122 | input = input[i+1:]
|
---|
| 123 | goto raw
|
---|
| 124 | }
|
---|
| 125 |
|
---|
| 126 | double:
|
---|
| 127 | {
|
---|
| 128 | cur := input
|
---|
| 129 | for len(cur) > 0 {
|
---|
| 130 | c, l := utf8.DecodeRuneInString(cur)
|
---|
| 131 | cur = cur[l:]
|
---|
| 132 | if c == doubleChar {
|
---|
| 133 | buf.WriteString(input[0 : len(input)-len(cur)-l])
|
---|
| 134 | input = cur
|
---|
| 135 | goto raw
|
---|
| 136 | } else if c == escapeChar {
|
---|
| 137 | // bash only supports certain escapes in double-quoted strings
|
---|
| 138 | c2, l2 := utf8.DecodeRuneInString(cur)
|
---|
| 139 | cur = cur[l2:]
|
---|
| 140 | if strings.ContainsRune(doubleEscapeChars, c2) {
|
---|
| 141 | buf.WriteString(input[0 : len(input)-len(cur)-l-l2])
|
---|
| 142 | if c2 == '\n' {
|
---|
| 143 | // newline is special, skip the backslash entirely
|
---|
| 144 | } else {
|
---|
| 145 | buf.WriteRune(c2)
|
---|
| 146 | }
|
---|
| 147 | input = cur
|
---|
| 148 | }
|
---|
| 149 | }
|
---|
| 150 | }
|
---|
| 151 | return "", "", UnterminatedDoubleQuoteError
|
---|
| 152 | }
|
---|
| 153 |
|
---|
| 154 | done:
|
---|
| 155 | return buf.String(), input, nil
|
---|
| 156 | }
|
---|