Quellcode durchsuchen

Merge pull request #22489 from Microsoft/jjh/shell

Builder shell configuration
Vincent Demeester vor 9 Jahren
Ursprung
Commit
df1dd1322d

+ 5 - 0
builder/dockerfile/builder_unix.go

@@ -0,0 +1,5 @@
+// +build !windows
+
+package dockerfile
+
+var defaultShell = []string{"/bin/sh", "-c"}

+ 3 - 0
builder/dockerfile/builder_windows.go

@@ -0,0 +1,3 @@
+package dockerfile
+
+var defaultShell = []string{"cmd", "/S", "/C"}

+ 24 - 22
builder/dockerfile/command/command.go

@@ -3,42 +3,44 @@ package command
 
 
 // Define constants for the command strings
 // Define constants for the command strings
 const (
 const (
-	Env         = "env"
-	Label       = "label"
-	Maintainer  = "maintainer"
 	Add         = "add"
 	Add         = "add"
+	Arg         = "arg"
+	Cmd         = "cmd"
 	Copy        = "copy"
 	Copy        = "copy"
+	Entrypoint  = "entrypoint"
+	Env         = "env"
+	Expose      = "expose"
 	From        = "from"
 	From        = "from"
+	Healthcheck = "healthcheck"
+	Label       = "label"
+	Maintainer  = "maintainer"
 	Onbuild     = "onbuild"
 	Onbuild     = "onbuild"
-	Workdir     = "workdir"
 	Run         = "run"
 	Run         = "run"
-	Cmd         = "cmd"
-	Entrypoint  = "entrypoint"
-	Expose      = "expose"
-	Volume      = "volume"
-	User        = "user"
+	Shell       = "shell"
 	StopSignal  = "stopsignal"
 	StopSignal  = "stopsignal"
-	Arg         = "arg"
-	Healthcheck = "healthcheck"
+	User        = "user"
+	Volume      = "volume"
+	Workdir     = "workdir"
 )
 )
 
 
 // Commands is list of all Dockerfile commands
 // Commands is list of all Dockerfile commands
 var Commands = map[string]struct{}{
 var Commands = map[string]struct{}{
-	Env:         {},
-	Label:       {},
-	Maintainer:  {},
 	Add:         {},
 	Add:         {},
+	Arg:         {},
+	Cmd:         {},
 	Copy:        {},
 	Copy:        {},
+	Entrypoint:  {},
+	Env:         {},
+	Expose:      {},
 	From:        {},
 	From:        {},
+	Healthcheck: {},
+	Label:       {},
+	Maintainer:  {},
 	Onbuild:     {},
 	Onbuild:     {},
-	Workdir:     {},
 	Run:         {},
 	Run:         {},
-	Cmd:         {},
-	Entrypoint:  {},
-	Expose:      {},
-	Volume:      {},
-	User:        {},
+	Shell:       {},
 	StopSignal:  {},
 	StopSignal:  {},
-	Arg:         {},
-	Healthcheck: {},
+	User:        {},
+	Volume:      {},
+	Workdir:     {},
 }
 }

+ 38 - 20
builder/dockerfile/dispatchers.go

