github.com/raychaser/docker@v1.5.0/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  	_, child, err := parseLine(rest)
    34  	if err != nil {
    35  		return nil, nil, err
    36  	}
    37  
    38  	return &Node{Children: []*Node{child}}, nil, nil
    39  }
    40  
    41  // parse environment like statements. Note that this does *not* handle
    42  // variable interpolation, which will be handled in the evaluator.
    43  func parseEnv(rest string) (*Node, map[string]bool, error) {
    44  	// This is kind of tricky because we need to support the old
    45  	// variant:   ENV name value
    46  	// as well as the new one:    ENV name=value ...
    47  	// The trigger to know which one is being used will be whether we hit
    48  	// a space or = first.  space ==> old, "=" ==> new
    49  
    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  
    63  	for pos := 0; pos <= len(rest); pos++ {
    64  		if pos != len(rest) {
    65  			ch = rune(rest[pos])
    66  		}
    67  
    68  		if phase == inSpaces { // Looking for start of word
    69  			if pos == len(rest) { // end of input
    70  				break
    71  			}
    72  			if unicode.IsSpace(ch) { // skip spaces
    73  				continue
    74  			}
    75  			phase = inWord // found it, fall thru
    76  		}
    77  		if (phase == inWord || phase == inQuote) && (pos == len(rest)) {
    78  			if blankOK || len(word) > 0 {
    79  				words = append(words, word)
    80  			}
    81  			break
    82  		}
    83  		if phase == inWord {
    84  			if unicode.IsSpace(ch) {
    85  				phase = inSpaces
    86  				if blankOK || len(word) > 0 {
    87  					words = append(words, word)
    88  
    89  					// Look for = and if no there assume
    90  					// we're doing the old stuff and
    91  					// just read the rest of the line
    92  					if !strings.Contains(word, "=") {
    93  						word = strings.TrimSpace(rest[pos:])
    94  						words = append(words, word)
    95  						break
    96  					}
    97  				}
    98  				word = ""
    99  				blankOK = false
   100  				continue
   101  			}
   102  			if ch == '\'' || ch == '"' {
   103  				quote = ch
   104  				blankOK = true
   105  				phase = inQuote
   106  				continue
   107  			}
   108  			if ch == '\\' {
   109  				if pos+1 == len(rest) {
   110  					continue // just skip \ at end
   111  				}
   112  				pos++
   113  				ch = rune(rest[pos])
   114  			}
   115  			word += string(ch)
   116  			continue
   117  		}
   118  		if phase == inQuote {
   119  			if ch == quote {
   120  				phase = inWord
   121  				continue
   122  			}
   123  			if ch == '\\' {
   124  				if pos+1 == len(rest) {
   125  					phase = inWord
   126  					continue // just skip \ at end
   127  				}
   128  				pos++
   129  				ch = rune(rest[pos])
   130  			}
   131  			word += string(ch)
   132  		}
   133  	}
   134  
   135  	if len(words) == 0 {
   136  		return nil, nil, fmt.Errorf("ENV must have some arguments")
   137  	}
   138  
   139  	// Old format (ENV name value)
   140  	var rootnode *Node
   141  
   142  	if !strings.Contains(words[0], "=") {
   143  		node := &Node{}
   144  		rootnode = node
   145  		strs := TOKEN_WHITESPACE.Split(rest, 2)
   146  
   147  		if len(strs) < 2 {
   148  			return nil, nil, fmt.Errorf("ENV must have two arguments")
   149  		}
   150  
   151  		node.Value = strs[0]
   152  		node.Next = &Node{}
   153  		node.Next.Value = strs[1]
   154  	} else {
   155  		var prevNode *Node
   156  		for i, word := range words {
   157  			if !strings.Contains(word, "=") {
   158  				return nil, nil, fmt.Errorf("Syntax error - can't find = in %q. Must be of the form: name=value", word)
   159  			}
   160  			parts := strings.SplitN(word, "=", 2)
   161  
   162  			name := &Node{}
   163  			value := &Node{}
   164  
   165  			name.Next = value
   166  			name.Value = parts[0]
   167  			value.Value = parts[1]
   168  
   169  			if i == 0 {
   170  				rootnode = name
   171  			} else {
   172  				prevNode.Next = name
   173  			}
   174  			prevNode = value
   175  		}
   176  	}
   177  
   178  	return rootnode, nil, nil
   179  }
   180  
   181  // parses a whitespace-delimited set of arguments. The result is effectively a
   182  // linked list of string arguments.
   183  func parseStringsWhitespaceDelimited(rest string) (*Node, map[string]bool, error) {
   184  	node := &Node{}
   185  	rootnode := node
   186  	prevnode := node
   187  	for _, str := range TOKEN_WHITESPACE.Split(rest, -1) { // use regexp
   188  		prevnode = node
   189  		node.Value = str
   190  		node.Next = &Node{}
   191  		node = node.Next
   192  	}
   193  
   194  	// XXX to get around regexp.Split *always* providing an empty string at the
   195  	// end due to how our loop is constructed, nil out the last node in the
   196  	// chain.
   197  	prevnode.Next = nil
   198  
   199  	return rootnode, nil, nil
   200  }
   201  
   202  // parsestring just wraps the string in quotes and returns a working node.
   203  func parseString(rest string) (*Node, map[string]bool, error) {
   204  	n := &Node{}
   205  	n.Value = rest
   206  	return n, nil, nil
   207  }
   208  
   209  // parseJSON converts JSON arrays to an AST.
   210  func parseJSON(rest string) (*Node, map[string]bool, error) {
   211  	var myJson []interface{}
   212  	if err := json.Unmarshal([]byte(rest), &myJson); err != nil {
   213  		return nil, nil, err
   214  	}
   215  
   216  	var top, prev *Node
   217  	for _, str := range myJson {
   218  		if s, ok := str.(string); !ok {
   219  			return nil, nil, errDockerfileNotStringArray
   220  		} else {
   221  			node := &Node{Value: s}
   222  			if prev == nil {
   223  				top = node
   224  			} else {
   225  				prev.Next = node
   226  			}
   227  			prev = node
   228  		}
   229  	}
   230  
   231  	return top, map[string]bool{"json": true}, nil
   232  }
   233  
   234  // parseMaybeJSON determines if the argument appears to be a JSON array. If
   235  // so, passes to parseJSON; if not, quotes the result and returns a single
   236  // node.
   237  func parseMaybeJSON(rest string) (*Node, map[string]bool, error) {
   238  	rest = strings.TrimSpace(rest)
   239  
   240  	node, attrs, err := parseJSON(rest)
   241  
   242  	if err == nil {
   243  		return node, attrs, nil
   244  	}
   245  	if err == errDockerfileNotStringArray {
   246  		return nil, nil, err
   247  	}
   248  
   249  	node = &Node{}
   250  	node.Value = rest
   251  	return node, nil, nil
   252  }
   253  
   254  // parseMaybeJSONToList determines if the argument appears to be a JSON array. If
   255  // so, passes to parseJSON; if not, attmpts to parse it as a whitespace
   256  // delimited string.
   257  func parseMaybeJSONToList(rest string) (*Node, map[string]bool, error) {
   258  	rest = strings.TrimSpace(rest)
   259  
   260  	node, attrs, err := parseJSON(rest)
   261  
   262  	if err == nil {
   263  		return node, attrs, nil
   264  	}
   265  	if err == errDockerfileNotStringArray {
   266  		return nil, nil, err
   267  	}
   268  
   269  	return parseStringsWhitespaceDelimited(rest)
   270  }