Browse code

Merge pull request #8251 from duglin/Issue2333

Add support for ENV of the form: ENV name=value ...

Michael Crosby authored on 2014/11/21 06:12:24
Showing 9 changed files
... ...
@@ -31,21 +31,39 @@ func nullDispatch(b *Builder, args []string, attributes map[string]bool, origina
31 31
 // in the dockerfile available from the next statement on via ${foo}.
32 32
 //
33 33
 func env(b *Builder, args []string, attributes map[string]bool, original string) error {
34
-	if len(args) != 2 {
35
-		return fmt.Errorf("ENV accepts two arguments")
34
+	if len(args) == 0 {
35
+		return fmt.Errorf("ENV is missing arguments")
36
+	}
37
+
38
+	if len(args)%2 != 0 {
39
+		// should never get here, but just in case
40
+		return fmt.Errorf("Bad input to ENV, too many args")
36 41
 	}
37 42
 
38
-	fullEnv := fmt.Sprintf("%s=%s", args[0], args[1])
43
+	commitStr := "ENV"
39 44
 
40
-	for i, envVar := range b.Config.Env {
41
-		envParts := strings.SplitN(envVar, "=", 2)
42
-		if args[0] == envParts[0] {
43
-			b.Config.Env[i] = fullEnv
44
-			return b.commit("", b.Config.Cmd, fmt.Sprintf("ENV %s", fullEnv))
45
+	for j := 0; j < len(args); j++ {
46
+		// name  ==> args[j]
47
+		// value ==> args[j+1]
48
+		newVar := args[j] + "=" + args[j+1] + ""
49
+		commitStr += " " + newVar
50
+
51
+		gotOne := false
52
+		for i, envVar := range b.Config.Env {
53
+			envParts := strings.SplitN(envVar, "=", 2)
54
+			if envParts[0] == args[j] {
55
+				b.Config.Env[i] = newVar
56
+				gotOne = true
57
+				break
58
+			}
59
+		}
60
+		if !gotOne {
61
+			b.Config.Env = append(b.Config.Env, newVar)
45 62
 		}
63
+		j++
46 64
 	}
47
-	b.Config.Env = append(b.Config.Env, fullEnv)
48
-	return b.commit("", b.Config.Cmd, fmt.Sprintf("ENV %s", fullEnv))
65
+
66
+	return b.commit("", b.Config.Cmd, commitStr)
49 67
 }
50 68
 
51 69
 // MAINTAINER some text <maybe@an.email.address>
... ...
@@ -12,6 +12,7 @@ import (
12 12
 	"fmt"
13 13
 	"strconv"
14 14
 	"strings"
15
+	"unicode"
15 16
 )
16 17
 
17 18
 var (
... ...
@@ -41,17 +42,139 @@ func parseSubCommand(rest string) (*Node, map[string]bool, error) {
41 41
 // parse environment like statements. Note that this does *not* handle
42 42
 // variable interpolation, which will be handled in the evaluator.
43 43
 func parseEnv(rest string) (*Node, map[string]bool, error) {
44
-	node := &Node{}
45
-	rootnode := node
46
-	strs := TOKEN_WHITESPACE.Split(rest, 2)
44
+	// This is kind of tricky because we need to support the old
45
+	// variant:   ENV name value
46
+	// as well as the new one:    ENV name=value ...
47
+	// The trigger to know which one is being used will be whether we hit
48
+	// a space or = first.  space ==> old, "=" ==> new
49
+
50
+	const (
51
+		inSpaces = iota // looking for start of a word
52
+		inWord
53
+		inQuote
54
+	)
55
+
56
+	words := []string{}
57
+	phase := inSpaces
58
+	word := ""
59
+	quote := '\000'
60
+	blankOK := false
61
+	var ch rune
62
+
63
+	for pos := 0; pos <= len(rest); pos++ {
64
+		if pos != len(rest) {
65
+			ch = rune(rest[pos])
66
+		}
67
+
68
+		if phase == inSpaces { // Looking for start of word
69
+			if pos == len(rest) { // end of input
70
+				break
71
+			}
72
+			if unicode.IsSpace(ch) { // skip spaces
73
+				continue
74
+			}
75
+			phase = inWord // found it, fall thru
76
+		}
77
+		if (phase == inWord || phase == inQuote) && (pos == len(rest)) {
78
+			if blankOK || len(word) > 0 {
79
+				words = append(words, word)
80
+			}
81
+			break
82
+		}
83
+		if phase == inWord {
84
+			if unicode.IsSpace(ch) {
85
+				phase = inSpaces
86
+				if blankOK || len(word) > 0 {
87
+					words = append(words, word)
88
+
89
+					// Look for = and if no there assume
90
+					// we're doing the old stuff and
91
+					// just read the rest of the line
92
+					if !strings.Contains(word, "=") {
93
+						word = strings.TrimSpace(rest[pos:])
94
+						words = append(words, word)
95
+						break
96
+					}
97
+				}
98
+				word = ""
99
+				blankOK = false
100
+				continue
101
+			}
102
+			if ch == '\'' || ch == '"' {
103
+				quote = ch
104
+				blankOK = true
105
+				phase = inQuote
106
+				continue
107
+			}
108
+			if ch == '\\' {
109
+				if pos+1 == len(rest) {
110
+					continue // just skip \ at end
111
+				}
112
+				pos++
113
+				ch = rune(rest[pos])
114
+			}
115
+			word += string(ch)
116
+			continue
117
+		}
118
+		if phase == inQuote {
119
+			if ch == quote {
120
+				phase = inWord
121
+				continue
122
+			}
123
+			if ch == '\\' {
124
+				if pos+1 == len(rest) {
125
+					phase = inWord
126
+					continue // just skip \ at end
127
+				}
128
+				pos++
129
+				ch = rune(rest[pos])
130
+			}
131
+			word += string(ch)
132
+		}
133
+	}
47 134
 
48
-	if len(strs) < 2 {
49
-		return nil, nil, fmt.Errorf("ENV must have two arguments")
135
+	if len(words) == 0 {
136
+		return nil, nil, fmt.Errorf("ENV must have some arguments")
50 137
 	}
51 138
 
52
-	node.Value = strs[0]
53
-	node.Next = &Node{}
54
-	node.Next.Value = strs[1]
139
+	// Old format (ENV name value)
140
+	var rootnode *Node
141
+
142
+	if !strings.Contains(words[0], "=") {
143
+		node := &Node{}
144
+		rootnode = node
145
+		strs := TOKEN_WHITESPACE.Split(rest, 2)
146
+
147
+		if len(strs) < 2 {
148
+			return nil, nil, fmt.Errorf("ENV must have two arguments")
149
+		}
150
+
151
+		node.Value = strs[0]
152
+		node.Next = &Node{}
153
+		node.Next.Value = strs[1]
154
+	} else {
155
+		var prevNode *Node
156
+		for i, word := range words {
157
+			if !strings.Contains(word, "=") {
158
+				return nil, nil, fmt.Errorf("Syntax error - can't find = in %q. Must be of the form: name=value", word)
159
+			}
160
+			parts := strings.SplitN(word, "=", 2)
161
+
162
+			name := &Node{}
163
+			value := &Node{}
164
+
165
+			name.Next = value
166
+			name.Value = parts[0]
167
+			value.Value = parts[1]
168
+
169
+			if i == 0 {
170
+				rootnode = name
171
+			} else {
172
+				prevNode.Next = name
173
+			}
174
+			prevNode = value
175
+		}
176
+	}
55 177
 
56 178
 	return rootnode, nil, nil
57 179
 }
... ...
@@ -125,6 +125,12 @@ func Parse(rwc io.Reader) (*Node, error) {
125 125
 					break
126 126
 				}
127 127
 			}
128
+			if child == nil && line != "" {
129
+				line, child, err = parseLine(line)
130
+				if err != nil {
131
+					return nil, err
132
+				}
133
+			}
128 134
 		}
129 135
 
130 136
 		if child != nil {
131 137
deleted file mode 100644
... ...
@@ -1,3 +0,0 @@
1
-FROM busybox
2
-
3
-ENV PATH=PATH
4 1
new file mode 100644
... ...
@@ -0,0 +1,3 @@
0
+FROM busybox
1
+
2
+ENV PATH
0 3
new file mode 100644
... ...
@@ -0,0 +1,15 @@
0
+FROM ubuntu
1
+ENV name value
2
+ENV name=value
3
+ENV name=value name2=value2
4
+ENV name="value value1"
5
+ENV name=value\ value2
6
+ENV name="value'quote space'value2"
7
+ENV name='value"double quote"value2'
8
+ENV name=value\ value2 name2=value2\ value3
9
+ENV name=value \
10
+    name1=value1 \
11
+    name2="value2a \
12
+           value2b" \
13
+    name3="value3a\n\"value3b\"" \
14
+	name4="value4a\\nvalue4b" \
0 15
new file mode 100644
... ...
@@ -0,0 +1,10 @@
0
+(from "ubuntu")
1
+(env "name" "value")
2
+(env "name" "value")
3
+(env "name" "value" "name2" "value2")
4
+(env "name" "value value1")
5
+(env "name" "value value2")
6
+(env "name" "value'quote space'value2")
7
+(env "name" "value\"double quote\"value2")
8
+(env "name" "value value2" "name2" "value2 value3")
9
+(env "name" "value" "name1" "value1" "name2" "value2a            value2b" "name3" "value3an\"value3b\"" "name4" "value4a\\nvalue4b")
... ...
@@ -337,11 +337,36 @@ expose ports to the host, at runtime,
337 337
 ## ENV
338 338
 
339 339
     ENV <key> <value>
340
+    ENV <key>=<value> ...
340 341
 
341 342
 The `ENV` instruction sets the environment variable `<key>` to the value
342 343
 `<value>`. This value will be passed to all future `RUN` instructions. This is
343 344
 functionally equivalent to prefixing the command with `<key>=<value>`
344 345
 
346
+The `ENV` instruction has two forms. The first form, `ENV <key> <value>`,
347
+will set a single variable to a value. The entire string after the first
348
+space will be treated as the `<value>` - including characters such as 
349
+spaces and quotes.
350
+
351
+The second form, `ENV <key>=<value> ...`, allows for multiple variables to 
352
+be set at one time. Notice that the second form uses the equals sign (=) 
353
+in the syntax, while the first form does not. Like command line parsing, 
354
+quotes and backslashes can be used to include spaces within values.
355
+
356
+For example:
357
+
358
+    ENV myName="John Doe" myDog=Rex\ The\ Dog \
359
+        myCat=fluffy
360
+
361
+and
362
+
363
+    ENV myName John Doe
364
+    ENV myDog Rex The Dog
365
+    ENV myCat fluffy
366
+
367
+will yield the same net results in the final container, but the first form 
368
+does it all in one layer.
369
+
345 370
 The environment variables set using `ENV` will persist when a container is run
346 371
 from the resulting image. You can view the values using `docker inspect`, and
347 372
 change them using `docker run --env <key>=<value>`.
... ...
@@ -2991,6 +2991,46 @@ RUN    [ "$(cat $TO)" = "hello" ]
2991 2991
 	logDone("build - environment variables usage")
2992 2992
 }
2993 2993
 
2994
+func TestBuildEnvUsage2(t *testing.T) {
2995
+	name := "testbuildenvusage2"
2996
+	defer deleteImages(name)
2997
+	dockerfile := `FROM busybox
2998
+ENV    abc=def
2999
+RUN    [ "$abc" = "def" ]
3000
+ENV    def="hello world"
3001
+RUN    [ "$def" = "hello world" ]
3002
+ENV    def=hello\ world
3003
+RUN    [ "$def" = "hello world" ]
3004
+ENV    v1=abc v2="hi there"
3005
+RUN    [ "$v1" = "abc" ]
3006
+RUN    [ "$v2" = "hi there" ]
3007
+ENV    v3='boogie nights' v4="with'quotes too"
3008
+RUN    [ "$v3" = "boogie nights" ]
3009
+RUN    [ "$v4" = "with'quotes too" ]
3010
+ENV    abc=zzz FROM=hello/docker/world
3011
+ENV    abc=zzz TO=/docker/world/hello
3012
+ADD    $FROM $TO
3013
+RUN    [ "$(cat $TO)" = "hello" ]
3014
+ENV    abc "zzz"
3015
+RUN    [ $abc = \"zzz\" ]
3016
+ENV    abc 'yyy'
3017
+RUN    [ $abc = \'yyy\' ]
3018
+ENV    abc=
3019
+RUN    [ "$abc" = "" ]
3020
+`
3021
+	ctx, err := fakeContext(dockerfile, map[string]string{
3022
+		"hello/docker/world": "hello",
3023
+	})
3024
+	if err != nil {
3025
+		t.Fatal(err)
3026
+	}
3027
+	_, err = buildImageFromContext(name, ctx, true)
3028
+	if err != nil {
3029
+		t.Fatal(err)
3030
+	}
3031
+	logDone("build - environment variables usage2")
3032
+}
3033
+
2994 3034
 func TestBuildAddScript(t *testing.T) {
2995 3035
 	name := "testbuildaddscript"
2996 3036
 	defer deleteImages(name)