Signed-off-by: John Howard <jhoward@microsoft.com>
| ... | ... |
@@ -3,42 +3,44 @@ package command |
| 3 | 3 |
|
| 4 | 4 |
// Define constants for the command strings |
| 5 | 5 |
const ( |
| 6 |
- Env = "env" |
|
| 7 |
- Label = "label" |
|
| 8 |
- Maintainer = "maintainer" |
|
| 9 | 6 |
Add = "add" |
| 7 |
+ Arg = "arg" |
|
| 8 |
+ Cmd = "cmd" |
|
| 10 | 9 |
Copy = "copy" |
| 10 |
+ Entrypoint = "entrypoint" |
|
| 11 |
+ Env = "env" |
|
| 12 |
+ Expose = "expose" |
|
| 11 | 13 |
From = "from" |
| 14 |
+ Healthcheck = "healthcheck" |
|
| 15 |
+ Label = "label" |
|
| 16 |
+ Maintainer = "maintainer" |
|
| 12 | 17 |
Onbuild = "onbuild" |
| 13 |
- Workdir = "workdir" |
|
| 14 | 18 |
Run = "run" |
| 15 |
- Cmd = "cmd" |
|
| 16 |
- Entrypoint = "entrypoint" |
|
| 17 |
- Expose = "expose" |
|
| 18 |
- Volume = "volume" |
|
| 19 |
- User = "user" |
|
| 19 |
+ Shell = "shell" |
|
| 20 | 20 |
StopSignal = "stopsignal" |
| 21 |
- Arg = "arg" |
|
| 22 |
- Healthcheck = "healthcheck" |
|
| 21 |
+ User = "user" |
|
| 22 |
+ Volume = "volume" |
|
| 23 |
+ Workdir = "workdir" |
|
| 23 | 24 |
) |
| 24 | 25 |
|
| 25 | 26 |
// Commands is list of all Dockerfile commands |
| 26 | 27 |
var Commands = map[string]struct{}{
|
| 27 |
- Env: {},
|
|
| 28 |
- Label: {},
|
|
| 29 |
- Maintainer: {},
|
|
| 30 | 28 |
Add: {},
|
| 29 |
+ Arg: {},
|
|
| 30 |
+ Cmd: {},
|
|
| 31 | 31 |
Copy: {},
|
| 32 |
+ Entrypoint: {},
|
|
| 33 |
+ Env: {},
|
|
| 34 |
+ Expose: {},
|
|
| 32 | 35 |
From: {},
|
| 36 |
+ Healthcheck: {},
|
|
| 37 |
+ Label: {},
|
|
| 38 |
+ Maintainer: {},
|
|
| 33 | 39 |
Onbuild: {},
|
| 34 |
- Workdir: {},
|
|
| 35 | 40 |
Run: {},
|
| 36 |
- Cmd: {},
|
|
| 37 |
- Entrypoint: {},
|
|
| 38 |
- Expose: {},
|
|
| 39 |
- Volume: {},
|
|
| 40 |
- User: {},
|
|
| 41 |
+ Shell: {},
|
|
| 41 | 42 |
StopSignal: {},
|
| 42 |
- Arg: {},
|
|
| 43 |
- Healthcheck: {},
|
|
| 43 |
+ User: {},
|
|
| 44 |
+ Volume: {},
|
|
| 45 |
+ Workdir: {},
|
|
| 44 | 46 |
} |
| ... | ... |
@@ -274,8 +274,8 @@ func workdir(b *Builder, args []string, attributes map[string]bool, original str |
| 274 | 274 |
// RUN some command yo |
| 275 | 275 |
// |
| 276 | 276 |
// run a command and commit the image. Args are automatically prepended with |
| 277 |
-// 'sh -c' under linux or 'cmd /S /C' under Windows, in the event there is |
|
| 278 |
-// only one argument. The difference in processing: |
|
| 277 |
+// the current SHELL which defaults to 'sh -c' under linux or 'cmd /S /C' under |
|
| 278 |
+// Windows, in the event there is only one argument The difference in processing: |
|
| 279 | 279 |
// |
| 280 | 280 |
// RUN echo hi # sh -c echo hi (Linux) |
| 281 | 281 |
// RUN echo hi # cmd /S /C echo hi (Windows) |
| ... | ... |
@@ -293,13 +293,8 @@ func run(b *Builder, args []string, attributes map[string]bool, original string) |
| 293 | 293 |
args = handleJSONArgs(args, attributes) |
| 294 | 294 |
|
| 295 | 295 |
if !attributes["json"] {
|
| 296 |
- if runtime.GOOS != "windows" {
|
|
| 297 |
- args = append([]string{"/bin/sh", "-c"}, args...)
|
|
| 298 |
- } else {
|
|
| 299 |
- args = append([]string{"cmd", "/S", "/C"}, args...)
|
|
| 300 |
- } |
|
| 296 |
+ args = append(getShell(b.runConfig), args...) |
|
| 301 | 297 |
} |
| 302 |
- |
|
| 303 | 298 |
config := &container.Config{
|
| 304 | 299 |
Cmd: strslice.StrSlice(args), |
| 305 | 300 |
Image: b.image, |
| ... | ... |
@@ -408,11 +403,7 @@ func cmd(b *Builder, args []string, attributes map[string]bool, original string) |
| 408 | 408 |
cmdSlice := handleJSONArgs(args, attributes) |
| 409 | 409 |
|
| 410 | 410 |
if !attributes["json"] {
|
| 411 |
- if runtime.GOOS != "windows" {
|
|
| 412 |
- cmdSlice = append([]string{"/bin/sh", "-c"}, cmdSlice...)
|
|
| 413 |
- } else {
|
|
| 414 |
- cmdSlice = append([]string{"cmd", "/S", "/C"}, cmdSlice...)
|
|
| 415 |
- } |
|
| 411 |
+ cmdSlice = append(getShell(b.runConfig), cmdSlice...) |
|
| 416 | 412 |
} |
| 417 | 413 |
|
| 418 | 414 |
b.runConfig.Cmd = strslice.StrSlice(cmdSlice) |
| ... | ... |
@@ -535,8 +526,8 @@ func healthcheck(b *Builder, args []string, attributes map[string]bool, original |
| 535 | 535 |
|
| 536 | 536 |
// ENTRYPOINT /usr/sbin/nginx |
| 537 | 537 |
// |
| 538 |
-// Set the entrypoint (which defaults to sh -c on linux, or cmd /S /C on Windows) to |
|
| 539 |
-// /usr/sbin/nginx. Will accept the CMD as the arguments to /usr/sbin/nginx. |
|
| 538 |
+// Set the entrypoint to /usr/sbin/nginx. Will accept the CMD as the arguments |
|
| 539 |
+// to /usr/sbin/nginx. Uses the default shell if not in JSON format. |
|
| 540 | 540 |
// |
| 541 | 541 |
// Handles command processing similar to CMD and RUN, only b.runConfig.Entrypoint |
| 542 | 542 |
// is initialized at NewBuilder time instead of through argument parsing. |
| ... | ... |
@@ -557,11 +548,7 @@ func entrypoint(b *Builder, args []string, attributes map[string]bool, original |
| 557 | 557 |
b.runConfig.Entrypoint = nil |
| 558 | 558 |
default: |
| 559 | 559 |
// ENTRYPOINT echo hi |
| 560 |
- if runtime.GOOS != "windows" {
|
|
| 561 |
- b.runConfig.Entrypoint = strslice.StrSlice{"/bin/sh", "-c", parsed[0]}
|
|
| 562 |
- } else {
|
|
| 563 |
- b.runConfig.Entrypoint = strslice.StrSlice{"cmd", "/S", "/C", parsed[0]}
|
|
| 564 |
- } |
|
| 560 |
+ b.runConfig.Entrypoint = strslice.StrSlice(append(getShell(b.runConfig), parsed[0])) |
|
| 565 | 561 |
} |
| 566 | 562 |
|
| 567 | 563 |
// when setting the entrypoint if a CMD was not explicitly set then |
| ... | ... |
@@ -727,6 +714,28 @@ func arg(b *Builder, args []string, attributes map[string]bool, original string) |
| 727 | 727 |
return b.commit("", b.runConfig.Cmd, fmt.Sprintf("ARG %s", arg))
|
| 728 | 728 |
} |
| 729 | 729 |
|
| 730 |
+// SHELL powershell -command |
|
| 731 |
+// |
|
| 732 |
+// Set the non-default shell to use. |
|
| 733 |
+func shell(b *Builder, args []string, attributes map[string]bool, original string) error {
|
|
| 734 |
+ if err := b.flags.Parse(); err != nil {
|
|
| 735 |
+ return err |
|
| 736 |
+ } |
|
| 737 |
+ shellSlice := handleJSONArgs(args, attributes) |
|
| 738 |
+ switch {
|
|
| 739 |
+ case len(shellSlice) == 0: |
|
| 740 |
+ // SHELL [] |
|
| 741 |
+ return errAtLeastOneArgument("SHELL")
|
|
| 742 |
+ case attributes["json"]: |
|
| 743 |
+ // SHELL ["powershell", "-command"] |
|
| 744 |
+ b.runConfig.Shell = strslice.StrSlice(shellSlice) |
|
| 745 |
+ default: |
|
| 746 |
+ // SHELL powershell -command - not JSON |
|
| 747 |
+ return errNotJSON("SHELL", original)
|
|
| 748 |
+ } |
|
| 749 |
+ return b.commit("", b.runConfig.Cmd, fmt.Sprintf("SHELL %v", shellSlice))
|
|
| 750 |
+} |
|
| 751 |
+ |
|
| 730 | 752 |
func errAtLeastOneArgument(command string) error {
|
| 731 | 753 |
return fmt.Errorf("%s requires at least one argument", command)
|
| 732 | 754 |
} |
| ... | ... |
@@ -738,3 +747,12 @@ func errExactlyOneArgument(command string) error {
|
| 738 | 738 |
func errTooManyArguments(command string) error {
|
| 739 | 739 |
return fmt.Errorf("Bad input to %s, too many arguments", command)
|
| 740 | 740 |
} |
| 741 |
+ |
|
| 742 |
+// getShell is a helper function which gets the right shell for prefixing the |
|
| 743 |
+// shell-form of RUN, ENTRYPOINT and CMD instructions |
|
| 744 |
+func getShell(c *container.Config) []string {
|
|
| 745 |
+ if 0 == len(c.Shell) {
|
|
| 746 |
+ return defaultShell[:] |
|
| 747 |
+ } |
|
| 748 |
+ return c.Shell[:] |
|
| 749 |
+} |
| ... | ... |
@@ -4,6 +4,7 @@ import ( |
| 4 | 4 |
"fmt" |
| 5 | 5 |
"os" |
| 6 | 6 |
"path/filepath" |
| 7 |
+ "regexp" |
|
| 7 | 8 |
"strings" |
| 8 | 9 |
|
| 9 | 10 |
"github.com/docker/docker/pkg/system" |
| ... | ... |
@@ -43,3 +44,22 @@ func normaliseWorkdir(current string, requested string) (string, error) {
|
| 43 | 43 |
// Upper-case drive letter |
| 44 | 44 |
return (strings.ToUpper(string(requested[0])) + requested[1:]), nil |
| 45 | 45 |
} |
| 46 |
+ |
|
| 47 |
+func errNotJSON(command, original string) error {
|
|
| 48 |
+ // For Windows users, give a hint if it looks like it might contain |
|
| 49 |
+ // a path which hasn't been escaped such as ["c:\windows\system32\prog.exe", "-param"], |
|
| 50 |
+ // as JSON must be escaped. Unfortunate... |
|
| 51 |
+ // |
|
| 52 |
+ // Specifically looking for quote-driveletter-colon-backslash, there's no |
|
| 53 |
+ // double backslash and a [] pair. No, this is not perfect, but it doesn't |
|
| 54 |
+ // have to be. It's simply a hint to make life a little easier. |
|
| 55 |
+ extra := "" |
|
| 56 |
+ original = filepath.FromSlash(strings.ToLower(strings.Replace(strings.ToLower(original), strings.ToLower(command)+" ", "", -1))) |
|
| 57 |
+ if len(regexp.MustCompile(`"[a-z]:\\.*`).FindStringSubmatch(original)) > 0 && |
|
| 58 |
+ !strings.Contains(original, `\\`) && |
|
| 59 |
+ strings.Contains(original, "[") && |
|
| 60 |
+ strings.Contains(original, "]") {
|
|
| 61 |
+ extra = fmt.Sprintf(`. It looks like '%s' includes a file path without an escaped back-slash. JSON requires back-slashes to be escaped such as ["c:\\path\\to\\file.exe", "/parameter"]`, original) |
|
| 62 |
+ } |
|
| 63 |
+ return fmt.Errorf("%s requires the arguments to be in JSON form%s", command, extra)
|
|
| 64 |
+} |
| ... | ... |
@@ -58,23 +58,24 @@ var evaluateTable map[string]func(*Builder, []string, map[string]bool, string) e |
| 58 | 58 |
|
| 59 | 59 |
func init() {
|
| 60 | 60 |
evaluateTable = map[string]func(*Builder, []string, map[string]bool, string) error{
|
| 61 |
- command.Env: env, |
|
| 62 |
- command.Label: label, |
|
| 63 |
- command.Maintainer: maintainer, |
|
| 64 | 61 |
command.Add: add, |
| 62 |
+ command.Arg: arg, |
|
| 63 |
+ command.Cmd: cmd, |
|
| 65 | 64 |
command.Copy: dispatchCopy, // copy() is a go builtin |
| 65 |
+ command.Entrypoint: entrypoint, |
|
| 66 |
+ command.Env: env, |
|
| 67 |
+ command.Expose: expose, |
|
| 66 | 68 |
command.From: from, |
| 69 |
+ command.Healthcheck: healthcheck, |
|
| 70 |
+ command.Label: label, |
|
| 71 |
+ command.Maintainer: maintainer, |
|
| 67 | 72 |
command.Onbuild: onbuild, |
| 68 |
- command.Workdir: workdir, |
|
| 69 | 73 |
command.Run: run, |
| 70 |
- command.Cmd: cmd, |
|
| 71 |
- command.Entrypoint: entrypoint, |
|
| 72 |
- command.Expose: expose, |
|
| 73 |
- command.Volume: volume, |
|
| 74 |
- command.User: user, |
|
| 74 |
+ command.Shell: shell, |
|
| 75 | 75 |
command.StopSignal: stopSignal, |
| 76 |
- command.Arg: arg, |
|
| 77 |
- command.Healthcheck: healthcheck, |
|
| 76 |
+ command.User: user, |
|
| 77 |
+ command.Volume: volume, |
|
| 78 |
+ command.Workdir: workdir, |
|
| 78 | 79 |
} |
| 79 | 80 |
} |
| 80 | 81 |
|
| ... | ... |
@@ -14,7 +14,6 @@ import ( |
| 14 | 14 |
"net/url" |
| 15 | 15 |
"os" |
| 16 | 16 |
"path/filepath" |
| 17 |
- "runtime" |
|
| 18 | 17 |
"sort" |
| 19 | 18 |
"strings" |
| 20 | 19 |
"sync" |
| ... | ... |
@@ -51,11 +50,7 @@ func (b *Builder) commit(id string, autoCmd strslice.StrSlice, comment string) e |
| 51 | 51 |
|
| 52 | 52 |
if id == "" {
|
| 53 | 53 |
cmd := b.runConfig.Cmd |
| 54 |
- if runtime.GOOS != "windows" {
|
|
| 55 |
- b.runConfig.Cmd = strslice.StrSlice{"/bin/sh", "-c", "#(nop) " + comment}
|
|
| 56 |
- } else {
|
|
| 57 |
- b.runConfig.Cmd = strslice.StrSlice{"cmd", "/S /C", "REM (nop) " + comment}
|
|
| 58 |
- } |
|
| 54 |
+ b.runConfig.Cmd = strslice.StrSlice(append(getShell(b.runConfig), "#(nop) ", comment)) |
|
| 59 | 55 |
defer func(cmd strslice.StrSlice) { b.runConfig.Cmd = cmd }(cmd)
|
| 60 | 56 |
|
| 61 | 57 |
hit, err := b.probeCache() |
| ... | ... |
@@ -177,11 +172,7 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalD |
| 177 | 177 |
} |
| 178 | 178 |
|
| 179 | 179 |
cmd := b.runConfig.Cmd |
| 180 |
- if runtime.GOOS != "windows" {
|
|
| 181 |
- b.runConfig.Cmd = strslice.StrSlice{"/bin/sh", "-c", fmt.Sprintf("#(nop) %s %s in %s", cmdName, srcHash, dest)}
|
|
| 182 |
- } else {
|
|
| 183 |
- b.runConfig.Cmd = strslice.StrSlice{"cmd", "/S", "/C", fmt.Sprintf("REM (nop) %s %s in %s", cmdName, srcHash, dest)}
|
|
| 184 |
- } |
|
| 180 |
+ b.runConfig.Cmd = strslice.StrSlice(append(getShell(b.runConfig), "#(nop) %s %s in %s ", cmdName, srcHash, dest)) |
|
| 185 | 181 |
defer func(cmd strslice.StrSlice) { b.runConfig.Cmd = cmd }(cmd)
|
| 186 | 182 |
|
| 187 | 183 |
if hit, err := b.probeCache(); err != nil {
|
| ... | ... |
@@ -67,23 +67,24 @@ func init() {
|
| 67 | 67 |
// functions. Errors are propagated up by Parse() and the resulting AST can |
| 68 | 68 |
// be incorporated directly into the existing AST as a next. |
| 69 | 69 |
dispatch = map[string]func(string) (*Node, map[string]bool, error){
|
| 70 |
- command.User: parseString, |
|
| 71 |
- command.Onbuild: parseSubCommand, |
|
| 72 |
- command.Workdir: parseString, |
|
| 73 |
- command.Env: parseEnv, |
|
| 74 |
- command.Label: parseLabel, |
|
| 75 |
- command.Maintainer: parseString, |
|
| 76 |
- command.From: parseString, |
|
| 77 | 70 |
command.Add: parseMaybeJSONToList, |
| 78 |
- command.Copy: parseMaybeJSONToList, |
|
| 79 |
- command.Run: parseMaybeJSON, |
|
| 71 |
+ command.Arg: parseNameOrNameVal, |
|
| 80 | 72 |
command.Cmd: parseMaybeJSON, |
| 73 |
+ command.Copy: parseMaybeJSONToList, |
|
| 81 | 74 |
command.Entrypoint: parseMaybeJSON, |
| 75 |
+ command.Env: parseEnv, |
|
| 82 | 76 |
command.Expose: parseStringsWhitespaceDelimited, |
| 83 |
- command.Volume: parseMaybeJSONToList, |
|
| 84 |
- command.StopSignal: parseString, |
|
| 85 |
- command.Arg: parseNameOrNameVal, |
|
| 77 |
+ command.From: parseString, |
|
| 86 | 78 |
command.Healthcheck: parseHealthConfig, |
| 79 |
+ command.Label: parseLabel, |
|
| 80 |
+ command.Maintainer: parseString, |
|
| 81 |
+ command.Onbuild: parseSubCommand, |
|
| 82 |
+ command.Run: parseMaybeJSON, |
|
| 83 |
+ command.Shell: parseMaybeJSON, |
|
| 84 |
+ command.StopSignal: parseString, |
|
| 85 |
+ command.User: parseString, |
|
| 86 |
+ command.Volume: parseMaybeJSONToList, |
|
| 87 |
+ command.Workdir: parseString, |
|
| 87 | 88 |
} |
| 88 | 89 |
} |
| 89 | 90 |
|
| ... | ... |
@@ -2,7 +2,7 @@ package dockerfile |
| 2 | 2 |
|
| 3 | 3 |
import "strings" |
| 4 | 4 |
|
| 5 |
-// handleJSONArgs parses command passed to CMD, ENTRYPOINT or RUN instruction in Dockerfile |
|
| 5 |
+// handleJSONArgs parses command passed to CMD, ENTRYPOINT, RUN and SHELL instruction in Dockerfile |
|
| 6 | 6 |
// for exec form it returns untouched args slice |
| 7 | 7 |
// for shell form it returns concatenated args as the first element of a slice |
| 8 | 8 |
func handleJSONArgs(args []string, attributes map[string]bool) []string {
|
| ... | ... |
@@ -497,7 +497,8 @@ generated images. |
| 497 | 497 |
|
| 498 | 498 |
RUN has 2 forms: |
| 499 | 499 |
|
| 500 |
-- `RUN <command>` (*shell* form, the command is run in a shell - `/bin/sh -c`) |
|
| 500 |
+- `RUN <command>` (*shell* form, the command is run in a shell, which by |
|
| 501 |
+default is `/bin/sh -c` on Linux or `cmd /S /C` on Windows) |
|
| 501 | 502 |
- `RUN ["executable", "param1", "param2"]` (*exec* form) |
| 502 | 503 |
|
| 503 | 504 |
The `RUN` instruction will execute any commands in a new layer on top of the |
| ... | ... |
@@ -509,7 +510,10 @@ concepts of Docker where commits are cheap and containers can be created from |
| 509 | 509 |
any point in an image's history, much like source control. |
| 510 | 510 |
|
| 511 | 511 |
The *exec* form makes it possible to avoid shell string munging, and to `RUN` |
| 512 |
-commands using a base image that does not contain `/bin/sh`. |
|
| 512 |
+commands using a base image that does not contain the specified shell executable. |
|
| 513 |
+ |
|
| 514 |
+The default shell for the *shell* form can be changed using the `SHELL` |
|
| 515 |
+command. |
|
| 513 | 516 |
|
| 514 | 517 |
In the *shell* form you can use a `\` (backslash) to continue a single |
| 515 | 518 |
RUN instruction onto the next line. For example, consider these two lines: |
| ... | ... |
@@ -1469,7 +1473,7 @@ For example you might add something like this: |
| 1469 | 1469 |
|
| 1470 | 1470 |
## STOPSIGNAL |
| 1471 | 1471 |
|
| 1472 |
- STOPSIGNAL signal |
|
| 1472 |
+ STOPSIGNAL signal |
|
| 1473 | 1473 |
|
| 1474 | 1474 |
The `STOPSIGNAL` instruction sets the system call signal that will be sent to the container to exit. |
| 1475 | 1475 |
This signal can be a valid unsigned number that matches a position in the kernel's syscall table, for instance 9, |
| ... | ... |
@@ -1541,6 +1545,120 @@ generated with the new status. |
| 1541 | 1541 |
The `HEALTHCHECK` feature was added in Docker 1.12. |
| 1542 | 1542 |
|
| 1543 | 1543 |
|
| 1544 |
+## SHELL |
|
| 1545 |
+ |
|
| 1546 |
+ SHELL ["executable", "parameters"] |
|
| 1547 |
+ |
|
| 1548 |
+The `SHELL` instruction allows the default shell used for the *shell* form of |
|
| 1549 |
+commands to be overridden. The default shell on Linux is `["/bin/sh", "-c"]`, and on |
|
| 1550 |
+Windows is `["cmd", "/S", "/C"]`. The `SHELL` instruction *must* be written in JSON |
|
| 1551 |
+form in a Dockerfile. |
|
| 1552 |
+ |
|
| 1553 |
+The `SHELL` instruction is particularly useful on Windows where there are |
|
| 1554 |
+two commonly used and quite different native shells: `cmd` and `powershell`, as |
|
| 1555 |
+well as alternate shells available including `sh`. |
|
| 1556 |
+ |
|
| 1557 |
+The `SHELL` instruction can appear multiple times. Each `SHELL` instruction overrides |
|
| 1558 |
+all previous `SHELL` instructions, and affects all subsequent instructions. For example: |
|
| 1559 |
+ |
|
| 1560 |
+ FROM windowsservercore |
|
| 1561 |
+ |
|
| 1562 |
+ # Executed as cmd /S /C echo default |
|
| 1563 |
+ RUN echo default |
|
| 1564 |
+ |
|
| 1565 |
+ # Executed as cmd /S /C powershell -command Write-Host default |
|
| 1566 |
+ RUN powershell -command Write-Host default |
|
| 1567 |
+ |
|
| 1568 |
+ # Executed as powershell -command Write-Host hello |
|
| 1569 |
+ SHELL ["powershell", "-command"] |
|
| 1570 |
+ RUN Write-Host hello |
|
| 1571 |
+ |
|
| 1572 |
+ # Executed as cmd /S /C echo hello |
|
| 1573 |
+ SHELL ["cmd", "/S"", "/C"] |
|
| 1574 |
+ RUN echo hello |
|
| 1575 |
+ |
|
| 1576 |
+The following instructions can be affected by the `SHELL` instruction when the |
|
| 1577 |
+*shell* form of them is used in a Dockerfile: `RUN`, `CMD` and `ENTRYPOINT`. |
|
| 1578 |
+ |
|
| 1579 |
+The following example is a common pattern found on Windows which can be |
|
| 1580 |
+streamlined by using the `SHELL` instruction: |
|
| 1581 |
+ |
|
| 1582 |
+ ... |
|
| 1583 |
+ RUN powershell -command Execute-MyCmdlet -param1 "c:\foo.txt" |
|
| 1584 |
+ ... |
|
| 1585 |
+ |
|
| 1586 |
+The command invoked by docker will be: |
|
| 1587 |
+ |
|
| 1588 |
+ cmd /S /C powershell -command Execute-MyCmdlet -param1 "c:\foo.txt" |
|
| 1589 |
+ |
|
| 1590 |
+ This is inefficient for two reasons. First, there is an un-necessary cmd.exe command |
|
| 1591 |
+ processor (aka shell) being invoked. Second, each `RUN` instruction in the *shell* |
|
| 1592 |
+ form requires an extra `powershell -command` prefixing the command. |
|
| 1593 |
+ |
|
| 1594 |
+To make this more efficient, one of two mechanisms can be employed. One is to |
|
| 1595 |
+use the JSON form of the RUN command such as: |
|
| 1596 |
+ |
|
| 1597 |
+ ... |
|
| 1598 |
+ RUN ["powershell", "-command", "Execute-MyCmdlet", "-param1 \"c:\\foo.txt\""] |
|
| 1599 |
+ ... |
|
| 1600 |
+ |
|
| 1601 |
+While the JSON form is unambiguous and does not use the un-necessary cmd.exe, |
|
| 1602 |
+it does require more verbosity through double-quoting and escaping. The alternate |
|
| 1603 |
+mechanism is to use the `SHELL` instruction and the *shell* form, |
|
| 1604 |
+making a more natural syntax for Windows users, especially when combined with |
|
| 1605 |
+the `escape` parser directive: |
|
| 1606 |
+ |
|
| 1607 |
+ # escape=` |
|
| 1608 |
+ |
|
| 1609 |
+ FROM windowsservercore |
|
| 1610 |
+ SHELL ["powershell","-command"] |
|
| 1611 |
+ RUN New-Item -ItemType Directory C:\Example |
|
| 1612 |
+ ADD Execute-MyCmdlet.ps1 c:\example\ |
|
| 1613 |
+ RUN c:\example\Execute-MyCmdlet -sample 'hello world' |
|
| 1614 |
+ |
|
| 1615 |
+Resulting in: |
|
| 1616 |
+ |
|
| 1617 |
+ PS E:\docker\build\shell> docker build -t shell . |
|
| 1618 |
+ Sending build context to Docker daemon 3.584 kB |
|
| 1619 |
+ Step 1 : FROM windowsservercore |
|
| 1620 |
+ ---> 5bc36a335344 |
|
| 1621 |
+ Step 2 : SHELL powershell -command |
|
| 1622 |
+ ---> Running in 87d7a64c9751 |
|
| 1623 |
+ ---> 4327358436c1 |
|
| 1624 |
+ Removing intermediate container 87d7a64c9751 |
|
| 1625 |
+ Step 3 : RUN New-Item -ItemType Directory C:\Example |
|
| 1626 |
+ ---> Running in 3e6ba16b8df9 |
|
| 1627 |
+ |
|
| 1628 |
+ |
|
| 1629 |
+ Directory: C:\ |
|
| 1630 |
+ |
|
| 1631 |
+ |
|
| 1632 |
+ Mode LastWriteTime Length Name |
|
| 1633 |
+ ---- ------------- ------ ---- |
|
| 1634 |
+ d----- 6/2/2016 2:59 PM Example |
|
| 1635 |
+ |
|
| 1636 |
+ |
|
| 1637 |
+ ---> 1f1dfdcec085 |
|
| 1638 |
+ Removing intermediate container 3e6ba16b8df9 |
|
| 1639 |
+ Step 4 : ADD Execute-MyCmdlet.ps1 c:\example\ |
|
| 1640 |
+ ---> 6770b4c17f29 |
|
| 1641 |
+ Removing intermediate container b139e34291dc |
|
| 1642 |
+ Step 5 : RUN c:\example\Execute-MyCmdlet -sample 'hello world' |
|
| 1643 |
+ ---> Running in abdcf50dfd1f |
|
| 1644 |
+ Hello from Execute-MyCmdlet.ps1 - passed hello world |
|
| 1645 |
+ ---> ba0e25255fda |
|
| 1646 |
+ Removing intermediate container abdcf50dfd1f |
|
| 1647 |
+ Successfully built ba0e25255fda |
|
| 1648 |
+ PS E:\docker\build\shell> |
|
| 1649 |
+ |
|
| 1650 |
+The `SHELL` instruction could also be used to modify the way in which |
|
| 1651 |
+a shell operates. For example, using `SHELL cmd /S /C /V:ON|OFF` on Windows, delayed |
|
| 1652 |
+environment variable expansion semantics could be modified. |
|
| 1653 |
+ |
|
| 1654 |
+The `SHELL` instruction can also be used on Linux should an alternate shell be |
|
| 1655 |
+required such `zsh`, `csh`, `tcsh` and others. |
|
| 1656 |
+ |
|
| 1657 |
+The `SHELL` feature was added in Docker 1.12. |
|
| 1544 | 1658 |
|
| 1545 | 1659 |
## Dockerfile examples |
| 1546 | 1660 |
|
| ... | ... |
@@ -6829,3 +6829,146 @@ func (s *DockerSuite) TestBuildWithUTF8BOMDockerignore(c *check.C) {
|
| 6829 | 6829 |
c.Fatal(err) |
| 6830 | 6830 |
} |
| 6831 | 6831 |
} |
| 6832 |
+ |
|
| 6833 |
+// #22489 Shell test to confirm config gets updated correctly |
|
| 6834 |
+func (s *DockerSuite) TestBuildShellUpdatesConfig(c *check.C) {
|
|
| 6835 |
+ name := "testbuildshellupdatesconfig" |
|
| 6836 |
+ |
|
| 6837 |
+ expected := `["foo","-bar","#(nop) ","SHELL [foo -bar]"]` |
|
| 6838 |
+ _, err := buildImage(name, |
|
| 6839 |
+ `FROM `+minimalBaseImage()+` |
|
| 6840 |
+ SHELL ["foo", "-bar"]`, |
|
| 6841 |
+ true) |
|
| 6842 |
+ if err != nil {
|
|
| 6843 |
+ c.Fatal(err) |
|
| 6844 |
+ } |
|
| 6845 |
+ res := inspectFieldJSON(c, name, "ContainerConfig.Cmd") |
|
| 6846 |
+ if res != expected {
|
|
| 6847 |
+ c.Fatalf("%s, expected %s", res, expected)
|
|
| 6848 |
+ } |
|
| 6849 |
+ res = inspectFieldJSON(c, name, "ContainerConfig.Shell") |
|
| 6850 |
+ if res != `["foo","-bar"]` {
|
|
| 6851 |
+ c.Fatalf(`%s, expected ["foo","-bar"]`, res) |
|
| 6852 |
+ } |
|
| 6853 |
+} |
|
| 6854 |
+ |
|
| 6855 |
+// #22489 Changing the shell multiple times and CMD after. |
|
| 6856 |
+func (s *DockerSuite) TestBuildShellMultiple(c *check.C) {
|
|
| 6857 |
+ name := "testbuildshellmultiple" |
|
| 6858 |
+ |
|
| 6859 |
+ _, out, _, err := buildImageWithStdoutStderr(name, |
|
| 6860 |
+ `FROM busybox |
|
| 6861 |
+ RUN echo defaultshell |
|
| 6862 |
+ SHELL ["echo"] |
|
| 6863 |
+ RUN echoshell |
|
| 6864 |
+ SHELL ["ls"] |
|
| 6865 |
+ RUN -l |
|
| 6866 |
+ CMD -l`, |
|
| 6867 |
+ true) |
|
| 6868 |
+ if err != nil {
|
|
| 6869 |
+ c.Fatal(err) |
|
| 6870 |
+ } |
|
| 6871 |
+ |
|
| 6872 |
+ // Must contain 'defaultshell' twice |
|
| 6873 |
+ if len(strings.Split(out, "defaultshell")) != 3 {
|
|
| 6874 |
+ c.Fatalf("defaultshell should have appeared twice in %s", out)
|
|
| 6875 |
+ } |
|
| 6876 |
+ |
|
| 6877 |
+ // Must contain 'echoshell' twice |
|
| 6878 |
+ if len(strings.Split(out, "echoshell")) != 3 {
|
|
| 6879 |
+ c.Fatalf("echoshell should have appeared twice in %s", out)
|
|
| 6880 |
+ } |
|
| 6881 |
+ |
|
| 6882 |
+ // Must contain "total " (part of ls -l) |
|
| 6883 |
+ if !strings.Contains(out, "total ") {
|
|
| 6884 |
+ c.Fatalf("%s should have contained 'total '", out)
|
|
| 6885 |
+ } |
|
| 6886 |
+ |
|
| 6887 |
+ // A container started from the image uses the shell-form CMD. |
|
| 6888 |
+ // Last shell is ls. CMD is -l. So should contain 'total '. |
|
| 6889 |
+ outrun, _ := dockerCmd(c, "run", "--rm", name) |
|
| 6890 |
+ if !strings.Contains(outrun, "total ") {
|
|
| 6891 |
+ c.Fatalf("Expected started container to run ls -l. %s", outrun)
|
|
| 6892 |
+ } |
|
| 6893 |
+} |
|
| 6894 |
+ |
|
| 6895 |
+// #22489. Changed SHELL with ENTRYPOINT |
|
| 6896 |
+func (s *DockerSuite) TestBuildShellEntrypoint(c *check.C) {
|
|
| 6897 |
+ name := "testbuildshellentrypoint" |
|
| 6898 |
+ |
|
| 6899 |
+ _, err := buildImage(name, |
|
| 6900 |
+ `FROM busybox |
|
| 6901 |
+ SHELL ["ls"] |
|
| 6902 |
+ ENTRYPOINT -l`, |
|
| 6903 |
+ true) |
|
| 6904 |
+ if err != nil {
|
|
| 6905 |
+ c.Fatal(err) |
|
| 6906 |
+ } |
|
| 6907 |
+ |
|
| 6908 |
+ // A container started from the image uses the shell-form ENTRYPOINT. |
|
| 6909 |
+ // Shell is ls. ENTRYPOINT is -l. So should contain 'total '. |
|
| 6910 |
+ outrun, _ := dockerCmd(c, "run", "--rm", name) |
|
| 6911 |
+ if !strings.Contains(outrun, "total ") {
|
|
| 6912 |
+ c.Fatalf("Expected started container to run ls -l. %s", outrun)
|
|
| 6913 |
+ } |
|
| 6914 |
+} |
|
| 6915 |
+ |
|
| 6916 |
+// #22489 Shell test to confirm shell is inherited in a subsequent build |
|
| 6917 |
+func (s *DockerSuite) TestBuildShellInherited(c *check.C) {
|
|
| 6918 |
+ name1 := "testbuildshellinherited1" |
|
| 6919 |
+ _, err := buildImage(name1, |
|
| 6920 |
+ `FROM busybox |
|
| 6921 |
+ SHELL ["ls"]`, |
|
| 6922 |
+ true) |
|
| 6923 |
+ if err != nil {
|
|
| 6924 |
+ c.Fatal(err) |
|
| 6925 |
+ } |
|
| 6926 |
+ |
|
| 6927 |
+ name2 := "testbuildshellinherited2" |
|
| 6928 |
+ _, out, _, err := buildImageWithStdoutStderr(name2, |
|
| 6929 |
+ `FROM `+name1+` |
|
| 6930 |
+ RUN -l`, |
|
| 6931 |
+ true) |
|
| 6932 |
+ if err != nil {
|
|
| 6933 |
+ c.Fatal(err) |
|
| 6934 |
+ } |
|
| 6935 |
+ |
|
| 6936 |
+ // ls -l has "total " followed by some number in it, ls without -l does not. |
|
| 6937 |
+ if !strings.Contains(out, "total ") {
|
|
| 6938 |
+ c.Fatalf("Should have seen total in 'ls -l'.\n%s", out)
|
|
| 6939 |
+ } |
|
| 6940 |
+} |
|
| 6941 |
+ |
|
| 6942 |
+// #22489 Shell test to confirm non-JSON doesn't work |
|
| 6943 |
+func (s *DockerSuite) TestBuildShellNotJSON(c *check.C) {
|
|
| 6944 |
+ name := "testbuildshellnotjson" |
|
| 6945 |
+ |
|
| 6946 |
+ _, err := buildImage(name, |
|
| 6947 |
+ `FROM `+minimalBaseImage()+` |
|
| 6948 |
+ sHeLl exec -form`, // Casing explicit to ensure error is upper-cased. |
|
| 6949 |
+ true) |
|
| 6950 |
+ if err == nil {
|
|
| 6951 |
+ c.Fatal("Image build should have failed")
|
|
| 6952 |
+ } |
|
| 6953 |
+ if !strings.Contains(err.Error(), "SHELL requires the arguments to be in JSON form") {
|
|
| 6954 |
+ c.Fatal("Error didn't indicate that arguments must be in JSON form")
|
|
| 6955 |
+ } |
|
| 6956 |
+} |
|
| 6957 |
+ |
|
| 6958 |
+// #22489 Windows shell test to confirm native is powershell if executing a PS command |
|
| 6959 |
+// This would error if the default shell were still cmd. |
|
| 6960 |
+func (s *DockerSuite) TestBuildShellWindowsPowershell(c *check.C) {
|
|
| 6961 |
+ testRequires(c, DaemonIsWindows) |
|
| 6962 |
+ name := "testbuildshellpowershell" |
|
| 6963 |
+ _, out, err := buildImageWithOut(name, |
|
| 6964 |
+ `FROM `+minimalBaseImage()+` |
|
| 6965 |
+ SHELL ["powershell", "-command"] |
|
| 6966 |
+ RUN Write-Host John`, |
|
| 6967 |
+ true) |
|
| 6968 |
+ if err != nil {
|
|
| 6969 |
+ c.Fatal(err) |
|
| 6970 |
+ } |
|
| 6971 |
+ if !strings.Contains(out, "\nJohn\n") {
|
|
| 6972 |
+ c.Fatalf("Line with 'John' not found in output %q", out)
|
|
| 6973 |
+ } |
|
| 6974 |
+} |