@@ -274,8 +274,8 @@ func workdir(b *Builder, args []string, attributes map[string]bool, original str
 // RUN some command yo
 // RUN some command yo
 //
 //
 // run a command and commit the image. Args are automatically prepended with
 // run a command and commit the image. Args are automatically prepended with
-// 'sh -c' under linux or 'cmd /S /C' under Windows, in the event there is
-// only one argument. The difference in processing:
+// the current SHELL which defaults to 'sh -c' under linux or 'cmd /S /C' under
+// Windows, in the event there is only one argument The difference in processing:
 //
 //
 // RUN echo hi          # sh -c echo hi       (Linux)
 // RUN echo hi          # sh -c echo hi       (Linux)
 // RUN echo hi          # cmd /S /C echo hi   (Windows)
 // 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)
 	args = handleJSONArgs(args, attributes)
 	args = handleJSONArgs(args, attributes)
 
 
 	if !attributes["json"] {
 	if !attributes["json"] {
-		if runtime.GOOS != "windows" {
-			args = append([]string{"/bin/sh", "-c"}, args...)
-		} else {
-			args = append([]string{"cmd", "/S", "/C"}, args...)
-		}
+		args = append(getShell(b.runConfig), args...)
 	}
 	}
-
 	config := &container.Config{
 	config := &container.Config{
 		Cmd:   strslice.StrSlice(args),
 		Cmd:   strslice.StrSlice(args),
 		Image: b.image,
 		Image: b.image,
@@ -408,11 +403,7 @@ func cmd(b *Builder, args []string, attributes map[string]bool, original string)
 	cmdSlice := handleJSONArgs(args, attributes)
 	cmdSlice := handleJSONArgs(args, attributes)
 
 
 	if !attributes["json"] {
 	if !attributes["json"] {
-		if runtime.GOOS != "windows" {
-			cmdSlice = append([]string{"/bin/sh", "-c"}, cmdSlice...)
-		} else {
-			cmdSlice = append([]string{"cmd", "/S", "/C"}, cmdSlice...)
-		}
+		cmdSlice = append(getShell(b.runConfig), cmdSlice...)
 	}
 	}
 
 
 	b.runConfig.Cmd = strslice.StrSlice(cmdSlice)
 	b.runConfig.Cmd = strslice.StrSlice(cmdSlice)
@@ -535,8 +526,8 @@ func healthcheck(b *Builder, args []string, attributes map[string]bool, original
 
 
 // ENTRYPOINT /usr/sbin/nginx
 // ENTRYPOINT /usr/sbin/nginx
 //
 //
-// Set the entrypoint (which defaults to sh -c on linux, or cmd /S /C on Windows) to
-// /usr/sbin/nginx. Will accept the CMD as the arguments to /usr/sbin/nginx.
+// Set the entrypoint to /usr/sbin/nginx. Will accept the CMD as the arguments
+// to /usr/sbin/nginx. Uses the default shell if not in JSON format.
 //
 //
 // Handles command processing similar to CMD and RUN, only b.runConfig.Entrypoint
 // Handles command processing similar to CMD and RUN, only b.runConfig.Entrypoint
 // is initialized at NewBuilder time instead of through argument parsing.
 // 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
 		b.runConfig.Entrypoint = nil
 		b.runConfig.Entrypoint = nil
 	default:
 	default:
 		// ENTRYPOINT echo hi
 		// ENTRYPOINT echo hi
-		if runtime.GOOS != "windows" {
-			b.runConfig.Entrypoint = strslice.StrSlice{"/bin/sh", "-c", parsed[0]}
-		} else {
-			b.runConfig.Entrypoint = strslice.StrSlice{"cmd", "/S", "/C", parsed[0]}
-		}
+		b.runConfig.Entrypoint = strslice.StrSlice(append(getShell(b.runConfig), parsed[0]))
 	}
 	}
 
 
 	// when setting the entrypoint if a CMD was not explicitly set then
 	// 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)
 	return b.commit("", b.runConfig.Cmd, fmt.Sprintf("ARG %s", arg))
 	return b.commit("", b.runConfig.Cmd, fmt.Sprintf("ARG %s", arg))
 }
 }
 
 
+// SHELL powershell -command
+//
+// Set the non-default shell to use.
+func shell(b *Builder, args []string, attributes map[string]bool, original string) error {
+	if err := b.flags.Parse(); err != nil {
+		return err
+	}
+	shellSlice := handleJSONArgs(args, attributes)
+	switch {
+	case len(shellSlice) == 0:
+		// SHELL []
+		return errAtLeastOneArgument("SHELL")
+	case attributes["json"]:
+		// SHELL ["powershell", "-command"]
+		b.runConfig.Shell = strslice.StrSlice(shellSlice)
+	default:
+		// SHELL powershell -command - not JSON
+		return errNotJSON("SHELL", original)
+	}
+	return b.commit("", b.runConfig.Cmd, fmt.Sprintf("SHELL %v", shellSlice))
+}
+
 func errAtLeastOneArgument(command string) error {
 func errAtLeastOneArgument(command string) error {
 	return fmt.Errorf("%s requires at least one argument", command)
 	return fmt.Errorf("%s requires at least one argument", command)
 }
 }
