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