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
|
// 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: {},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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[:]
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue