Skip to content

Commit b18ae8c

Browse files
author
John Howard
committed
Builder default shell
Signed-off-by: John Howard <[email protected]>
1 parent 2d40e36 commit b18ae8c

12 files changed

Lines changed: 386 additions & 80 deletions

File tree

builder/dockerfile/builder_unix.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// +build !windows
2+
3+
package dockerfile
4+
5+
var defaultShell = []string{"/bin/sh", "-c"}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package dockerfile
2+
3+
var defaultShell = []string{"cmd", "/S", "/C"}

builder/dockerfile/command/command.go

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,44 @@ package command
33

44
// Define constants for the command strings
55
const (
6-
Env = "env"
7-
Label = "label"
8-
Maintainer = "maintainer"
96
Add = "add"
7+
Arg = "arg"
8+
Cmd = "cmd"
109
Copy = "copy"
10+
Entrypoint = "entrypoint"
11+
Env = "env"
12+
Expose = "expose"
1113
From = "from"
14+
Healthcheck = "healthcheck"
15+
Label = "label"
16+
Maintainer = "maintainer"
1217
Onbuild = "onbuild"
13-
Workdir = "workdir"
1418
Run = "run"
15-
Cmd = "cmd"
16-
Entrypoint = "entrypoint"
17-
Expose = "expose"
18-
Volume = "volume"
19-
User = "user"
19+
Shell = "shell"
2020
StopSignal = "stopsignal"
21-
Arg = "arg"
22-
Healthcheck = "healthcheck"
21+
User = "user"
22+
Volume = "volume"
23+
Workdir = "workdir"
2324
)
2425

2526
// Commands is list of all Dockerfile commands
2627
var Commands = map[string]struct{}{
27-
Env: {},
28-
Label: {},
29-
Maintainer: {},
3028
Add: {},
29+
Arg: {},
30+
Cmd: {},
3131
Copy: {},
32+
Entrypoint: {},
33+
Env: {},
34+
Expose: {},
3235
From: {},
36+
Healthcheck: {},
37+
Label: {},
38+
Maintainer: {},
3339
Onbuild: {},
34-
Workdir: {},
3540
Run: {},
36-
Cmd: {},
37-
Entrypoint: {},
38-
Expose: {},
39-
Volume: {},
40-
User: {},
41+
Shell: {},
4142
StopSignal: {},
42-
Arg: {},
43-
Healthcheck: {},
43+
User: {},
44+
Volume: {},
45+
Workdir: {},
4446
}

