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