mirror of https://github.com/docker/docs.git
				
				
				
			Merge pull request #22489 from Microsoft/jjh/shell
Builder shell configuration
This commit is contained in:
		
						commit
						df1dd1322d
					
				|  | @ -0,0 +1,5 @@ | |||
| // +build !windows
 | ||||
| 
 | ||||
| package dockerfile | ||||
| 
 | ||||
| var defaultShell = []string{"/bin/sh", "-c"} | ||||
|  | @ -0,0 +1,3 @@ | |||
| package dockerfile | ||||
| 
 | ||||
| var defaultShell = []string{"cmd", "/S", "/C"} | ||||
|  | @ -3,42 +3,44 @@ package command | |||
| 
 | ||||
| // Define constants for the command strings
 | ||||
| const ( | ||||
| 	Add         = "add" | ||||
| 	Arg         = "arg" | ||||
| 	Cmd         = "cmd" | ||||
| 	Copy        = "copy" | ||||
| 	Entrypoint  = "entrypoint" | ||||
| 	Env         = "env" | ||||
| 	Expose      = "expose" | ||||
| 	From        = "from" | ||||
| 	Healthcheck = "healthcheck" | ||||
| 	Label       = "label" | ||||
| 	Maintainer  = "maintainer" | ||||
| 	Add         = "add" | ||||
| 	Copy        = "copy" | ||||
| 	From        = "from" | ||||
| 	Onbuild     = "onbuild" | ||||
| 	Workdir     = "workdir" | ||||
| 	Run         = "run" | ||||
| 	Cmd         = "cmd" | ||||
| 	Entrypoint  = "entrypoint" | ||||
| 	Expose      = "expose" | ||||
| 	Volume      = "volume" | ||||
| 	User        = "user" | ||||
| 	Shell       = "shell" | ||||
| 	StopSignal  = "stopsignal" | ||||
| 	Arg         = "arg" | ||||
| 	Healthcheck = "healthcheck" | ||||
| 	User        = "user" | ||||
| 	Volume      = "volume" | ||||
| 	Workdir     = "workdir" | ||||
| ) | ||||
| 
 | ||||
| // Commands is list of all Dockerfile commands
 | ||||
| var Commands = map[string]struct{}{ | ||||
| 	Add:         {}, | ||||
| 	Arg:         {}, | ||||
| 	Cmd:         {}, | ||||
| 	Copy:        {}, | ||||
| 	Entrypoint:  {}, | ||||
| 	Env:         {}, | ||||
| 	Expose:      {}, | ||||
| 	From:        {}, | ||||
| 	Healthcheck: {}, | ||||
| 	Label:       {}, | ||||
| 	Maintainer:  {}, | ||||
| 	Add:         {}, | ||||
| 	Copy:        {}, | ||||
| 	From:        {}, | ||||
| 	Onbuild:     {}, | ||||
| 	Workdir:     {}, | ||||
| 	Run:         {}, | ||||
| 	Cmd:         {}, | ||||
| 	Entrypoint:  {}, | ||||
| 	Expose:      {}, | ||||
| 	Volume:      {}, | ||||
| 	User:        {}, | ||||
| 	Shell:       {}, | ||||
| 	StopSignal:  {}, | ||||
| 	Arg:         {}, | ||||
| 	Healthcheck: {}, | ||||
| 	User:        {}, | ||||
| 	Volume:      {}, | ||||
| 	Workdir:     {}, | ||||
| } | ||||
|  |  | |||
|  | @ -274,8 +274,8 @@ func workdir(b *Builder, args []string, attributes map[string]bool, original str | |||
| // RUN some command yo
 | ||||
| //
 | ||||
| // 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          # 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) | ||||
| 
 | ||||
| 	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{ | ||||
| 		Cmd:   strslice.StrSlice(args), | ||||
| 		Image: b.image, | ||||
|  | @ -408,11 +403,7 @@ func cmd(b *Builder, args []string, attributes map[string]bool, original string) | |||
| 	cmdSlice := handleJSONArgs(args, attributes) | ||||
| 
 | ||||
| 	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) | ||||
|  | @ -535,8 +526,8 @@ func healthcheck(b *Builder, args []string, attributes map[string]bool, original | |||
| 
 | ||||
| // 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
 | ||||
| // 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 | ||||
| 	default: | ||||
| 		// 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
 | ||||
|  | @ -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)) | ||||
| } | ||||
| 
 | ||||
| // 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 { | ||||
| 	return fmt.Errorf("%s requires at least one argument", command) | ||||
| } | ||||
|  | @ -738,3 +747,12 @@ func errExactlyOneArgument(command string) error { | |||
| func errTooManyArguments(command string) error { | ||||
| 	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[:] | ||||
| } | ||||
|  |  | |||
|  | @ -21,3 +21,7 @@ func normaliseWorkdir(current string, requested string) (string, error) { | |||
| 	} | ||||
| 	return requested, nil | ||||
| } | ||||
| 
 | ||||