@@ -738,3 +747,12 @@ func errExactlyOneArgument(command string) error {
 func errTooManyArguments(command string) error {
 func errTooManyArguments(command string) error {
 	return fmt.Errorf("Bad input to %s, too many arguments", command)
 	return fmt.Errorf("Bad input to %s, too many arguments", command)
 }
 }
+
+// getShell is a helper function which gets the right shell for prefixing the
+// shell-form of RUN, ENTRYPOINT and CMD instructions
+func getShell(c *container.Config) []string {
+	if 0 == len(c.Shell) {
+		return defaultShell[:]
+	}
+	return c.Shell[:]
+}

+ 4 - 0
builder/dockerfile/dispatchers_unix.go

@@ -21,3 +21,7 @@ func normaliseWorkdir(current string, requested string) (string, error) {
 	}
 	}
 	return requested, nil
 	return requested, nil
 }
 }
+
+func errNotJSON(command, _ string) error {
+	return fmt.Errorf("%s requires the arguments to be in JSON form", command)
+}

+ 20 - 0
builder/dockerfile/dispatchers_windows.go

@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"fmt"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
+	"regexp"
 	"strings"
 	"strings"
 
 
 	"github.com/docker/docker/pkg/system"
 	"github.com/docker/docker/pkg/system"
@@ -43,3 +44,22 @@ func normaliseWorkdir(current string, requested string) (string, error) {
 	// Upper-case drive letter
 	// Upper-case drive letter
 	return (strings.ToUpper(string(requested[0])) + requested[1:]), nil
 	return (strings.ToUpper(string(requested[0])) + requested[1:]), nil
 }
 }
+
+func errNotJSON(command, original string) error {
+	// For Windows users, give a hint if it looks like it might contain
+	// a path which hasn't been escaped such as ["c:\windows\system32\prog.exe", "-param"],
+	// as JSON must be escaped. Unfortunate...
+	//
+	// Specifically looking for quote-driveletter-colon-backslash, there's no
+	// double backslash and a [] pair. No, this is not perfect, but it doesn't
+	// have to be. It's simply a hint to make life a little easier.
+	extra := ""
+	original = filepath.FromSlash(strings.ToLower(strings.Replace(strings.ToLower(original), strings.ToLower(command)+" ", "", -1)))
+	if len(regexp.MustCompile(`"[a-z]:\\.*`).FindStringSubmatch(original)) > 0 &&
+		!strings.Contains(original, `\\`) &&
+		strings.Contains(original, "[") &&
+		strings.Contains(original, "]") {
+		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)
+	}
+	return fmt.Errorf("%s requires the arguments to be in JSON form%s", command, extra)
+}

+ 12 - 11
builder/dockerfile/evaluator.go

@@ -58,23 +58,24 @@ var evaluateTable map[string]func(*Builder, []string, map[string]bool, string) e
 
 
 func init() {
 func init() {
 	evaluateTable = map[string]func(*Builder, []string, map[string]bool, string) error{
 	evaluateTable = map[string]func(*Builder, []string, map[string]bool, string) error{
-		command.Env:         env,
-		command.Label:       label,
-		command.Maintainer:  maintainer,
 		command.Add:         add,
 		command.Add:         add,
+		command.Arg:         arg,
+		command.Cmd:         cmd,
 		command.Copy:        dispatchCopy, // copy() is a go builtin
 		command.Copy:        dispatchCopy, // copy() is a go builtin
+		command.Entrypoint:  entrypoint,
+		command.Env:         env,
+		command.Expose:      expose,
 		command.From:        from,
 		command.From:        from,
+		command.Healthcheck: healthcheck,
+		command.Label:       label,
+		command.Maintainer:  maintainer,
 		command.Onbuild:     onbuild,
 		command.Onbuild:     onbuild,
-		command.Workdir:     workdir,
 		command.Run:         run,
 		command.Run:         run,
-		command.Cmd:         cmd,
-		command.Entrypoint:  entrypoint,
-		command.Expose:      expose,
-		command.Volume:      volume,
-		command.User:        user,
+		command.Shell:       shell,
 		command.StopSignal:  stopSignal,
 		command.StopSignal:  stopSignal,
-		command.Arg:         arg,
-		command.Healthcheck: healthcheck,
+		command.User:        user,
+		command.Volume:      volume,
+		command.Workdir:     workdir,
 	}
 	}
 }
 }
 
 

+ 2 - 11
builder/dockerfile/internals.go