builder/dockerfile/dispatchers.go

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,8 @@ func workdir(b *Builder, args []string, attributes map[string]bool, original str
274274
// RUN some command yo
275275
//
276276
// run a command and commit the image. Args are automatically prepended with
277-
// 'sh -c' under linux or 'cmd /S /C' under Windows, in the event there is
278-
// only one argument. The difference in processing:
277+
// the current SHELL which defaults to 'sh -c' under linux or 'cmd /S /C' under
278+
// Windows, in the event there is only one argument The difference in processing:
279279
//
280280
// RUN echo hi # sh -c echo hi (Linux)
281281
// 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)
293293
args = handleJSONArgs(args, attributes)
294294

295295
if !attributes["json"] {
296-
if runtime.GOOS != "windows" {
297-
args = append([]string{"/bin/sh", "-c"}, args...)
298-
} else {
299-
args = append([]string{"cmd", "/S", "/C"}, args...)
300-
}
296+
args = append(getShell(b.runConfig), args...)
301297
}
302-
303298
config := &container.Config{
304299
Cmd: strslice.StrSlice(args),
305300
Image: b.image,
@@ -408,11 +403,7 @@ func cmd(b *Builder, args []string, attributes map[string]bool, original string)
408403
cmdSlice := handleJSONArgs(args, attributes)
409404

410405
if !attributes["json"] {
411-
if runtime.GOOS != "windows" {
412-
cmdSlice = append([]string{"/bin/sh", "-c"}, cmdSlice...)
413-
} else {
414-
cmdSlice = append([]string{"cmd", "/S", "/C"}, cmdSlice...)
415-
}
406+
cmdSlice = append(getShell(b.runConfig), cmdSlice...)
416407
}
417408

418409
b.runConfig.Cmd = strslice.StrSlice(cmdSlice)
@@ -535,8 +526,8 @@ func healthcheck(b *Builder, args []string, attributes map[string]bool, original
535526

536527
// ENTRYPOINT /usr/sbin/nginx
537528
//
538-
// Set the entrypoint (which defaults to sh -c on linux, or cmd /S /C on Windows) to
539-
// /usr/sbin/nginx. Will accept the CMD as the arguments to /usr/sbin/nginx.
529+
// Set the entrypoint to /usr/sbin/nginx. Will accept the CMD as the arguments
530+
// to /usr/sbin/nginx. Uses the default shell if not in JSON format.
540531
//
541532
// Handles command processing similar to CMD and RUN, only b.runConfig.Entrypoint
542533
// 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
557548
b.runConfig.Entrypoint = nil
558549
default:
559550
// ENTRYPOINT echo hi
560-
if runtime.GOOS != "windows" {
561-
b.runConfig.Entrypoint = strslice.StrSlice{"/bin/sh", "-c", parsed[0]}
562-
} else {
563-
b.runConfig.Entrypoint = strslice.StrSlice{"cmd", "/S", "/C", parsed[0]}
564-
}
551+
b.runConfig.Entrypoint = strslice.StrSlice(append(getShell(b.runConfig), parsed[0]))
565552
}
566553

567554
// 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)
727714
return b.commit("", b.runConfig.Cmd, fmt.Sprintf("ARG %s", arg))
728715
}
729716

717+
// SHELL powershell -command
718+
//
719+
// Set the non-default shell to use.
720+
func shell(b *Builder, args []string, attributes map[string]bool, original string) error {
721+
if err := b.flags.Parse(); err != nil {
722+
return err
723+
}
724+
shellSlice := handleJSONArgs(args, attributes)
725+
switch {
726+
case len(shellSlice) == 0:
727+
// SHELL []
728+
return errAtLeastOneArgument("SHELL")
729+
case attributes["json"]:
730+
// SHELL ["powershell", "-command"]
731+
b.runConfig.Shell = strslice.StrSlice(shellSlice)
732+
default:
733+
// SHELL powershell -command - not JSON
734+
return errNotJSON("SHELL", original)
735+
}
736+
return b.commit("", b.runConfig.Cmd, fmt.Sprintf("SHELL %v", shellSlice))
737+
}
738+
730739
func errAtLeastOneArgument(command string) error {
731740
return fmt.Errorf("%s requires at least one argument", command)
732741
}
@@ -738,3 +747,12 @@ func errExactlyOneArgument(command string) error {
738747
func errTooManyArguments(command string) error {
739748
return fmt.Errorf("Bad input to %s, too many arguments", command)
740749
}
750+
751+
// getShell is a helper function which gets the right shell for prefixing the
752+
// shell-form of RUN, ENTRYPOINT and CMD instructions
753+
func getShell(c *container.Config) []string {
754+
if 0 == len(c.Shell) {
755+
return defaultShell[:]
756+
}
757+
return c.Shell[:]
758+
}

builder/dockerfile/dispatchers_unix.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,7 @@ func normaliseWorkdir(current string, requested string) (string, error) {
2121
}
2222
return requested, nil
2323
}
24+
25+
func errNotJSON(command, _ string) error {
26+
return fmt.Errorf("%s requires the arguments to be in JSON form", command)
27+
}

builder/dockerfile/dispatchers_windows.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"regexp"
78
"strings"
89

910
"github.com/docker/docker/pkg/system"
@@ -43,3 +44,22 @@ func normaliseWorkdir(current string, requested string) (string, error) {
4344
// Upper-case drive letter
4445
return (strings.ToUpper(string(requested[0])) + requested[1:]), nil
4546
}
47+
48+
func errNotJSON(command, original string) error {
49+
// For Windows users, give a hint if it looks like it might contain
50+
// a path which hasn't been escaped such as ["c:\windows\system32\prog.exe", "-param"],
51+
// as JSON must be escaped. Unfortunate...
52+
//
53+
// Specifically looking for quote-driveletter-colon-backslash, there's no
54+
// double backslash and a [] pair. No, this is not perfect, but it doesn't
55+
// have to be. It's simply a hint to make life a little easier.
56+
extra := ""
57+
original = filepath.FromSlash(strings.ToLower(strings.Replace(strings.ToLower(original), strings.ToLower(command)+" ", "", -1)))
58+
if len(regexp.MustCompile(`"[a-z]:\\.*`).FindStringSubmatch(original)) > 0 &&
59+
!strings.Contains(original, `\\`) &&
60+
strings.Contains(original, "[") &&
61+
strings.Contains(original, "]") {
62+
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)
63+
}
64+
return fmt.Errorf("%s requires the arguments to be in JSON form%s", command, extra)
65+
}

builder/dockerfile/evaluator.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,23 +58,24 @@ var evaluateTable map[string]func(*Builder, []string, map[string]bool, string) e
5858

