[Builder] Remove b.escapeToken, create ShellLex
| ... | ... |
@@ -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") != "" {
|