github.com/ralexstokes/docker@v1.6.2/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  		return "", fmt.Errorf("Unsupported ${} substitution: %s", sw.word)
   161  	} else {
   162  		// $xxx case
   163  		name := sw.processName()
   164  		if name == "" {
   165  			return "$", nil
   166  		}
   167  		return sw.getEnv(name), nil
   168  	}
   169  }
   170  
   171  func (sw *shellWord) processName() string {
   172  	// Read in a name (alphanumeric or _)
   173  	// If it starts with a numeric then just return $#
   174  	var name string
   175  
   176  	for sw.pos < len(sw.word) {
   177  		ch := sw.peek()
   178  		if len(name) == 0 && unicode.IsDigit(ch) {
   179  			ch = sw.next()
   180  			return string(ch)
   181  		}
   182  		if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' {
   183  			break
   184  		}
   185  		ch = sw.next()
   186  		name += string(ch)
   187  	}
   188  
   189  	return name
   190  }
   191  
   192  func (sw *shellWord) getEnv(name string) string {
   193  	for _, env := range sw.envs {
   194  		i := strings.Index(env, "=")
   195  		if i < 0 {
   196  			if name == env {
   197  				// Should probably never get here, but just in case treat
   198  				// it like "var" and "var=" are the same
   199  				return ""
   200  			}
   201  			continue
   202  		}
   203  		if name != env[:i] {
   204  			continue
   205  		}
   206  		return env[i+1:]
   207  	}
   208  	return ""
   209  }