| func errNotJSON(command, _ string) error { | ||||
| 	return fmt.Errorf("%s requires the arguments to be in JSON form", command) | ||||
| } | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import ( | |||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/docker/docker/pkg/system" | ||||
|  | @ -43,3 +44,22 @@ func normaliseWorkdir(current string, requested string) (string, error) { | |||
| 	// Upper-case drive letter
 | ||||
| 	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) | ||||
| } | ||||
|  |  | |||
|  | @ -58,23 +58,24 @@ var evaluateTable map[string]func(*Builder, []string, map[string]bool, string) e | |||
| 
 | ||||
| func init() { | ||||
| 	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.Expose:      expose, | ||||
| 		command.From:        from, | ||||
| 		command.Healthcheck: healthcheck, | ||||
| 		command.Label:       label, | ||||
| 		command.Maintainer:  maintainer, | ||||
| 		command.Add:         add, | ||||
| 		command.Copy:        dispatchCopy, // copy() is a go builtin
 | ||||
| 		command.From:        from, | ||||
| 		command.Onbuild:     onbuild, | ||||
| 		command.Workdir:     workdir, | ||||
| 		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.Arg:         arg, | ||||
| 		command.Healthcheck: healthcheck, | ||||
| 		command.User:        user, | ||||
| 		command.Volume:      volume, | ||||
| 		command.Workdir:     workdir, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -14,7 +14,6 @@ import ( | |||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"runtime" | ||||
| 	"sort" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
|  | @ -51,11 +50,7 @@ func (b *Builder) commit(id string, autoCmd strslice.StrSlice, comment string) e | |||
| 
 | ||||
| 	if id == "" { | ||||
| 		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) | ||||
| 
 | ||||
| 		hit, err := b.probeCache() | ||||
|  | @ -177,11 +172,7 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalD | |||
| 	} | ||||
| 
 | ||||
| 	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) | ||||
| 
 | ||||
| 	if hit, err := b.probeCache(); err != nil { | ||||
|  |  | |||
|  | @ -67,23 +67,24 @@ func init() { | |||
| 	// functions. Errors are propagated up by Parse() and the resulting AST can
 | ||||
| 	// be incorporated directly into the existing AST as a next.
 | ||||
| 	dispatch = map[string]func(string) (*Node, map[string]bool, error){ | ||||
| 		command.User:        parseString, | ||||
| 		command.Onbuild:     parseSubCommand, | ||||
| 		command.Workdir:     parseString, | ||||
| 		command.Add:         parseMaybeJSONToList, | ||||
| 		command.Arg:         parseNameOrNameVal, | ||||
| 		command.Cmd:         parseMaybeJSON, | ||||
| 		command.Copy:        parseMaybeJSONToList, | ||||
| 		command.Entrypoint:  parseMaybeJSON, | ||||
| 		command.Env:         parseEnv, | ||||
| 		command.Expose:      parseStringsWhitespaceDelimited, | ||||
| 		command.From:        parseString, | ||||
| 		command.Healthcheck: parseHealthConfig, | ||||
| 		command.Label:       parseLabel, | ||||
| 		command.Maintainer:  parseString, | ||||
| 		command.From:        parseString, | ||||
| 		command.Add:         parseMaybeJSONToList, | ||||
| 		command.Copy:        parseMaybeJSONToList, | ||||
| 		command.Onbuild:     parseSubCommand, | ||||
| 		command.Run:         parseMaybeJSON, | ||||
| 		command.Cmd:         parseMaybeJSON, | ||||
| 		command.Entrypoint:  parseMaybeJSON, | ||||
| 		command.Expose:      parseStringsWhitespaceDelimited, | ||||
| 		command.Volume:      parseMaybeJSONToList, | ||||
| 		command.Shell:       parseMaybeJSON, | ||||
| 		command.StopSignal:  parseString, | ||||
| 		command.Arg:         parseNameOrNameVal, | ||||
| 		command.Healthcheck: parseHealthConfig, | ||||
| 		command.User:        parseString, | ||||
| 		command.Volume:      parseMaybeJSONToList, | ||||
| 		command.Workdir:     parseString, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ package dockerfile | |||
| 
 | ||||
| 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 shell form it returns concatenated args as the first element of a slice
 | ||||
| func handleJSONArgs(args []string, attributes map[string]bool) []string { | ||||
|  |  | |||
|  | @ -497,7 +497,8 @@ generated images. | |||
| 
 | ||||
| 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) | ||||
| 
 | ||||
| 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. | ||||
| 
 | ||||
| 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 | ||||
| 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 signal | ||||
|     STOPSIGNAL signal | ||||
| 
 | ||||
| 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, | ||||
|  | @ -1541,6 +1545,120 @@ generated with the new status. | |||
| 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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -6834,3 +6834,146 @@ func (s *DockerSuite) TestBuildWithUTF8BOMDockerignore(c *check.C) { | |||
| 		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) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue