github.com/tompao/docker@v1.9.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  	"unicode"
    13  )
    14  
    15  type shellWord struct {
    16  	word string
    17  	envs []string
    18  	pos  int
    19  }
    20  
    21  // ProcessWord will use the 'env' list of environment variables,
    22  // and replace any env var references in 'word'.
    23  func ProcessWord(word string, env []string) (string, error) {
    24  	sw := &shellWord{
    25  		word: word,
    26  		envs: env,
    27  		pos:  0,
    28  	}
    29  	return sw.process()
    30  }
    31  
    32  func (sw *shellWord) process() (string, error) {
    33  	return sw.processStopOn('\000')
    34  }
    35  
    36  // Process the word, starting at 'pos', and stop when we get to the
    37  // end of the word or the 'stopChar' character
    38  func (sw *shellWord) processStopOn(stopChar rune) (string, error) {
    39  	var result string
    40  	var charFuncMapping = map[rune]func() (string, error){
    41  		'\'': sw.processSingleQuote,
    42  		'"':  sw.processDoubleQuote,
    43  		'$':  sw.processDollar,
    44  	}
    45  
    46  	for sw.pos < len(sw.word) {
    47  		ch := sw.peek()
    48  		if stopChar != '\000' && ch == stopChar {
    49  			sw.next()
    50  			break
    51  		}
    52  		if fn, ok := charFuncMapping[ch]; ok {
    53  			// Call special processing func for certain chars
    54  			tmp, err := fn()
    55  			if err != nil {
    56  				return "", err
    57  			}
    58  			result += tmp
    59  		} else {
    60  			// Not special, just add it to the result
    61  			ch = sw.next()
    62  			if ch == '\\' {
    63  				// '\' escapes, except end of line
    64  				ch = sw.next()
    65  				if ch == '\000' {
    66  					continue
    67  				}
    68  			}
    69  			result += string(ch)
    70  		}
    71  	}
    72  
    73  	return result, nil
    74  }
    75  
    76  func (sw *shellWord) peek() rune {
    77  	if sw.pos == len(sw.word) {
    78  		return '\000'
    79  	}
    80  	return rune(sw.word[sw.pos])
    81  }
    82  
    83  func (sw *shellWord) next() rune {
    84  	if sw.pos == len(sw.word) {
    85  		return '\000'
    86  	}
    87  	ch := rune(sw.word[sw.pos])
    88  	sw.pos++
    89  	return ch
    90  }
    91  
    92  func (sw *shellWord) processSingleQuote() (string, error) {
    93  	// All chars between single quotes are taken as-is
    94  	// Note, you can't escape '
    95  	var result string
    96  
    97  	sw.next()
    98  
    99  	for {
   100  		ch := sw.next()
   101  		if ch == '\000' || ch == '\'' {
   102  			break
   103  		}
   104  		result += string(ch)
   105  	}
   106  	return result, nil
   107  }
   108  
   109  func (sw *shellWord) processDoubleQuote() (string, error) {
   110  	// All chars up to the next " are taken as-is, even ', except any $ chars
   111  	// But you can escape " with a \
   112  	var result string
   113  
   114  	sw.next()
   115  
   116  	for sw.pos < len(sw.word) {
   117  		ch := sw.peek()
   118  		if ch == '"' {
   119  			sw.next()
   120  			break
   121  		}
   122  		if ch == '$' {
   123  			tmp, err := sw.processDollar()
   124  			if err != nil {
   125  				return "", err
   126  			}
   127  			result += tmp
   128  		} else {
   129  			ch = sw.next()
   130  			if ch == '\\' {
   131  				chNext := sw.peek()
   132  
   133  				if chNext == '\000' {
   134  					// Ignore \ at end of word
   135  					continue
   136  				}
   137  
   138  				if chNext == '"' || chNext == '$' {
   139  					// \" and \$ can be escaped, all other \'s are left as-is
   140  					ch = sw.next()
   141  				}
   142  			}
   143  			result += string(ch)
   144  		}
   145  	}
   146  
   147  	return result, nil
   148  }
   149  
   150  func (sw *shellWord) processDollar() (string, error) {
   151  	sw.next()
   152  	ch := sw.peek()
   153  	if ch == '{' {
   154  		sw.next()
   155  		name := sw.processName()
   156  		ch = sw.peek()
   157  		if ch == '}' {
   158  			// Normal ${xx} case
   159  			sw.next()
   160  			return sw.getEnv(name), nil
   161  		}
   162  		if ch == ':' {
   163  			// Special ${xx:...} format processing
   164  			// Yes it allows for recursive $'s in the ... spot
   165  
   166  			sw.next() // skip over :
   167  			modifier := sw.next()
   168  
   169  			word, err := sw.processStopOn('}')
   170  			if err != nil {
   171  				return "", err
   172  			}
   173  
   174  			// Grab the current value of the variable in question so we
   175  			// can use to to determine what to do based on the modifier
   176  			newValue := sw.getEnv(name)
   177  
   178  			switch modifier {
   179  			case '+':
   180  				if newValue != "" {
   181  					newValue = word
   182  				}
   183  				return newValue, nil
   184  
   185  			case '-':
   186  				if newValue == "" {
   187  					newValue = word
   188  				}
   189  				return newValue, nil
   190  
   191  			default:
   192  				return "", fmt.Errorf("Unsupported modifier (%c) in substitution: %s", modifier, sw.word)
   193  			}
   194  		}
   195  		return "", fmt.Errorf("Missing ':' in substitution: %s", sw.word)
   196  	}
   197  	// $xxx case
   198  	name := sw.processName()
   199  	if name == "" {
   200  		return "$", nil
   201  	}
   202  	return sw.getEnv(name), nil
   203  }
   204  
   205  func (sw *shellWord) processName() string {
   206  	// Read in a name (alphanumeric or _)
   207  	// If it starts with a numeric then just return $#
   208  	var name string
   209  
   210  	for sw.pos < len(sw.word) {
   211  		ch := sw.peek()
   212  		if len(name) == 0 && unicode.IsDigit(ch) {
   213  			ch = sw.next()
   214  			return string(ch)
   215  		}
   216  		if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' {
   217  			break
   218  		}
   219  		ch = sw.next()
   220  		name += string(ch)
   221  	}
   222  
   223  	return name
   224  }
   225  
   226  func (sw *shellWord) getEnv(name string) string {
   227  	for _, env := range sw.envs {
   228  		i := strings.Index(env, "=")
   229  		if i < 0 {
   230  			if name == env {
   231  				// Should probably never get here, but just in case treat
   232  				// it like "var" and "var=" are the same
   233  				return ""
   234  			}
   235  			continue
   236  		}
   237  		if name != env[:i] {
   238  			continue
   239  		}
   240  		return env[i+1:]
   241  	}
   242  	return ""
   243  }