github.com/sams1990/dockerrepo@v17.12.1-ce-rc2+incompatible/builder/dockerfile/shell_parser.go (about)

     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  }