Add support for ENV of the form: ENV name=value ...
| ... | ... |
@@ -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 |
} |
| 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) |