diff --git a/frontend/dockerfile/shell/lex.go b/frontend/dockerfile/shell/lex.go index b930ab32601a..80806f8ba778 100644 --- a/frontend/dockerfile/shell/lex.go +++ b/frontend/dockerfile/shell/lex.go @@ -335,39 +335,23 @@ func (sw *shellWord) processDollar() (string, error) { } name := sw.processName() ch := sw.scanner.Next() + chs := string(ch) + nullIsUnset := false + switch ch { case '}': // Normal ${xx} case - value, found := sw.getEnv(name) - if !found && sw.skipUnsetEnv { + value, set := sw.getEnv(name) + if !set && sw.skipUnsetEnv { return fmt.Sprintf("${%s}", name), nil } return value, nil - case '?': - word, _, err := sw.processStopOn('}') - if err != nil { - if sw.scanner.Peek() == scanner.EOF { - return "", errors.New("syntax error: missing '}'") - } - return "", err - } - newValue, found := sw.getEnv(name) - if !found { - if sw.skipUnsetEnv { - return fmt.Sprintf("${%s?%s}", name, word), nil - } - message := "is not allowed to be unset" - if word != "" { - message = word - } - return "", errors.Errorf("%s: %s", name, message) - } - return newValue, nil case ':': - // Special ${xx:...} format processing - // Yes it allows for recursive $'s in the ... spot - modifier := sw.scanner.Next() - + nullIsUnset = true + ch = sw.scanner.Next() + chs += string(ch) + fallthrough + case '+', '-', '?': word, _, err := sw.processStopOn('}') if err != nil { if sw.scanner.Peek() == scanner.EOF { @@ -378,53 +362,44 @@ func (sw *shellWord) processDollar() (string, error) { // Grab the current value of the variable in question so we // can use it to determine what to do based on the modifier - newValue, found := sw.getEnv(name) - - switch modifier { - case '+': - if newValue != "" { - newValue = word - } - if !found && sw.skipUnsetEnv { - return fmt.Sprintf("${%s:%s%s}", name, string(modifier), word), nil - } - return newValue, nil + value, set := sw.getEnv(name) + if sw.skipUnsetEnv && !set { + return fmt.Sprintf("${%s%s%s}", name, chs, word), nil + } + switch ch { case '-': - if newValue == "" { - newValue = word + if !set || (nullIsUnset && value == "") { + return word, nil } - if !found && sw.skipUnsetEnv { - return fmt.Sprintf("${%s:%s%s}", name, string(modifier), word), nil + return value, nil + case '+': + if !set || (nullIsUnset && value == "") { + return "", nil } - - return newValue, nil - + return word, nil case '?': - if !found { - if sw.skipUnsetEnv { - return fmt.Sprintf("${%s:%s%s}", name, string(modifier), word), nil - } + if !set { message := "is not allowed to be unset" if word != "" { message = word } return "", errors.Errorf("%s: %s", name, message) } - if newValue == "" { + if nullIsUnset && value == "" { message := "is not allowed to be empty" if word != "" { message = word } return "", errors.Errorf("%s: %s", name, message) } - return newValue, nil - + return value, nil default: - return "", errors.Errorf("unsupported modifier (%c) in substitution", modifier) + return "", errors.Errorf("unsupported modifier (%s) in substitution", chs) } + default: + return "", errors.Errorf("unsupported modifier (%s) in substitution", chs) } - return "", errors.Errorf("missing ':' in substitution") } func (sw *shellWord) processName() string { diff --git a/frontend/dockerfile/shell/lex_test.go b/frontend/dockerfile/shell/lex_test.go index 3f75d8183abd..eae71e2bcaa8 100644 --- a/frontend/dockerfile/shell/lex_test.go +++ b/frontend/dockerfile/shell/lex_test.go @@ -226,24 +226,157 @@ func TestGetEnv(t *testing.T) { func TestProcessWithMatches(t *testing.T) { shlex := NewLex('\\') - w, matches, err := shlex.ProcessWordWithMatches("foo ${BAR} ${UNUSED}", map[string]string{ - "ANOTHER": "bar", - "BAR": "baz", - }) - require.NoError(t, err) - require.Equal(t, "foo baz ", w) - - require.Equal(t, 1, len(matches)) - _, ok := matches["BAR"] - require.True(t, ok) + tc := []struct { + input string + envs map[string]string + expected string + expectedErr bool + matches map[string]struct{} + }{ + { + input: "x", + envs: map[string]string{"DUMMY": "dummy"}, + expected: "x", + matches: nil, + }, + { + input: "x ${UNUSED}", + envs: map[string]string{"DUMMY": "dummy"}, + expected: "x ", + matches: nil, + }, + { + input: "x ${FOO}", + envs: map[string]string{"FOO": "y"}, + expected: "x y", + matches: map[string]struct{}{"FOO": {}}, + }, + + { + input: "${FOO-aaa} ${BAR-bbb} ${BAZ-ccc}", + envs: map[string]string{ + "FOO": "xxx", + "BAR": "", + }, + expected: "xxx ccc", + matches: map[string]struct{}{"FOO": {}, "BAR": {}}, + }, + { + input: "${FOO:-aaa} ${BAR:-bbb} ${BAZ:-ccc}", + envs: map[string]string{ + "FOO": "xxx", + "BAR": "", + }, + expected: "xxx bbb ccc", + matches: map[string]struct{}{"FOO": {}, "BAR": {}}, + }, + + { + input: "${FOO+aaa} ${BAR+bbb} ${BAZ+ccc}", + envs: map[string]string{ + "FOO": "xxx", + "BAR": "", + }, + expected: "aaa bbb ", + matches: map[string]struct{}{"FOO": {}, "BAR": {}}, + }, + { + input: "${FOO:+aaa} ${BAR:+bbb} ${BAZ:+ccc}", + envs: map[string]string{ + "FOO": "xxx", + "BAR": "", + }, + expected: "aaa ", + matches: map[string]struct{}{"FOO": {}, "BAR": {}}, + }, + + { + input: "${FOO?aaa}", + envs: map[string]string{ + "FOO": "xxx", + "BAR": "", + }, + expected: "xxx", + matches: map[string]struct{}{"FOO": {}}, + }, + { + input: "${BAR?bbb}", + envs: map[string]string{ + "FOO": "xxx", + "BAR": "", + }, + expected: "", + matches: map[string]struct{}{"BAR": {}}, + }, + { + input: "${BAZ?ccc}", + envs: map[string]string{ + "FOO": "xxx", + "BAR": "", + }, + expectedErr: true, + }, + { + input: "${FOO:?aaa}", + envs: map[string]string{ + "FOO": "xxx", + "BAR": "", + }, + expected: "xxx", + matches: map[string]struct{}{"FOO": {}}, + }, + { + input: "${BAR:?bbb}", + envs: map[string]string{ + "FOO": "xxx", + "BAR": "", + }, + expectedErr: true, + }, + { + input: "${BAZ:?ccc}", + envs: map[string]string{ + "FOO": "xxx", + "BAR": "", + }, + expectedErr: true, + }, + + { + input: "${FOO=aaa}", + envs: map[string]string{ + "FOO": "xxx", + "BAR": "", + }, + expectedErr: true, + }, + { + input: "${FOO=:aaa}", + envs: map[string]string{ + "FOO": "xxx", + "BAR": "", + }, + expectedErr: true, + }, + } - w, matches, err = shlex.ProcessWordWithMatches("foo ${BAR:-abc} ${UNUSED}", map[string]string{ - "ANOTHER": "bar", - }) - require.NoError(t, err) - require.Equal(t, "foo abc ", w) + for _, c := range tc { + c := c + t.Run(c.input, func(t *testing.T) { + w, matches, err := shlex.ProcessWordWithMatches(c.input, c.envs) + if c.expectedErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, c.expected, w) - require.Equal(t, 0, len(matches)) + require.Equal(t, len(c.matches), len(matches)) + for k := range c.matches { + require.Contains(t, matches, k) + } + }) + } } func TestProcessWithMatchesPlatform(t *testing.T) {