unquote.go (3700B)
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 }