5959
func init() {
6060
evaluateTable = map[string]func(*Builder, []string, map[string]bool, string) error{
61-
command.Env: env,
62-
command.Label: label,
63-
command.Maintainer: maintainer,
6461
command.Add: add,
62+
command.Arg: arg,
63+
command.Cmd: cmd,
6564
command.Copy: dispatchCopy, // copy() is a go builtin
65+
command.Entrypoint: entrypoint,
66+
command.Env: env,
67+
command.Expose: expose,
6668
command.From: from,
69+
command.Healthcheck: healthcheck,
70+
command.Label: label,
71+
command.Maintainer: maintainer,
6772
command.Onbuild: onbuild,
68-
command.Workdir: workdir,
6973
command.Run: run,
70-
command.Cmd: cmd,
71-
command.Entrypoint: entrypoint,
72-
command.Expose: expose,
73-
command.Volume: volume,
74-
command.User: user,
74+
command.Shell: shell,
7575
command.StopSignal: stopSignal,
76-
command.Arg: arg,
77-
command.Healthcheck: healthcheck,
76+
command.User: user,
77+
command.Volume: volume,
78+
command.Workdir: workdir,
7879
}
7980
}
8081

builder/dockerfile/internals.go

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"net/url"
1515
"os"
1616
"path/filepath"
17-
"runtime"
1817
"sort"
1918
"strings"
2019
"sync"
@@ -51,11 +50,7 @@ func (b *Builder) commit(id string, autoCmd strslice.StrSlice, comment string) e
5150

5251
if id == "" {
5352
cmd := b.runConfig.Cmd
54-
if runtime.GOOS != "windows" {
55-
b.runConfig.Cmd = strslice.StrSlice{"/bin/sh", "-c", "#(nop) " + comment}
56-
} else {
57-
b.runConfig.Cmd = strslice.StrSlice{"cmd", "/S /C", "REM (nop) " + comment}
58-
}
53+
b.runConfig.Cmd = strslice.StrSlice(append(getShell(b.runConfig), "#(nop) ", comment))
5954
defer func(cmd strslice.StrSlice) { b.runConfig.Cmd = cmd }(cmd)
6055

6156
hit, err := b.probeCache()
@@ -177,11 +172,7 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalD
177172
}
178173

179174
cmd := b.runConfig.Cmd
180-
if runtime.GOOS != "windows" {
181-
b.runConfig.Cmd = strslice.StrSlice{"/bin/sh", "-c", fmt.Sprintf("#(nop) %s %s in %s", cmdName, srcHash, dest)}
182-
} else {
183-
b.runConfig.Cmd = strslice.StrSlice{"cmd", "/S", "/C", fmt.Sprintf("REM (nop) %s %s in %s", cmdName, srcHash, dest)}
184-
}
175+
b.runConfig.Cmd = strslice.StrSlice(append(getShell(b.runConfig), "#(nop) %s %s in %s ", cmdName, srcHash, dest))
185176
defer func(cmd strslice.StrSlice) { b.runConfig.Cmd = cmd }(cmd)
186177

187178
if hit, err := b.probeCache(); err != nil {

builder/dockerfile/parser/parser.go

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,23 +67,24 @@ func init() {
6767
// functions. Errors are propagated up by Parse() and the resulting AST can
6868
// be incorporated directly into the existing AST as a next.
6969
dispatch = map[string]func(string) (*Node, map[string]bool, error){
70-
command.User: parseString,
71-
command.Onbuild: parseSubCommand,
72-
command.Workdir: parseString,
73-
command.Env: parseEnv,
74-
command.Label: parseLabel,
75-
command.Maintainer: parseString,
76-
command.From: parseString,
7770
command.Add: parseMaybeJSONToList,
78-
command.Copy: parseMaybeJSONToList,
79-
command.Run: parseMaybeJSON,
71+
command.Arg: parseNameOrNameVal,
8072
command.Cmd: parseMaybeJSON,
73+
command.Copy: parseMaybeJSONToList,
8174
command.Entrypoint: parseMaybeJSON,
75+
command.Env: parseEnv,
8276
command.Expose: parseStringsWhitespaceDelimited,
83-
command.Volume: parseMaybeJSONToList,
84-
command.StopSignal: parseString,
85-
command.Arg: parseNameOrNameVal,
77+
command.From: parseString,
8678
command.Healthcheck: parseHealthConfig,
79+
command.Label: parseLabel,
80+
command.Maintainer: parseString,
81+
command.Onbuild: parseSubCommand,
82+
command.Run: parseMaybeJSON,
83+
command.Shell: parseMaybeJSON,
84+
command.StopSignal: parseString,
85+
command.User: parseString,
86+
command.Volume: parseMaybeJSONToList,
87+
command.Workdir: parseString,
8788
}
8889
}
8990

builder/dockerfile/support.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package dockerfile
22

33
import "strings"
44

5-
// handleJSONArgs parses command passed to CMD, ENTRYPOINT or RUN instruction in Dockerfile
5+
// handleJSONArgs parses command passed to CMD, ENTRYPOINT, RUN and SHELL instruction in Dockerfile
66
// for exec form it returns untouched args slice
77
// for shell form it returns concatenated args as the first element of a slice
88
func handleJSONArgs(args []string, attributes map[string]bool) []string {

0 commit comments

Comments
 (0)