github.com/tompao/docker@v1.9.1/builder/dockerfile/parser/line_parsers.go (about)

     1  package parser
     2  
     3  // line parsers are dispatch calls that parse a single unit of text into a
     4  // Node object which contains the whole statement. Dockerfiles have varied
     5  // (but not usually unique, see ONBUILD for a unique example) parsing rules
     6  // per-command, and these unify the processing in a way that makes it
     7  // manageable.
     8  
     9  import (
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"strings"
    14  	"unicode"
    15  )
    16  
    17  var (
    18  	errDockerfileNotStringArray = errors.New("When using JSON array syntax, arrays must be comprised of strings only.")
    19  )
    20  
    21  // ignore the current argument. This will still leave a command parsed, but
    22  // will not incorporate the arguments into the ast.
    23  func parseIgnore(rest string) (*Node, map[string]bool, error) {
    24  	return &Node{}, nil, nil
    25  }
    26  
    27  // used for onbuild. Could potentially be used for anything that represents a
    28  // statement with sub-statements.
    29  //
    30  // ONBUILD RUN foo bar -> (onbuild (run foo bar))
    31  //
    32  func parseSubCommand(rest string) (*Node, map[string]bool, error) {
    33  	if rest == "" {
    34  		return nil, nil, nil
    35  	}
    36  
    37  	_, child, err := parseLine(rest)
    38  	if err != nil {
    39  		return nil, nil, err
    40  	}
    41  
    42  	return &Node{Children: []*Node{child}}, nil, nil
    43  }
    44  
    45  // helper to parse words (i.e space delimited or quoted strings) in a statement.
    46  // The quotes are preserved as part of this function and they are stripped later
    47  // as part of processWords().
    48  func parseWords(rest string) []string {
    49  	const (
    50  		inSpaces = iota // looking for start of a word
    51  		inWord
    52  		inQuote
    53  	)
    54  
    55  	words := []string{}
    56  	phase := inSpaces
    57  	word := ""
    58  	quote := '\000'
    59  	blankOK := false
    60  	var ch rune
    61  
    62  	for pos := 0; pos <= len(rest); pos++ {
    63  		if pos != len(rest) {
    64  			ch = rune(rest[pos])
    65  		}
    66  
    67  		if phase == inSpaces { // Looking for start of word
    68  			if pos == len(rest) { // end of input
    69  				break
    70  			}
    71  			if unicode.IsSpace(ch) { // skip spaces
    72  				continue
    73  			}
    74  			phase = inWord // found it, fall thru
    75  		}
    76  		if (phase == inWord || phase == inQuote) && (pos == len(rest)) {
    77  			if blankOK || len(word) > 0 {
    78  				words = append(words, word)
    79  			}
    80  			break
    81  		}
    82  		if phase == inWord {
    83  			if unicode.IsSpace(ch) {
    84  				phase = inSpaces
    85  				if blankOK || len(word) > 0 {
    86  					words = append(words, word)
    87  				}
    88  				word = ""
    89  				blankOK = false
    90  				continue
    91  			}
    92  			if ch == '\'' || ch == '"' {
    93  				quote = ch
    94  				blankOK = true
    95  				phase = inQuote
    96  			}
    97  			if ch == '\\' {
    98  				if pos+1 == len(rest) {
    99  					continue // just skip \ at end
   100  				}
   101  				// If we're not quoted and we see a \, then always just
   102  				// add \ plus the char to the word, even if the char
   103  				// is a quote.
   104  				word += string(ch)
   105  				pos++
   106  				ch = rune(rest[pos])
   107  			}
   108  			word += string(ch)
   109  			continue
   110  		}
   111  		if phase == inQuote {
   112  			if ch == quote {
   113  				phase = inWord
   114  			}
   115  			// \ is special except for ' quotes - can't escape anything for '
   116  			if ch == '\\' && quote != '\'' {
   117  				if pos+1 == len(rest) {
   118  					phase = inWord
   119  					continue // just skip \ at end
   120  				}
   121  				pos++
   122  				nextCh := rune(rest[pos])
   123  				word += string(ch)
   124  				ch = nextCh
   125  			}
   126  			word += string(ch)
   127  		}
   128  	}
   129  
   130  	return words
   131  }
   132  
   133  // parse environment like statements. Note that this does *not* handle
   134  // variable interpolation, which will be handled in the evaluator.
   135  func parseNameVal(rest string, key string) (*Node, map[string]bool, error) {
   136  	// This is kind of tricky because we need to support the old
   137  	// variant:   KEY name value
   138  	// as well as the new one:    KEY name=value ...
   139  	// The trigger to know which one is being used will be whether we hit
   140  	// a space or = first.  space ==> old, "=" ==> new
   141  
   142  	words := parseWords(rest)
   143  	if len(words) == 0 {
   144  		return nil, nil, nil
   145  	}
   146  
   147  	var rootnode *Node
   148  
   149  	// Old format (KEY name value)
   150  	if !strings.Contains(words[0], "=") {
   151  		node := &Node{}
   152  		rootnode = node
   153  		strs := tokenWhitespace.Split(rest, 2)
   154  
   155  		if len(strs) < 2 {
   156  			return nil, nil, fmt.Errorf(key + " must have two arguments")
   157  		}
   158  
   159  		node.Value = strs[0]
   160  		node.Next = &Node{}
   161  		node.Next.Value = strs[1]
   162  	} else {
   163  		var prevNode *Node
   164  		for i, word := range words {
   165  			if !strings.Contains(word, "=") {
   166  				return nil, nil, fmt.Errorf("Syntax error - can't find = in %q. Must be of the form: name=value", word)
   167  			}
   168  			parts := strings.SplitN(word, "=", 2)
   169  
   170  			name := &Node{}
   171  			value := &Node{}
   172  
   173  			name.Next = value
   174  			name.Value = parts[0]
   175  			value.Value = parts[1]
   176  
   177  			if i == 0 {
   178  				rootnode = name
   179  			} else {
   180  				prevNode.Next = name
   181  			}
   182  			prevNode = value
   183  		}
   184  	}
   185  
   186  	return rootnode, nil, nil
   187  }
   188  
   189  func parseEnv(rest string) (*Node, map[string]bool, error) {
   190  	return parseNameVal(rest, "ENV")
   191  }
   192  
   193  func parseLabel(rest string) (*Node, map[string]bool, error) {
   194  	return parseNameVal(rest, "LABEL")
   195  }
   196  
   197  // parses a statement containing one or more keyword definition(s) and/or
   198  // value assignments, like `name1 name2= name3="" name4=value`.
   199  // Note that this is a stricter format than the old format of assignment,
   200  // allowed by parseNameVal(), in a way that this only allows assignment of the
   201  // form `keyword=[<value>]` like  `name2=`, `name3=""`, and `name4=value` above.
   202  // In addition, a keyword definition alone is of the form `keyword` like `name1`
   203  // above. And the assignments `name2=` and `name3=""` are equivalent and
   204  // assign an empty value to the respective keywords.
   205  func parseNameOrNameVal(rest string) (*Node, map[string]bool, error) {
   206  	words := parseWords(rest)
   207  	if len(words) == 0 {
   208  		return nil, nil, nil
   209  	}
   210  
   211  	var (
   212  		rootnode *Node
   213  		prevNode *Node
   214  	)
   215  	for i, word := range words {
   216  		node := &Node{}
   217  		node.Value = word
   218  		if i == 0 {
   219  			rootnode = node
   220  		} else {
   221  			prevNode.Next = node
   222  		}
   223  		prevNode = node
   224  	}
   225  
   226  	return rootnode, nil, nil
   227  }
   228  
   229  // parses a whitespace-delimited set of arguments. The result is effectively a
   230  // linked list of string arguments.
   231  func parseStringsWhitespaceDelimited(rest string) (*Node, map[string]bool, error) {
   232  	if rest == "" {
   233  		return nil, nil, nil
   234  	}
   235  
   236  	node := &Node{}
   237  	rootnode := node
   238  	prevnode := node
   239  	for _, str := range tokenWhitespace.Split(rest, -1) { // use regexp
   240  		prevnode = node
   241  		node.Value = str
   242  		node.Next = &Node{}
   243  		node = node.Next
   244  	}
   245  
   246  	// XXX to get around regexp.Split *always* providing an empty string at the
   247  	// end due to how our loop is constructed, nil out the last node in the
   248  	// chain.
   249  	prevnode.Next = nil
   250  
   251  	return rootnode, nil, nil
   252  }
   253  
   254  // parsestring just wraps the string in quotes and returns a working node.
   255  func parseString(rest string) (*Node, map[string]bool, error) {
   256  	if rest == "" {
   257  		return nil, nil, nil
   258  	}
   259  	n := &Node{}
   260  	n.Value = rest
   261  	return n, nil, nil
   262  }
   263  
   264  // parseJSON converts JSON arrays to an AST.
   265  func parseJSON(rest string) (*Node, map[string]bool, error) {
   266  	rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
   267  	if !strings.HasPrefix(rest, "[") {
   268  		return nil, nil, fmt.Errorf(`Error parsing "%s" as a JSON array`, rest)
   269  	}
   270  
   271  	var myJSON []interface{}
   272  	if err := json.NewDecoder(strings.NewReader(rest)).Decode(&myJSON); err != nil {
   273  		return nil, nil, err
   274  	}
   275  
   276  	var top, prev *Node
   277  	for _, str := range myJSON {
   278  		s, ok := str.(string)
   279  		if !ok {
   280  			return nil, nil, errDockerfileNotStringArray
   281  		}
   282  
   283  		node := &Node{Value: s}
   284  		if prev == nil {
   285  			top = node
   286  		} else {
   287  			prev.Next = node
   288  		}
   289  		prev = node
   290  	}
   291  
   292  	return top, map[string]bool{"json": true}, nil
   293  }
   294  
   295  // parseMaybeJSON determines if the argument appears to be a JSON array. If
   296  // so, passes to parseJSON; if not, quotes the result and returns a single
   297  // node.
   298  func parseMaybeJSON(rest string) (*Node, map[string]bool, error) {
   299  	if rest == "" {
   300  		return nil, nil, nil
   301  	}
   302  
   303  	node, attrs, err := parseJSON(rest)
   304  
   305  	if err == nil {
   306  		return node, attrs, nil
   307  	}
   308  	if err == errDockerfileNotStringArray {
   309  		return nil, nil, err
   310  	}
   311  
   312  	node = &Node{}
   313  	node.Value = rest
   314  	return node, nil, nil
   315  }
   316  
   317  // parseMaybeJSONToList determines if the argument appears to be a JSON array. If
   318  // so, passes to parseJSON; if not, attempts to parse it as a whitespace
   319  // delimited string.
   320  func parseMaybeJSONToList(rest string) (*Node, map[string]bool, error) {
   321  	node, attrs, err := parseJSON(rest)
   322  
   323  	if err == nil {
   324  		return node, attrs, nil
   325  	}
   326  	if err == errDockerfileNotStringArray {
   327  		return nil, nil, err
   328  	}
   329  
   330  	return parseStringsWhitespaceDelimited(rest)
   331  }