diff --git a/envsubst/eval_test.go b/envsubst/eval_test.go index 0702c985e..0236f97ba 100644 --- a/envsubst/eval_test.go +++ b/envsubst/eval_test.go @@ -126,6 +126,18 @@ func TestExpand(t *testing.T) { input: "${stringZ/#abc/XYZ}", output: "XYZABC123ABCabc", }, + // replace prefix with empty pattern (prepend) + { + params: map[string]string{"stringZ": "somevalue"}, + input: "${stringZ/#/prefix-}", + output: "prefix-somevalue", + }, + // replace suffix with empty pattern (append) + { + params: map[string]string{"stringZ": "somevalue"}, + input: "${stringZ/%/-suffix}", + output: "somevalue-suffix", + }, // replace all { params: map[string]string{"stringZ": "abcABC123ABCabc"}, diff --git a/envsubst/parse/parse.go b/envsubst/parse/parse.go index 39c72009f..1570cc73f 100644 --- a/envsubst/parse/parse.go +++ b/envsubst/parse/parse.go @@ -283,11 +283,18 @@ func (t *Tree) parseReplaceFunc(name string) (Node, error) { // scan arg[1] { - param, err := t.parseParam(acceptNotSlash, scanIdent|scanEscape) - if err != nil { - return nil, err + // An empty pattern (e.g. ${param/#/string}) means the next rune is the + // delimiter, so there is nothing to scan. Record an empty pattern node + // to keep the two-argument shape the eval side expects. + if t.scanner.peek() == '/' { + node.Args = append(node.Args, newTextNode("")) + } else { + param, err := t.parseParam(acceptNotSlash, scanIdent|scanEscape) + if err != nil { + return nil, err + } + node.Args = append(node.Args, param) } - node.Args = append(node.Args, param) } // expect delimiter diff --git a/envsubst/parse/parse_test.go b/envsubst/parse/parse_test.go index d51e6dbaf..171081d56 100644 --- a/envsubst/parse/parse_test.go +++ b/envsubst/parse/parse_test.go @@ -219,6 +219,28 @@ var tests = []struct { }, }, }, + { + Text: "${string/#/replacement}", + Node: &FuncNode{ + Param: "string", + Name: "/#", + Args: []Node{ + &TextNode{Value: ""}, + &TextNode{Value: "replacement"}, + }, + }, + }, + { + Text: "${string/%/replacement}", + Node: &FuncNode{ + Param: "string", + Name: "/%", + Args: []Node{ + &TextNode{Value: ""}, + &TextNode{Value: "replacement"}, + }, + }, + }, // // default value functions