github.com/webwurst/docker@v1.7.0/builder/shell_parser.go (about)

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