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