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