Browse code

Merge pull request #32858 from dnephin/builder-shell-words-interface

[Builder] Remove b.escapeToken, create ShellLex

Daniel Nephin authored on 2017/04/28 02:16:40
Showing 7 changed files
... ...
@@ -61,7 +61,6 @@ type Builder struct {
61 61
 	imageCache    builder.ImageCache
62 62
 
63 63
 	// TODO: these move to DispatchState
64
-	escapeToken rune
65 64
 	maintainer  string
66 65
 	cmdSet      bool
67 66
 	noBaseImage bool   // A flag to track the use of `scratch` as the base image
... ...
@@ -125,7 +124,6 @@ func NewBuilder(clientCtx context.Context, config *types.ImageBuildOptions, back
125 125
 		runConfig:     new(container.Config),
126 126
 		tmpContainers: map[string]struct{}{},
127 127
 		buildArgs:     newBuildArgs(config.BuildArgs),
128
-		escapeToken:   parser.DefaultEscapeToken,
129 128
 	}
130 129
 	b.imageContexts = &imageContexts{b: b}
131 130
 	return b, nil
... ...
@@ -219,8 +217,7 @@ func (b *Builder) build(dockerfile *parser.Result, stdout io.Writer, stderr io.W
219 219
 }
220 220
 
221 221
 func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result) (string, error) {
222
-	// TODO: pass this to dispatchRequest instead
223
-	b.escapeToken = dockerfile.EscapeToken
222
+	shlex := NewShellLex(dockerfile.EscapeToken)
224 223
 
225 224
 	total := len(dockerfile.AST.Children)
226 225
 	var imageID string
... ...
@@ -238,7 +235,7 @@ func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result)
238 238
 			break
239 239
 		}
240 240
 
