Merge pull request #22489 from Microsoft/jjh/shell

Builder shell configuration
This commit is contained in:
Vincent Demeester 2016-06-05 17:43:12 +02:00
commit df1dd1322d
12 changed files with 386 additions and 80 deletions

View File

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

View File

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

View File

@ -3,42 +3,44 @@ package command
// Define constants for the command strings // Define constants for the command strings
const ( const (
Add = "add"
Arg = "arg"
Cmd = "cmd"
Copy = "copy"
Entrypoint = "entrypoint"
Env = "env" Env = "env"
Expose = "expose"
From = "from"
Healthcheck = "healthcheck"
Label = "label" Label = "label"
Maintainer = "maintainer" Maintainer = "maintainer"
Add = "add"
Copy = "copy"
From = "from"
Onbuild = "onbuild" Onbuild = "onbuild"
Workdir = "workdir"
Run = "run" Run = "run"
Cmd = "cmd" Shell = "shell"
Entrypoint = "entrypoint"
Expose = "expose"
Volume = "volume"
User = "user"
StopSignal = "stopsignal" StopSignal = "stopsignal"
Arg = "arg" User = "user"
Healthcheck = "healthcheck" 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{}{
Add: {},
Arg: {},
Cmd: {},
Copy: {},
Entrypoint: {},
Env: {}, Env: {},
Expose: {},
From: {},
Healthcheck: {},
Label: {}, Label: {},
Maintainer: {}, Maintainer: {},
Add: {},
Copy: {},
From: {},
Onbuild: {}, Onbuild: {},
Workdir: {},
Run: {}, Run: {},
Cmd: {}, Shell: {},
Entrypoint: {},
Expose: {},
Volume: {},
User: {},
StopSignal: {}, StopSignal: {},
Arg: {}, User: {},
Healthcheck: {}, Volume: {},
Workdir: {},
} }

View File

@ -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 // the current SHELL which defaults to 'sh -c' under linux or 'cmd /S /C' under
// only one argument. The difference in processing: // 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(getShell(b.runConfig), args...)
args = append([]string{"/bin/sh", "-c"}, args...)
} else {
args = append([]string{"cmd", "/S", "/C"}, 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(getShell(b.runConfig), cmdSlice...)
cmdSlice = append([]string{"/bin/sh", "-c"}, cmdSlice...)
} else {
cmdSlice = append([]string{"cmd", "/S", "/C"}, 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 // Set the entrypoint to /usr/sbin/nginx. Will accept the CMD as the arguments
// /usr/sbin/nginx. Will accept the CMD as the arguments to /usr/sbin/nginx. // 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(append(getShell(b.runConfig), parsed[0]))
b.runConfig.Entrypoint = strslice.StrSlice{"/bin/sh", "-c", parsed[0]}
} else {
b.runConfig.Entrypoint = strslice.StrSlice{"cmd", "/S", "/C", 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[:]
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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.Add: add,
command.Arg: arg,
command.Cmd: cmd,
command.Copy: dispatchCopy, // copy() is a go builtin
command.Entrypoint: entrypoint,
command.Env: env, command.Env: env,
command.Expose: expose,
command.From: from,
command.Healthcheck: healthcheck,
command.Label: label, command.Label: label,
command.Maintainer: maintainer, command.Maintainer: maintainer,
command.Add: add,
command.Copy: dispatchCopy, // copy() is a go builtin
command.From: from,
command.Onbuild: onbuild, command.Onbuild: onbuild,
command.Workdir: workdir,
command.Run: run, command.Run: run,
command.Cmd: cmd, command.Shell: shell,
command.Entrypoint: entrypoint,
command.Expose: expose,
command.Volume: volume,
command.User: user,
command.StopSignal: stopSignal, command.StopSignal: stopSignal,
command.Arg: arg, command.User: user,
command.Healthcheck: healthcheck, command.Volume: volume,
command.Workdir: workdir,
} }
} }

View File

@ -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(append(getShell(b.runConfig), "#(nop) ", comment))
b.runConfig.Cmd = strslice.StrSlice{"/bin/sh", "-c", "#(nop) " + comment}
} else {
b.runConfig.Cmd = strslice.StrSlice{"cmd", "/S /C", "REM (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(append(getShell(b.runConfig), "#(nop) %s %s in %s ", cmdName, srcHash, dest))
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)}
}
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 {

View File

@ -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.Add: parseMaybeJSONToList,
command.Onbuild: parseSubCommand, command.Arg: parseNameOrNameVal,
command.Workdir: parseString, command.Cmd: parseMaybeJSON,
command.Copy: parseMaybeJSONToList,
command.Entrypoint: parseMaybeJSON,
command.Env: parseEnv, command.Env: parseEnv,
command.Expose: parseStringsWhitespaceDelimited,
command.From: parseString,
command.Healthcheck: parseHealthConfig,
command.Label: parseLabel, command.Label: parseLabel,
command.Maintainer: parseString, command.Maintainer: parseString,
command.From: parseString, command.Onbuild: parseSubCommand,
command.Add: parseMaybeJSONToList,
command.Copy: parseMaybeJSONToList,
command.Run: parseMaybeJSON, command.Run: parseMaybeJSON,
command.Cmd: parseMaybeJSON, command.Shell: parseMaybeJSON,
command.Entrypoint: parseMaybeJSON,
command.Expose: parseStringsWhitespaceDelimited,
command.Volume: parseMaybeJSONToList,
command.StopSignal: parseString, command.StopSignal: parseString,
command.Arg: parseNameOrNameVal, command.User: parseString,
command.Healthcheck: parseHealthConfig, command.Volume: parseMaybeJSONToList,
command.Workdir: parseString,
} }
} }

View File

@ -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 {

View File

@ -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

View File

@ -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)
}
}