diff --git a/context_test.go b/context_test.go index 397b8e2fb..4106020bb 100644 --- a/context_test.go +++ b/context_test.go @@ -96,21 +96,32 @@ func TestEnv(t *testing.T) { func TestEnvsubst(t *testing.T) { c := Context{} + c.Setenv("REPLACE", "me") + for s, expected := range map[string]string{ + "BEFORE${REPLACE}AFTER": "BEFOREmeAFTER", + "BEFORE${!REPLACE}AFTER": "BEFORE${REPLACE}AFTER", - if s, err := c.Envsubst("start${example}end"); s != "startend" || err != nil { - t.Fail() - } + "BEFORE${REPLACE:-default}AFTER": "BEFOREmeAFTER", + "BEFORE${!REPLACE:-default}AFTER": "BEFORE${REPLACE:-default}AFTER", - if s, err := c.Envsubst("start${example:-default}end"); s != "startdefaultend" || err != nil { - t.Fail() - } + "BEFORE${REPLACE/me/you}AFTER": "BEFOREyouAFTER", + "BEFORE${!REPLACE/me/you}AFTER": "BEFORE${REPLACE/me/you}AFTER", - c.Setenv("example", "value") - if s, err := c.Envsubst("start${example}end"); s != "startvalueend" || err != nil { - t.Fail() - } + // TODO support curly brackets + // "BEFORE${REPLACE/me/with\\}curly}AFTER": "BEFOREwith}curlyAFTER", + // "BEFORE${!REPLACE/me/with\\}curly}AFTER": "BEFORE${REPLACE/me/with\\}curly}AFTER", - if s, err := c.Envsubst("start${example:-default}end"); s != "startvalueend" || err != nil { - t.Fail() + "BEFORE${UNSET:-default}AFTER": "BEFOREdefaultAFTER", + "BEFORE${!UNSET:-default}AFTER": "BEFORE${UNSET:-default}AFTER", + } { + t.Run(s, func(t *testing.T) { + actual, err := c.Envsubst(s) + if err != nil { + t.Fatal(err) + } + if actual != expected { + t.Fatalf("invalid replacement\nexpected: %#v\nactual : %#v", expected, actual) + } + }) } } diff --git a/docs/src/carapace/context/envSubst.md b/docs/src/carapace/context/envSubst.md index 6d5650dc4..e0b5d3400 100644 --- a/docs/src/carapace/context/envSubst.md +++ b/docs/src/carapace/context/envSubst.md @@ -1 +1,32 @@ # Envsubst + +Expands variables in a string using [drone/envsubst]. + +> Expressions are ignored if the variable name has a `!` prefix (which is stripped). + +## Supported Functions + +| Expression | Meaning | +| ----------------- | -------------- | +| `${var}` | Value of `$var` | +| `${#var}` | String length of `$var` | +| `${var^}` | Uppercase first character of `$var` | +| `${var^^}` | Uppercase all characters in `$var` | +| `${var,}` | Lowercase first character of `$var` | +| `${var,,}` | Lowercase all characters in `$var` | +| `${var:n}` | Offset `$var` `n` characters from start | +| `${var:n:len}` | Offset `$var` `n` characters with max length of `len` | +| `${var#pattern}` | Strip shortest `pattern` match from start | +| `${var##pattern}` | Strip longest `pattern` match from start | +| `${var%pattern}` | Strip shortest `pattern` match from end | +| `${var%%pattern}` | Strip longest `pattern` match from end | +| `${var-default` | If `$var` is not set, evaluate expression as `$default` | +| `${var:-default` | If `$var` is not set or is empty, evaluate expression as `$default` | +| `${var=default` | If `$var` is not set, evaluate expression as `$default` | +| `${var:=default` | If `$var` is not set or is empty, evaluate expression as `$default` | +| `${var/pattern/replacement}` | Replace as few `pattern` matches as possible with `replacement` | +| `${var//pattern/replacement}` | Replace as many `pattern` matches as possible with `replacement` | +| `${var/#pattern/replacement}` | Replace `pattern` match with `replacement` from `$var` start | +| `${var/%pattern/replacement}` | Replace `pattern` match with `replacement` from `$var` end | + +[drone/envsubst]:https://github.com/drone/envsubst diff --git a/third_party/github.com/drone/envsubst/parse/parse.go b/third_party/github.com/drone/envsubst/parse/parse.go index bc418fbfb..47b9c7415 100644 --- a/third_party/github.com/drone/envsubst/parse/parse.go +++ b/third_party/github.com/drone/envsubst/parse/parse.go @@ -90,6 +90,8 @@ func (t *Tree) parseFunc() (Node, error) { // Turn on all escape characters t.scanner.escapeChars = escapeAll switch t.scanner.peek() { + case '!': + return t.parseIgnoredFunc() case '#': return t.parseLenFunc() } @@ -355,6 +357,29 @@ func (t *Tree) parseCasingFunc(name string) (Node, error) { return node, t.consumeRbrack() } +// parses the ignored ${!...} function +func (t *Tree) parseIgnoredFunc() (Node, error) { + node := new(TextNode) + + t.scanner.accept = acceptOneExclamationMark + t.scanner.mode = scanIdent + switch t.scanner.scan() { + case tokenIdent: + default: + return nil, ErrBadSubstitution + } + + t.scanner.accept = acceptNotClosing + t.scanner.mode = scanIdent + switch t.scanner.scan() { + case tokenIdent: + node.Value = "${" + t.scanner.string() + "}" + default: + return nil, ErrBadSubstitution + } + return node, t.consumeRbrack() +} + // parses the ${#param} string function func (t *Tree) parseLenFunc() (Node, error) { node := new(FuncNode) diff --git a/third_party/github.com/drone/envsubst/parse/scan.go b/third_party/github.com/drone/envsubst/parse/scan.go index 2710879ac..c30c500c3 100644 --- a/third_party/github.com/drone/envsubst/parse/scan.go +++ b/third_party/github.com/drone/envsubst/parse/scan.go @@ -227,6 +227,10 @@ func acceptColon(r rune, i int) bool { return r == ':' } +func acceptOneExclamationMark(r rune, i int) bool { + return r == '!' && i == 1 +} + func acceptOneHash(r rune, i int) bool { return r == '#' && i == 1 }