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