[Builder] Remove b.escapeToken, create ShellLex
Daniel Nephin authored on 2017/04/28 02:16:40... | ... |
@@ -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") != "" { |