123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- package dockerfile
- // This will take a single word and an array of env variables and
- // process all quotes (" and ') as well as $xxx and ${xxx} env variable
- // tokens. Tries to mimic bash shell process.
- // It doesn't support all flavors of ${xx:...} formats but new ones can
- // be added by adding code to the "special ${} format processing" section
- import (
- "fmt"
- "strings"
- "text/scanner"
- "unicode"
- )
- type shellWord struct {
- word string
- scanner scanner.Scanner
- envs []string
- pos int
- }
- // ProcessWord will use the 'env' list of environment variables,
- // and replace any env var references in 'word'.
- func ProcessWord(word string, env []string) (string, error) {
- sw := &shellWord{
- word: word,
- envs: env,
- pos: 0,
- }
- sw.scanner.Init(strings.NewReader(word))
- word, _, err := sw.process()
- return word, err
- }
- // ProcessWords will use the 'env' list of environment variables,
- // and replace any env var references in 'word' then it will also
- // return a slice of strings which represents the 'word'
- // split up based on spaces - taking into account quotes. Note that
- // this splitting is done **after** the env var substitutions are done.
- // Note, each one is trimmed to remove leading and trailing spaces (unless
- // they are quoted", but ProcessWord retains spaces between words.
- func ProcessWords(word string, env []string) ([]string, error) {
- sw := &shellWord{
- word: word,
- envs: env,
- pos: 0,
- }
- sw.scanner.Init(strings.NewReader(word))
- _, words, err := sw.process()
- return words, err
- }
- func (sw *shellWord) process() (string, []string, error) {
- return sw.processStopOn(scanner.EOF)
- }
- type wordsStruct struct {
- word string
- words []string
- inWord bool
- }
- func (w *wordsStruct) addChar(ch rune) {
- if unicode.IsSpace(ch) && w.inWord {
- if len(w.word) != 0 {
- w.words = append(w.words, w.word)
- w.word = ""
- w.inWord = false
- }
- } else if !unicode.IsSpace(ch) {
- w.addRawChar(ch)
- }
- }
- func (w *wordsStruct) addRawChar(ch rune) {
- w.word += string(ch)
- w.inWord = true
- }
- func (w *wordsStruct) addString(str string) {
- var scan scanner.Scanner
- scan.Init(strings.NewReader(str))
- for scan.Peek() != scanner.EOF {
- w.addChar(scan.Next())
- }
- }
- func (w *wordsStruct) addRawString(str string) {
- w.word += str
- w.inWord = true
- }
- func (w *wordsStruct) getWords() []string {
- if len(w.word) > 0 {
- w.words = append(w.words, w.word)
- // Just in case we're called again by mistake
- w.word = ""
- w.inWord = false
- }
- return w.words
- }
- // Process the word, starting at 'pos', and stop when we get to the
- // end of the word or the 'stopChar' character
- func (sw *shellWord) processStopOn(stopChar rune) (string, []string, error) {
- var result string
- var words wordsStruct
- var charFuncMapping = map[rune]func() (string, error){
- '\'': sw.processSingleQuote,
- '"': sw.processDoubleQuote,
- '$': sw.processDollar,
- }
- for sw.scanner.Peek() != scanner.EOF {
- ch := sw.scanner.Peek()
- if stopChar != scanner.EOF && ch == stopChar {
- sw.scanner.Next()
- break
- }
- if fn, ok := charFuncMapping[ch]; ok {
- // Call special processing func for certain chars
- tmp, err := fn()
- if err != nil {
- return "", []string{}, err
- }
- result += tmp
- if ch == rune('$') {
- words.addString(tmp)
- } else {
- words.addRawString(tmp)
- }
- } else {
- // Not special, just add it to the result
- ch = sw.scanner.Next()
- if ch == '\\' {
- // '\' escapes, except end of line
- ch = sw.scanner.Next()
- if ch == scanner.EOF {
- break
- }
- words.addRawChar(ch)
- } else {
- words.addChar(ch)
- }
- result += string(ch)
- }
- }
- return result, words.getWords(), nil
- }
- func (sw *shellWord) processSingleQuote() (string, error) {
- // All chars between single quotes are taken as-is
- // Note, you can't escape '
- var result string
- sw.scanner.Next()
- for {
- ch := sw.scanner.Next()
- if ch == '\'' || ch == scanner.EOF {
- break
- }
- result += string(ch)
- }
- return result, nil
- }
- func (sw *shellWord) processDoubleQuote() (string, error) {
- // All chars up to the next " are taken as-is, even ', except any $ chars
- // But you can escape " with a \
- var result string
- sw.scanner.Next()
- for sw.scanner.Peek() != scanner.EOF {
- ch := sw.scanner.Peek()
- if ch == '"' {
- sw.scanner.Next()
- break
- }
- if ch == '$' {
- tmp, err := sw.processDollar()
- if err != nil {
- return "", err
- }
- result += tmp
- } else {
- ch = sw.scanner.Next()
- if ch == '\\' {
- chNext := sw.scanner.Peek()
- if chNext == scanner.EOF {
- // Ignore \ at end of word
- continue
- }
- if chNext == '"' || chNext == '$' {
- // \" and \$ can be escaped, all other \'s are left as-is
- ch = sw.scanner.Next()
- }
- }
- result += string(ch)
- }
- }
- return result, nil
- }
- func (sw *shellWord) processDollar() (string, error) {
- sw.scanner.Next()
- ch := sw.scanner.Peek()
- if ch == '{' {
- sw.scanner.Next()
- name := sw.processName()
- ch = sw.scanner.Peek()
- if ch == '}' {
- // Normal ${xx} case
- sw.scanner.Next()
- return sw.getEnv(name), nil
- }
- if ch == ':' {
- // Special ${xx:...} format processing
- // Yes it allows for recursive $'s in the ... spot
- sw.scanner.Next() // skip over :
- modifier := sw.scanner.Next()
- word, _, err := sw.processStopOn('}')
- if err != nil {
- return "", err
- }
- // Grab the current value of the variable in question so we
- // can use to to determine what to do based on the modifier
- newValue := sw.getEnv(name)
- switch modifier {
- case '+':
- if newValue != "" {
- newValue = word
- }
- return newValue, nil
- case '-':
- if newValue == "" {
- newValue = word
- }
- return newValue, nil
- default:
- return "", fmt.Errorf("Unsupported modifier (%c) in substitution: %s", modifier, sw.word)
- }
- }
- return "", fmt.Errorf("Missing ':' in substitution: %s", sw.word)
- }
- // $xxx case
- name := sw.processName()
- if name == "" {
- return "$", nil
- }
- return sw.getEnv(name), nil
- }
- func (sw *shellWord) processName() string {
- // Read in a name (alphanumeric or _)
- // If it starts with a numeric then just return $#
- var name string
- for sw.scanner.Peek() != scanner.EOF {
- ch := sw.scanner.Peek()
- if len(name) == 0 && unicode.IsDigit(ch) {
- ch = sw.scanner.Next()
- return string(ch)
- }
- if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' {
- break
- }
- ch = sw.scanner.Next()
- name += string(ch)
- }
- return name
- }
- func (sw *shellWord) getEnv(name string) string {
- for _, env := range sw.envs {
- i := strings.Index(env, "=")
- if i < 0 {
- if name == env {
- // Should probably never get here, but just in case treat
- // it like "var" and "var=" are the same
- return ""
- }
- continue
- }
- if name != env[:i] {
- continue
- }
- return env[i+1:]
- }
- return ""
- }
|