Tizen_4.0 base
[platform/upstream/docker-engine.git] / builder / dockerfile / shell_parser.go
1 package dockerfile
2
3 import (
4         "bytes"
5         "strings"
6         "text/scanner"
7         "unicode"
8
9         "github.com/pkg/errors"
10 )
11
12 // ShellLex performs shell word splitting and variable expansion.
13 //
14 // ShellLex takes a string and an array of env variables and
15 // process all quotes (" and ') as well as $xxx and ${xxx} env variable
16 // tokens.  Tries to mimic bash shell process.
17 // It doesn't support all flavors of ${xx:...} formats but new ones can
18 // be added by adding code to the "special ${} format processing" section
19 type ShellLex struct {
20         escapeToken rune
21 }
22
23 // NewShellLex creates a new ShellLex which uses escapeToken to escape quotes.
24 func NewShellLex(escapeToken rune) *ShellLex {
25         return &ShellLex{escapeToken: escapeToken}
26 }
27
28 // ProcessWord will use the 'env' list of environment variables,
29 // and replace any env var references in 'word'.
30 func (s *ShellLex) ProcessWord(word string, env []string) (string, error) {
31         word, _, err := s.process(word, env)
32         return word, err
33 }
34
35 // ProcessWords will use the 'env' list of environment variables,
36 // and replace any env var references in 'word' then it will also
37 // return a slice of strings which represents the 'word'
38 // split up based on spaces - taking into account quotes.  Note that
39 // this splitting is done **after** the env var substitutions are done.
40 // Note, each one is trimmed to remove leading and trailing spaces (unless
41 // they are quoted", but ProcessWord retains spaces between words.
42 func (s *ShellLex) ProcessWords(word string, env []string) ([]string, error) {
43         _, words, err := s.process(word, env)
44         return words, err
45 }
46
47 func (s *ShellLex) process(word string, env []string) (string, []string, error) {
48         sw := &shellWord{
49                 envs:        env,
50                 escapeToken: s.escapeToken,
51         }
52         sw.scanner.Init(strings.NewReader(word))
53         return sw.process(word)
54 }
55
56 type shellWord struct {
57         scanner     scanner.Scanner
58         envs        []string
59         escapeToken rune
60 }
61
62 func (sw *shellWord) process(source string) (string, []string, error) {
63         word, words, err := sw.processStopOn(scanner.EOF)
64         if err != nil {
65                 err = errors.Wrapf(err, "failed to process %q", source)
66         }
67         return word, words, err
68 }
69
70 type wordsStruct struct {
71         word   string
72         words  []string
73         inWord bool
74 }
75
76 func (w *wordsStruct) addChar(ch rune) {
77         if unicode.IsSpace(ch) && w.inWord {
78                 if len(w.word) != 0 {
79                         w.words = append(w.words, w.word)
80                         w.word = ""
81                         w.inWord = false
82                 }
83         } else if !unicode.IsSpace(ch) {
84                 w.addRawChar(ch)
85         }
86 }
87
88 func (w *wordsStruct) addRawChar(ch rune) {
89         w.word += string(ch)
90         w.inWord = true
91 }
92
93 func (w *wordsStruct) addString(str string) {
94         var scan scanner.Scanner
95         scan.Init(strings.NewReader(str))
96         for scan.Peek() != scanner.EOF {
97                 w.addChar(scan.Next())
98         }
99 }
100
101 func (w *wordsStruct) addRawString(str string) {
102         w.word += str
103         w.inWord = true
104 }
105
106 func (w *wordsStruct) getWords() []string {
107         if len(w.word) > 0 {
108                 w.words = append(w.words, w.word)
109
110                 // Just in case we're called again by mistake
111                 w.word = ""
112                 w.inWord = false
113         }
114         return w.words
115 }
116
117 // Process the word, starting at 'pos', and stop when we get to the
118 // end of the word or the 'stopChar' character
119 func (sw *shellWord) processStopOn(stopChar rune) (string, []string, error) {
120         var result bytes.Buffer
121         var words wordsStruct
122
123         var charFuncMapping = map[rune]func() (string, error){
124                 '\'': sw.processSingleQuote,
125                 '"':  sw.processDoubleQuote,
126                 '$':  sw.processDollar,
127         }
128
129         for sw.scanner.Peek() != scanner.EOF {
130                 ch := sw.scanner.Peek()
131
132                 if stopChar != scanner.EOF && ch == stopChar {
133                         sw.scanner.Next()
134                         break
135                 }
136                 if fn, ok := charFuncMapping[ch]; ok {
137                         // Call special processing func for certain chars
138                         tmp, err := fn()
139                         if err != nil {
140                                 return "", []string{}, err
141                         }
142                         result.WriteString(tmp)
143
144                         if ch == rune('$') {
145                                 words.addString(tmp)
146                         } else {
147                                 words.addRawString(tmp)
148                         }
149                 } else {
150                         // Not special, just add it to the result
151                         ch = sw.scanner.Next()
152
153                         if ch == sw.escapeToken {
154                                 // '\' (default escape token, but ` allowed) escapes, except end of line
155                                 ch = sw.scanner.Next()
156
157                                 if ch == scanner.EOF {
158                                         break
159                                 }
160
161                                 words.addRawChar(ch)
162                         } else {
163                                 words.addChar(ch)
164                         }
165
166                         result.WriteRune(ch)
167                 }
168         }
169
170         return result.String(), words.getWords(), nil
171 }
172
173 func (sw *shellWord) processSingleQuote() (string, error) {
174         // All chars between single quotes are taken as-is
175         // Note, you can't escape '
176         //
177         // From the "sh" man page:
178         // Single Quotes
179         //   Enclosing characters in single quotes preserves the literal meaning of
180         //   all the characters (except single quotes, making it impossible to put
181         //   single-quotes in a single-quoted string).
182
183         var result bytes.Buffer
184
185         sw.scanner.Next()
186
187         for {
188                 ch := sw.scanner.Next()
189                 switch ch {
190                 case scanner.EOF:
191                         return "", errors.New("unexpected end of statement while looking for matching single-quote")
192                 case '\'':
193                         return result.String(), nil
194                 }
195                 result.WriteRune(ch)
196         }
197 }
198
199 func (sw *shellWord) processDoubleQuote() (string, error) {
200         // All chars up to the next " are taken as-is, even ', except any $ chars
201         // But you can escape " with a \ (or ` if escape token set accordingly)
202         //
203         // From the "sh" man page:
204         // Double Quotes
205         //  Enclosing characters within double quotes preserves the literal meaning
206         //  of all characters except dollarsign ($), backquote (`), and backslash
207         //  (\).  The backslash inside double quotes is historically weird, and
208         //  serves to quote only the following characters:
209         //    $ ` " \ <newline>.
210         //  Otherwise it remains literal.
211
212         var result bytes.Buffer
213
214         sw.scanner.Next()
215
216         for {
217                 switch sw.scanner.Peek() {
218                 case scanner.EOF:
219                         return "", errors.New("unexpected end of statement while looking for matching double-quote")
220                 case '"':
221                         sw.scanner.Next()
222                         return result.String(), nil
223                 case '$':
224                         value, err := sw.processDollar()
225                         if err != nil {
226                                 return "", err
227                         }
228                         result.WriteString(value)
229                 default:
230                         ch := sw.scanner.Next()
231                         if ch == sw.escapeToken {
232                                 switch sw.scanner.Peek() {
233                                 case scanner.EOF:
234                                         // Ignore \ at end of word
235                                         continue
236                                 case '"', '$', sw.escapeToken:
237                                         // These chars can be escaped, all other \'s are left as-is
238                                         // Note: for now don't do anything special with ` chars.
239                                         // Not sure what to do with them anyway since we're not going
240                                         // to execute the text in there (not now anyway).
241                                         ch = sw.scanner.Next()
242                                 }
243                         }
244                         result.WriteRune(ch)
245                 }
246         }
247 }
248
249 func (sw *shellWord) processDollar() (string, error) {
250         sw.scanner.Next()
251
252         // $xxx case
253         if sw.scanner.Peek() != '{' {
254                 name := sw.processName()
255                 if name == "" {
256                         return "$", nil
257                 }
258                 return sw.getEnv(name), nil
259         }
260
261         sw.scanner.Next()
262         name := sw.processName()
263         ch := sw.scanner.Peek()
264         if ch == '}' {
265                 // Normal ${xx} case
266                 sw.scanner.Next()
267                 return sw.getEnv(name), nil
268         }
269         if ch == ':' {
270                 // Special ${xx:...} format processing
271                 // Yes it allows for recursive $'s in the ... spot
272
273                 sw.scanner.Next() // skip over :
274                 modifier := sw.scanner.Next()
275
276                 word, _, err := sw.processStopOn('}')
277                 if err != nil {
278                         return "", err
279                 }
280
281                 // Grab the current value of the variable in question so we
282                 // can use to to determine what to do based on the modifier
283                 newValue := sw.getEnv(name)
284
285                 switch modifier {
286                 case '+':
287                         if newValue != "" {
288                                 newValue = word
289                         }
290                         return newValue, nil
291
292                 case '-':
293                         if newValue == "" {
294                                 newValue = word
295                         }
296                         return newValue, nil
297
298                 default:
299                         return "", errors.Errorf("unsupported modifier (%c) in substitution", modifier)
300                 }
301         }
302         return "", errors.Errorf("missing ':' in substitution")
303 }
304
305 func (sw *shellWord) processName() string {
306         // Read in a name (alphanumeric or _)
307         // If it starts with a numeric then just return $#
308         var name bytes.Buffer
309
310         for sw.scanner.Peek() != scanner.EOF {
311                 ch := sw.scanner.Peek()
312                 if name.Len() == 0 && unicode.IsDigit(ch) {
313                         ch = sw.scanner.Next()
314                         return string(ch)
315                 }
316                 if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' {
317                         break
318                 }
319                 ch = sw.scanner.Next()
320                 name.WriteRune(ch)
321         }
322
323         return name.String()
324 }
325
326 func (sw *shellWord) getEnv(name string) string {
327         for _, env := range sw.envs {
328                 i := strings.Index(env, "=")
329                 if i < 0 {
330                         if equalEnvKeys(name, env) {
331                                 // Should probably never get here, but just in case treat
332                                 // it like "var" and "var=" are the same
333                                 return ""
334                         }
335                         continue
336                 }
337                 compareName := env[:i]
338                 if !equalEnvKeys(name, compareName) {
339                         continue
340                 }
341                 return env[i+1:]
342         }
343         return ""
344 }