github.com/ralexstokes/docker@v1.6.2/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 return "", fmt.Errorf("Unsupported ${} substitution: %s", sw.word) 161 } else { 162 // $xxx case 163 name := sw.processName() 164 if name == "" { 165 return "$", nil 166 } 167 return sw.getEnv(name), nil 168 } 169 } 170 171 func (sw *shellWord) processName() string { 172 // Read in a name (alphanumeric or _) 173 // If it starts with a numeric then just return $# 174 var name string 175 176 for sw.pos < len(sw.word) { 177 ch := sw.peek() 178 if len(name) == 0 && unicode.IsDigit(ch) { 179 ch = sw.next() 180 return string(ch) 181 } 182 if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' { 183 break 184 } 185 ch = sw.next() 186 name += string(ch) 187 } 188 189 return name 190 } 191 192 func (sw *shellWord) getEnv(name string) string { 193 for _, env := range sw.envs { 194 i := strings.Index(env, "=") 195 if i < 0 { 196 if name == env { 197 // Should probably never get here, but just in case treat 198 // it like "var" and "var=" are the same 199 return "" 200 } 201 continue 202 } 203 if name != env[:i] { 204 continue 205 } 206 return env[i+1:] 207 } 208 return "" 209 }