mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Merge pull request #31750 from dnephin/some-builder-cleanup
Fix `docker build --label` when the label includes single quotes and a space
This commit is contained in:
commit
6abbc933ae
9 changed files with 292 additions and 225 deletions
|
@ -7,7 +7,6 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
|
@ -209,26 +208,13 @@ func sanitizeRepoAndTags(names []string) ([]reference.Named, error) {
|
||||||
return repoAndTags, nil
|
return repoAndTags, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) processLabels() error {
|
func (b *Builder) processLabels() {
|
||||||
if len(b.options.Labels) == 0 {
|
if len(b.options.Labels) == 0 {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var labels []string
|
node := parser.NodeFromLabels(b.options.Labels)
|
||||||
for k, v := range b.options.Labels {
|
|
||||||
labels = append(labels, fmt.Sprintf("%q='%s'", k, v))
|
|
||||||
}
|
|
||||||
// Sort the label to have a repeatable order
|
|
||||||
sort.Strings(labels)
|
|
||||||
|
|
||||||
line := "LABEL " + strings.Join(labels, " ")
|
|
||||||
_, node, err := parser.ParseLine(line, &b.directive, false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
b.dockerfile.Children = append(b.dockerfile.Children, node)
|
b.dockerfile.Children = append(b.dockerfile.Children, node)
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// build runs the Dockerfile builder from a context and a docker object that allows to make calls
|
// build runs the Dockerfile builder from a context and a docker object that allows to make calls
|
||||||
|
@ -263,9 +249,7 @@ func (b *Builder) build(stdout io.Writer, stderr io.Writer, out io.Writer) (stri
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := b.processLabels(); err != nil {
|
b.processLabels()
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var shortImgID string
|
var shortImgID string
|
||||||
total := len(b.dockerfile.Children)
|
total := len(b.dockerfile.Children)
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/builder/dockerfile/parser"
|
"github.com/docker/docker/builder/dockerfile/parser"
|
||||||
|
"github.com/docker/docker/pkg/testutil/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBuildProcessLabels(t *testing.T) {
|
func TestBuildProcessLabels(t *testing.T) {
|
||||||
|
@ -15,9 +16,7 @@ func TestBuildProcessLabels(t *testing.T) {
|
||||||
d := parser.Directive{}
|
d := parser.Directive{}
|
||||||
parser.SetEscapeToken(parser.DefaultEscapeToken, &d)
|
parser.SetEscapeToken(parser.DefaultEscapeToken, &d)
|
||||||
n, err := parser.Parse(strings.NewReader(dockerfile), &d)
|
n, err := parser.Parse(strings.NewReader(dockerfile), &d)
|
||||||
if err != nil {
|
assert.NilError(t, err)
|
||||||
t.Fatalf("Error when parsing Dockerfile: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
options := &types.ImageBuildOptions{
|
options := &types.ImageBuildOptions{
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
|
@ -34,21 +33,14 @@ func TestBuildProcessLabels(t *testing.T) {
|
||||||
directive: d,
|
directive: d,
|
||||||
dockerfile: n,
|
dockerfile: n,
|
||||||
}
|
}
|
||||||
err = b.processLabels()
|
b.processLabels()
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error when processing labels: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := []string{
|
expected := []string{
|
||||||
"FROM scratch",
|
"FROM scratch",
|
||||||
`LABEL "org.a"='cli-a' "org.b"='cli-b' "org.c"='cli-c' "org.d"='cli-d' "org.e"='cli-e'`,
|
`LABEL "org.a"='cli-a' "org.b"='cli-b' "org.c"='cli-c' "org.d"='cli-d' "org.e"='cli-e'`,
|
||||||
}
|
}
|
||||||
if len(b.dockerfile.Children) != 2 {
|
assert.Equal(t, len(b.dockerfile.Children), 2)
|
||||||
t.Fatalf("Expect 2, got %d", len(b.dockerfile.Children))
|
|
||||||
}
|
|
||||||
for i, v := range b.dockerfile.Children {
|
for i, v := range b.dockerfile.Children {
|
||||||
if v.Original != expected[i] {
|
assert.Equal(t, v.Original, expected[i])
|
||||||
t.Fatalf("Expect '%s' for %dth children, got, '%s'", expected[i], i, v.Original)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,47 +129,18 @@ func (b *Builder) dispatch(stepN int, stepTotal int, ast *parser.Node) error {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// count the number of nodes that we are going to traverse first
|
msgList := initMsgList(ast)
|
||||||
// so we can pre-create the argument and message array. This speeds up the
|
|
||||||
// allocation of those list a lot when they have a lot of arguments
|
|
||||||
cursor := ast
|
|
||||||
var n int
|
|
||||||
for cursor.Next != nil {
|
|
||||||
cursor = cursor.Next
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
msgList := make([]string, n)
|
|
||||||
|
|
||||||
var i int
|
|
||||||
// Append build args to runConfig environment variables
|
// Append build args to runConfig environment variables
|
||||||
envs := append(b.runConfig.Env, b.buildArgsWithoutConfigEnv()...)
|
envs := append(b.runConfig.Env, b.buildArgsWithoutConfigEnv()...)
|
||||||
|
|
||||||
for ast.Next != nil {
|
for i := 0; ast.Next != nil; i++ {
|
||||||
ast = ast.Next
|
ast = ast.Next
|
||||||
var str string
|
words, err := b.evaluateEnv(cmd, ast.Value, envs)
|
||||||
str = ast.Value
|
if err != nil {
|
||||||
if replaceEnvAllowed[cmd] {
|
return err
|
||||||
var err error
|
|
||||||
var words []string
|
|
||||||
|
|
||||||
if allowWordExpansion[cmd] {
|
|
||||||
words, err = ProcessWords(str, envs, b.directive.EscapeToken)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
strList = append(strList, words...)
|
|
||||||
} else {
|
|
||||||
str, err = ProcessWord(str, envs, b.directive.EscapeToken)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
strList = append(strList, str)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
strList = append(strList, str)
|
|
||||||
}
|
}
|
||||||
|
strList = append(strList, words...)
|
||||||
msgList[i] = ast.Value
|
msgList[i] = ast.Value
|
||||||
i++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
msg += " " + strings.Join(msgList, " ")
|
msg += " " + strings.Join(msgList, " ")
|
||||||
|
@ -186,6 +157,29 @@ func (b *Builder) dispatch(stepN int, stepTotal int, ast *parser.Node) error {
|
||||||
return fmt.Errorf("Unknown instruction: %s", upperCasedCmd)
|
return fmt.Errorf("Unknown instruction: %s", upperCasedCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// count the number of nodes that we are going to traverse first
|
||||||
|
// allocation of those list a lot when they have a lot of arguments
|
||||||
|
func initMsgList(cursor *parser.Node) []string {
|
||||||
|
var n int
|
||||||
|
for ; cursor.Next != nil; n++ {
|
||||||
|
cursor = cursor.Next
|
||||||
|
}
|
||||||
|
return make([]string, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) evaluateEnv(cmd string, str string, envs []string) ([]string, error) {
|
||||||
|
if !replaceEnvAllowed[cmd] {
|
||||||
|
return []string{str}, nil
|
||||||
|
}
|
||||||
|
var processFunc func(string, []string, rune) ([]string, error)
|
||||||
|
if allowWordExpansion[cmd] {
|
||||||
|
processFunc = ProcessWords
|
||||||
|
} else {
|
||||||
|
processFunc = ProcessWord
|
||||||
|
}
|
||||||
|
return processFunc(str, envs, b.directive.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
|
||||||
// args that are not overriden by runConfig environment variables.
|
// args that are not overriden by runConfig environment variables.
|
||||||
func (b *Builder) buildArgsWithoutConfigEnv() []string {
|
func (b *Builder) buildArgsWithoutConfigEnv() []string {
|
||||||
|
|
|
@ -10,15 +10,22 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/docker/docker/builder/dockerfile/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errDockerfileNotStringArray = errors.New("When using JSON array syntax, arrays must be comprised of strings only.")
|
errDockerfileNotStringArray = errors.New("When using JSON array syntax, arrays must be comprised of strings only.")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
commandLabel = "LABEL"
|
||||||
|
)
|
||||||
|
|
||||||
// ignore the current argument. This will still leave a command parsed, but
|
// ignore the current argument. This will still leave a command parsed, but
|
||||||
// will not incorporate the arguments into the ast.
|
// will not incorporate the arguments into the ast.
|
||||||
func parseIgnore(rest string, d *Directive) (*Node, map[string]bool, error) {
|
func parseIgnore(rest string, d *Directive) (*Node, map[string]bool, error) {
|
||||||
|
@ -133,7 +140,7 @@ func parseWords(rest string, d *Directive) []string {
|
||||||
|
|
||||||
// parse environment like statements. Note that this does *not* handle
|
// parse environment like statements. Note that this does *not* handle
|
||||||
// variable interpolation, which will be handled in the evaluator.
|
// variable interpolation, which will be handled in the evaluator.
|
||||||
func parseNameVal(rest string, key string, d *Directive) (*Node, map[string]bool, error) {
|
func parseNameVal(rest string, key string, d *Directive) (*Node, error) {
|
||||||
// This is kind of tricky because we need to support the old
|
// This is kind of tricky because we need to support the old
|
||||||
// variant: KEY name value
|
// variant: KEY name value
|
||||||
// as well as the new one: KEY name=value ...
|
// as well as the new one: KEY name=value ...
|
||||||
|
@ -142,57 +149,88 @@ func parseNameVal(rest string, key string, d *Directive) (*Node, map[string]bool
|
||||||
|
|
||||||
words := parseWords(rest, d)
|
words := parseWords(rest, d)
|
||||||
if len(words) == 0 {
|
if len(words) == 0 {
|
||||||
return nil, nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var rootnode *Node
|
|
||||||
|
|
||||||
// Old format (KEY name value)
|
// Old format (KEY name value)
|
||||||
if !strings.Contains(words[0], "=") {
|
if !strings.Contains(words[0], "=") {
|
||||||
node := &Node{}
|
parts := tokenWhitespace.Split(rest, 2)
|
||||||
rootnode = node
|
if len(parts) < 2 {
|
||||||
strs := tokenWhitespace.Split(rest, 2)
|
return nil, fmt.Errorf(key + " must have two arguments")
|
||||||
|
|
||||||
if len(strs) < 2 {
|
|
||||||
return nil, nil, fmt.Errorf(key + " must have two arguments")
|
|
||||||
}
|
|
||||||
|
|
||||||
node.Value = strs[0]
|
|
||||||
node.Next = &Node{}
|
|
||||||
node.Next.Value = strs[1]
|
|
||||||
} else {
|
|
||||||
var prevNode *Node
|
|
||||||
for i, word := range words {
|
|
||||||
if !strings.Contains(word, "=") {
|
|
||||||
return nil, nil, fmt.Errorf("Syntax error - can't find = in %q. Must be of the form: name=value", word)
|
|
||||||
}
|
|
||||||
parts := strings.SplitN(word, "=", 2)
|
|
||||||
|
|
||||||
name := &Node{}
|
|
||||||
value := &Node{}
|
|
||||||
|
|
||||||
name.Next = value
|
|
||||||
name.Value = parts[0]
|
|
||||||
value.Value = parts[1]
|
|
||||||
|
|
||||||
if i == 0 {
|
|
||||||
rootnode = name
|
|
||||||
} else {
|
|
||||||
prevNode.Next = name
|
|
||||||
}
|
|
||||||
prevNode = value
|
|
||||||
}
|
}
|
||||||
|
return newKeyValueNode(parts[0], parts[1]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return rootnode, nil, nil
|
var rootNode *Node
|
||||||
|
var prevNode *Node
|
||||||
|
for _, word := range words {
|
||||||
|
if !strings.Contains(word, "=") {
|
||||||
|
return nil, fmt.Errorf("Syntax error - can't find = in %q. Must be of the form: name=value", word)
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(word, "=", 2)
|
||||||
|
node := newKeyValueNode(parts[0], parts[1])
|
||||||
|
rootNode, prevNode = appendKeyValueNode(node, rootNode, prevNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootNode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newKeyValueNode(key, value string) *Node {
|
||||||
|
return &Node{
|
||||||
|
Value: key,
|
||||||
|
Next: &Node{Value: value},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendKeyValueNode(node, rootNode, prevNode *Node) (*Node, *Node) {
|
||||||
|
if rootNode == nil {
|
||||||
|
rootNode = node
|
||||||
|
}
|
||||||
|
if prevNode != nil {
|
||||||
|
prevNode.Next = node
|
||||||
|
}
|
||||||
|
|
||||||
|
prevNode = node.Next
|
||||||
|
return rootNode, prevNode
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseEnv(rest string, d *Directive) (*Node, map[string]bool, error) {
|
func parseEnv(rest string, d *Directive) (*Node, map[string]bool, error) {
|
||||||
return parseNameVal(rest, "ENV", d)
|
node, err := parseNameVal(rest, "ENV", d)
|
||||||
|
return node, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseLabel(rest string, d *Directive) (*Node, map[string]bool, error) {
|
func parseLabel(rest string, d *Directive) (*Node, map[string]bool, error) {
|
||||||
return parseNameVal(rest, "LABEL", d)
|
node, err := parseNameVal(rest, commandLabel, d)
|
||||||
|
return node, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeFromLabels returns a Node for the injected labels
|
||||||
|
func NodeFromLabels(labels map[string]string) *Node {
|
||||||
|
keys := []string{}
|
||||||
|
for key := range labels {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
// Sort the label to have a repeatable order
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
labelPairs := []string{}
|
||||||
|
var rootNode *Node
|
||||||
|
var prevNode *Node
|
||||||
|
for _, key := range keys {
|
||||||
|
value := labels[key]
|
||||||
|
labelPairs = append(labelPairs, fmt.Sprintf("%q='%s'", key, value))
|
||||||
|
// Value must be single quoted to prevent env variable expansion
|
||||||
|
// See https://github.com/docker/docker/issues/26027
|
||||||
|
node := newKeyValueNode(key, "'"+value+"'")
|
||||||
|
rootNode, prevNode = appendKeyValueNode(node, rootNode, prevNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Node{
|
||||||
|
Value: command.Label,
|
||||||
|
Original: commandLabel + " " + strings.Join(labelPairs, " "),
|
||||||
|
Next: rootNode,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parses a statement containing one or more keyword definition(s) and/or
|
// parses a statement containing one or more keyword definition(s) and/or
|
||||||
|
|
65
builder/dockerfile/parser/line_parsers_test.go
Normal file
65
builder/dockerfile/parser/line_parsers_test.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/docker/pkg/testutil/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseNameValOldFormat(t *testing.T) {
|
||||||
|
directive := Directive{}
|
||||||
|
node, err := parseNameVal("foo bar", "LABEL", &directive)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
expected := &Node{
|
||||||
|
Value: "foo",
|
||||||
|
Next: &Node{Value: "bar"},
|
||||||
|
}
|
||||||
|
assert.DeepEqual(t, node, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseNameValNewFormat(t *testing.T) {
|
||||||
|
directive := Directive{}
|
||||||
|
node, err := parseNameVal("foo=bar thing=star", "LABEL", &directive)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
expected := &Node{
|
||||||
|
Value: "foo",
|
||||||
|
Next: &Node{
|
||||||
|
Value: "bar",
|
||||||
|
Next: &Node{
|
||||||
|
Value: "thing",
|
||||||
|
Next: &Node{
|
||||||
|
Value: "star",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.DeepEqual(t, node, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodeFromLabels(t *testing.T) {
|
||||||
|
labels := map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
"weird": "first' second",
|
||||||
|
}
|
||||||
|
expected := &Node{
|
||||||
|
Value: "label",
|
||||||
|
Original: `LABEL "foo"='bar' "weird"='first' second'`,
|
||||||
|
Next: &Node{
|
||||||
|
Value: "foo",
|
||||||
|
Next: &Node{
|
||||||
|
Value: "'bar'",
|
||||||
|
Next: &Node{
|
||||||
|
Value: "weird",
|
||||||
|
Next: &Node{
|
||||||
|
Value: "'first' second'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
node := NodeFromLabels(labels)
|
||||||
|
assert.DeepEqual(t, node, expected)
|
||||||
|
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
@ -36,6 +37,31 @@ type Node struct {
|
||||||
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.
|
||||||
|
// Returns a string suitable for printing.
|
||||||
|
func (node *Node) Dump() string {
|
||||||
|
str := ""
|
||||||
|
str += node.Value
|
||||||
|
|
||||||
|
if len(node.Flags) > 0 {
|
||||||
|
str += fmt.Sprintf(" %q", node.Flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range node.Children {
|
||||||
|
str += "(" + n.Dump() + ")\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
for n := node.Next; n != nil; n = n.Next {
|
||||||
|
if len(n.Children) > 0 {
|
||||||
|
str += " " + n.Dump()
|
||||||
|
} else {
|
||||||
|
str += " " + strconv.Quote(n.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(str)
|
||||||
|
}
|
||||||
|
|
||||||
// Directive is the structure used during a build run to hold the state of
|
// Directive is the structure used during a build run to hold the state of
|
||||||
// parsing directives.
|
// parsing directives.
|
||||||
type Directive struct {
|
type Directive struct {
|
||||||
|
@ -96,24 +122,9 @@ func init() {
|
||||||
|
|
||||||
// ParseLine parses a line and returns the remainder.
|
// ParseLine parses a line and returns the remainder.
|
||||||
func ParseLine(line string, d *Directive, ignoreCont bool) (string, *Node, error) {
|
func ParseLine(line string, d *Directive, ignoreCont bool) (string, *Node, error) {
|
||||||
// Handle the parser directive '# escape=<char>. Parser directives must precede
|
if escapeFound, err := handleParserDirective(line, d); err != nil || escapeFound {
|
||||||
// any builder instruction or other comments, and cannot be repeated.
|
d.EscapeSeen = escapeFound
|
||||||
if d.LookingForDirectives {
|
return "", nil, err
|
||||||
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
|
d.LookingForDirectives = false
|
||||||
|
@ -127,25 +138,60 @@ func ParseLine(line string, d *Directive, ignoreCont bool) (string, *Node, error
|
||||||
return line, nil, nil
|
return line, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
node, err := newNodeFromLine(line, d)
|
||||||
|
return "", node, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// newNodeFromLine splits the line into parts, and dispatches to a function
|
||||||
|
// based on the command and command arguments. A Node is created from the
|
||||||
|
// result of the dispatch.
|
||||||
|
func newNodeFromLine(line string, directive *Directive) (*Node, error) {
|
||||||
cmd, flags, args, err := splitCommand(line)
|
cmd, flags, args, err := splitCommand(line)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
node := &Node{}
|
fn := dispatch[cmd]
|
||||||
node.Value = cmd
|
// Ignore invalid Dockerfile instructions
|
||||||
|
if fn == nil {
|
||||||
sexp, attrs, err := fullDispatch(cmd, args, d)
|
fn = parseIgnore
|
||||||
|
}
|
||||||
|
next, attrs, err := fn(args, directive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
node.Next = sexp
|
return &Node{
|
||||||
node.Attributes = attrs
|
Value: cmd,
|
||||||
node.Original = line
|
Original: line,
|
||||||
node.Flags = flags
|
Flags: flags,
|
||||||
|
Next: next,
|
||||||
|
Attributes: attrs,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
return "", node, nil
|
// Handle the parser directive '# escape=<char>. Parser directives must precede
|
||||||
|
// any builder instruction or other comments, and cannot be repeated.
|
||||||
|
func handleParserDirective(line string, d *Directive) (bool, error) {
|
||||||
|
if !d.LookingForDirectives {
|
||||||
|
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 is the main parse routine.
|
||||||
|
@ -219,3 +265,14 @@ func Parse(rwc io.Reader, d *Directive) (*Node, error) {
|
||||||
|
|
||||||
return root, nil
|
return root, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// covers comments and empty lines. Lines should be trimmed before passing to
|
||||||
|
// this function.
|
||||||
|
func stripComments(line string) string {
|
||||||
|
// string is already trimmed at this point
|
||||||
|
if tokenComment.MatchString(line) {
|
||||||
|
return tokenComment.ReplaceAllString(line, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
|
@ -1,55 +1,10 @@
|
||||||
package parser
|
package parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Dump dumps the AST defined by `node` as a list of sexps.
|
|
||||||
// Returns a string suitable for printing.
|
|
||||||
func (node *Node) Dump() string {
|
|
||||||
str := ""
|
|
||||||
str += node.Value
|
|
||||||
|
|
||||||
if len(node.Flags) > 0 {
|
|
||||||
str += fmt.Sprintf(" %q", node.Flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, n := range node.Children {
|
|
||||||
str += "(" + n.Dump() + ")\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
for n := node.Next; n != nil; n = n.Next {
|
|
||||||
if len(n.Children) > 0 {
|
|
||||||
str += " " + n.Dump()
|
|
||||||
} else {
|
|
||||||
str += " " + strconv.Quote(n.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.TrimSpace(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// performs the dispatch based on the two primal strings, cmd and args. Please
|
|
||||||
// look at the dispatch table in parser.go to see how these dispatchers work.
|
|
||||||
func fullDispatch(cmd, args string, d *Directive) (*Node, map[string]bool, error) {
|
|
||||||
fn := dispatch[cmd]
|
|
||||||
|
|
||||||
// Ignore invalid Dockerfile instructions
|
|
||||||
if fn == nil {
|
|
||||||
fn = parseIgnore
|
|
||||||
}
|
|
||||||
|
|
||||||
sexp, attrs, err := fn(args, d)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return sexp, attrs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// splitCommand takes a single line of text and parses out the cmd and args,
|
// splitCommand takes a single line of text and parses out the cmd and args,
|
||||||
// which are used for dispatching to more exact parsing functions.
|
// which are used for dispatching to more exact parsing functions.
|
||||||
func splitCommand(line string) (string, []string, string, error) {
|
func splitCommand(line string) (string, []string, string, error) {
|
||||||
|
@ -71,17 +26,6 @@ func splitCommand(line string) (string, []string, string, error) {
|
||||||
return cmd, flags, strings.TrimSpace(args), nil
|
return cmd, flags, strings.TrimSpace(args), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// covers comments and empty lines. Lines should be trimmed before passing to
|
|
||||||
// this function.
|
|
||||||
func stripComments(line string) string {
|
|
||||||
// string is already trimmed at this point
|
|
||||||
if tokenComment.MatchString(line) {
|
|
||||||
return tokenComment.ReplaceAllString(line, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
return line
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractBuilderFlags(line string) (string, []string, error) {
|
func extractBuilderFlags(line string) (string, []string, error) {
|
||||||
// Parses the BuilderFlags and returns the remaining part of the line
|
// Parses the BuilderFlags and returns the remaining part of the line
|
||||||
|
|
|
@ -24,16 +24,9 @@ type shellWord struct {
|
||||||
|
|
||||||
// ProcessWord will use the 'env' list of environment variables,
|
// ProcessWord will use the 'env' list of environment variables,
|
||||||
// and replace any env var references in 'word'.
|
// and replace any env var references in 'word'.
|
||||||
func ProcessWord(word string, env []string, escapeToken rune) (string, error) {
|
func ProcessWord(word string, env []string, escapeToken rune) ([]string, error) {
|
||||||
sw := &shellWord{
|
word, _, err := process(word, env, escapeToken)
|
||||||
word: word,
|
return []string{word}, err
|
||||||
envs: env,
|
|
||||||
pos: 0,
|
|
||||||
escapeToken: escapeToken,
|
|
||||||
}
|
|
||||||
sw.scanner.Init(strings.NewReader(word))
|
|
||||||
word, _, err := sw.process()
|
|
||||||
return word, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProcessWords will use the 'env' list of environment variables,
|
// ProcessWords will use the 'env' list of environment variables,
|
||||||
|
@ -44,6 +37,11 @@ func ProcessWord(word string, env []string, escapeToken rune) (string, error) {
|
||||||
// Note, each one is trimmed to remove leading and trailing spaces (unless
|
// Note, each one is trimmed to remove leading and trailing spaces (unless
|
||||||
// they are quoted", but ProcessWord retains spaces between words.
|
// they are quoted", but ProcessWord retains spaces between words.
|
||||||
func ProcessWords(word string, env []string, escapeToken rune) ([]string, error) {
|
func ProcessWords(word string, env []string, escapeToken rune) ([]string, error) {
|
||||||
|
_, words, err := process(word, env, escapeToken)
|
||||||
|
return words, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func process(word string, env []string, escapeToken rune) (string, []string, error) {
|
||||||
sw := &shellWord{
|
sw := &shellWord{
|
||||||
word: word,
|
word: word,
|
||||||
envs: env,
|
envs: env,
|
||||||
|
@ -51,8 +49,7 @@ func ProcessWords(word string, env []string, escapeToken rune) ([]string, error)
|
||||||
escapeToken: escapeToken,
|
escapeToken: escapeToken,
|
||||||
}
|
}
|
||||||
sw.scanner.Init(strings.NewReader(word))
|
sw.scanner.Init(strings.NewReader(word))
|
||||||
_, words, err := sw.process()
|
return sw.process()
|
||||||
return words, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sw *shellWord) process() (string, []string, error) {
|
func (sw *shellWord) process() (string, []string, error) {
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/pkg/testutil/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestShellParser4EnvVars(t *testing.T) {
|
func TestShellParser4EnvVars(t *testing.T) {
|
||||||
|
@ -13,9 +15,7 @@ func TestShellParser4EnvVars(t *testing.T) {
|
||||||
lineCount := 0
|
lineCount := 0
|
||||||
|
|
||||||
file, err := os.Open(fn)
|
file, err := os.Open(fn)
|
||||||
if err != nil {
|
assert.NilError(t, err)
|
||||||
t.Fatalf("Can't open '%s': %s", err, fn)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file)
|
scanner := bufio.NewScanner(file)
|
||||||
|
@ -36,29 +36,25 @@ func TestShellParser4EnvVars(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
words := strings.Split(line, "|")
|
words := strings.Split(line, "|")
|
||||||
if len(words) != 3 {
|
assert.Equal(t, len(words), 3)
|
||||||
t.Fatalf("Error in '%s' - should be exactly one | in:%q", fn, line)
|
|
||||||
}
|
|
||||||
|
|
||||||
words[0] = strings.TrimSpace(words[0])
|
platform := strings.TrimSpace(words[0])
|
||||||
words[1] = strings.TrimSpace(words[1])
|
source := strings.TrimSpace(words[1])
|
||||||
words[2] = strings.TrimSpace(words[2])
|
expected := strings.TrimSpace(words[2])
|
||||||
|
|
||||||
// Key W=Windows; A=All; U=Unix
|
// Key W=Windows; A=All; U=Unix
|
||||||
if (words[0] != "W") && (words[0] != "A") && (words[0] != "U") {
|
if platform != "W" && platform != "A" && platform != "U" {
|
||||||
t.Fatalf("Invalid tag %s at line %d of %s. Must be W, A or U", words[0], lineCount, fn)
|
t.Fatalf("Invalid tag %s at line %d of %s. Must be W, A or U", platform, lineCount, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((words[0] == "W" || words[0] == "A") && runtime.GOOS == "windows") ||
|
if ((platform == "W" || platform == "A") && runtime.GOOS == "windows") ||
|
||||||
((words[0] == "U" || words[0] == "A") && runtime.GOOS != "windows") {
|
((platform == "U" || platform == "A") && runtime.GOOS != "windows") {
|
||||||
newWord, err := ProcessWord(words[1], envs, '\\')
|
newWord, err := ProcessWord(source, envs, '\\')
|
||||||
|
if expected == "error" {
|
||||||
if err != nil {
|
assert.Error(t, err, "")
|
||||||
newWord = "error"
|
} else {
|
||||||
}
|
assert.NilError(t, err)
|
||||||
|
assert.DeepEqual(t, newWord, []string{expected})
|
||||||
if newWord != words[2] {
|
|
||||||
t.Fatalf("Error. Src: %s Calc: %s Expected: %s at line %d", words[1], newWord, words[2], lineCount)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue