Skip to content

Commit

Permalink
lex: add support for optional colon in variable expansion
Browse files Browse the repository at this point in the history
Signed-off-by: Justin Chadwell <me@jedevc.com>
  • Loading branch information
jedevc committed Feb 9, 2023
1 parent e98c2fb commit 580eb93
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 67 deletions.
81 changes: 28 additions & 53 deletions frontend/dockerfile/shell/lex.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
142 changes: 128 additions & 14 deletions frontend/dockerfile/shell/lex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,34 +234,148 @@ func TestProcessWithMatches(t *testing.T) {
matches map[string]struct{}
}{
{
input: "foo ${BAR} ${UNUSED}",
envs: map[string]string{"ANOTHER": "bar", "BAR": "baz"},
expected: "foo baz ",
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: "foo ${BAR:-abc} ${UNUSED}",
envs: map[string]string{"ANOTHER": "bar"},
expected: "foo abc ",
matches: map[string]struct{}{},
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,
},
}

for _, c := range tc {
c := c
t.Run(c.input, func(t *testing.T) {
w, matches, err := shlex.ProcessWordWithMatches(c.input, c.envs)
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.NoError(t, err)
require.Equal(t, c.expected, w)

require.Equal(t, len(c.matches), len(matches))
for k := range c.matches {
require.Contains(t, matches, k)
}
require.Equal(t, len(c.matches), len(matches))
for k := range c.matches {
require.Contains(t, matches, k)
}
})
}
}

Expand Down

0 comments on commit 580eb93

Please sign in to comment.