@@ -14,7 +14,6 @@ import (
 	"net/url"
 	"net/url"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
-	"runtime"
 	"sort"
 	"sort"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
@@ -51,11 +50,7 @@ func (b *Builder) commit(id string, autoCmd strslice.StrSlice, comment string) e
 
 
 	if id == "" {
 	if id == "" {
 		cmd := b.runConfig.Cmd
 		cmd := b.runConfig.Cmd
-		if runtime.GOOS != "windows" {
-			b.runConfig.Cmd = strslice.StrSlice{"/bin/sh", "-c", "#(nop) " + comment}
-		} else {
-			b.runConfig.Cmd = strslice.StrSlice{"cmd", "/S /C", "REM (nop) " + comment}
-		}
+		b.runConfig.Cmd = strslice.StrSlice(append(getShell(b.runConfig), "#(nop) ", comment))
 		defer func(cmd strslice.StrSlice) { b.runConfig.Cmd = cmd }(cmd)
 		defer func(cmd strslice.StrSlice) { b.runConfig.Cmd = cmd }(cmd)
 
 
 		hit, err := b.probeCache()
 		hit, err := b.probeCache()
@@ -177,11 +172,7 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalD
 	}
 	}
 
 
 	cmd := b.runConfig.Cmd
 	cmd := b.runConfig.Cmd
-	if runtime.GOOS != "windows" {
-		b.runConfig.Cmd = strslice.StrSlice{"/bin/sh", "-c", fmt.Sprintf("#(nop) %s %s in %s", cmdName, srcHash, dest)}
-	} else {
-		b.runConfig.Cmd = strslice.StrSlice{"cmd", "/S", "/C", fmt.Sprintf("REM (nop) %s %s in %s", cmdName, srcHash, dest)}
-	}
+	b.runConfig.Cmd = strslice.StrSlice(append(getShell(b.runConfig), "#(nop) %s %s in %s ", cmdName, srcHash, dest))
 	defer func(cmd strslice.StrSlice) { b.runConfig.Cmd = cmd }(cmd)
 	defer func(cmd strslice.StrSlice) { b.runConfig.Cmd = cmd }(cmd)
 
 
 	if hit, err := b.probeCache(); err != nil {
 	if hit, err := b.probeCache(); err != nil {

+ 13 - 12
builder/dockerfile/parser/parser.go

@@ -67,23 +67,24 @@ func init() {
 	// functions. Errors are propagated up by Parse() and the resulting AST can
 	// functions. Errors are propagated up by Parse() and the resulting AST can
 	// be incorporated directly into the existing AST as a next.
 	// be incorporated directly into the existing AST as a next.
 	dispatch = map[string]func(string) (*Node, map[string]bool, error){
 	dispatch = map[string]func(string) (*Node, map[string]bool, error){
-		command.User:        parseString,
-		command.Onbuild:     parseSubCommand,
-		command.Workdir:     parseString,
-		command.Env:         parseEnv,
-		command.Label:       parseLabel,
-		command.Maintainer:  parseString,
-		command.From:        parseString,
 		command.Add:         parseMaybeJSONToList,
 		command.Add:         parseMaybeJSONToList,
-		command.Copy:        parseMaybeJSONToList,
-		command.Run:         parseMaybeJSON,
+		command.Arg:         parseNameOrNameVal,
 		command.Cmd:         parseMaybeJSON,
 		command.Cmd:         parseMaybeJSON,
+		command.Copy:        parseMaybeJSONToList,
 		command.Entrypoint:  parseMaybeJSON,
 		command.Entrypoint:  parseMaybeJSON,
+		command.Env:         parseEnv,
 		command.Expose:      parseStringsWhitespaceDelimited,
 		command.Expose:      parseStringsWhitespaceDelimited,
-		command.Volume:      parseMaybeJSONToList,
-		command.StopSignal:  parseString,
-		command.Arg:         parseNameOrNameVal,
+		command.From:        parseString,
 		command.Healthcheck: parseHealthConfig,
 		command.Healthcheck: parseHealthConfig,
+		command.Label:       parseLabel,
+		command.Maintainer:  parseString,
+		command.Onbuild:     parseSubCommand,
+		command.Run:         parseMaybeJSON,
+		command.Shell:       parseMaybeJSON,
+		command.StopSignal:  parseString,
+		command.User:        parseString,
+		command.Volume:      parseMaybeJSONToList,
+		command.Workdir:     parseString,
 	}
 	}
 }
 }
 
 

+ 1 - 1
builder/dockerfile/support.go

@@ -2,7 +2,7 @@ package dockerfile
 
 
 import "strings"
 import "strings"
 
 
-// handleJSONArgs parses command passed to CMD, ENTRYPOINT or RUN instruction in Dockerfile
+// handleJSONArgs parses command passed to CMD, ENTRYPOINT, RUN and SHELL instruction in Dockerfile
 // for exec form it returns untouched args slice
 // for exec form it returns untouched args slice
 // for shell form it returns concatenated args as the first element of a slice
 // for shell form it returns concatenated args as the first element of a slice
 func handleJSONArgs(args []string, attributes map[string]bool) []string {
 func handleJSONArgs(args []string, attributes map[string]bool) []string {

+ 121 - 3
docs/reference/builder.md

@@ -497,7 +497,8 @@ generated images.
 
 
 RUN has 2 forms:
 RUN has 2 forms:
 
 
-- `RUN <command>` (*shell* form, the command is run in a shell - `/bin/sh -c`)
+- `RUN <command>` (*shell* form, the command is run in a shell, which by 
+default is `/bin/sh -c` on Linux or `cmd /S /C` on Windows)
 - `RUN ["executable", "param1", "param2"]` (*exec* form)
 - `RUN ["executable", "param1", "param2"]` (*exec* form)
 
 
 The `RUN` instruction will execute any commands in a new layer on top of the
 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
 any point in an image's history, much like source control.
 any point in an image's history, much like source control.
 
 
 The *exec* form makes it possible to avoid shell string munging, and to `RUN`
 The *exec* form makes it possible to avoid shell string munging, and to `RUN`
-commands using a base image that does not contain `/bin/sh`.
+commands using a base image that does not contain the specified shell executable.
+
+The default shell for the *shell* form can be changed using the `SHELL`
+command.
 
 
 In the *shell* form you can use a `\` (backslash) to continue a single
 In the *shell* form you can use a `\` (backslash) to continue a single
 RUN instruction onto the next line. For example, consider these two lines:
 RUN instruction onto the next line. For example, consider these two lines:
@@ -1469,7 +1473,7 @@ For example you might add something like this:
 
 
 ## STOPSIGNAL
 ## STOPSIGNAL
 
 
-	STOPSIGNAL signal
+    STOPSIGNAL signal
 
 
 The `STOPSIGNAL` instruction sets the system call signal that will be sent to the container to exit.
 The `STOPSIGNAL` instruction sets the system call signal that will be sent to the container to exit.
 This signal can be a valid unsigned number that matches a position in the kernel's syscall table, for instance 9,
 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.
 The `HEALTHCHECK` feature was added in Docker 1.12.
 The `HEALTHCHECK` feature was added in Docker 1.12.
 
 
 
 
+## SHELL
+
+    SHELL ["executable", "parameters"]
+    
+The `SHELL` instruction allows the default shell used for the *shell* form of
+commands to be overridden. The default shell on Linux is `["/bin/sh", "-c"]`, and on
+Windows is `["cmd", "/S", "/C"]`. The `SHELL` instruction *must* be written in JSON
+form in a Dockerfile.
+
+The `SHELL` instruction is particularly useful on Windows where there are
+two commonly used and quite different native shells: `cmd` and `powershell`, as
+well as alternate shells available including `sh`.
+
+The `SHELL` instruction can appear multiple times. Each `SHELL` instruction overrides 
+all previous `SHELL` instructions, and affects all subsequent instructions. For example:
+
+    FROM windowsservercore
+    
+    # Executed as cmd /S /C echo default 
+    RUN echo default
+    
+    # Executed as cmd /S /C powershell -command Write-Host default 
+    RUN powershell -command Write-Host default
+    
+    # Executed as powershell -command Write-Host hello
+    SHELL ["powershell", "-command"]
+    RUN Write-Host hello
+    
+    # Executed as cmd /S /C echo hello
+    SHELL ["cmd", "/S"", "/C"]
+    RUN echo hello
+
+The following instructions can be affected by the `SHELL` instruction when the
+*shell* form of them is used in a Dockerfile: `RUN`, `CMD` and `ENTRYPOINT`.
+
+The following example is a common pattern found on Windows which can be 
+streamlined by using the `SHELL` instruction: 
+
+    ...
+    RUN powershell -command Execute-MyCmdlet -param1 "c:\foo.txt"
+    ... 
+
+The command invoked by docker will be:
+
+    cmd /S /C powershell -command Execute-MyCmdlet -param1 "c:\foo.txt"
+   
+ This is inefficient for two reasons. First, there is an un-necessary cmd.exe command
+ processor (aka shell) being invoked. Second, each `RUN` instruction in the *shell*
+ form requires an extra `powershell -command` prefixing the command.
+ 
+To make this more efficient, one of two mechanisms can be employed. One is to
+use the JSON form of the RUN command such as:
+
+    ...
+    RUN ["powershell", "-command", "Execute-MyCmdlet", "-param1 \"c:\\foo.txt\""]
+    ...
+
+While the JSON form is unambiguous and does not use the un-necessary cmd.exe, 
+it does require more verbosity through double-quoting and escaping. The alternate
+mechanism is to use the `SHELL` instruction and the *shell* form,
+making a more natural syntax for Windows users, especially when combined with 
+the `escape` parser directive:
+  
+    # escape=`
+    
+    FROM windowsservercore
+    SHELL ["powershell","-command"]
+    RUN New-Item -ItemType Directory C:\Example
+    ADD Execute-MyCmdlet.ps1 c:\example\
+    RUN c:\example\Execute-MyCmdlet -sample 'hello world'
+
+Resulting in:
+
+    PS E:\docker\build\shell> docker build -t shell .
+    Sending build context to Docker daemon 3.584 kB
+    Step 1 : FROM windowsservercore
+     ---> 5bc36a335344
+    Step 2 : SHELL powershell -command
+     ---> Running in 87d7a64c9751
+     ---> 4327358436c1
+    Removing intermediate container 87d7a64c9751
+    Step 3 : RUN New-Item -ItemType Directory C:\Example
+     ---> Running in 3e6ba16b8df9
+    
+    
+        Directory: C:\
+    
+    
+    Mode                LastWriteTime         Length Name
+    ----                -------------         ------ ----
+    d-----         6/2/2016   2:59 PM                Example
+    
+    
+     ---> 1f1dfdcec085
+    Removing intermediate container 3e6ba16b8df9
+    Step 4 : ADD Execute-MyCmdlet.ps1 c:\example\
+     ---> 6770b4c17f29
+    Removing intermediate container b139e34291dc
+    Step 5 : RUN c:\example\Execute-MyCmdlet -sample 'hello world'
+     ---> Running in abdcf50dfd1f
+    Hello from Execute-MyCmdlet.ps1 - passed hello world
+     ---> ba0e25255fda
+    Removing intermediate container abdcf50dfd1f
+    Successfully built ba0e25255fda
+    PS E:\docker\build\shell>
+
+The `SHELL` instruction could also be used to modify the way in which
+a shell operates. For example, using `SHELL cmd /S /C /V:ON|OFF` on Windows, delayed
+environment variable expansion semantics could be modified.
+    
+The `SHELL` instruction can also be used on Linux should an alternate shell be
+required such `zsh`, `csh`, `tcsh` and others.
+
+The `SHELL` feature was added in Docker 1.12.
 
 
 ## Dockerfile examples
 ## Dockerfile examples
 
 

+ 143 - 0
integration-cli/docker_cli_build_test.go

@@ -6834,3 +6834,146 @@ func (s *DockerSuite) TestBuildWithUTF8BOMDockerignore(c *check.C) {
 		c.Fatal(err)
 		c.Fatal(err)
 	}
 	}
 }
 }
+
+// #22489 Shell test to confirm config gets updated correctly
+func (s *DockerSuite) TestBuildShellUpdatesConfig(c *check.C) {
+	name := "testbuildshellupdatesconfig"
+
+	expected := `["foo","-bar","#(nop) ","SHELL [foo -bar]"]`
+	_, err := buildImage(name,
+		`FROM `+minimalBaseImage()+`
+        SHELL ["foo", "-bar"]`,
+		true)
+	if err != nil {
+		c.Fatal(err)
+	}
+	res := inspectFieldJSON(c, name, "ContainerConfig.Cmd")
+	if res != expected {
+		c.Fatalf("%s, expected %s", res, expected)
+	}
+	res = inspectFieldJSON(c, name, "ContainerConfig.Shell")
+	if res != `["foo","-bar"]` {
+		c.Fatalf(`%s, expected ["foo","-bar"]`, res)
+	}
+}
+
+// #22489 Changing the shell multiple times and CMD after.
+func (s *DockerSuite) TestBuildShellMultiple(c *check.C) {
+	name := "testbuildshellmultiple"
+
+	_, out, _, err := buildImageWithStdoutStderr(name,
+		`FROM busybox
+		RUN echo defaultshell
+		SHELL ["echo"]
+		RUN echoshell
+		SHELL ["ls"]
+		RUN -l
+		CMD -l`,
+		true)
+	if err != nil {
+		c.Fatal(err)
+	}
+
+	// Must contain 'defaultshell' twice
+	if len(strings.Split(out, "defaultshell")) != 3 {
+		c.Fatalf("defaultshell should have appeared twice in %s", out)
+	}
+
+	// Must contain 'echoshell' twice
+	if len(strings.Split(out, "echoshell")) != 3 {
+		c.Fatalf("echoshell should have appeared twice in %s", out)
+	}
+
+	// Must contain "total " (part of ls -l)
+	if !strings.Contains(out, "total ") {
+		c.Fatalf("%s should have contained 'total '", out)
+	}
+
+	// A container started from the image uses the shell-form CMD.
+	// Last shell is ls. CMD is -l. So should contain 'total '.
+	outrun, _ := dockerCmd(c, "run", "--rm", name)
+	if !strings.Contains(outrun, "total ") {
+		c.Fatalf("Expected started container to run ls -l. %s", outrun)
+	}
+}
+
+// #22489. Changed SHELL with ENTRYPOINT
+func (s *DockerSuite) TestBuildShellEntrypoint(c *check.C) {
+	name := "testbuildshellentrypoint"
+
+	_, err := buildImage(name,
+		`FROM busybox
+		SHELL ["ls"]
+		ENTRYPOINT -l`,
+		true)
+	if err != nil {
+		c.Fatal(err)
+	}
+
+	// A container started from the image uses the shell-form ENTRYPOINT.
+	// Shell is ls. ENTRYPOINT is -l. So should contain 'total '.
+	outrun, _ := dockerCmd(c, "run", "--rm", name)
+	if !strings.Contains(outrun, "total ") {
+		c.Fatalf("Expected started container to run ls -l. %s", outrun)
+	}
+}
+
+// #22489 Shell test to confirm shell is inherited in a subsequent build
+func (s *DockerSuite) TestBuildShellInherited(c *check.C) {
+	name1 := "testbuildshellinherited1"
+	_, err := buildImage(name1,
+		`FROM busybox
+        SHELL ["ls"]`,
+		true)
+	if err != nil {
+		c.Fatal(err)
+	}
+
+	name2 := "testbuildshellinherited2"
+	_, out, _, err := buildImageWithStdoutStderr(name2,
+		`FROM `+name1+`
+        RUN -l`,
+		true)
+	if err != nil {
+		c.Fatal(err)
+	}
+
+	// ls -l has "total " followed by some number in it, ls without -l does not.
+	if !strings.Contains(out, "total ") {
+		c.Fatalf("Should have seen total in 'ls -l'.\n%s", out)
+	}
+}
+
+// #22489 Shell test to confirm non-JSON doesn't work
+func (s *DockerSuite) TestBuildShellNotJSON(c *check.C) {
+	name := "testbuildshellnotjson"
+
+	_, err := buildImage(name,
+		`FROM `+minimalBaseImage()+`
+        sHeLl exec -form`, // Casing explicit to ensure error is upper-cased.
+		true)
+	if err == nil {
+		c.Fatal("Image build should have failed")
+	}
+	if !strings.Contains(err.Error(), "SHELL requires the arguments to be in JSON form") {
+		c.Fatal("Error didn't indicate that arguments must be in JSON form")
+	}
+}
+
+// #22489 Windows shell test to confirm native is powershell if executing a PS command
+// This would error if the default shell were still cmd.
+func (s *DockerSuite) TestBuildShellWindowsPowershell(c *check.C) {
+	testRequires(c, DaemonIsWindows)
+	name := "testbuildshellpowershell"
+	_, out, err := buildImageWithOut(name,
+		`FROM `+minimalBaseImage()+`
+        SHELL ["powershell", "-command"]
+		RUN Write-Host John`,
+		true)
+	if err != nil {
+		c.Fatal(err)
+	}
+	if !strings.Contains(out, "\nJohn\n") {
+		c.Fatalf("Line with 'John' not found in output %q", out)
+	}
+}