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