github.com/tompao/docker@v1.9.1/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 "unicode" 13 ) 14 15 type shellWord struct { 16 word string 17 envs []string 18 pos int 19 } 20 21 // ProcessWord will use the 'env' list of environment variables, 22 // and replace any env var references in 'word'. 23 func ProcessWord(word string, env []string) (string, error) { 24 sw := &shellWord{ 25 word: word, 26 envs: env, 27 pos: 0, 28 } 29 return sw.process() 30 } 31 32 func (sw *shellWord) process() (string, error) { 33 return sw.processStopOn('\000') 34 } 35 36 // Process the word, starting at 'pos', and stop when we get to the 37 // end of the word or the 'stopChar' character 38 func (sw *shellWord) processStopOn(stopChar rune) (string, error) { 39 var result string 40 var charFuncMapping = map[rune]func() (string, error){ 41 '\'': sw.processSingleQuote, 42 '"': sw.processDoubleQuote, 43 '$': sw.processDollar, 44 } 45 46 for sw.pos < len(sw.word) { 47 ch := sw.peek() 48 if stopChar != '\000' && ch == stopChar { 49 sw.next() 50 break 51 } 52 if fn, ok := charFuncMapping[ch]; ok { 53 // Call special processing func for certain chars 54 tmp, err := fn() 55 if err != nil { 56 return "", err 57 } 58 result += tmp 59 } else { 60 // Not special, just add it to the result 61 ch = sw.next() 62 if ch == '\\' { 63 // '\' escapes, except end of line 64 ch = sw.next() 65 if ch == '\000' { 66 continue 67 } 68 } 69 result += string(ch) 70 } 71 } 72 73 return result, nil 74 } 75 76 func (sw *shellWord) peek() rune { 77 if sw.pos == len(sw.word) { 78 return '\000' 79 } 80 return rune(sw.word[sw.pos]) 81 } 82 83 func (sw *shellWord) next() rune { 84 if sw.pos == len(sw.word) { 85 return '\000' 86 } 87 ch := rune(sw.word[sw.pos]) 88 sw.pos++ 89 return ch 90 } 91 92 func (sw *shellWord) processSingleQuote() (string, error) { 93 // All chars between single quotes are taken as-is 94 // Note, you can't escape ' 95 var result string 96 97 sw.next() 98 99 for { 100 ch := sw.next() 101 if ch == '\000' || ch == '\'' { 102 break 103 } 104 result += string(ch) 105 } 106 return result, nil 107 } 108 109 func (sw *shellWord) processDoubleQuote() (string, error) { 110 // All chars up to the next " are taken as-is, even ', except any $ chars 111 // But you can escape " with a \ 112 var result string 113 114 sw.next() 115 116 for sw.pos < len(sw.word) { 117 ch := sw.peek() 118 if ch == '"' { 119 sw.next() 120 break 121 } 122 if ch == '$' { 123 tmp, err := sw.processDollar() 124 if err != nil { 125 return "", err 126 } 127 result += tmp 128 } else { 129 ch = sw.next() 130 if ch == '\\' { 131 chNext := sw.peek() 132 133 if chNext == '\000' { 134 // Ignore \ at end of word 135 continue 136 } 137 138 if chNext == '"' || chNext == '$' { 139 // \" and \$ can be escaped, all other \'s are left as-is 140 ch = sw.next() 141 } 142 } 143 result += string(ch) 144 } 145 } 146 147 return result, nil 148 } 149 150 func (sw *shellWord) processDollar() (string, error) { 151 sw.next() 152 ch := sw.peek() 153 if ch == '{' { 154 sw.next() 155 name := sw.processName() 156 ch = sw.peek() 157 if ch == '}' { 158 // Normal ${xx} case 159 sw.next() 160 return sw.getEnv(name), nil 161 } 162 if ch == ':' { 163 // Special ${xx:...} format processing 164 // Yes it allows for recursive $'s in the ... spot 165 166 sw.next() // skip over : 167 modifier := sw.next() 168 169 word, err := sw.processStopOn('}') 170 if err != nil { 171 return "", err 172 } 173 174 // Grab the current value of the variable in question so we 175 // can use to to determine what to do based on the modifier 176 newValue := sw.getEnv(name) 177 178 switch modifier { 179 case '+': 180 if newValue != "" { 181 newValue = word 182 } 183 return newValue, nil 184 185 case '-': 186 if newValue == "" { 187 newValue = word 188 } 189 return newValue, nil 190 191 default: 192 return "", fmt.Errorf("Unsupported modifier (%c) in substitution: %s", modifier, sw.word) 193 } 194 } 195 return "", fmt.Errorf("Missing ':' in substitution: %s", sw.word) 196 } 197 // $xxx case 198 name := sw.processName() 199 if name == "" { 200 return "$", nil 201 } 202 return sw.getEnv(name), nil 203 } 204 205 func (sw *shellWord) processName() string { 206 // Read in a name (alphanumeric or _) 207 // If it starts with a numeric then just return $# 208 var name string 209 210 for sw.pos < len(sw.word) { 211 ch := sw.peek() 212 if len(name) == 0 && unicode.IsDigit(ch) { 213 ch = sw.next() 214 return string(ch) 215 } 216 if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' { 217 break 218 } 219 ch = sw.next() 220 name += string(ch) 221 } 222 223 return name 224 } 225 226 func (sw *shellWord) getEnv(name string) string { 227 for _, env := range sw.envs { 228 i := strings.Index(env, "=") 229 if i < 0 { 230 if name == env { 231 // Should probably never get here, but just in case treat 232 // it like "var" and "var=" are the same 233 return "" 234 } 235 continue 236 } 237 if name != env[:i] { 238 continue 239 } 240 return env[i+1:] 241 } 242 return "" 243 }