241
-		if err := b.dispatch(i, total, n); err != nil {
241
+		if err := b.dispatch(i, total, n, shlex); err != nil {
242 242
 			if b.options.ForceRemove {
243 243
 				b.clearTmp()
244 244
 			}
... ...
@@ -361,13 +358,12 @@ func checkDispatchDockerfile(dockerfile *parser.Node) error {
361 361
 }
362 362
 
363 363
 func dispatchFromDockerfile(b *Builder, result *parser.Result) error {
364
-	// TODO: pass this to dispatchRequest instead
365
-	b.escapeToken = result.EscapeToken
364
+	shlex := NewShellLex(result.EscapeToken)
366 365
 	ast := result.AST
367 366
 	total := len(ast.Children)
368 367
 
369 368
 	for i, n := range ast.Children {
370
-		if err := b.dispatch(i, total, n); err != nil {
369
+		if err := b.dispatch(i, total, n, shlex); err != nil {
371 370
 			return err
372 371
 		}
373 372
 	}
... ...
@@ -194,7 +194,7 @@ func from(req dispatchRequest) error {
194 194
 		return err
195 195
 	}
196 196
 
197
-	image, err := req.builder.getFromImage(req.args[0])
197
+	image, err := req.builder.getFromImage(req.shlex, req.args[0])
198 198
 	if err != nil {
199 199
 		return err
200 200
 	}
... ...
@@ -222,13 +222,13 @@ func parseBuildStageName(args []string) (string, error) {
222 222
 	return stageName, nil
223 223
 }
224 224
 
225
-func (b *Builder) getFromImage(name string) (builder.Image, error) {
225
+func (b *Builder) getFromImage(shlex *ShellLex, name string) (builder.Image, error) {
226 226
 	substitutionArgs := []string{}
227 227
 	for key, value := range b.buildArgs.GetAllMeta() {
228 228
 		substitutionArgs = append(substitutionArgs, key+"="+value)
229 229
 	}
230 230
 
231
-	name, err := ProcessWord(name, substitutionArgs, b.escapeToken)
231
+	name, err := shlex.ProcessWord(name, substitutionArgs)
232 232
 	if err != nil {
233 233
 		return nil, err
234 234
 	}
... ...
@@ -9,6 +9,7 @@ import (
9 9
 	"github.com/docker/docker/api/types/container"
10 10
 	"github.com/docker/docker/api/types/strslice"
11 11
 	"github.com/docker/docker/builder"
12
+	"github.com/docker/docker/builder/dockerfile/parser"
12 13
 	"github.com/docker/docker/pkg/testutil"
13 14
 	"github.com/docker/go-connections/nat"
14 15
 	"github.com/stretchr/testify/assert"
... ...
@@ -38,6 +39,7 @@ func defaultDispatchReq(builder *Builder, args ...string) dispatchRequest {
38 38
 		args:      args,
39 39
 		flags:     NewBFlags(),
40 40
 		runConfig: &container.Config{},
41
+		shlex:     NewShellLex(parser.DefaultEscapeToken),
41 42
 	}
42 43
 }
43 44
 
... ...
@@ -65,9 +65,10 @@ type dispatchRequest struct {
65 65
 	flags      *BFlags
66 66
 	original   string
67 67
 	runConfig  *container.Config
68
+	shlex      *ShellLex
68 69
 }
69 70
 
70
-func newDispatchRequestFromNode(node *parser.Node, builder *Builder, args []string) dispatchRequest {
71
+func newDispatchRequestFromNode(node *parser.Node, builder *Builder, args []string, shlex *ShellLex) dispatchRequest {
71 72
 	return dispatchRequest{
72 73
 		builder:    builder,
73 74
 		args:       args,
... ...
@@ -75,6 +76,7 @@ func newDispatchRequestFromNode(node *parser.Node, builder *Builder, args []stri
75 75
 		original:   node.Original,
76 76
 		flags:      NewBFlagsWithArgs(node.Flags),
77 77
 		runConfig:  builder.runConfig,
78
+		shlex:      shlex,
78 79
 	}
79 80
 }
80 81
 
... ...
@@ -119,7 +121,7 @@ func init() {
119 119
 // such as `RUN` in ONBUILD RUN foo. There is special case logic in here to
120 120
 // deal with that, at least until it becomes more of a general concern with new
121 121
 // features.
122
-func (b *Builder) dispatch(stepN int, stepTotal int, node *parser.Node) error {
122
+func (b *Builder) dispatch(stepN int, stepTotal int, node *parser.Node, shlex *ShellLex) error {
123 123
 	cmd := node.Value
124 124
 	upperCasedCmd := strings.ToUpper(cmd)
125 125
 
... ...
@@ -154,9 +156,10 @@ func (b *Builder) dispatch(stepN int, stepTotal int, node *parser.Node) error {
154 154
 	// Append build args to runConfig environment variables
155 155
 	envs := append(b.runConfig.Env, b.buildArgsWithoutConfigEnv()...)
156 156
 
157
+	processFunc := getProcessFunc(shlex, cmd)
157 158
 	for i := 0; ast.Next != nil; i++ {
158 159
 		ast = ast.Next
159
-		words, err := b.evaluateEnv(cmd, ast.Value, envs)
160
+		words, err := processFunc(ast.Value, envs)
160 161
 		if err != nil {
161 162
 			return err
162 163
 		}
... ...
@@ -170,7 +173,7 @@ func (b *Builder) dispatch(stepN int, stepTotal int, node *parser.Node) error {
170 170
 	// XXX yes, we skip any cmds that are not valid; the parser should have
171 171
 	// picked these out already.
172 172
 	if f, ok := evaluateTable[cmd]; ok {
173
-		return f(newDispatchRequestFromNode(node, b, strList))
173
+		return f(newDispatchRequestFromNode(node, b, strList, shlex))
174 174
 	}
175 175
 
176 176
 	return fmt.Errorf("Unknown instruction: %s", upperCasedCmd)
... ...
@@ -186,20 +189,22 @@ func initMsgList(cursor *parser.Node) []string {
186 186
 	return make([]string, n)
187 187
 }
188 188
 
189
-func (b *Builder) evaluateEnv(cmd string, str string, envs []string) ([]string, error) {
190
-	if !replaceEnvAllowed[cmd] {
191
-		return []string{str}, nil
192
-	}
193
-	var processFunc func(string, []string, rune) ([]string, error)
194
-	if allowWordExpansion[cmd] {
195
-		processFunc = ProcessWords
196
-	} else {
197
-		processFunc = func(word string, envs []string, escape rune) ([]string, error) {
198
-			word, err := ProcessWord(word, envs, escape)
189
+type processFunc func(string, []string) ([]string, error)
190
+
191
+func getProcessFunc(shlex *ShellLex, cmd string) processFunc {
192
+	switch {
193
+	case !replaceEnvAllowed[cmd]:
194
+		return func(word string, _ []string) ([]string, error) {
195
+			return []string{word}, nil
196
+		}
197
+	case allowWordExpansion[cmd]:
198
+		return shlex.ProcessWords
199
+	default:
200
+		return func(word string, envs []string) ([]string, error) {
201
+			word, err := shlex.ProcessWord(word, envs)
199 202
 			return []string{word}, err
200 203
 		}
201 204
 	}
202
-	return processFunc(str, envs, b.escapeToken)
203 205
 }
204 206
 
205 207
 // buildArgsWithoutConfigEnv returns a list of key=value pairs for all the build
... ...
@@ -190,8 +190,9 @@ func executeTestCase(t *testing.T, testCase dispatchTestCase) {
190 190
 		buildArgs: newBuildArgs(options.BuildArgs),
191 191
 	}
192 192
 
193
+	shlex := NewShellLex(parser.DefaultEscapeToken)
193 194
 	n := result.AST
194
-	err = b.dispatch(0, len(n.Children), n.Children[0])
195
+	err = b.dispatch(0, len(n.Children), n.Children[0], shlex)
195 196
 
196 197
 	if err == nil {
197 198
 		t.Fatalf("No error when executing test %s", testCase.name)
... ...
@@ -1,11 +1,5 @@
1 1
 package dockerfile
2 2
 
3
-// This will take a single word and an array of env variables and
4
-// process all quotes (" and ') as well as $xxx and ${xxx} env variable
5
-// tokens.  Tries to mimic bash shell process.
6
-// It doesn't support all flavors of ${xx:...} formats but new ones can
7
-// be added by adding code to the "special ${} format processing" section
8
-
9 3
 import (
10 4
 	"bytes"
11 5
 	"strings"
... ...
@@ -15,18 +9,26 @@ import (
15 15
 	"github.com/pkg/errors"
16 16
 )
17 17
 
18
-type shellWord struct {
19
-	word        string
20
-	scanner     scanner.Scanner
21
-	envs        []string
22
-	pos         int
18
+// ShellLex performs shell word splitting and variable expansion.
19
+//
20
+// ShellLex takes a string and an array of env variables and
21
+// process all quotes (" and ') as well as $xxx and ${xxx} env variable
22
+// tokens.  Tries to mimic bash shell process.
23
+// It doesn't support all flavors of ${xx:...} formats but new ones can
24
+// be added by adding code to the "special ${} format processing" section
25
+type ShellLex struct {
23 26
 	escapeToken rune
24 27
 }
25 28
 
29
+// NewShellLex creates a new ShellLex which uses escapeToken to escape quotes.
30
+func NewShellLex(escapeToken rune) *ShellLex {
31
+	return &ShellLex{escapeToken: escapeToken}
32
+}
33
+
26 34
 // ProcessWord will use the 'env' list of environment variables,
27 35
 // and replace any env var references in 'word'.
28
-func ProcessWord(word string, env []string, escapeToken rune) (string, error) {
29
-	word, _, err := process(word, env, escapeToken)
36
+func (s *ShellLex) ProcessWord(word string, env []string) (string, error) {
37
+	word, _, err := s.process(word, env)
30 38
 	return word, err
31 39
 }
32 40
 
... ...
@@ -37,24 +39,32 @@ func ProcessWord(word string, env []string, escapeToken rune) (string, error) {
37 37
 // this splitting is done **after** the env var substitutions are done.
38 38
 // Note, each one is trimmed to remove leading and trailing spaces (unless
39 39
 // they are quoted", but ProcessWord retains spaces between words.
40
-func ProcessWords(word string, env []string, escapeToken rune) ([]string, error) {
41
-	_, words, err := process(word, env, escapeToken)
40
+func (s *ShellLex) ProcessWords(word string, env []string) ([]string, error) {
41
+	_, words, err := s.process(word, env)
42 42
 	return words, err
43 43
 }
44 44
 
45
-func process(word string, env []string, escapeToken rune) (string, []string, error) {
45
+func (s *ShellLex) process(word string, env []string) (string, []string, error) {
46 46
 	sw := &shellWord{
47
-		word:        word,
48 47
 		envs:        env,
49
-		pos:         0,
50
-		escapeToken: escapeToken,
48
+		escapeToken: s.escapeToken,
51 49
 	}
52 50
 	sw.scanner.Init(strings.NewReader(word))
53
-	return sw.process()
51
+	return sw.process(word)
52
+}
53
+
54
+type shellWord struct {
55
+	scanner     scanner.Scanner
56
+	envs        []string
57
+	escapeToken rune
54 58
 }
55 59
 
56
-func (sw *shellWord) process() (string, []string, error) {
57
-	return sw.processStopOn(scanner.EOF)
60
+func (sw *shellWord) process(source string) (string, []string, error) {
61
+	word, words, err := sw.processStopOn(scanner.EOF)
62
+	if err != nil {
63
+		err = errors.Wrapf(err, "failed to process %q", source)
64
+	}
65
+	return word, words, err
58 66
 }
59 67
 
60 68
 type wordsStruct struct {
... ...
@@ -286,10 +296,10 @@ func (sw *shellWord) processDollar() (string, error) {
286 286
 			return newValue, nil
287 287
 
288 288
 		default:
289
-			return "", errors.Errorf("unsupported modifier (%c) in substitution: %s", modifier, sw.word)
289
+			return "", errors.Errorf("unsupported modifier (%c) in substitution", modifier)
290 290
 		}
291 291
 	}
292
-	return "", errors.Errorf("missing ':' in substitution: %s", sw.word)
292
+	return "", errors.Errorf("missing ':' in substitution")
293 293
 }
294 294
 
295 295
 func (sw *shellWord) processName() string {
... ...
@@ -18,6 +18,7 @@ func TestShellParser4EnvVars(t *testing.T) {
18 18
 	assert.NoError(t, err)
19 19
 	defer file.Close()
20 20
 
21
+	shlex := NewShellLex('\\')
21 22
 	scanner := bufio.NewScanner(file)
22 23
 	envs := []string{"PWD=/home", "SHELL=bash", "KOREAN=한국어"}
23 24
 	for scanner.Scan() {
... ...
@@ -49,7 +50,7 @@ func TestShellParser4EnvVars(t *testing.T) {
49 49
 
50 50
 		if ((platform == "W" || platform == "A") && runtime.GOOS == "windows") ||
51 51
 			((platform == "U" || platform == "A") && runtime.GOOS != "windows") {
52
-			newWord, err := ProcessWord(source, envs, '\\')
52
+			newWord, err := shlex.ProcessWord(source, envs)
53 53
 			if expected == "error" {
54 54
 				assert.Error(t, err)
55 55
 			} else {
... ...
@@ -69,6 +70,7 @@ func TestShellParser4Words(t *testing.T) {
69 69
 	}
70 70
 	defer file.Close()
71 71
 
72
+	shlex := NewShellLex('\\')
72 73
 	envs := []string{}
73 74
 	scanner := bufio.NewScanner(file)
74 75
 	lineNum := 0
... ...
@@ -93,7 +95,7 @@ func TestShellParser4Words(t *testing.T) {
93 93
 		test := strings.TrimSpace(words[0])
94 94
 		expected := strings.Split(strings.TrimLeft(words[1], " "), ",")
95 95
 
96
-		result, err := ProcessWords(test, envs, '\\')
96
+		result, err := shlex.ProcessWords(test, envs)
97 97
 
98 98
 		if err != nil {
99 99
 			result = []string{"error"}
... ...
@@ -111,11 +113,7 @@ func TestShellParser4Words(t *testing.T) {
111 111
 }
112 112
 
113 113
 func TestGetEnv(t *testing.T) {
114
-	sw := &shellWord{
115
-		word: "",
116
-		envs: nil,
117
-		pos:  0,
118
-	}
114
+	sw := &shellWord{envs: nil}
119 115
 
120 116
 	sw.envs = []string{}
121 117
 	if sw.getEnv("foo") != "" {