1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00
moby--moby/builder/dockerfile/parser/parser.go
Yong Tang 3e1b539e8d Fix dockerfile parser with empty line after escape
This fix tries to fix the bug reported by #24693 where an empty
line after escape will not be stopped by the parser.

This fix addresses this issue by stop the parser from continue
with an empty line after escape.

An additional integration test has been added.

This fix fixes #24693.

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>
2016-10-27 10:43:08 -07:00

222 lines
6.8 KiB
Go

// Package parser implements a parser and parse tree dumper for Dockerfiles.
package parser
import (
"bufio"
"bytes"
"fmt"
"io"
"regexp"
"strings"
"unicode"
"github.com/docker/docker/builder/dockerfile/command"
)
// Node is a structure used to represent a parse tree.
//
// In the node there are three fields, Value, Next, and Children. Value is the
// current token's string value. Next is always the next non-child token, and
// children contains all the children. Here's an example:
//
// (value next (child child-next child-next-next) next-next)
//
// This data structure is frankly pretty lousy for handling complex languages,
// but lucky for us the Dockerfile isn't very complicated. This structure
// works a little more effectively than a "proper" parse tree for our needs.
//
type Node struct {
Value string // actual content
Next *Node // the next item in the current sexp
Children []*Node // the children of this sexp
Attributes map[string]bool // special attributes for this node
Original string // original line used before parsing
Flags []string // only top Node should have this set
StartLine int // the line in the original dockerfile where the node begins
EndLine int // the line in the original dockerfile where the node ends
}
// Directive is the structure used during a build run to hold the state of
// parsing directives.
type Directive struct {
EscapeToken rune // Current escape token
LineContinuationRegex *regexp.Regexp // Current line contination regex
LookingForDirectives bool // Whether we are currently looking for directives
EscapeSeen bool // Whether the escape directive has been seen
}
var (
dispatch map[string]func(string, *Directive) (*Node, map[string]bool, error)
tokenWhitespace = regexp.MustCompile(`[\t\v\f\r ]+`)
tokenEscapeCommand = regexp.MustCompile(`^#[ \t]*escape[ \t]*=[ \t]*(?P<escapechar>.).*$`)
tokenComment = regexp.MustCompile(`^#.*$`)
)
// DefaultEscapeToken is the default escape token
const DefaultEscapeToken = "\\"
// SetEscapeToken sets the default token for escaping characters in a Dockerfile.
func SetEscapeToken(s string, d *Directive) error {
if s != "`" && s != "\\" {
return fmt.Errorf("invalid ESCAPE '%s'. Must be ` or \\", s)
}
d.EscapeToken = rune(s[0])
d.LineContinuationRegex = regexp.MustCompile(`\` + s + `$`)
return nil
}
func init() {
// Dispatch Table. see line_parsers.go for the parse functions.
// The command is parsed and mapped to the line parser. The line parser
// receives the arguments but not the command, and returns an AST after
// reformulating the arguments according to the rules in the parser
// functions. Errors are propagated up by Parse() and the resulting AST can
// be incorporated directly into the existing AST as a next.
dispatch = map[string]func(string, *Directive) (*Node, map[string]bool, error){
command.Add: parseMaybeJSONToList,
command.Arg: parseNameOrNameVal,
command.Cmd: parseMaybeJSON,
command.Copy: parseMaybeJSONToList,
command.Entrypoint: parseMaybeJSON,
command.Env: parseEnv,
command.Expose: parseStringsWhitespaceDelimited,
command.From: parseString,
command.Healthcheck: parseHealthConfig,
command.Label: parseLabel,
command.Maintainer: parseString,
command.Onbuild: parseSubCommand,
command.Run: parseMaybeJSON,
command.Shell: parseMaybeJSON,
command.StopSignal: parseString,
command.User: parseString,
command.Volume: parseMaybeJSONToList,
command.Workdir: parseString,
}
}
// ParseLine parses a line and returns the remainder.
func ParseLine(line string, d *Directive) (string, *Node, error) {
// Handle the parser directive '# escape=<char>. Parser directives must precede
// any builder instruction or other comments, and cannot be repeated.
if d.LookingForDirectives {
tecMatch := tokenEscapeCommand.FindStringSubmatch(strings.ToLower(line))
if len(tecMatch) > 0 {
if d.EscapeSeen == true {
return "", nil, fmt.Errorf("only one escape parser directive can be used")
}
for i, n := range tokenEscapeCommand.SubexpNames() {
if n == "escapechar" {
if err := SetEscapeToken(tecMatch[i], d); err != nil {
return "", nil, err
}
d.EscapeSeen = true
return "", nil, nil
}
}
}
}
d.LookingForDirectives = false
if line = stripComments(line); line == "" {
return "", nil, nil
}
if d.LineContinuationRegex.MatchString(line) {
line = d.LineContinuationRegex.ReplaceAllString(line, "")
return line, nil, nil
}
cmd, flags, args, err := splitCommand(line)
if err != nil {
return "", nil, err
}
node := &Node{}
node.Value = cmd
sexp, attrs, err := fullDispatch(cmd, args, d)
if err != nil {
return "", nil, err
}
node.Next = sexp
node.Attributes = attrs
node.Original = line
node.Flags = flags
return "", node, nil
}
// Parse is the main parse routine.
// It handles an io.ReadWriteCloser and returns the root of the AST.
func Parse(rwc io.Reader, d *Directive) (*Node, error) {
currentLine := 0
root := &Node{}
root.StartLine = -1
scanner := bufio.NewScanner(rwc)
utf8bom := []byte{0xEF, 0xBB, 0xBF}
for scanner.Scan() {
scannedBytes := scanner.Bytes()
// We trim UTF8 BOM
if currentLine == 0 {
scannedBytes = bytes.TrimPrefix(scannedBytes, utf8bom)
}
scannedLine := strings.TrimLeftFunc(string(scannedBytes), unicode.IsSpace)
currentLine++
line, child, err := ParseLine(scannedLine, d)
if err != nil {
return nil, err
}
startLine := currentLine
if line != "" && child == nil {
for scanner.Scan() {
newline := scanner.Text()
currentLine++
// If escape followed by a comment line then stop
// Note here that comment line starts with `#` at
// the first pos of the line
if stripComments(newline) == "" {
break
}
// If escape followed by an empty line then stop
if strings.TrimSpace(newline) == "" {
break
}
line, child, err = ParseLine(line+newline, d)
if err != nil {
return nil, err
}
if child != nil {
break
}
}
if child == nil && line != "" {
_, child, err = ParseLine(line, d)
if err != nil {
return nil, err
}
}
}
if child != nil {
// Update the line information for the current child.
child.StartLine = startLine
child.EndLine = currentLine
// Update the line information for the root. The starting line of the root is always the
// starting line of the first child and the ending line is the ending line of the last child.
if root.StartLine < 0 {
root.StartLine = currentLine
}
root.EndLine = currentLine
root.Children = append(root.Children, child)
}
}
return root, nil
}