github.com/ralexstokes/docker@v1.6.2/builder/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  // parse environment like statements. Note that this does *not* handle
    46  // variable interpolation, which will be handled in the evaluator.
    47  func parseNameVal(rest string, key string) (*Node, map[string]bool, error) {
    48  	// This is kind of tricky because we need to support the old
    49  	// variant:   KEY name value
    50  	// as well as the new one:    KEY name=value ...
    51  	// The trigger to know which one is being used will be whether we hit
    52  	// a space or = first.  space ==> old, "=" ==> new
    53  
    54  	const (
    55  		inSpaces = iota // looking for start of a word
    56  		inWord
    57  		inQuote
    58  	)
    59  
    60  	words := []string{}
    61  	phase := inSpaces
    62  	word := ""
    63  	quote := '\000'
    64  	blankOK := false
    65  	var ch rune
    66  
    67  	for pos := 0; pos <= len(rest); pos++ {
    68  		if pos != len(rest) {
    69  			ch = rune(rest[pos])
    70  		}
    71  
    72  		if phase == inSpaces { // Looking for start of word
    73  			if pos == len(rest) { // end of input
    74  				break
    75  			}
    76  			if unicode.IsSpace(ch) { // skip spaces
    77  				continue
    78  			}
    79  			phase = inWord // found it, fall thru
    80  		}
    81  		if (phase == inWord || phase == inQuote) && (pos == len(rest)) {
    82  			if blankOK || len(word) > 0 {
    83  				words = append(words, word)
    84  			}
    85  			break
    86  		}
    87  		if phase == inWord {
    88  			if unicode.IsSpace(ch) {
    89  				phase = inSpaces
    90  				if blankOK || len(word) > 0 {
    91  					words = append(words, word)
    92  
    93  					// Look for = and if not there assume
    94  					// we're doing the old stuff and
    95  					// just read the rest of the line
    96  					if !strings.Contains(word, "=") {
    97  						word = strings.TrimSpace(rest[pos:])
    98  						words = append(words, word)
    99  						break
   100  					}
   101  				}
   102  				word = ""
   103  				blankOK = false
   104  				continue
   105  			}
   106  			if ch == '\'' || ch == '"' {
   107  				quote = ch
   108  				blankOK = true
   109  				phase = inQuote
   110  			}
   111  			if ch == '\\' {
   112  				if pos+1 == len(rest) {
   113  					continue // just skip \ at end
   114  				}
   115  				// If we're not quoted and we see a \, then always just
   116  				// add \ plus the char to the word, even if the char
   117  				// is a quote.
   118  				word += string(ch)
   119  				pos++
   120  				ch = rune(rest[pos])
   121  			}
   122  			word += string(ch)
   123  			continue
   124  		}
   125  		if phase == inQuote {
   126  			if ch == quote {
   127  				phase = inWord
   128  			}
   129  			// \ is special except for ' quotes - can't escape anything for '
   130  			if ch == '\\' && quote != '\'' {
   131  				if pos+1 == len(rest) {
   132  					phase = inWord
   133  					continue // just skip \ at end
   134  				}
   135  				pos++
   136  				nextCh := rune(rest[pos])
   137  				word += string(ch)
   138  				ch = nextCh
   139  			}
   140  			word += string(ch)
   141  		}
   142  	}
   143  
   144  	if len(words) == 0 {
   145  		return nil, nil, nil
   146  	}
   147  
   148  	// Old format (KEY name value)
   149  	var rootnode *Node
   150  
   151  	if !strings.Contains(words[0], "=") {
   152  		node := &Node{}
   153  		rootnode = node
   154  		strs := TOKEN_WHITESPACE.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) (*Node, map[string]bool, error) {
   191  	return parseNameVal(rest, "ENV")
   192  }
   193  
   194  func parseLabel(rest string) (*Node, map[string]bool, error) {
   195  	return parseNameVal(rest, "LABEL")
   196  }
   197  
   198  // parses a whitespace-delimited set of arguments. The result is effectively a
   199  // linked list of string arguments.
   200  func parseStringsWhitespaceDelimited(rest string) (*Node, map[string]bool, error) {
   201  	if rest == "" {
   202  		return nil, nil, nil
   203  	}
   204  
   205  	node := &Node{}
   206  	rootnode := node
   207  	prevnode := node
   208  	for _, str := range TOKEN_WHITESPACE.Split(rest, -1) { // use regexp
   209  		prevnode = node
   210  		node.Value = str
   211  		node.Next = &Node{}
   212  		node = node.Next
   213  	}
   214  
   215  	// XXX to get around regexp.Split *always* providing an empty string at the
   216  	// end due to how our loop is constructed, nil out the last node in the
   217  	// chain.
   218  	prevnode.Next = nil
   219  
   220  	return rootnode, nil, nil
   221  }
   222  
   223  // parsestring just wraps the string in quotes and returns a working node.
   224  func parseString(rest string) (*Node, map[string]bool, error) {
   225  	if rest == "" {
   226  		return nil, nil, nil
   227  	}
   228  	n := &Node{}
   229  	n.Value = rest
   230  	return n, nil, nil
   231  }
   232  
   233  // parseJSON converts JSON arrays to an AST.
   234  func parseJSON(rest string) (*Node, map[string]bool, error) {
   235  	var myJson []interface{}
   236  	if err := json.Unmarshal([]byte(rest), &myJson); err != nil {
   237  		return nil, nil, err
   238  	}
   239  
   240  	var top, prev *Node
   241  	for _, str := range myJson {
   242  		if s, ok := str.(string); !ok {
   243  			return nil, nil, errDockerfileNotStringArray
   244  		} else {
   245  			node := &Node{Value: s}
   246  			if prev == nil {
   247  				top = node
   248  			} else {
   249  				prev.Next = node
   250  			}
   251  			prev = node
   252  		}
   253  	}
   254  
   255  	return top, map[string]bool{"json": true}, nil
   256  }
   257  
   258  // parseMaybeJSON determines if the argument appears to be a JSON array. If
   259  // so, passes to parseJSON; if not, quotes the result and returns a single
   260  // node.
   261  func parseMaybeJSON(rest string) (*Node, map[string]bool, error) {
   262  	if rest == "" {
   263  		return nil, nil, nil
   264  	}
   265  
   266  	node, attrs, err := parseJSON(rest)
   267  
   268  	if err == nil {
   269  		return node, attrs, nil
   270  	}
   271  	if err == errDockerfileNotStringArray {
   272  		return nil, nil, err
   273  	}
   274  
   275  	node = &Node{}
   276  	node.Value = rest
   277  	return node, nil, nil
   278  }
   279  
   280  // parseMaybeJSONToList determines if the argument appears to be a JSON array. If
   281  // so, passes to parseJSON; if not, attmpts to parse it as a whitespace
   282  // delimited string.
   283  func parseMaybeJSONToList(rest string) (*Node, map[string]bool, error) {
   284  	node, attrs, err := parseJSON(rest)
   285  
   286  	if err == nil {
   287  		return node, attrs, nil
   288  	}
   289  	if err == errDockerfileNotStringArray {
   290  		return nil, nil, err
   291  	}
   292  
   293  	return parseStringsWhitespaceDelimited(rest)
   294  }