github.com/lazyboychen7/engine@v17.12.1-ce-rc2+incompatible/builder/dockerfile/shell_parser.go (about) 1 package dockerfile 2 3 import ( 4 "bytes" 5 "strings" 6 "text/scanner" 7 "unicode" 8 9 "github.com/pkg/errors" 10 ) 11 12 // ShellLex performs shell word splitting and variable expansion. 13 // 14 // ShellLex takes a string and an array of env variables and 15 // process all quotes (" and ') as well as $xxx and ${xxx} env variable 16 // tokens. Tries to mimic bash shell process. 17 // It doesn't support all flavors of ${xx:...} formats but new ones can 18 // be added by adding code to the "special ${} format processing" section 19 type ShellLex struct { 20 escapeToken rune 21 } 22 23 // NewShellLex creates a new ShellLex which uses escapeToken to escape quotes. 24 func NewShellLex(escapeToken rune) *ShellLex { 25 return &ShellLex{escapeToken: escapeToken} 26 } 27 28 // ProcessWord will use the 'env' list of environment variables, 29 // and replace any env var references in 'word'. 30 func (s *ShellLex) ProcessWord(word string, env []string) (string, error) { 31 word, _, err := s.process(word, env) 32 return word, err 33 } 34 35 // ProcessWords will use the 'env' list of environment variables, 36 // and replace any env var references in 'word' then it will also 37 // return a slice of strings which represents the 'word' 38 // split up based on spaces - taking into account quotes. Note that 39 // this splitting is done **after** the env var substitutions are done. 40 // Note, each one is trimmed to remove leading and trailing spaces (unless 41 // they are quoted", but ProcessWord retains spaces between words. 42 func (s *ShellLex) ProcessWords(word string, env []string) ([]string, error) { 43 _, words, err := s.process(word, env) 44 return words, err 45 } 46 47 func (s *ShellLex) process(word string, env []string) (string, []string, error) { 48 sw := &shellWord{ 49 envs: env, 50 escapeToken: s.escapeToken, 51 } 52 sw.scanner.Init(strings.NewReader(word)) 53 return sw.process(word) 54 } 55 56 type shellWord struct { 57 scanner scanner.Scanner 58 envs []string 59 escapeToken rune 60 } 61 62 func (sw *shellWord) process(source string) (string, []string, error) { 63 word, words, err := sw.processStopOn(scanner.EOF) 64 if err != nil { 65 err = errors.Wrapf(err, "failed to process %q", source) 66 } 67 return word, words, err 68 } 69 70 type wordsStruct struct { 71 word string 72 words []string 73 inWord bool 74 } 75 76 func (w *wordsStruct) addChar(ch rune) { 77 if unicode.IsSpace(ch) && w.inWord { 78 if len(w.word) != 0 { 79 w.words = append(w.words, w.word) 80 w.word = "" 81 w.inWord = false 82 } 83 } else if !unicode.IsSpace(ch) { 84 w.addRawChar(ch) 85 } 86 } 87 88 func (w *wordsStruct) addRawChar(ch rune) { 89 w.word += string(ch) 90 w.inWord = true 91 } 92 93 func (w *wordsStruct) addString(str string) { 94 var scan scanner.Scanner 95 scan.Init(strings.NewReader(str)) 96 for scan.Peek() != scanner.EOF { 97 w.addChar(scan.Next()) 98 } 99 } 100 101 func (w *wordsStruct) addRawString(str string) { 102 w.word += str 103 w.inWord = true 104 } 105 106 func (w *wordsStruct) getWords() []string { 107 if len(w.word) > 0 { 108 w.words = append(w.words, w.word) 109 110 // Just in case we're called again by mistake 111 w.word = "" 112 w.inWord = false 113 } 114 return w.words 115 } 116 117 // Process the word, starting at 'pos', and stop when we get to the 118 // end of the word or the 'stopChar' character 119 func (sw *shellWord) processStopOn(stopChar rune) (string, []string, error) { 120 var result bytes.Buffer 121 var words wordsStruct 122 123 var charFuncMapping = map[rune]func() (string, error){ 124 '\'': sw.processSingleQuote, 125 '"': sw.processDoubleQuote, 126 '$': sw.processDollar, 127 } 128 129 for sw.scanner.Peek() != scanner.EOF { 130 ch := sw.scanner.Peek() 131 132 if stopChar != scanner.EOF && ch == stopChar { 133 sw.scanner.Next() 134 break 135 } 136 if fn, ok := charFuncMapping[ch]; ok { 137 // Call special processing func for certain chars 138 tmp, err := fn() 139 if err != nil { 140 return "", []string{}, err 141 } 142 result.WriteString(tmp) 143 144 if ch == rune('$') { 145 words.addString(tmp) 146 } else { 147 words.addRawString(tmp) 148 } 149 } else { 150 // Not special, just add it to the result 151 ch = sw.scanner.Next() 152 153 if ch == sw.escapeToken { 154 // '\' (default escape token, but ` allowed) escapes, except end of line 155 ch = sw.scanner.Next() 156 157 if ch == scanner.EOF { 158 break 159 } 160 161 words.addRawChar(ch) 162 } else { 163 words.addChar(ch) 164 } 165 166 result.WriteRune(ch) 167 } 168 } 169 170 return result.String(), words.getWords(), nil 171 } 172 173 func (sw *shellWord) processSingleQuote() (string, error) { 174 // All chars between single quotes are taken as-is 175 // Note, you can't escape ' 176 // 177 // From the "sh" man page: 178 // Single Quotes 179 // Enclosing characters in single quotes preserves the literal meaning of 180 // all the characters (except single quotes, making it impossible to put 181 // single-quotes in a single-quoted string). 182 183 var result bytes.Buffer 184 185 sw.scanner.Next() 186 187 for { 188 ch := sw.scanner.Next() 189 switch ch { 190 case scanner.EOF: 191 return "", errors.New("unexpected end of statement while looking for matching single-quote") 192 case '\'': 193 return result.String(), nil 194 } 195 result.WriteRune(ch) 196 } 197 } 198 199 func (sw *shellWord) processDoubleQuote() (string, error) { 200 // All chars up to the next " are taken as-is, even ', except any $ chars 201 // But you can escape " with a \ (or ` if escape token set accordingly) 202 // 203 // From the "sh" man page: 204 // Double Quotes 205 // Enclosing characters within double quotes preserves the literal meaning 206 // of all characters except dollarsign ($), backquote (`), and backslash 207 // (\). The backslash inside double quotes is historically weird, and 208 // serves to quote only the following characters: 209 // $ ` " \ <newline>. 210 // Otherwise it remains literal. 211 212 var result bytes.Buffer 213 214 sw.scanner.Next() 215 216 for { 217 switch sw.scanner.Peek() { 218 case scanner.EOF: 219 return "", errors.New("unexpected end of statement while looking for matching double-quote") 220 case '"': 221 sw.scanner.Next() 222 return result.String(), nil 223 case '$': 224 value, err := sw.processDollar() 225 if err != nil { 226 return "", err 227 } 228 result.WriteString(value) 229 default: 230 ch := sw.scanner.Next() 231 if ch == sw.escapeToken { 232 switch sw.scanner.Peek() { 233 case scanner.EOF: 234 // Ignore \ at end of word 235 continue 236 case '"', '$', sw.escapeToken: 237 // These chars can be escaped, all other \'s are left as-is 238 // Note: for now don't do anything special with ` chars. 239 // Not sure what to do with them anyway since we're not going 240 // to execute the text in there (not now anyway). 241 ch = sw.scanner.Next() 242 } 243 } 244 result.WriteRune(ch) 245 } 246 } 247 } 248 249 func (sw *shellWord) processDollar() (string, error) { 250 sw.scanner.Next() 251 252 // $xxx case 253 if sw.scanner.Peek() != '{' { 254 name := sw.processName() 255 if name == "" { 256 return "$", nil 257 } 258 return sw.getEnv(name), nil 259 } 260 261 sw.scanner.Next() 262 name := sw.processName() 263 ch := sw.scanner.Peek() 264 if ch == '}' { 265 // Normal ${xx} case 266 sw.scanner.Next() 267 return sw.getEnv(name), nil 268 } 269 if ch == ':' { 270 // Special ${xx:...} format processing 271 // Yes it allows for recursive $'s in the ... spot 272 273 sw.scanner.Next() // skip over : 274 modifier := sw.scanner.Next() 275 276 word, _, err := sw.processStopOn('}') 277 if err != nil { 278 return "", err 279 } 280 281 // Grab the current value of the variable in question so we 282 // can use to to determine what to do based on the modifier 283 newValue := sw.getEnv(name) 284 285 switch modifier { 286 case '+': 287 if newValue != "" { 288 newValue = word 289 } 290 return newValue, nil 291 292 case '-': 293 if newValue == "" { 294 newValue = word 295 } 296 return newValue, nil 297 298 default: 299 return "", errors.Errorf("unsupported modifier (%c) in substitution", modifier) 300 } 301 } 302 return "", errors.Errorf("missing ':' in substitution") 303 } 304 305 func (sw *shellWord) processName() string { 306 // Read in a name (alphanumeric or _) 307 // If it starts with a numeric then just return $# 308 var name bytes.Buffer 309 310 for sw.scanner.Peek() != scanner.EOF { 311 ch := sw.scanner.Peek() 312 if name.Len() == 0 && unicode.IsDigit(ch) { 313 ch = sw.scanner.Next() 314 return string(ch) 315 } 316 if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' { 317 break 318 } 319 ch = sw.scanner.Next() 320 name.WriteRune(ch) 321 } 322 323 return name.String() 324 } 325 326 func (sw *shellWord) getEnv(name string) string { 327 for _, env := range sw.envs { 328 i := strings.Index(env, "=") 329 if i < 0 { 330 if equalEnvKeys(name, env) { 331 // Should probably never get here, but just in case treat 332 // it like "var" and "var=" are the same 333 return "" 334 } 335 continue 336 } 337 compareName := env[:i] 338 if !equalEnvKeys(name, compareName) { 339 continue 340 } 341 return env[i+1:] 342 } 343 return "" 344 }