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