Browse code

Merge pull request #31750 from dnephin/some-builder-cleanup

Fix `docker build --label` when the label includes single quotes and a space

Tõnis Tiigi authored on 2017/04/05 01:37:00
Showing 10 changed files
... ...
@@ -7,7 +7,6 @@ import (
7 7
 	"io"
8 8
 	"io/ioutil"
9 9
 	"os"
10
-	"sort"
11 10
 	"strings"
12 11
 
13 12
 	"github.com/Sirupsen/logrus"
... ...
@@ -209,26 +208,13 @@ func sanitizeRepoAndTags(names []string) ([]reference.Named, error) {
209 209
 	return repoAndTags, nil
210 210
 }
211 211
 
212
-func (b *Builder) processLabels() error {
212
+func (b *Builder) processLabels() {
213 213
 	if len(b.options.Labels) == 0 {
214
-		return nil
214
+		return
215 215
 	}
216 216
 
217
-	var labels []string
218
-	for k, v := range b.options.Labels {
219
-		labels = append(labels, fmt.Sprintf("%q='%s'", k, v))
220
-	}
221
-	// Sort the label to have a repeatable order
222
-	sort.Strings(labels)
223
-
224
-	line := "LABEL " + strings.Join(labels, " ")
225
-	_, node, err := parser.ParseLine(line, &b.directive, false)
226
-	if err != nil {
227
-		return err
228
-	}
217
+	node := parser.NodeFromLabels(b.options.Labels)
229 218
 	b.dockerfile.Children = append(b.dockerfile.Children, node)
230
-
231
-	return nil
232 219
 }
233 220
 
234 221
 // 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
263 263
 		return "", err
264 264
 	}
265 265
 
266
-	if err := b.processLabels(); err != nil {
267
-		return "", err
268
-	}
266
+	b.processLabels()
269 267
 
270 268
 	var shortImgID string
271 269
 	total := len(b.dockerfile.Children)
... ...
@@ -8,6 +8,7 @@ import (
8 8
 	"github.com/docker/docker/api/types"
9 9
 	"github.com/docker/docker/api/types/container"
10 10
 	"github.com/docker/docker/builder/dockerfile/parser"
11
+	"github.com/docker/docker/pkg/testutil/assert"
11 12
 )
12 13
 
13 14
 func TestBuildProcessLabels(t *testing.T) {
... ...
@@ -15,9 +16,7 @@ func TestBuildProcessLabels(t *testing.T) {
15 15
 	d := parser.Directive{}
16 16
 	parser.SetEscapeToken(parser.DefaultEscapeToken, &d)
17 17
 	n, err := parser.Parse(strings.NewReader(dockerfile), &d)
18
-	if err != nil {
19
-		t.Fatalf("Error when parsing Dockerfile: %s", err)
20
-	}
18
+	assert.NilError(t, err)
21 19
 
22 20
 	options := &types.ImageBuildOptions{
23 21
 		Labels: map[string]string{
... ...
@@ -34,21 +33,14 @@ func TestBuildProcessLabels(t *testing.T) {
34 34
 		directive:  d,
35 35
 		dockerfile: n,
36 36
 	}
37
-	err = b.processLabels()
38
-	if err != nil {
39
-		t.Fatalf("Error when processing labels: %s", err)
40
-	}
37
+	b.processLabels()
41 38
 
42 39
 	expected := []string{
43 40
 		"FROM scratch",
44 41
 		`LABEL "org.a"='cli-a' "org.b"='cli-b' "org.c"='cli-c' "org.d"='cli-d' "org.e"='cli-e'`,
45 42
 	}
46
-	if len(b.dockerfile.Children) != 2 {
47
-		t.Fatalf("Expect 2, got %d", len(b.dockerfile.Children))
48
-	}
43
+	assert.Equal(t, len(b.dockerfile.Children), 2)
49 44
 	for i, v := range b.dockerfile.Children {
50
-		if v.Original != expected[i] {
51
-			t.Fatalf("Expect '%s' for %dth children, got, '%s'", expected[i], i, v.Original)
52
-		}
45
+		assert.Equal(t, v.Original, expected[i])
53 46
 	}
54 47
 }
... ...
@@ -129,47 +129,18 @@ func (b *Builder) dispatch(stepN int, stepTotal int, ast *parser.Node) error {
129 129
 
130 130
 	}
131 131
 
132
-	// count the number of nodes that we are going to traverse first
133
-	// so we can pre-create the argument and message array. This speeds up the
134
-	// allocation of those list a lot when they have a lot of arguments
135
-	cursor := ast
136
-	var n int
137
-	for cursor.Next != nil {
138
-		cursor = cursor.Next
139
-		n++
140
-	}
141
-	msgList := make([]string, n)
142
-
143
-	var i int
132
+	msgList := initMsgList(ast)
144 133
 	// Append build args to runConfig environment variables
145 134
 	envs := append(b.runConfig.Env, b.buildArgsWithoutConfigEnv()...)
146 135
 
147
-	for ast.Next != nil {
136
+	for i := 0; ast.Next != nil; i++ {
148 137
 		ast = ast.Next
149
-		var str string
150
-		str = ast.Value
151
-		if replaceEnvAllowed[cmd] {
152
-			var err error
153
-			var words []string
154
-
155
-			if allowWordExpansion[cmd] {
156
-				words, err = ProcessWords(str, envs, b.directive.EscapeToken)
157
-				if err != nil {
158
-					return err
159
-				}
160
-				strList = append(strList, words...)
161
-			} else {
162
-				str, err = ProcessWord(str, envs, b.directive.EscapeToken)
163
-				if err != nil {
164
-					return err
165
-				}
166
-				strList = append(strList, str)
167
-			}
168
-		} else {
169
-			strList = append(strList, str)
138
+		words, err := b.evaluateEnv(cmd, ast.Value, envs)
139
+		if err != nil {
140
+			return err
170 141
 		}
142
+		strList = append(strList, words...)
171 143
 		msgList[i] = ast.Value
172
-		i++
173 144
 	}
174 145
 
175 146
 	msg += " " + strings.Join(msgList, " ")
... ...
@@ -186,6 +157,29 @@ func (b *Builder) dispatch(stepN int, stepTotal int, ast *parser.Node) error {
186 186
 	return fmt.Errorf("Unknown instruction: %s", upperCasedCmd)
187 187
 }
188 188
 
189
+// count the number of nodes that we are going to traverse first
190
+// allocation of those list a lot when they have a lot of arguments
191
+func initMsgList(cursor *parser.Node) []string {
192
+	var n int
193
+	for ; cursor.Next != nil; n++ {
194
+		cursor = cursor.Next
195
+	}
196
+	return make([]string, n)
197
+}
198
+
199
+func (b *Builder) evaluateEnv(cmd string, str string, envs []string) ([]string, error) {
200
+	if !replaceEnvAllowed[cmd] {
201
+		return []string{str}, nil
202
+	}
203
+	var processFunc func(string, []string, rune) ([]string, error)
204
+	if allowWordExpansion[cmd] {
205
+		processFunc = ProcessWords
206
+	} else {
207
+		processFunc = ProcessWord
208
+	}
209
+	return processFunc(str, envs, b.directive.EscapeToken)
210
+}
211
+
189 212
 // buildArgsWithoutConfigEnv returns a list of key=value pairs for all the build
190 213
 // args that are not overriden by runConfig environment variables.
191 214
 func (b *Builder) buildArgsWithoutConfigEnv() []string {
... ...
@@ -10,15 +10,22 @@ import (
10 10
 	"encoding/json"
11 11
 	"errors"
12 12
 	"fmt"
13
+	"sort"
13 14
 	"strings"
14 15
 	"unicode"
15 16
 	"unicode/utf8"
17
+
18
+	"github.com/docker/docker/builder/dockerfile/command"
16 19
 )
17 20
 
18 21
 var (
19 22
 	errDockerfileNotStringArray = errors.New("When using JSON array syntax, arrays must be comprised of strings only.")
20 23
 )
21 24
 
25
+const (
26
+	commandLabel = "LABEL"
27
+)
28
+
22 29
 // ignore the current argument. This will still leave a command parsed, but
23 30
 // will not incorporate the arguments into the ast.
24 31
 func parseIgnore(rest string, d *Directive) (*Node, map[string]bool, error) {
... ...
@@ -133,7 +140,7 @@ func parseWords(rest string, d *Directive) []string {
133 133
 
134 134
 // parse environment like statements. Note that this does *not* handle
135 135
 // variable interpolation, which will be handled in the evaluator.
136
-func parseNameVal(rest string, key string, d *Directive) (*Node, map[string]bool, error) {
136
+func parseNameVal(rest string, key string, d *Directive) (*Node, error) {
137 137
 	// This is kind of tricky because we need to support the old
138 138
 	// variant:   KEY name value
139 139
 	// 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
142 142
 
143 143
 	words := parseWords(rest, d)
144 144
 	if len(words) == 0 {
145
-		return nil, nil, nil
145
+		return nil, nil
146 146
 	}
147 147
 
148
-	var rootnode *Node
149
-
150 148
 	// Old format (KEY name value)
151 149
 	if !strings.Contains(words[0], "=") {
152
-		node := &Node{}
153
-		rootnode = node
154
-		strs := tokenWhitespace.Split(rest, 2)
150
+		parts := tokenWhitespace.Split(rest, 2)
151
+		if len(parts) < 2 {
152
+			return nil, fmt.Errorf(key + " must have two arguments")
153
+		}
154
+		return newKeyValueNode(parts[0], parts[1]), nil
155
+	}
155 156
 
156
-		if len(strs) < 2 {
157
-			return nil, nil, fmt.Errorf(key + " must have two arguments")
157
+	var rootNode *Node
158
+	var prevNode *Node
159
+	for _, word := range words {
160
+		if !strings.Contains(word, "=") {
161
+			return nil, fmt.Errorf("Syntax error - can't find = in %q. Must be of the form: name=value", word)
158 162
 		}
159 163
 
160
-		node.Value = strs[0]
161
-		node.Next = &Node{}
162
-		node.Next.Value = strs[1]
163
-	} else {
164
-		var prevNode *Node
165
-		for i, word := range words {
166
-			if !strings.Contains(word, "=") {
167
-				return nil, nil, fmt.Errorf("Syntax error - can't find = in %q. Must be of the form: name=value", word)
168
-			}
169
-			parts := strings.SplitN(word, "=", 2)
164
+		parts := strings.SplitN(word, "=", 2)
165
+		node := newKeyValueNode(parts[0], parts[1])
166
+		rootNode, prevNode = appendKeyValueNode(node, rootNode, prevNode)
167
+	}
170 168
 
171
-			name := &Node{}
172
-			value := &Node{}
169
+	return rootNode, nil
170
+}
173 171
 
174
-			name.Next = value
175
-			name.Value = parts[0]
176
-			value.Value = parts[1]
172
+func newKeyValueNode(key, value string) *Node {
173
+	return &Node{
174
+		Value: key,
175
+		Next:  &Node{Value: value},
176
+	}
177
+}
177 178
 
178
-			if i == 0 {
179
-				rootnode = name
180
-			} else {
181
-				prevNode.Next = name
182
-			}
183
-			prevNode = value
184
-		}
179
+func appendKeyValueNode(node, rootNode, prevNode *Node) (*Node, *Node) {
180
+	if rootNode == nil {
181
+		rootNode = node
182
+	}
183
+	if prevNode != nil {
184
+		prevNode.Next = node
185 185
 	}
186 186
 
187
-	return rootnode, nil, nil
187
+	prevNode = node.Next
188
+	return rootNode, prevNode
188 189
 }
189 190
 
190 191
 func parseEnv(rest string, d *Directive) (*Node, map[string]bool, error) {
191
-	return parseNameVal(rest, "ENV", d)
192
+	node, err := parseNameVal(rest, "ENV", d)
193
+	return node, nil, err
192 194
 }
193 195
 
194 196
 func parseLabel(rest string, d *Directive) (*Node, map[string]bool, error) {
195
-	return parseNameVal(rest, "LABEL", d)
197
+	node, err := parseNameVal(rest, commandLabel, d)
198
+	return node, nil, err
199
+}
200
+
201
+// NodeFromLabels returns a Node for the injected labels
202
+func NodeFromLabels(labels map[string]string) *Node {
203
+	keys := []string{}
204
+	for key := range labels {
205
+		keys = append(keys, key)
206
+	}
207
+	// Sort the label to have a repeatable order
208
+	sort.Strings(keys)
209
+
210
+	labelPairs := []string{}
211
+	var rootNode *Node
212
+	var prevNode *Node
213
+	for _, key := range keys {
214
+		value := labels[key]
215
+		labelPairs = append(labelPairs, fmt.Sprintf("%q='%s'", key, value))
216
+		// Value must be single quoted to prevent env variable expansion
217
+		// See https://github.com/docker/docker/issues/26027
218
+		node := newKeyValueNode(key, "'"+value+"'")
219
+		rootNode, prevNode = appendKeyValueNode(node, rootNode, prevNode)
220
+	}
221
+
222
+	return &Node{
223
+		Value:    command.Label,
224
+		Original: commandLabel + " " + strings.Join(labelPairs, " "),
225
+		Next:     rootNode,
226
+	}
196 227
 }
197 228
 
198 229
 // parses a statement containing one or more keyword definition(s) and/or
199 230
new file mode 100644
... ...
@@ -0,0 +1,65 @@
0
+package parser
1
+
2
+import (
3
+	"github.com/docker/docker/pkg/testutil/assert"
4
+	"testing"
5
+)
6
+
7
+func TestParseNameValOldFormat(t *testing.T) {
8
+	directive := Directive{}
9
+	node, err := parseNameVal("foo bar", "LABEL", &directive)
10
+	assert.NilError(t, err)
11
+
12
+	expected := &Node{
13
+		Value: "foo",
14
+		Next:  &Node{Value: "bar"},
15
+	}
16
+	assert.DeepEqual(t, node, expected)
17
+}
18
+
19
+func TestParseNameValNewFormat(t *testing.T) {
20
+	directive := Directive{}
21
+	node, err := parseNameVal("foo=bar thing=star", "LABEL", &directive)
22
+	assert.NilError(t, err)
23
+
24
+	expected := &Node{
25
+		Value: "foo",
26
+		Next: &Node{
27
+			Value: "bar",
28
+			Next: &Node{
29
+				Value: "thing",
30
+				Next: &Node{
31
+					Value: "star",
32
+				},
33
+			},
34
+		},
35
+	}
36
+	assert.DeepEqual(t, node, expected)
37
+}
38
+
39
+func TestNodeFromLabels(t *testing.T) {
40
+	labels := map[string]string{
41
+		"foo":   "bar",
42
+		"weird": "first' second",
43
+	}
44
+	expected := &Node{
45
+		Value:    "label",
46
+		Original: `LABEL "foo"='bar' "weird"='first' second'`,
47
+		Next: &Node{
48
+			Value: "foo",
49
+			Next: &Node{
50
+				Value: "'bar'",
51
+				Next: &Node{
52
+					Value: "weird",
53
+					Next: &Node{
54
+						Value: "'first' second'",
55
+					},
56
+				},
57
+			},
58
+		},
59
+	}
60
+
61
+	node := NodeFromLabels(labels)
62
+	assert.DeepEqual(t, node, expected)
63
+
64
+}
... ...
@@ -7,6 +7,7 @@ import (
7 7
 	"fmt"
8 8
 	"io"
9 9
 	"regexp"
10
+	"strconv"
10 11
 	"strings"
11 12
 	"unicode"
12 13
 
... ...
@@ -36,6 +37,31 @@ type Node struct {
36 36
 	EndLine    int             // the line in the original dockerfile where the node ends
37 37
 }
38 38
 
39
+// Dump dumps the AST defined by `node` as a list of sexps.
40
+// Returns a string suitable for printing.
41
+func (node *Node) Dump() string {
42
+	str := ""
43
+	str += node.Value
44
+
45
+	if len(node.Flags) > 0 {
46
+		str += fmt.Sprintf(" %q", node.Flags)
47
+	}
48
+
49
+	for _, n := range node.Children {
50
+		str += "(" + n.Dump() + ")\n"
51
+	}
52
+
53
+	for n := node.Next; n != nil; n = n.Next {
54
+		if len(n.Children) > 0 {
55
+			str += " " + n.Dump()
56
+		} else {
57
+			str += " " + strconv.Quote(n.Value)
58
+		}
59
+	}
60
+
61
+	return strings.TrimSpace(str)
62
+}
63
+
39 64
 // Directive is the structure used during a build run to hold the state of
40 65
 // parsing directives.
41 66
 type Directive struct {
... ...
@@ -96,24 +122,9 @@ func init() {
96 96
 
97 97
 // ParseLine parses a line and returns the remainder.
98 98
 func ParseLine(line string, d *Directive, ignoreCont bool) (string, *Node, error) {
99
-	// Handle the parser directive '# escape=<char>. Parser directives must precede
100
-	// any builder instruction or other comments, and cannot be repeated.
101
-	if d.LookingForDirectives {
102
-		tecMatch := tokenEscapeCommand.FindStringSubmatch(strings.ToLower(line))
103
-		if len(tecMatch) > 0 {
104
-			if d.EscapeSeen == true {
105
-				return "", nil, fmt.Errorf("only one escape parser directive can be used")
106
-			}
107
-			for i, n := range tokenEscapeCommand.SubexpNames() {
108
-				if n == "escapechar" {
109
-					if err := SetEscapeToken(tecMatch[i], d); err != nil {
110
-						return "", nil, err
111
-					}
112
-					d.EscapeSeen = true
113
-					return "", nil, nil
114
-				}
115
-			}
116
-		}
99
+	if escapeFound, err := handleParserDirective(line, d); err != nil || escapeFound {
100
+		d.EscapeSeen = escapeFound
101
+		return "", nil, err
117 102
 	}
118 103
 
119 104
 	d.LookingForDirectives = false
... ...
@@ -127,25 +138,60 @@ func ParseLine(line string, d *Directive, ignoreCont bool) (string, *Node, error
127 127
 		return line, nil, nil
128 128
 	}
129 129
 
130
+	node, err := newNodeFromLine(line, d)
131
+	return "", node, err
132
+}
133
+
134
+// newNodeFromLine splits the line into parts, and dispatches to a function
135
+// based on the command and command arguments. A Node is created from the
136
+// result of the dispatch.
137
+func newNodeFromLine(line string, directive *Directive) (*Node, error) {
130 138
 	cmd, flags, args, err := splitCommand(line)
131 139
 	if err != nil {
132
-		return "", nil, err
140
+		return nil, err
133 141
 	}
134 142
 
135
-	node := &Node{}
136
-	node.Value = cmd
137
-
138
-	sexp, attrs, err := fullDispatch(cmd, args, d)
143
+	fn := dispatch[cmd]
144
+	// Ignore invalid Dockerfile instructions
145
+	if fn == nil {
146
+		fn = parseIgnore
147
+	}
148
+	next, attrs, err := fn(args, directive)
139 149
 	if err != nil {
140
-		return "", nil, err
150
+		return nil, err
141 151
 	}
142 152
 
143
-	node.Next = sexp
144
-	node.Attributes = attrs
145
-	node.Original = line
146
-	node.Flags = flags
153
+	return &Node{
154
+		Value:      cmd,
155
+		Original:   line,
156
+		Flags:      flags,
157
+		Next:       next,
158
+		Attributes: attrs,
159
+	}, nil
160
+}
147 161
 
148
-	return "", node, nil
162
+// Handle the parser directive '# escape=<char>. Parser directives must precede
163
+// any builder instruction or other comments, and cannot be repeated.
164
+func handleParserDirective(line string, d *Directive) (bool, error) {
165
+	if !d.LookingForDirectives {
166
+		return false, nil
167
+	}
168
+	tecMatch := tokenEscapeCommand.FindStringSubmatch(strings.ToLower(line))
169
+	if len(tecMatch) == 0 {
170
+		return false, nil
171
+	}
172
+	if d.EscapeSeen == true {
173
+		return false, fmt.Errorf("only one escape parser directive can be used")
174
+	}
175
+	for i, n := range tokenEscapeCommand.SubexpNames() {
176
+		if n == "escapechar" {
177
+			if err := SetEscapeToken(tecMatch[i], d); err != nil {
178
+				return false, err
179
+			}
180
+			return true, nil
181
+		}
182
+	}
183
+	return false, nil
149 184
 }
150 185
 
151 186
 // Parse is the main parse routine.
... ...
@@ -219,3 +265,14 @@ func Parse(rwc io.Reader, d *Directive) (*Node, error) {
219 219
 
220 220
 	return root, nil
221 221
 }
222
+
223
+// covers comments and empty lines. Lines should be trimmed before passing to
224
+// this function.
225
+func stripComments(line string) string {
226
+	// string is already trimmed at this point
227
+	if tokenComment.MatchString(line) {
228
+		return tokenComment.ReplaceAllString(line, "")
229
+	}
230
+
231
+	return line
232
+}
222 233
new file mode 100644
... ...
@@ -0,0 +1,118 @@
0
+package parser
1
+
2
+import (
3
+	"strings"
4
+	"unicode"
5
+)
6
+
7
+// splitCommand takes a single line of text and parses out the cmd and args,
8
+// which are used for dispatching to more exact parsing functions.
9
+func splitCommand(line string) (string, []string, string, error) {
10
+	var args string
11
+	var flags []string
12
+
13
+	// Make sure we get the same results irrespective of leading/trailing spaces
14
+	cmdline := tokenWhitespace.Split(strings.TrimSpace(line), 2)
15
+	cmd := strings.ToLower(cmdline[0])
16
+
17
+	if len(cmdline) == 2 {
18
+		var err error
19
+		args, flags, err = extractBuilderFlags(cmdline[1])
20
+		if err != nil {
21
+			return "", nil, "", err
22
+		}
23
+	}
24
+
25
+	return cmd, flags, strings.TrimSpace(args), nil
26
+}
27
+
28
+func extractBuilderFlags(line string) (string, []string, error) {
29
+	// Parses the BuilderFlags and returns the remaining part of the line
30
+
31
+	const (
32
+		inSpaces = iota // looking for start of a word
33
+		inWord
34
+		inQuote
35
+	)
36
+
37
+	words := []string{}
38
+	phase := inSpaces
39
+	word := ""
40
+	quote := '\000'
41
+	blankOK := false
42
+	var ch rune
43
+
44
+	for pos := 0; pos <= len(line); pos++ {
45
+		if pos != len(line) {
46
+			ch = rune(line[pos])
47
+		}
48
+
49
+		if phase == inSpaces { // Looking for start of word
50
+			if pos == len(line) { // end of input
51
+				break
52
+			}
53
+			if unicode.IsSpace(ch) { // skip spaces
54
+				continue
55
+			}
56
+
57
+			// Only keep going if the next word starts with --
58
+			if ch != '-' || pos+1 == len(line) || rune(line[pos+1]) != '-' {
59
+				return line[pos:], words, nil
60
+			}
61
+
62
+			phase = inWord // found something with "--", fall through
63
+		}
64
+		if (phase == inWord || phase == inQuote) && (pos == len(line)) {
65
+			if word != "--" && (blankOK || len(word) > 0) {
66
+				words = append(words, word)
67
+			}
68
+			break
69
+		}
70
+		if phase == inWord {
71
+			if unicode.IsSpace(ch) {
72
+				phase = inSpaces
73
+				if word == "--" {
74
+					return line[pos:], words, nil
75
+				}
76
+				if blankOK || len(word) > 0 {
77
+					words = append(words, word)
78
+				}
79
+				word = ""
80
+				blankOK = false
81
+				continue
82
+			}
83
+			if ch == '\'' || ch == '"' {
84
+				quote = ch
85
+				blankOK = true
86
+				phase = inQuote
87
+				continue
88
+			}
89
+			if ch == '\\' {
90
+				if pos+1 == len(line) {
91
+					continue // just skip \ at end
92
+				}
93
+				pos++
94
+				ch = rune(line[pos])
95
+			}
96
+			word += string(ch)
97
+			continue
98
+		}
99
+		if phase == inQuote {
100
+			if ch == quote {
101
+				phase = inWord
102
+				continue
103
+			}
104
+			if ch == '\\' {
105
+				if pos+1 == len(line) {
106
+					phase = inWord
107
+					continue // just skip \ at end
108
+				}
109
+				pos++
110
+				ch = rune(line[pos])
111
+			}
112
+			word += string(ch)
113
+		}
114
+	}
115
+
116
+	return "", words, nil
117
+}
0 118
deleted file mode 100644
... ...
@@ -1,174 +0,0 @@
1
-package parser
2
-
3
-import (
4
-	"fmt"
5
-	"strconv"
6
-	"strings"
7
-	"unicode"
8
-)
9
-
10
-// Dump dumps the AST defined by `node` as a list of sexps.
11
-// Returns a string suitable for printing.
12
-func (node *Node) Dump() string {
13
-	str := ""
14
-	str += node.Value
15
-
16
-	if len(node.Flags) > 0 {
17
-		str += fmt.Sprintf(" %q", node.Flags)
18
-	}
19
-
20
-	for _, n := range node.Children {
21
-		str += "(" + n.Dump() + ")\n"
22
-	}
23
-
24
-	for n := node.Next; n != nil; n = n.Next {
25
-		if len(n.Children) > 0 {
26
-			str += " " + n.Dump()
27
-		} else {
28
-			str += " " + strconv.Quote(n.Value)
29
-		}
30
-	}
31
-
32
-	return strings.TrimSpace(str)
33
-}
34
-
35
-// performs the dispatch based on the two primal strings, cmd and args. Please
36
-// look at the dispatch table in parser.go to see how these dispatchers work.
37
-func fullDispatch(cmd, args string, d *Directive) (*Node, map[string]bool, error) {
38
-	fn := dispatch[cmd]
39
-
40
-	// Ignore invalid Dockerfile instructions
41
-	if fn == nil {
42
-		fn = parseIgnore
43
-	}
44
-
45
-	sexp, attrs, err := fn(args, d)
46
-	if err != nil {
47
-		return nil, nil, err
48
-	}
49
-
50
-	return sexp, attrs, nil
51
-}
52
-
53
-// splitCommand takes a single line of text and parses out the cmd and args,
54
-// which are used for dispatching to more exact parsing functions.
55
-func splitCommand(line string) (string, []string, string, error) {
56
-	var args string
57
-	var flags []string
58
-
59
-	// Make sure we get the same results irrespective of leading/trailing spaces
60
-	cmdline := tokenWhitespace.Split(strings.TrimSpace(line), 2)
61
-	cmd := strings.ToLower(cmdline[0])
62
-
63
-	if len(cmdline) == 2 {
64
-		var err error
65
-		args, flags, err = extractBuilderFlags(cmdline[1])
66
-		if err != nil {
67
-			return "", nil, "", err
68
-		}
69
-	}
70
-
71
-	return cmd, flags, strings.TrimSpace(args), nil
72
-}
73
-
74
-// covers comments and empty lines. Lines should be trimmed before passing to
75
-// this function.
76
-func stripComments(line string) string {
77
-	// string is already trimmed at this point
78
-	if tokenComment.MatchString(line) {
79
-		return tokenComment.ReplaceAllString(line, "")
80
-	}
81
-
82
-	return line
83
-}
84
-
85
-func extractBuilderFlags(line string) (string, []string, error) {
86
-	// Parses the BuilderFlags and returns the remaining part of the line
87
-
88
-	const (
89
-		inSpaces = iota // looking for start of a word
90
-		inWord
91
-		inQuote
92
-	)
93
-
94
-	words := []string{}
95
-	phase := inSpaces
96
-	word := ""
97
-	quote := '\000'
98
-	blankOK := false
99
-	var ch rune
100
-
101
-	for pos := 0; pos <= len(line); pos++ {
102
-		if pos != len(line) {
103
-			ch = rune(line[pos])
104
-		}
105
-
106
-		if phase == inSpaces { // Looking for start of word
107
-			if pos == len(line) { // end of input
108
-				break
109
-			}
110
-			if unicode.IsSpace(ch) { // skip spaces
111
-				continue
112
-			}
113
-
114
-			// Only keep going if the next word starts with --
115
-			if ch != '-' || pos+1 == len(line) || rune(line[pos+1]) != '-' {
116
-				return line[pos:], words, nil
117
-			}
118
-
119
-			phase = inWord // found something with "--", fall through
120
-		}
121
-		if (phase == inWord || phase == inQuote) && (pos == len(line)) {
122
-			if word != "--" && (blankOK || len(word) > 0) {
123
-				words = append(words, word)
124
-			}
125
-			break
126
-		}
127
-		if phase == inWord {
128
-			if unicode.IsSpace(ch) {
129
-				phase = inSpaces
130
-				if word == "--" {
131
-					return line[pos:], words, nil
132
-				}
133
-				if blankOK || len(word) > 0 {
134
-					words = append(words, word)
135
-				}
136
-				word = ""
137
-				blankOK = false
138
-				continue
139
-			}
140
-			if ch == '\'' || ch == '"' {
141
-				quote = ch
142
-				blankOK = true
143
-				phase = inQuote
144
-				continue
145
-			}
146
-			if ch == '\\' {
147
-				if pos+1 == len(line) {
148
-					continue // just skip \ at end
149
-				}
150
-				pos++
151
-				ch = rune(line[pos])
152
-			}
153
-			word += string(ch)
154
-			continue
155
-		}
156
-		if phase == inQuote {
157
-			if ch == quote {
158
-				phase = inWord
159
-				continue
160
-			}
161
-			if ch == '\\' {
162
-				if pos+1 == len(line) {
163
-					phase = inWord
164
-					continue // just skip \ at end
165
-				}
166
-				pos++
167
-				ch = rune(line[pos])
168
-			}
169
-			word += string(ch)
170
-		}
171
-	}
172
-
173
-	return "", words, nil
174
-}
... ...
@@ -24,16 +24,9 @@ type shellWord struct {
24 24
 
25 25
 // ProcessWord will use the 'env' list of environment variables,
26 26
 // and replace any env var references in 'word'.
27
-func ProcessWord(word string, env []string, escapeToken rune) (string, error) {
28
-	sw := &shellWord{
29
-		word:        word,
30
-		envs:        env,
31
-		pos:         0,
32
-		escapeToken: escapeToken,
33
-	}
34
-	sw.scanner.Init(strings.NewReader(word))
35
-	word, _, err := sw.process()
36
-	return word, err
27
+func ProcessWord(word string, env []string, escapeToken rune) ([]string, error) {
28
+	word, _, err := process(word, env, escapeToken)
29
+	return []string{word}, err
37 30
 }
38 31
 
39 32
 // ProcessWords will use the 'env' list of environment variables,
... ...
@@ -44,6 +37,11 @@ func ProcessWord(word string, env []string, escapeToken rune) (string, error) {
44 44
 // Note, each one is trimmed to remove leading and trailing spaces (unless
45 45
 // they are quoted", but ProcessWord retains spaces between words.
46 46
 func ProcessWords(word string, env []string, escapeToken rune) ([]string, error) {
47
+	_, words, err := process(word, env, escapeToken)
48
+	return words, err
49
+}
50
+
51
+func process(word string, env []string, escapeToken rune) (string, []string, error) {
47 52
 	sw := &shellWord{
48 53
 		word:        word,
49 54
 		envs:        env,
... ...
@@ -51,8 +49,7 @@ func ProcessWords(word string, env []string, escapeToken rune) ([]string, error)
51 51
 		escapeToken: escapeToken,
52 52
 	}
53 53
 	sw.scanner.Init(strings.NewReader(word))
54
-	_, words, err := sw.process()
55
-	return words, err
54
+	return sw.process()
56 55
 }
57 56
 
58 57
 func (sw *shellWord) process() (string, []string, error) {
... ...
@@ -6,6 +6,8 @@ import (
6 6
 	"runtime"
7 7
 	"strings"
8 8
 	"testing"
9
+
10
+	"github.com/docker/docker/pkg/testutil/assert"
9 11
 )
10 12
 
11 13
 func TestShellParser4EnvVars(t *testing.T) {
... ...
@@ -13,9 +15,7 @@ func TestShellParser4EnvVars(t *testing.T) {
13 13
 	lineCount := 0
14 14
 
15 15
 	file, err := os.Open(fn)
16
-	if err != nil {
17
-		t.Fatalf("Can't open '%s': %s", err, fn)
18
-	}
16
+	assert.NilError(t, err)
19 17
 	defer file.Close()
20 18
 
21 19
 	scanner := bufio.NewScanner(file)
... ...
@@ -36,29 +36,25 @@ func TestShellParser4EnvVars(t *testing.T) {
36 36
 		}
37 37
 
38 38
 		words := strings.Split(line, "|")
39
-		if len(words) != 3 {
40
-			t.Fatalf("Error in '%s' - should be exactly one | in:%q", fn, line)
41
-		}
39
+		assert.Equal(t, len(words), 3)
42 40
 
43
-		words[0] = strings.TrimSpace(words[0])
44
-		words[1] = strings.TrimSpace(words[1])
45
-		words[2] = strings.TrimSpace(words[2])
41
+		platform := strings.TrimSpace(words[0])
42
+		source := strings.TrimSpace(words[1])
43
+		expected := strings.TrimSpace(words[2])
46 44
 
47 45
 		// Key W=Windows; A=All; U=Unix
48
-		if (words[0] != "W") && (words[0] != "A") && (words[0] != "U") {
49
-			t.Fatalf("Invalid tag %s at line %d of %s. Must be W, A or U", words[0], lineCount, fn)
46
+		if platform != "W" && platform != "A" && platform != "U" {
47
+			t.Fatalf("Invalid tag %s at line %d of %s. Must be W, A or U", platform, lineCount, fn)
50 48
 		}
51 49
 
52
-		if ((words[0] == "W" || words[0] == "A") && runtime.GOOS == "windows") ||
53
-			((words[0] == "U" || words[0] == "A") && runtime.GOOS != "windows") {
54
-			newWord, err := ProcessWord(words[1], envs, '\\')
55
-
56
-			if err != nil {
57
-				newWord = "error"
58
-			}
59
-
60
-			if newWord != words[2] {
61
-				t.Fatalf("Error. Src: %s  Calc: %s  Expected: %s at line %d", words[1], newWord, words[2], lineCount)
50
+		if ((platform == "W" || platform == "A") && runtime.GOOS == "windows") ||
51
+			((platform == "U" || platform == "A") && runtime.GOOS != "windows") {
52
+			newWord, err := ProcessWord(source, envs, '\\')
53
+			if expected == "error" {
54
+				assert.Error(t, err, "")
55
+			} else {
56
+				assert.NilError(t, err)
57
+				assert.DeepEqual(t, newWord, []string{expected})
62 58
 			}
63 59
 		}
64 60
 	}