Skip to content

Commit

Permalink
Builder default shell
Browse files Browse the repository at this point in the history
Signed-off-by: John Howard <[email protected]>
  • Loading branch information
John Howard committed Jun 3, 2016
1 parent 2d40e36 commit b18ae8c
Show file tree
Hide file tree
Showing 12 changed files with 386 additions and 80 deletions.
5 changes: 5 additions & 0 deletions builder/dockerfile/builder_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// +build !windows

package dockerfile

var defaultShell = []string{"/bin/sh", "-c"}
3 changes: 3 additions & 0 deletions builder/dockerfile/builder_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package dockerfile

var defaultShell = []string{"cmd", "/S", "/C"}
46 changes: 24 additions & 22 deletions builder/dockerfile/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,44 @@ package command

// Define constants for the command strings
const (
Env = "env"
Label = "label"
Maintainer = "maintainer"
Add = "add"
Arg = "arg"
Cmd = "cmd"
Copy = "copy"
Entrypoint = "entrypoint"
Env = "env"
Expose = "expose"
From = "from"
Healthcheck = "healthcheck"
Label = "label"
Maintainer = "maintainer"
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{}{
Env: {},
Label: {},
Maintainer: {},
Add: {},
Arg: {},
Cmd: {},
Copy: {},
Entrypoint: {},
Env: {},
Expose: {},
From: {},
Healthcheck: {},
Label: {},
Maintainer: {},
Onbuild: {},
Workdir: {},
Run: {},
Cmd: {},
Entrypoint: {},
Expose: {},
Volume: {},
User: {},
Shell: {},
StopSignal: {},
Arg: {},
Healthcheck: {},
User: {},
Volume: {},
Workdir: {},
}
58 changes: 38 additions & 20 deletions builder/dockerfile/dispatchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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[:]
}
4 changes: 4 additions & 0 deletions builder/dockerfile/dispatchers_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
20 changes: 20 additions & 0 deletions builder/dockerfile/dispatchers_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/docker/docker/pkg/system"
Expand Down Expand Up @@ -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)
}
23 changes: 12 additions & 11 deletions builder/dockerfile/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.Env: env,
command.Label: label,
command.Maintainer: maintainer,
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.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,
}
}

Expand Down
13 changes: 2 additions & 11 deletions builder/dockerfile/internals.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"net/url"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down
25 changes: 13 additions & 12 deletions builder/dockerfile/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.Env: parseEnv,
command.Label: parseLabel,
command.Maintainer: parseString,
command.From: parseString,
command.Add: parseMaybeJSONToList,
command.Copy: parseMaybeJSONToList,
command.Run: parseMaybeJSON,
command.Arg: parseNameOrNameVal,
command.Cmd: parseMaybeJSON,
command.Copy: parseMaybeJSONToList,
command.Entrypoint: parseMaybeJSON,
command.Env: parseEnv,
command.Expose: parseStringsWhitespaceDelimited,
command.Volume: parseMaybeJSONToList,
command.StopSignal: parseString,
command.Arg: parseNameOrNameVal,
command.From: parseString,
command.Healthcheck: parseHealthConfig,
command.Label: parseLabel,
command.Maintainer: parseString,
command.Onbuild: parseSubCommand,
command.Run: parseMaybeJSON,
command.Shell: parseMaybeJSON,
command.StopSignal: parseString,
command.User: parseString,
command.Volume: parseMaybeJSONToList,
command.Workdir: parseString,
}
}

Expand Down
2 changes: 1 addition & 1 deletion builder/dockerfile/support.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit b18ae8c

Please sign in to comment.