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