mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Merge pull request #32858 from dnephin/builder-shell-words-interface
[Builder] Remove b.escapeToken, create ShellLex
This commit is contained in:
commit
aee2da3bdf
7 changed files with 70 additions and 58 deletions
|
@ -61,7 +61,6 @@ type Builder struct {
|
||||||
imageCache builder.ImageCache
|
imageCache builder.ImageCache
|
||||||
|
|
||||||
// TODO: these move to DispatchState
|
// TODO: these move to DispatchState
|
||||||
escapeToken rune
|
|
||||||
maintainer string
|
maintainer string
|
||||||
cmdSet bool
|
cmdSet bool
|
||||||
noBaseImage bool // A flag to track the use of `scratch` as the base image
|
noBaseImage bool // A flag to track the use of `scratch` as the base image
|
||||||
|
@ -125,7 +124,6 @@ func NewBuilder(clientCtx context.Context, config *types.ImageBuildOptions, back
|
||||||
runConfig: new(container.Config),
|
runConfig: new(container.Config),
|
||||||
tmpContainers: map[string]struct{}{},
|
tmpContainers: map[string]struct{}{},
|
||||||
buildArgs: newBuildArgs(config.BuildArgs),
|
buildArgs: newBuildArgs(config.BuildArgs),
|
||||||
escapeToken: parser.DefaultEscapeToken,
|
|
||||||
}
|
}
|
||||||
b.imageContexts = &imageContexts{b: b}
|
b.imageContexts = &imageContexts{b: b}
|
||||||
return b, nil
|
return b, nil
|
||||||
|
@ -219,8 +217,7 @@ func (b *Builder) build(dockerfile *parser.Result, stdout io.Writer, stderr io.W
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result) (string, error) {
|
func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result) (string, error) {
|
||||||
// TODO: pass this to dispatchRequest instead
|
shlex := NewShellLex(dockerfile.EscapeToken)
|
||||||
b.escapeToken = dockerfile.EscapeToken
|
|
||||||
|
|
||||||
total := len(dockerfile.AST.Children)
|
total := len(dockerfile.AST.Children)
|
||||||
var imageID string
|
var imageID string
|
||||||
|
@ -238,7 +235,7 @@ func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := b.dispatch(i, total, n); err != nil {
|
if err := b.dispatch(i, total, n, shlex); err != nil {
|
||||||
if b.options.ForceRemove {
|
if b.options.ForceRemove {
|
||||||
b.clearTmp()
|
b.clearTmp()
|
||||||
}
|
}
|
||||||
|
@ -361,13 +358,12 @@ func checkDispatchDockerfile(dockerfile *parser.Node) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func dispatchFromDockerfile(b *Builder, result *parser.Result) error {
|
func dispatchFromDockerfile(b *Builder, result *parser.Result) error {
|
||||||
// TODO: pass this to dispatchRequest instead
|
shlex := NewShellLex(result.EscapeToken)
|
||||||
b.escapeToken = result.EscapeToken
|
|
||||||
ast := result.AST
|
ast := result.AST
|
||||||
total := len(ast.Children)
|
total := len(ast.Children)
|
||||||
|
|
||||||
for i, n := range ast.Children {
|
for i, n := range ast.Children {
|
||||||
if err := b.dispatch(i, total, n); err != nil {
|
if err := b.dispatch(i, total, n, shlex); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -194,7 +194,7 @@ func from(req dispatchRequest) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
image, err := req.builder.getFromImage(req.args[0])
|
image, err := req.builder.getFromImage(req.shlex, req.args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -222,13 +222,13 @@ func parseBuildStageName(args []string) (string, error) {
|
||||||
return stageName, nil
|
return stageName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) getFromImage(name string) (builder.Image, error) {
|
func (b *Builder) getFromImage(shlex *ShellLex, name string) (builder.Image, error) {
|
||||||
substitutionArgs := []string{}
|
substitutionArgs := []string{}
|
||||||
for key, value := range b.buildArgs.GetAllMeta() {
|
for key, value := range b.buildArgs.GetAllMeta() {
|
||||||
substitutionArgs = append(substitutionArgs, key+"="+value)
|
substitutionArgs = append(substitutionArgs, key+"="+value)
|
||||||
}
|
}
|
||||||
|
|
||||||
name, err := ProcessWord(name, substitutionArgs, b.escapeToken)
|
name, err := shlex.ProcessWord(name, substitutionArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/strslice"
|
"github.com/docker/docker/api/types/strslice"
|
||||||
"github.com/docker/docker/builder"
|
"github.com/docker/docker/builder"
|
||||||
|
"github.com/docker/docker/builder/dockerfile/parser"
|
||||||
"github.com/docker/docker/pkg/testutil"
|
"github.com/docker/docker/pkg/testutil"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -38,6 +39,7 @@ func defaultDispatchReq(builder *Builder, args ...string) dispatchRequest {
|
||||||
args: args,
|
args: args,
|
||||||
flags: NewBFlags(),
|
flags: NewBFlags(),
|
||||||
runConfig: &container.Config{},
|
runConfig: &container.Config{},
|
||||||
|
shlex: NewShellLex(parser.DefaultEscapeToken),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,9 +65,10 @@ type dispatchRequest struct {
|
||||||
flags *BFlags
|
flags *BFlags
|
||||||
original string
|
original string
|
||||||
runConfig *container.Config
|
runConfig *container.Config
|
||||||
|
shlex *ShellLex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDispatchRequestFromNode(node *parser.Node, builder *Builder, args []string) dispatchRequest {
|
func newDispatchRequestFromNode(node *parser.Node, builder *Builder, args []string, shlex *ShellLex) dispatchRequest {
|
||||||
return dispatchRequest{
|
return dispatchRequest{
|
||||||
builder: builder,
|
builder: builder,
|
||||||
args: args,
|
args: args,
|
||||||
|
@ -75,6 +76,7 @@ func newDispatchRequestFromNode(node *parser.Node, builder *Builder, args []stri
|
||||||
original: node.Original,
|
original: node.Original,
|
||||||
flags: NewBFlagsWithArgs(node.Flags),
|
flags: NewBFlagsWithArgs(node.Flags),
|
||||||
runConfig: builder.runConfig,
|
runConfig: builder.runConfig,
|
||||||
|
shlex: shlex,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +121,7 @@ func init() {
|
||||||
// such as `RUN` in ONBUILD RUN foo. There is special case logic in here to
|
// such as `RUN` in ONBUILD RUN foo. There is special case logic in here to
|
||||||
// deal with that, at least until it becomes more of a general concern with new
|
// deal with that, at least until it becomes more of a general concern with new
|
||||||
// features.
|
// features.
|
||||||
func (b *Builder) dispatch(stepN int, stepTotal int, node *parser.Node) error {
|
func (b *Builder) dispatch(stepN int, stepTotal int, node *parser.Node, shlex *ShellLex) error {
|
||||||
cmd := node.Value
|
cmd := node.Value
|
||||||
upperCasedCmd := strings.ToUpper(cmd)
|
upperCasedCmd := strings.ToUpper(cmd)
|
||||||
|
|
||||||
|
@ -154,9 +156,10 @@ func (b *Builder) dispatch(stepN int, stepTotal int, node *parser.Node) error {
|
||||||
// Append build args to runConfig environment variables
|
// Append build args to runConfig environment variables
|
||||||
envs := append(b.runConfig.Env, b.buildArgsWithoutConfigEnv()...)
|
envs := append(b.runConfig.Env, b.buildArgsWithoutConfigEnv()...)
|
||||||
|
|
||||||
|
processFunc := getProcessFunc(shlex, cmd)
|
||||||
for i := 0; ast.Next != nil; i++ {
|
for i := 0; ast.Next != nil; i++ {
|
||||||
ast = ast.Next
|
ast = ast.Next
|
||||||
words, err := b.evaluateEnv(cmd, ast.Value, envs)
|
words, err := processFunc(ast.Value, envs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -170,7 +173,7 @@ func (b *Builder) dispatch(stepN int, stepTotal int, node *parser.Node) error {
|
||||||
// XXX yes, we skip any cmds that are not valid; the parser should have
|
// XXX yes, we skip any cmds that are not valid; the parser should have
|
||||||
// picked these out already.
|
// picked these out already.
|
||||||
if f, ok := evaluateTable[cmd]; ok {
|
if f, ok := evaluateTable[cmd]; ok {
|
||||||
return f(newDispatchRequestFromNode(node, b, strList))
|
return f(newDispatchRequestFromNode(node, b, strList, shlex))
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("Unknown instruction: %s", upperCasedCmd)
|
return fmt.Errorf("Unknown instruction: %s", upperCasedCmd)
|
||||||
|
@ -186,20 +189,22 @@ func initMsgList(cursor *parser.Node) []string {
|
||||||
return make([]string, n)
|
return make([]string, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) evaluateEnv(cmd string, str string, envs []string) ([]string, error) {
|
type processFunc func(string, []string) ([]string, error)
|
||||||
if !replaceEnvAllowed[cmd] {
|
|
||||||
return []string{str}, nil
|
func getProcessFunc(shlex *ShellLex, cmd string) processFunc {
|
||||||
|
switch {
|
||||||
|
case !replaceEnvAllowed[cmd]:
|
||||||
|
return func(word string, _ []string) ([]string, error) {
|
||||||
|
return []string{word}, nil
|
||||||
}
|
}
|
||||||
var processFunc func(string, []string, rune) ([]string, error)
|
case allowWordExpansion[cmd]:
|
||||||
if allowWordExpansion[cmd] {
|
return shlex.ProcessWords
|
||||||
processFunc = ProcessWords
|
default:
|
||||||
} else {
|
return func(word string, envs []string) ([]string, error) {
|
||||||
processFunc = func(word string, envs []string, escape rune) ([]string, error) {
|
word, err := shlex.ProcessWord(word, envs)
|
||||||
word, err := ProcessWord(word, envs, escape)
|
|
||||||
return []string{word}, err
|
return []string{word}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return processFunc(str, envs, b.escapeToken)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildArgsWithoutConfigEnv returns a list of key=value pairs for all the build
|
// buildArgsWithoutConfigEnv returns a list of key=value pairs for all the build
|
||||||
|
|
|
@ -190,8 +190,9 @@ func executeTestCase(t *testing.T, testCase dispatchTestCase) {
|
||||||
buildArgs: newBuildArgs(options.BuildArgs),
|
buildArgs: newBuildArgs(options.BuildArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shlex := NewShellLex(parser.DefaultEscapeToken)
|
||||||
n := result.AST
|
n := result.AST
|
||||||
err = b.dispatch(0, len(n.Children), n.Children[0])
|
err = b.dispatch(0, len(n.Children), n.Children[0], shlex)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("No error when executing test %s", testCase.name)
|
t.Fatalf("No error when executing test %s", testCase.name)
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
package dockerfile
|
package dockerfile
|
||||||
|
|
||||||
// This will take a single word and an array of env variables and
|
|
||||||
// process all quotes (" and ') as well as $xxx and ${xxx} env variable
|
|
||||||
// tokens. Tries to mimic bash shell process.
|
|
||||||
// It doesn't support all flavors of ${xx:...} formats but new ones can
|
|
||||||
// be added by adding code to the "special ${} format processing" section
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -15,18 +9,26 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type shellWord struct {
|
// ShellLex performs shell word splitting and variable expansion.
|
||||||
word string
|
//
|
||||||
scanner scanner.Scanner
|
// ShellLex takes a string and an array of env variables and
|
||||||
envs []string
|
// process all quotes (" and ') as well as $xxx and ${xxx} env variable
|
||||||
pos int
|
// tokens. Tries to mimic bash shell process.
|
||||||
|
// It doesn't support all flavors of ${xx:...} formats but new ones can
|
||||||
|
// be added by adding code to the "special ${} format processing" section
|
||||||
|
type ShellLex struct {
|
||||||
escapeToken rune
|
escapeToken rune
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewShellLex creates a new ShellLex which uses escapeToken to escape quotes.
|
||||||
|
func NewShellLex(escapeToken rune) *ShellLex {
|
||||||
|
return &ShellLex{escapeToken: escapeToken}
|
||||||
|
}
|
||||||
|
|
||||||
// ProcessWord will use the 'env' list of environment variables,
|
// ProcessWord will use the 'env' list of environment variables,
|
||||||
// and replace any env var references in 'word'.
|
// and replace any env var references in 'word'.
|
||||||
func ProcessWord(word string, env []string, escapeToken rune) (string, error) {
|
func (s *ShellLex) ProcessWord(word string, env []string) (string, error) {
|
||||||
word, _, err := process(word, env, escapeToken)
|
word, _, err := s.process(word, env)
|
||||||
return word, err
|
return word, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,24 +39,32 @@ func ProcessWord(word string, env []string, escapeToken rune) (string, error) {
|
||||||
// this splitting is done **after** the env var substitutions are done.
|
// this splitting is done **after** the env var substitutions are done.
|
||||||
// Note, each one is trimmed to remove leading and trailing spaces (unless
|
// Note, each one is trimmed to remove leading and trailing spaces (unless
|
||||||
// they are quoted", but ProcessWord retains spaces between words.
|
// they are quoted", but ProcessWord retains spaces between words.
|
||||||
func ProcessWords(word string, env []string, escapeToken rune) ([]string, error) {
|
func (s *ShellLex) ProcessWords(word string, env []string) ([]string, error) {
|
||||||
_, words, err := process(word, env, escapeToken)
|
_, words, err := s.process(word, env)
|
||||||
return words, err
|
return words, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func process(word string, env []string, escapeToken rune) (string, []string, error) {
|
func (s *ShellLex) process(word string, env []string) (string, []string, error) {
|
||||||
sw := &shellWord{
|
sw := &shellWord{
|
||||||
word: word,
|
|
||||||
envs: env,
|
envs: env,
|
||||||
pos: 0,
|
escapeToken: s.escapeToken,
|
||||||
escapeToken: escapeToken,
|
|
||||||
}
|
}
|
||||||
sw.scanner.Init(strings.NewReader(word))
|
sw.scanner.Init(strings.NewReader(word))
|
||||||
return sw.process()
|
return sw.process(word)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sw *shellWord) process() (string, []string, error) {
|
type shellWord struct {
|
||||||
return sw.processStopOn(scanner.EOF)
|
scanner scanner.Scanner
|
||||||
|
envs []string
|
||||||
|
escapeToken rune
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sw *shellWord) process(source string) (string, []string, error) {
|
||||||
|
word, words, err := sw.processStopOn(scanner.EOF)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrapf(err, "failed to process %q", source)
|
||||||
|
}
|
||||||
|
return word, words, err
|
||||||
}
|
}
|
||||||
|
|
||||||
type wordsStruct struct {
|
type wordsStruct struct {
|
||||||
|
@ -286,10 +296,10 @@ func (sw *shellWord) processDollar() (string, error) {
|
||||||
return newValue, nil
|
return newValue, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return "", errors.Errorf("unsupported modifier (%c) in substitution: %s", modifier, sw.word)
|
return "", errors.Errorf("unsupported modifier (%c) in substitution", modifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "", errors.Errorf("missing ':' in substitution: %s", sw.word)
|
return "", errors.Errorf("missing ':' in substitution")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sw *shellWord) processName() string {
|
func (sw *shellWord) processName() string {
|
||||||
|
|
|
@ -18,6 +18,7 @@ func TestShellParser4EnvVars(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
|
shlex := NewShellLex('\\')
|
||||||
scanner := bufio.NewScanner(file)
|
scanner := bufio.NewScanner(file)
|
||||||
envs := []string{"PWD=/home", "SHELL=bash", "KOREAN=한국어"}
|
envs := []string{"PWD=/home", "SHELL=bash", "KOREAN=한국어"}
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
|
@ -49,7 +50,7 @@ func TestShellParser4EnvVars(t *testing.T) {
|
||||||
|
|
||||||
if ((platform == "W" || platform == "A") && runtime.GOOS == "windows") ||
|
if ((platform == "W" || platform == "A") && runtime.GOOS == "windows") ||
|
||||||
((platform == "U" || platform == "A") && runtime.GOOS != "windows") {
|
((platform == "U" || platform == "A") && runtime.GOOS != "windows") {
|
||||||
newWord, err := ProcessWord(source, envs, '\\')
|
newWord, err := shlex.ProcessWord(source, envs)
|
||||||
if expected == "error" {
|
if expected == "error" {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
} else {
|
} else {
|
||||||
|
@ -69,6 +70,7 @@ func TestShellParser4Words(t *testing.T) {
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
|
shlex := NewShellLex('\\')
|
||||||
envs := []string{}
|
envs := []string{}
|
||||||
scanner := bufio.NewScanner(file)
|
scanner := bufio.NewScanner(file)
|
||||||
lineNum := 0
|
lineNum := 0
|
||||||
|
@ -93,7 +95,7 @@ func TestShellParser4Words(t *testing.T) {
|
||||||
test := strings.TrimSpace(words[0])
|
test := strings.TrimSpace(words[0])
|
||||||
expected := strings.Split(strings.TrimLeft(words[1], " "), ",")
|
expected := strings.Split(strings.TrimLeft(words[1], " "), ",")
|
||||||
|
|
||||||
result, err := ProcessWords(test, envs, '\\')
|
result, err := shlex.ProcessWords(test, envs)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result = []string{"error"}
|
result = []string{"error"}
|
||||||
|
@ -111,11 +113,7 @@ func TestShellParser4Words(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetEnv(t *testing.T) {
|
func TestGetEnv(t *testing.T) {
|
||||||
sw := &shellWord{
|
sw := &shellWord{envs: nil}
|
||||||
word: "",
|
|
||||||
envs: nil,
|
|
||||||
pos: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
sw.envs = []string{}
|
sw.envs = []string{}
|
||||||
if sw.getEnv("foo") != "" {
|
if sw.getEnv("foo") != "" {
|
||||||
|
|
Loading…
Add table
Reference in a new issue