github.com/raychaser/docker@v1.5.0/builder/parser/parser.go (about)

     1  // This package implements a parser and parse tree dumper for Dockerfiles.
     2  package parser
     3  
     4  import (
     5  	"bufio"
     6  	"fmt"
     7  	"io"
     8  	"regexp"
     9  	"strings"
    10  	"unicode"
    11  )
    12  
    13  // Node is a structure used to represent a parse tree.
    14  //
    15  // In the node there are three fields, Value, Next, and Children. Value is the
    16  // current token's string value. Next is always the next non-child token, and
    17  // children contains all the children. Here's an example:
    18  //
    19  // (value next (child child-next child-next-next) next-next)
    20  //
    21  // This data structure is frankly pretty lousy for handling complex languages,
    22  // but lucky for us the Dockerfile isn't very complicated. This structure
    23  // works a little more effectively than a "proper" parse tree for our needs.
    24  //
    25  type Node struct {
    26  	Value      string          // actual content
    27  	Next       *Node           // the next item in the current sexp
    28  	Children   []*Node         // the children of this sexp
    29  	Attributes map[string]bool // special attributes for this node
    30  	Original   string          // original line used before parsing
    31  }
    32  
    33  var (
    34  	dispatch                map[string]func(string) (*Node, map[string]bool, error)
    35  	TOKEN_WHITESPACE        = regexp.MustCompile(`[\t\v\f\r ]+`)
    36  	TOKEN_LINE_CONTINUATION = regexp.MustCompile(`\\[ \t]*$`)
    37  	TOKEN_COMMENT           = regexp.MustCompile(`^#.*$`)
    38  )
    39  
    40  func init() {
    41  	// Dispatch Table. see line_parsers.go for the parse functions.
    42  	// The command is parsed and mapped to the line parser. The line parser
    43  	// recieves the arguments but not the command, and returns an AST after
    44  	// reformulating the arguments according to the rules in the parser
    45  	// functions. Errors are propogated up by Parse() and the resulting AST can
    46  	// be incorporated directly into the existing AST as a next.
    47  	dispatch = map[string]func(string) (*Node, map[string]bool, error){
    48  		"user":       parseString,
    49  		"onbuild":    parseSubCommand,
    50  		"workdir":    parseString,
    51  		"env":        parseEnv,
    52  		"maintainer": parseString,
    53  		"from":       parseString,
    54  		"add":        parseMaybeJSONToList,
    55  		"copy":       parseMaybeJSONToList,
    56  		"run":        parseMaybeJSON,
    57  		"cmd":        parseMaybeJSON,
    58  		"entrypoint": parseMaybeJSON,
    59  		"expose":     parseStringsWhitespaceDelimited,
    60  		"volume":     parseMaybeJSONToList,
    61  		"insert":     parseIgnore,
    62  	}
    63  }
    64  
    65  // parse a line and return the remainder.
    66  func parseLine(line string) (string, *Node, error) {
    67  	if line = stripComments(line); line == "" {
    68  		return "", nil, nil
    69  	}
    70  
    71  	if TOKEN_LINE_CONTINUATION.MatchString(line) {
    72  		line = TOKEN_LINE_CONTINUATION.ReplaceAllString(line, "")
    73  		return line, nil, nil
    74  	}
    75  
    76  	cmd, args, err := splitCommand(line)
    77  	if err != nil {
    78  		return "", nil, err
    79  	}
    80  
    81  	if len(args) == 0 {
    82  		return "", nil, fmt.Errorf("Instruction %q is empty; cannot continue", cmd)
    83  	}
    84  
    85  	node := &Node{}
    86  	node.Value = cmd
    87  
    88  	sexp, attrs, err := fullDispatch(cmd, args)
    89  	if err != nil {
    90  		return "", nil, err
    91  	}
    92  
    93  	node.Next = sexp
    94  	node.Attributes = attrs
    95  	node.Original = line
    96  
    97  	return "", node, nil
    98  }
    99  
   100  // The main parse routine. Handles an io.ReadWriteCloser and returns the root
   101  // of the AST.
   102  func Parse(rwc io.Reader) (*Node, error) {
   103  	root := &Node{}
   104  	scanner := bufio.NewScanner(rwc)
   105  
   106  	for scanner.Scan() {
   107  		scannedLine := strings.TrimLeftFunc(scanner.Text(), unicode.IsSpace)
   108  		line, child, err := parseLine(scannedLine)
   109  		if err != nil {
   110  			return nil, err
   111  		}
   112  
   113  		if line != "" && child == nil {
   114  			for scanner.Scan() {
   115  				newline := scanner.Text()
   116  
   117  				if stripComments(strings.TrimSpace(newline)) == "" {
   118  					continue
   119  				}
   120  
   121  				line, child, err = parseLine(line + newline)
   122  				if err != nil {
   123  					return nil, err
   124  				}
   125  
   126  				if child != nil {
   127  					break
   128  				}
   129  			}
   130  			if child == nil && line != "" {
   131  				line, child, err = parseLine(line)
   132  				if err != nil {
   133  					return nil, err
   134  				}
   135  			}
   136  		}
   137  
   138  		if child != nil {
   139  			root.Children = append(root.Children, child)
   140  		}
   141  	}
   142  
   143  	return root, nil
   144  }