mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Merge pull request #32580 from dnephin/refactor-builder-parser-directive
Refactor Dockerfile.parser and directive
This commit is contained in:
commit
700b4807c3
13 changed files with 211 additions and 235 deletions
|
@ -62,7 +62,7 @@ type Builder struct {
|
||||||
disableCommit bool
|
disableCommit bool
|
||||||
cacheBusted bool
|
cacheBusted bool
|
||||||
buildArgs *buildArgs
|
buildArgs *buildArgs
|
||||||
directive parser.Directive
|
escapeToken rune
|
||||||
|
|
||||||
imageCache builder.ImageCache
|
imageCache builder.ImageCache
|
||||||
from builder.Image
|
from builder.Image
|
||||||
|
@ -122,14 +122,9 @@ func NewBuilder(clientCtx context.Context, config *types.ImageBuildOptions, back
|
||||||
runConfig: new(container.Config),
|
runConfig: new(container.Config),
|
||||||
tmpContainers: map[string]struct{}{},
|
tmpContainers: map[string]struct{}{},
|
||||||
buildArgs: newBuildArgs(config.BuildArgs),
|
buildArgs: newBuildArgs(config.BuildArgs),
|
||||||
directive: parser.Directive{
|
escapeToken: parser.DefaultEscapeToken,
|
||||||
EscapeSeen: false,
|
|
||||||
LookingForDirectives: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
b.imageContexts = &imageContexts{b: b}
|
b.imageContexts = &imageContexts{b: b}
|
||||||
|
|
||||||
parser.SetEscapeToken(parser.DefaultEscapeToken, &b.directive) // Assume the default token for escape
|
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,9 +190,9 @@ func (b *Builder) build(stdout io.Writer, stderr io.Writer, out io.Writer) (stri
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
addNodesForLabelOption(dockerfile, b.options.Labels)
|
addNodesForLabelOption(dockerfile.AST, b.options.Labels)
|
||||||
|
|
||||||
if err := checkDispatchDockerfile(dockerfile); err != nil {
|
if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,10 +220,13 @@ func (b *Builder) build(stdout io.Writer, stderr io.Writer, out io.Writer) (stri
|
||||||
return b.image, nil
|
return b.image, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Node) (string, error) {
|
func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result) (string, error) {
|
||||||
total := len(dockerfile.Children)
|
// TODO: pass this to dispatchRequest instead
|
||||||
|
b.escapeToken = dockerfile.EscapeToken
|
||||||
|
|
||||||
|
total := len(dockerfile.AST.Children)
|
||||||
var shortImgID string
|
var shortImgID string
|
||||||
for i, n := range dockerfile.Children {
|
for i, n := range dockerfile.AST.Children {
|
||||||
select {
|
select {
|
||||||
case <-b.clientCtx.Done():
|
case <-b.clientCtx.Done():
|
||||||
logrus.Debug("Builder: build cancelled!")
|
logrus.Debug("Builder: build cancelled!")
|
||||||
|
@ -325,13 +323,13 @@ func BuildFromConfig(config *container.Config, changes []string) (*container.Con
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ast, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")), &b.directive)
|
result, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure that the commands are valid
|
// ensure that the commands are valid
|
||||||
for _, n := range ast.Children {
|
for _, n := range result.AST.Children {
|
||||||
if !validCommitCommands[n.Value] {
|
if !validCommitCommands[n.Value] {
|
||||||
return nil, fmt.Errorf("%s is not a valid change command", n.Value)
|
return nil, fmt.Errorf("%s is not a valid change command", n.Value)
|
||||||
}
|
}
|
||||||
|
@ -342,11 +340,11 @@ func BuildFromConfig(config *container.Config, changes []string) (*container.Con
|
||||||
b.Stderr = ioutil.Discard
|
b.Stderr = ioutil.Discard
|
||||||
b.disableCommit = true
|
b.disableCommit = true
|
||||||
|
|
||||||
if err := checkDispatchDockerfile(ast); err != nil {
|
if err := checkDispatchDockerfile(result.AST); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dispatchFromDockerfile(b, ast); err != nil {
|
if err := dispatchFromDockerfile(b, result); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return b.runConfig, nil
|
return b.runConfig, nil
|
||||||
|
@ -361,8 +359,12 @@ func checkDispatchDockerfile(dockerfile *parser.Node) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func dispatchFromDockerfile(b *Builder, ast *parser.Node) error {
|
func dispatchFromDockerfile(b *Builder, result *parser.Result) error {
|
||||||
|
// TODO: pass this to dispatchRequest instead
|
||||||
|
b.escapeToken = result.EscapeToken
|
||||||
|
ast := result.AST
|
||||||
total := len(ast.Children)
|
total := len(ast.Children)
|
||||||
|
|
||||||
for i, n := range ast.Children {
|
for i, n := range ast.Children {
|
||||||
if err := b.dispatch(i, total, n); err != nil {
|
if err := b.dispatch(i, total, n); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -10,9 +10,7 @@ import (
|
||||||
|
|
||||||
func TestAddNodesForLabelOption(t *testing.T) {
|
func TestAddNodesForLabelOption(t *testing.T) {
|
||||||
dockerfile := "FROM scratch"
|
dockerfile := "FROM scratch"
|
||||||
d := parser.Directive{}
|
result, err := parser.Parse(strings.NewReader(dockerfile))
|
||||||
parser.SetEscapeToken(parser.DefaultEscapeToken, &d)
|
|
||||||
nodes, err := parser.Parse(strings.NewReader(dockerfile), &d)
|
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
labels := map[string]string{
|
labels := map[string]string{
|
||||||
|
@ -22,6 +20,7 @@ func TestAddNodesForLabelOption(t *testing.T) {
|
||||||
"org.b": "cli-b",
|
"org.b": "cli-b",
|
||||||
"org.a": "cli-a",
|
"org.a": "cli-a",
|
||||||
}
|
}
|
||||||
|
nodes := result.AST
|
||||||
addNodesForLabelOption(nodes, labels)
|
addNodesForLabelOption(nodes, labels)
|
||||||
|
|
||||||
expected := []string{
|
expected := []string{
|
||||||
|
|
|
@ -200,7 +200,7 @@ func from(b *Builder, args []string, attributes map[string]bool, original string
|
||||||
substituionArgs = append(substituionArgs, key+"="+value)
|
substituionArgs = append(substituionArgs, key+"="+value)
|
||||||
}
|
}
|
||||||
|
|
||||||
name, err := ProcessWord(args[0], substituionArgs, b.directive.EscapeToken)
|
name, err := ProcessWord(args[0], substituionArgs, b.escapeToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,7 +180,7 @@ func (b *Builder) evaluateEnv(cmd string, str string, envs []string) ([]string,
|
||||||
return []string{word}, err
|
return []string{word}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return processFunc(str, envs, b.directive.EscapeToken)
|
return processFunc(str, envs, b.escapeToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildArgsWithoutConfigEnv returns a list of key=value pairs for all the build
|
// buildArgsWithoutConfigEnv returns a list of key=value pairs for all the build
|
||||||
|
|
|
@ -171,9 +171,7 @@ func executeTestCase(t *testing.T, testCase dispatchTestCase) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
r := strings.NewReader(testCase.dockerfile)
|
r := strings.NewReader(testCase.dockerfile)
|
||||||
d := parser.Directive{}
|
result, err := parser.Parse(r)
|
||||||
parser.SetEscapeToken(parser.DefaultEscapeToken, &d)
|
|
||||||
n, err := parser.Parse(r, &d)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error when parsing Dockerfile: %s", err)
|
t.Fatalf("Error when parsing Dockerfile: %s", err)
|
||||||
|
@ -192,6 +190,7 @@ func executeTestCase(t *testing.T, testCase dispatchTestCase) {
|
||||||
buildArgs: newBuildArgs(options.BuildArgs),
|
buildArgs: newBuildArgs(options.BuildArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
n := result.AST
|
||||||
err = b.dispatch(0, len(n.Children), n.Children[0])
|
err = b.dispatch(0, len(n.Children), n.Children[0])
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
|
@ -462,12 +462,12 @@ func (b *Builder) processImageFrom(img builder.Image) error {
|
||||||
|
|
||||||
// parse the ONBUILD triggers by invoking the parser
|
// parse the ONBUILD triggers by invoking the parser
|
||||||
for _, step := range onBuildTriggers {
|
for _, step := range onBuildTriggers {
|
||||||
ast, err := parser.Parse(strings.NewReader(step), &b.directive)
|
result, err := parser.Parse(strings.NewReader(step))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, n := range ast.Children {
|
for _, n := range result.AST.Children {
|
||||||
if err := checkDispatch(n); err != nil {
|
if err := checkDispatch(n); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -481,7 +481,7 @@ func (b *Builder) processImageFrom(img builder.Image) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dispatchFromDockerfile(b, ast); err != nil {
|
if err := dispatchFromDockerfile(b, result); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -650,7 +650,7 @@ func (b *Builder) clearTmp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// readAndParseDockerfile reads a Dockerfile from the current context.
|
// readAndParseDockerfile reads a Dockerfile from the current context.
|
||||||
func (b *Builder) readAndParseDockerfile() (*parser.Node, error) {
|
func (b *Builder) readAndParseDockerfile() (*parser.Result, error) {
|
||||||
// If no -f was specified then look for 'Dockerfile'. If we can't find
|
// If no -f was specified then look for 'Dockerfile'. If we can't find
|
||||||
// that then look for 'dockerfile'. If neither are found then default
|
// that then look for 'dockerfile'. If neither are found then default
|
||||||
// back to 'Dockerfile' and use that in the error message.
|
// back to 'Dockerfile' and use that in the error message.
|
||||||
|
@ -664,9 +664,9 @@ func (b *Builder) readAndParseDockerfile() (*parser.Node, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes, err := b.parseDockerfile()
|
result, err := b.parseDockerfile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nodes, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// After the Dockerfile has been parsed, we need to check the .dockerignore
|
// After the Dockerfile has been parsed, we need to check the .dockerignore
|
||||||
|
@ -680,10 +680,10 @@ func (b *Builder) readAndParseDockerfile() (*parser.Node, error) {
|
||||||
if dockerIgnore, ok := b.context.(builder.DockerIgnoreContext); ok {
|
if dockerIgnore, ok := b.context.(builder.DockerIgnoreContext); ok {
|
||||||
dockerIgnore.Process([]string{b.options.Dockerfile})
|
dockerIgnore.Process([]string{b.options.Dockerfile})
|
||||||
}
|
}
|
||||||
return nodes, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) parseDockerfile() (*parser.Node, error) {
|
func (b *Builder) parseDockerfile() (*parser.Result, error) {
|
||||||
f, err := b.context.Open(b.options.Dockerfile)
|
f, err := b.context.Open(b.options.Dockerfile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
|
@ -702,5 +702,5 @@ func (b *Builder) parseDockerfile() (*parser.Node, error) {
|
||||||
return nil, fmt.Errorf("The Dockerfile (%s) cannot be empty", b.options.Dockerfile)
|
return nil, fmt.Errorf("The Dockerfile (%s) cannot be empty", b.options.Dockerfile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return parser.Parse(f, &b.directive)
|
return parser.Parse(f)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,14 +23,10 @@ func main() {
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
d := parser.Directive{LookingForDirectives: true}
|
result, err := parser.Parse(f)
|
||||||
parser.SetEscapeToken(parser.DefaultEscapeToken, &d)
|
|
||||||
|
|
||||||
ast, err := parser.Parse(f, &d)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
} else {
|
|
||||||
fmt.Println(ast.Dump())
|
|
||||||
}
|
}
|
||||||
|
fmt.Println(result.AST.Dump())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,10 +28,9 @@ var validJSONArraysOfStrings = map[string][]string{
|
||||||
|
|
||||||
func TestJSONArraysOfStrings(t *testing.T) {
|
func TestJSONArraysOfStrings(t *testing.T) {
|
||||||
for json, expected := range validJSONArraysOfStrings {
|
for json, expected := range validJSONArraysOfStrings {
|
||||||
d := Directive{}
|
d := NewDefaultDirective()
|
||||||
SetEscapeToken(DefaultEscapeToken, &d)
|
|
||||||
|
|
||||||
if node, _, err := parseJSON(json, &d); err != nil {
|
if node, _, err := parseJSON(json, d); err != nil {
|
||||||
t.Fatalf("%q should be a valid JSON array of strings, but wasn't! (err: %q)", json, err)
|
t.Fatalf("%q should be a valid JSON array of strings, but wasn't! (err: %q)", json, err)
|
||||||
} else {
|
} else {
|
||||||
i := 0
|
i := 0
|
||||||
|
@ -51,10 +50,9 @@ func TestJSONArraysOfStrings(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, json := range invalidJSONArraysOfStrings {
|
for _, json := range invalidJSONArraysOfStrings {
|
||||||
d := Directive{}
|
d := NewDefaultDirective()
|
||||||
SetEscapeToken(DefaultEscapeToken, &d)
|
|
||||||
|
|
||||||
if _, _, err := parseJSON(json, &d); err != errDockerfileNotStringArray {
|
if _, _, err := parseJSON(json, d); err != errDockerfileNotStringArray {
|
||||||
t.Fatalf("%q should be an invalid JSON array of strings, but wasn't!", json)
|
t.Fatalf("%q should be an invalid JSON array of strings, but wasn't!", json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ func parseSubCommand(rest string, d *Directive) (*Node, map[string]bool, error)
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, child, err := ParseLine(rest, d, false)
|
child, err := newNodeFromLine(rest, d)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,7 @@ func parseWords(rest string, d *Directive) []string {
|
||||||
blankOK = true
|
blankOK = true
|
||||||
phase = inQuote
|
phase = inQuote
|
||||||
}
|
}
|
||||||
if ch == d.EscapeToken {
|
if ch == d.escapeToken {
|
||||||
if pos+chWidth == len(rest) {
|
if pos+chWidth == len(rest) {
|
||||||
continue // just skip an escape token at end of line
|
continue // just skip an escape token at end of line
|
||||||
}
|
}
|
||||||
|
@ -122,7 +122,7 @@ func parseWords(rest string, d *Directive) []string {
|
||||||
phase = inWord
|
phase = inWord
|
||||||
}
|
}
|
||||||
// The escape token is special except for ' quotes - can't escape anything for '
|
// The escape token is special except for ' quotes - can't escape anything for '
|
||||||
if ch == d.EscapeToken && quote != '\'' {
|
if ch == d.escapeToken && quote != '\'' {
|
||||||
if pos+chWidth == len(rest) {
|
if pos+chWidth == len(rest) {
|
||||||
phase = inWord
|
phase = inWord
|
||||||
continue // just skip the escape token at end
|
continue // just skip the escape token at end
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/docker/docker/builder/dockerfile/command"
|
"github.com/docker/docker/builder/dockerfile/command"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Node is a structure used to represent a parse tree.
|
// Node is a structure used to represent a parse tree.
|
||||||
|
@ -34,7 +35,7 @@ type Node struct {
|
||||||
Original string // original line used before parsing
|
Original string // original line used before parsing
|
||||||
Flags []string // only top Node should have this set
|
Flags []string // only top Node should have this set
|
||||||
StartLine int // the line in the original dockerfile where the node begins
|
StartLine int // the line in the original dockerfile where the node begins
|
||||||
EndLine int // the line in the original dockerfile where the node ends
|
endLine int // the line in the original dockerfile where the node ends
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dump dumps the AST defined by `node` as a list of sexps.
|
// Dump dumps the AST defined by `node` as a list of sexps.
|
||||||
|
@ -62,13 +63,19 @@ func (node *Node) Dump() string {
|
||||||
return strings.TrimSpace(str)
|
return strings.TrimSpace(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Directive is the structure used during a build run to hold the state of
|
func (node *Node) lines(start, end int) {
|
||||||
// parsing directives.
|
node.StartLine = start
|
||||||
type Directive struct {
|
node.endLine = end
|
||||||
EscapeToken rune // Current escape token
|
}
|
||||||
LineContinuationRegex *regexp.Regexp // Current line continuation regex
|
|
||||||
LookingForDirectives bool // Whether we are currently looking for directives
|
// AddChild adds a new child node, and updates line information
|
||||||
EscapeSeen bool // Whether the escape directive has been seen
|
func (node *Node) AddChild(child *Node, startLine, endLine int) {
|
||||||
|
child.lines(startLine, endLine)
|
||||||
|
if node.StartLine < 0 {
|
||||||
|
node.StartLine = startLine
|
||||||
|
}
|
||||||
|
node.endLine = endLine
|
||||||
|
node.Children = append(node.Children, child)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -79,18 +86,60 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultEscapeToken is the default escape token
|
// DefaultEscapeToken is the default escape token
|
||||||
const DefaultEscapeToken = "\\"
|
const DefaultEscapeToken = '\\'
|
||||||
|
|
||||||
// SetEscapeToken sets the default token for escaping characters in a Dockerfile.
|
// Directive is the structure used during a build run to hold the state of
|
||||||
func SetEscapeToken(s string, d *Directive) error {
|
// parsing directives.
|
||||||
|
type Directive struct {
|
||||||
|
escapeToken rune // Current escape token
|
||||||
|
lineContinuationRegex *regexp.Regexp // Current line continuation regex
|
||||||
|
processingComplete bool // Whether we are done looking for directives
|
||||||
|
escapeSeen bool // Whether the escape directive has been seen
|
||||||
|
}
|
||||||
|
|
||||||
|
// setEscapeToken sets the default token for escaping characters in a Dockerfile.
|
||||||
|
func (d *Directive) setEscapeToken(s string) error {
|
||||||
if s != "`" && s != "\\" {
|
if s != "`" && s != "\\" {
|
||||||
return fmt.Errorf("invalid ESCAPE '%s'. Must be ` or \\", s)
|
return fmt.Errorf("invalid ESCAPE '%s'. Must be ` or \\", s)
|
||||||
}
|
}
|
||||||
d.EscapeToken = rune(s[0])
|
d.escapeToken = rune(s[0])
|
||||||
d.LineContinuationRegex = regexp.MustCompile(`\` + s + `[ \t]*$`)
|
d.lineContinuationRegex = regexp.MustCompile(`\` + s + `[ \t]*$`)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// processLine looks for a parser directive '# escapeToken=<char>. Parser
|
||||||
|
// directives must precede any builder instruction or other comments, and cannot
|
||||||
|
// be repeated.
|
||||||
|
func (d *Directive) processLine(line string) error {
|
||||||
|
if d.processingComplete {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Processing is finished after the first call
|
||||||
|
defer func() { d.processingComplete = true }()
|
||||||
|
|
||||||
|
tecMatch := tokenEscapeCommand.FindStringSubmatch(strings.ToLower(line))
|
||||||
|
if len(tecMatch) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if d.escapeSeen == true {
|
||||||
|
return errors.New("only one escape parser directive can be used")
|
||||||
|
}
|
||||||
|
for i, n := range tokenEscapeCommand.SubexpNames() {
|
||||||
|
if n == "escapechar" {
|
||||||
|
d.escapeSeen = true
|
||||||
|
return d.setEscapeToken(tecMatch[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultDirective returns a new Directive with the default escapeToken token
|
||||||
|
func NewDefaultDirective() *Directive {
|
||||||
|
directive := Directive{}
|
||||||
|
directive.setEscapeToken(string(DefaultEscapeToken))
|
||||||
|
return &directive
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Dispatch Table. see line_parsers.go for the parse functions.
|
// Dispatch Table. see line_parsers.go for the parse functions.
|
||||||
// The command is parsed and mapped to the line parser. The line parser
|
// The command is parsed and mapped to the line parser. The line parser
|
||||||
|
@ -120,28 +169,6 @@ func init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseLine parses a line and returns the remainder.
|
|
||||||
func ParseLine(line string, d *Directive, ignoreCont bool) (string, *Node, error) {
|
|
||||||
if escapeFound, err := handleParserDirective(line, d); err != nil || escapeFound {
|
|
||||||
d.EscapeSeen = escapeFound
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
d.LookingForDirectives = false
|
|
||||||
|
|
||||||
if line = stripComments(line); line == "" {
|
|
||||||
return "", nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ignoreCont && d.LineContinuationRegex.MatchString(line) {
|
|
||||||
line = d.LineContinuationRegex.ReplaceAllString(line, "")
|
|
||||||
return line, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
node, err := newNodeFromLine(line, d)
|
|
||||||
return "", node, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// newNodeFromLine splits the line into parts, and dispatches to a function
|
// newNodeFromLine splits the line into parts, and dispatches to a function
|
||||||
// based on the command and command arguments. A Node is created from the
|
// based on the command and command arguments. A Node is created from the
|
||||||
// result of the dispatch.
|
// result of the dispatch.
|
||||||
|
@ -170,109 +197,98 @@ func newNodeFromLine(line string, directive *Directive) (*Node, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the parser directive '# escape=<char>. Parser directives must precede
|
// Result is the result of parsing a Dockerfile
|
||||||
// any builder instruction or other comments, and cannot be repeated.
|
type Result struct {
|
||||||
func handleParserDirective(line string, d *Directive) (bool, error) {
|
AST *Node
|
||||||
if !d.LookingForDirectives {
|
EscapeToken rune
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
tecMatch := tokenEscapeCommand.FindStringSubmatch(strings.ToLower(line))
|
|
||||||
if len(tecMatch) == 0 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
if d.EscapeSeen == true {
|
|
||||||
return false, 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 false, err
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse is the main parse routine.
|
// Parse reads lines from a Reader, parses the lines into an AST and returns
|
||||||
// It handles an io.ReadWriteCloser and returns the root of the AST.
|
// the AST and escape token
|
||||||
func Parse(rwc io.Reader, d *Directive) (*Node, error) {
|
func Parse(rwc io.Reader) (*Result, error) {
|
||||||
|
d := NewDefaultDirective()
|
||||||
currentLine := 0
|
currentLine := 0
|
||||||
root := &Node{}
|
root := &Node{StartLine: -1}
|
||||||
root.StartLine = -1
|
|
||||||
scanner := bufio.NewScanner(rwc)
|
scanner := bufio.NewScanner(rwc)
|
||||||
|
|
||||||
utf8bom := []byte{0xEF, 0xBB, 0xBF}
|
var err error
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
scannedBytes := scanner.Bytes()
|
bytes := scanner.Bytes()
|
||||||
// We trim UTF8 BOM
|
switch currentLine {
|
||||||
if currentLine == 0 {
|
case 0:
|
||||||
scannedBytes = bytes.TrimPrefix(scannedBytes, utf8bom)
|
bytes, err = processFirstLine(d, bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
bytes = processLine(bytes, true)
|
||||||
}
|
}
|
||||||
scannedLine := strings.TrimLeftFunc(string(scannedBytes), unicode.IsSpace)
|
|
||||||
currentLine++
|
currentLine++
|
||||||
line, child, err := ParseLine(scannedLine, d, false)
|
|
||||||
|
startLine := currentLine
|
||||||
|
line, isEndOfLine := trimContinuationCharacter(string(bytes), d)
|
||||||
|
if isEndOfLine && line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for !isEndOfLine && scanner.Scan() {
|
||||||
|
bytes := processLine(scanner.Bytes(), false)
|
||||||
|
currentLine++
|
||||||
|
|
||||||
|
// TODO: warn this is being deprecated/removed
|
||||||
|
if isEmptyContinuationLine(bytes) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
continuationLine := string(bytes)
|
||||||
|
continuationLine, isEndOfLine = trimContinuationCharacter(continuationLine, d)
|
||||||
|
line += continuationLine
|
||||||
|
}
|
||||||
|
|
||||||
|
child, err := newNodeFromLine(line, d)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
startLine := currentLine
|
root.AddChild(child, startLine, currentLine)
|
||||||
|
|
||||||
if line != "" && child == nil {
|
|
||||||
for scanner.Scan() {
|
|
||||||
newline := scanner.Text()
|
|
||||||
currentLine++
|
|
||||||
|
|
||||||
if stripComments(strings.TrimSpace(newline)) == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
line, child, err = ParseLine(line+newline, d, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if child != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if child == nil && line != "" {
|
|
||||||
// When we call ParseLine we'll pass in 'true' for
|
|
||||||
// the ignoreCont param if we're at the EOF. This will
|
|
||||||
// prevent the func from returning immediately w/o
|
|
||||||
// parsing the line thinking that there's more input
|
|
||||||
// to come.
|
|
||||||
|
|
||||||
_, child, err = ParseLine(line, d, scanner.Err() == nil)
|
|
||||||
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
|
return &Result{AST: root, EscapeToken: d.escapeToken}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// covers comments and empty lines. Lines should be trimmed before passing to
|
func trimComments(src []byte) []byte {
|
||||||
// this function.
|
return tokenComment.ReplaceAll(src, []byte{})
|
||||||
func stripComments(line string) string {
|
}
|
||||||
// string is already trimmed at this point
|
|
||||||
if tokenComment.MatchString(line) {
|
func trimWhitespace(src []byte) []byte {
|
||||||
return tokenComment.ReplaceAllString(line, "")
|
return bytes.TrimLeftFunc(src, unicode.IsSpace)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEmptyContinuationLine(line []byte) bool {
|
||||||
|
return len(trimComments(trimWhitespace(line))) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var utf8bom = []byte{0xEF, 0xBB, 0xBF}
|
||||||
|
|
||||||
|
func trimContinuationCharacter(line string, d *Directive) (string, bool) {
|
||||||
|
if d.lineContinuationRegex.MatchString(line) {
|
||||||
|
line = d.lineContinuationRegex.ReplaceAllString(line, "")
|
||||||
|
return line, false
|
||||||
}
|
}
|
||||||
|
return line, true
|
||||||
return line
|
}
|
||||||
|
|
||||||
|
// TODO: remove stripLeftWhitespace after deprecation period. It seems silly
|
||||||
|
// to preserve whitespace on continuation lines. Why is that done?
|
||||||
|
func processLine(token []byte, stripLeftWhitespace bool) []byte {
|
||||||
|
if stripLeftWhitespace {
|
||||||
|
token = trimWhitespace(token)
|
||||||
|
}
|
||||||
|
return trimComments(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func processFirstLine(d *Directive, token []byte) ([]byte, error) {
|
||||||
|
token = bytes.TrimPrefix(token, utf8bom)
|
||||||
|
token = trimWhitespace(token)
|
||||||
|
err := d.processLine(string(token))
|
||||||
|
return trimComments(token), err
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/pkg/testutil/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
const testDir = "testfiles"
|
const testDir = "testfiles"
|
||||||
|
@ -16,17 +18,11 @@ const testFileLineInfo = "testfile-line/Dockerfile"
|
||||||
|
|
||||||
func getDirs(t *testing.T, dir string) []string {
|
func getDirs(t *testing.T, dir string) []string {
|
||||||
f, err := os.Open(dir)
|
f, err := os.Open(dir)
|
||||||
if err != nil {
|
assert.NilError(t, err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
dirs, err := f.Readdirnames(0)
|
dirs, err := f.Readdirnames(0)
|
||||||
if err != nil {
|
assert.NilError(t, err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dirs
|
return dirs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,17 +31,11 @@ func TestTestNegative(t *testing.T) {
|
||||||
dockerfile := filepath.Join(negativeTestDir, dir, "Dockerfile")
|
dockerfile := filepath.Join(negativeTestDir, dir, "Dockerfile")
|
||||||
|
|
||||||
df, err := os.Open(dockerfile)
|
df, err := os.Open(dockerfile)
|
||||||
if err != nil {
|
assert.NilError(t, err)
|
||||||
t.Fatalf("Dockerfile missing for %s: %v", dir, err)
|
|
||||||
}
|
|
||||||
defer df.Close()
|
defer df.Close()
|
||||||
|
|
||||||
d := Directive{LookingForDirectives: true}
|
_, err = Parse(df)
|
||||||
SetEscapeToken(DefaultEscapeToken, &d)
|
assert.Error(t, err, "")
|
||||||
_, err = Parse(df, &d)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("No error parsing broken dockerfile for %s", dir)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,33 +45,21 @@ func TestTestData(t *testing.T) {
|
||||||
resultfile := filepath.Join(testDir, dir, "result")
|
resultfile := filepath.Join(testDir, dir, "result")
|
||||||
|
|
||||||
df, err := os.Open(dockerfile)
|
df, err := os.Open(dockerfile)
|
||||||
if err != nil {
|
assert.NilError(t, err)
|
||||||
t.Fatalf("Dockerfile missing for %s: %v", dir, err)
|
|
||||||
}
|
|
||||||
defer df.Close()
|
defer df.Close()
|
||||||
|
|
||||||
d := Directive{LookingForDirectives: true}
|
result, err := Parse(df)
|
||||||
SetEscapeToken(DefaultEscapeToken, &d)
|
assert.NilError(t, err)
|
||||||
ast, err := Parse(df, &d)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error parsing %s's dockerfile: %v", dir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := ioutil.ReadFile(resultfile)
|
content, err := ioutil.ReadFile(resultfile)
|
||||||
if err != nil {
|
assert.NilError(t, err)
|
||||||
t.Fatalf("Error reading %s's result file: %v", dir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
// CRLF --> CR to match Unix behavior
|
// CRLF --> CR to match Unix behavior
|
||||||
content = bytes.Replace(content, []byte{'\x0d', '\x0a'}, []byte{'\x0a'}, -1)
|
content = bytes.Replace(content, []byte{'\x0d', '\x0a'}, []byte{'\x0a'}, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ast.Dump()+"\n" != string(content) {
|
assert.Equal(t, result.AST.Dump()+"\n", string(content), "In "+dockerfile)
|
||||||
fmt.Fprintln(os.Stderr, "Result:\n"+ast.Dump())
|
|
||||||
fmt.Fprintln(os.Stderr, "Expected:\n"+string(content))
|
|
||||||
t.Fatalf("%s: AST dump of dockerfile does not match result", dir)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,51 +100,34 @@ func TestParseWords(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
d := Directive{LookingForDirectives: true}
|
words := parseWords(test["input"][0], NewDefaultDirective())
|
||||||
SetEscapeToken(DefaultEscapeToken, &d)
|
assert.DeepEqual(t, words, test["expect"])
|
||||||
words := parseWords(test["input"][0], &d)
|
|
||||||
if len(words) != len(test["expect"]) {
|
|
||||||
t.Fatalf("length check failed. input: %v, expect: %q, output: %q", test["input"][0], test["expect"], words)
|
|
||||||
}
|
|
||||||
for i, word := range words {
|
|
||||||
if word != test["expect"][i] {
|
|
||||||
t.Fatalf("word check failed for word: %q. input: %q, expect: %q, output: %q", word, test["input"][0], test["expect"], words)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLineInformation(t *testing.T) {
|
func TestLineInformation(t *testing.T) {
|
||||||
df, err := os.Open(testFileLineInfo)
|
df, err := os.Open(testFileLineInfo)
|
||||||
if err != nil {
|
assert.NilError(t, err)
|
||||||
t.Fatalf("Dockerfile missing for %s: %v", testFileLineInfo, err)
|
|
||||||
}
|
|
||||||
defer df.Close()
|
defer df.Close()
|
||||||
|
|
||||||
d := Directive{LookingForDirectives: true}
|
result, err := Parse(df)
|
||||||
SetEscapeToken(DefaultEscapeToken, &d)
|
assert.NilError(t, err)
|
||||||
ast, err := Parse(df, &d)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error parsing dockerfile %s: %v", testFileLineInfo, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ast.StartLine != 5 || ast.EndLine != 31 {
|
ast := result.AST
|
||||||
fmt.Fprintf(os.Stderr, "Wrong root line information: expected(%d-%d), actual(%d-%d)\n", 5, 31, ast.StartLine, ast.EndLine)
|
if ast.StartLine != 5 || ast.endLine != 31 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Wrong root line information: expected(%d-%d), actual(%d-%d)\n", 5, 31, ast.StartLine, ast.endLine)
|
||||||
t.Fatal("Root line information doesn't match result.")
|
t.Fatal("Root line information doesn't match result.")
|
||||||
}
|
}
|
||||||
if len(ast.Children) != 3 {
|
assert.Equal(t, len(ast.Children), 3)
|
||||||
fmt.Fprintf(os.Stderr, "Wrong number of child: expected(%d), actual(%d)\n", 3, len(ast.Children))
|
|
||||||
t.Fatalf("Root line information doesn't match result for %s", testFileLineInfo)
|
|
||||||
}
|
|
||||||
expected := [][]int{
|
expected := [][]int{
|
||||||
{5, 5},
|
{5, 5},
|
||||||
{11, 12},
|
{11, 12},
|
||||||
{17, 31},
|
{17, 31},
|
||||||
}
|
}
|
||||||
for i, child := range ast.Children {
|
for i, child := range ast.Children {
|
||||||
if child.StartLine != expected[i][0] || child.EndLine != expected[i][1] {
|
if child.StartLine != expected[i][0] || child.endLine != expected[i][1] {
|
||||||
t.Logf("Wrong line information for child %d: expected(%d-%d), actual(%d-%d)\n",
|
t.Logf("Wrong line information for child %d: expected(%d-%d), actual(%d-%d)\n",
|
||||||
i, expected[i][0], expected[i][1], child.StartLine, child.EndLine)
|
i, expected[i][0], expected[i][1], child.StartLine, child.endLine)
|
||||||
t.Fatal("Root line information doesn't match result.")
|
t.Fatal("Root line information doesn't match result.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
FROM alpine:3.5
|
||||||
|
|
||||||
|
RUN something \
|
|
@ -0,0 +1,2 @@
|
||||||
|
(from "alpine:3.5")
|
||||||
|
(run "something")
|
Loading…
Add table
Reference in a new issue