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