Browse code

Builder default shell

Signed-off-by: John Howard <jhoward@microsoft.com>

John Howard authored on 2016/05/04 05:56:59
Showing 12 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,5 @@
0
+// +build !windows
1
+
2
+package dockerfile
3
+
4
+var defaultShell = []string{"/bin/sh", "-c"}
0 5
new file mode 100644
... ...
@@ -0,0 +1,3 @@
0
+package dockerfile
1
+
2
+var defaultShell = []string{"cmd", "/S", "/C"}
... ...
@@ -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
+}
... ...
@@ -21,3 +21,7 @@ func normaliseWorkdir(current string, requested string) (string, error) {
21 21
 	}
22 22
 	return requested, nil
23 23
 }
24
+
25
+func errNotJSON(command, _ string) error {
26
+	return fmt.Errorf("%s requires the arguments to be in JSON form", command)
27
+}
... ...
@@ -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
+}