Repository: rsteube/carapace
Pull request #526 (2022-05-06 10:22:43)

context: envsubst


Patch:
diff --git a/context.go b/context.go
index 509602a..bdcc45b 100644
--- a/context.go
+++ b/context.go
@@ -6,6 +6,7 @@ import (
 	"path/filepath"
 	"strings"
 
+	"github.com/rsteube/carapace/third_party/github.com/drone/envsubst"
 	"github.com/rsteube/carapace/third_party/golang.org/x/sys/execabs"
 )
 
@@ -23,6 +24,24 @@ type Context struct {
 	Dir string
 }
 
+// LookupEnv retrieves the value of the environment variable named by the key.
+func (c *Context) LookupEnv(key string) (string, bool) {
+	prefix := key + "="
+	for i := len(c.Env) - 1; i >= 0; i-- {
+		if env := c.Env[i]; strings.HasPrefix(env, prefix) {
+			return strings.SplitN(env, "=", 2)[1], true
+		}
+	}
+	return "", false
+
+}
+
+// Getenv retrieves the value of the environment variable named by the key.
+func (c *Context) Getenv(key string) string {
+	v, _ := c.LookupEnv(key)
+	return v
+}
+
 // Setenv sets the value of the environment variable named by the key.
 func (c *Context) Setenv(key, value string) {
 	if c.Env == nil {
@@ -31,6 +50,10 @@ func (c *Context) Setenv(key, value string) {
 	c.Env = append(c.Env, fmt.Sprintf("%v=%v", key, value))
 }
 
+func (c *Context) Envsubst(s string) (string, error) {
+	return envsubst.Eval(s, c.Getenv)
+}
+
 // Command returns the Cmd struct to execute the named program with the given arguments.
 // Env and Dir are set using the Context.
 // See exec.Command for most details.
diff --git a/third_party/github.com/drone/envsubst/LICENSE b/third_party/github.com/drone/envsubst/LICENSE
new file mode 100644
index 0000000..1de55b7
--- /dev/null
+++ b/third_party/github.com/drone/envsubst/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 drone.io
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/third_party/github.com/drone/envsubst/eval.go b/third_party/github.com/drone/envsubst/eval.go
new file mode 100644
index 0000000..375ca4c
--- /dev/null
+++ b/third_party/github.com/drone/envsubst/eval.go
@@ -0,0 +1,19 @@
+package envsubst
+
+import "os"
+
+// Eval replaces ${var} in the string based on the mapping function.
+func Eval(s string, mapping func(string) string) (string, error) {
+	t, err := Parse(s)
+	if err != nil {
+		return s, err
+	}
+	return t.Execute(mapping)
+}
+
+// EvalEnv replaces ${var} in the string according to the values of the
+// current environment variables. References to undefined variables are
+// replaced by the empty string.
+func EvalEnv(s string) (string, error) {
+	return Eval(s, os.Getenv)
+}
diff --git a/third_party/github.com/drone/envsubst/funcs.go b/third_party/github.com/drone/envsubst/funcs.go
new file mode 100644
index 0000000..a3ed0a6
--- /dev/null
+++ b/third_party/github.com/drone/envsubst/funcs.go
@@ -0,0 +1,246 @@
+package envsubst
+
+import (
+	"strconv"
+	"strings"
+	"unicode"
+	"unicode/utf8"
+
+	"github.com/rsteube/carapace/third_party/github.com/drone/envsubst/path"
+)
+
+// defines a parameter substitution function.
+type substituteFunc func(string, ...string) string
+
+// toLen returns the length of string s.
+func toLen(s string, args ...string) string {
+	return strconv.Itoa(len(s))
+}
+
+// toLower returns a copy of the string s with all characters
+// mapped to their lower case.
+func toLower(s string, args ...string) string {
+	return strings.ToLower(s)
+}
+
+// toUpper returns a copy of the string s with all characters
+// mapped to their upper case.
+func toUpper(s string, args ...string) string {
+	return strings.ToUpper(s)
+}
+
+// toLowerFirst returns a copy of the string s with the first
+// character mapped to its lower case.
+func toLowerFirst(s string, args ...string) string {
+	if s == "" {
+		return s
+	}
+	r, n := utf8.DecodeRuneInString(s)
+	return string(unicode.ToLower(r)) + s[n:]
+}
+
+// toUpperFirst returns a copy of the string s with the first
+// character mapped to its upper case.
+func toUpperFirst(s string, args ...string) string {
+	if s == "" {
+		return s
+	}
+	r, n := utf8.DecodeRuneInString(s)
+	return string(unicode.ToUpper(r)) + s[n:]
+}
+
+// toDefault returns a copy of the string s if not empty, else
+// returns a concatenation of the args without a separator.
+func toDefault(s string, args ...string) string {
+	if len(s) == 0 && len(args) > 0 {
+		// don't use any separator
+		s = strings.Join(args, "")
+	}
+	return s
+}
+
+// toSubstr returns a slice of the string s at the specified
+// length and position.
+func toSubstr(s string, args ...string) string {
+	if len(args) == 0 {
+		return s // should never happen
+	}
+
+	pos, err := strconv.Atoi(args[0])
+	if err != nil {
+		// bash returns the string if the position
+		// cannot be parsed.
+		return s
+	}
+
+	if pos < 0 {
+		// if pos is negative (counts from the end) add it
+		// to length to get first character offset
+		pos = len(s) + pos
+
+		// if negative offset exceeds the length of the string
+		// start from 0
+		if pos < 0 {
+			pos = 0
+		}
+	}
+
+	if len(args) == 1 {
+		if pos < len(s) {
+			return s[pos:]
+		}
+		// if the position exceeds the length of the
+		// string an empty string is returned
+		return ""
+	}
+
+	length, err := strconv.Atoi(args[1])
+	if err != nil {
+		// bash returns the string if the length
+		// cannot be parsed.
+		return s
+	}
+
+	if pos+length >= len(s) {
+		if pos < len(s) {
+			// if the position exceeds the length of the
+			// string just return the rest of it like bash
+			return s[pos:]
+		}
+		// if the position exceeds the length of the
+		// string an empty string is returned
+		return ""
+	}
+
+	return s[pos : pos+length]
+}
+
+// replaceAll returns a copy of the string s with all instances
+// of the substring replaced with the replacement string.
+func replaceAll(s string, args ...string) string {
+	switch len(args) {
+	case 0:
+		return s
+	case 1:
+		return strings.Replace(s, args[0], "", -1)
+	default:
+		return strings.Replace(s, args[0], args[1], -1)
+	}
+}
+
+// replaceFirst returns a copy of the string s with the first
+// instance of the substring replaced with the replacement string.
+func replaceFirst(s string, args ...string) string {
+	switch len(args) {
+	case 0:
+		return s
+	case 1:
+		return strings.Replace(s, args[0], "", 1)
+	default:
+		return strings.Replace(s, args[0], args[1], 1)
+	}
+}
+
+// replacePrefix returns a copy of the string s with the matching
+// prefix replaced with the replacement string.
+func replacePrefix(s string, args ...string) string {
+	if len(args) != 2 {
+		return s
+	}
+	if strings.HasPrefix(s, args[0]) {
+		return strings.Replace(s, args[0], args[1], 1)
+	}
+	return s
+}
+
+// replaceSuffix returns a copy of the string s with the matching
+// suffix replaced with the replacement string.
+func replaceSuffix(s string, args ...string) string {
+	if len(args) != 2 {
+		return s
+	}
+	if strings.HasSuffix(s, args[0]) {
+		s = strings.TrimSuffix(s, args[0])
+		s = s + args[1]
+	}
+	return s
+}
+
+// TODO
+
+func trimShortestPrefix(s string, args ...string) string {
+	if len(args) != 0 {
+		s = trimShortest(s, args[0])
+	}
+	return s
+}
+
+func trimShortestSuffix(s string, args ...string) string {
+	if len(args) != 0 {
+		r := reverse(s)
+		rarg := reverse(args[0])
+		s = reverse(trimShortest(r, rarg))
+	}
+	return s
+}
+
+func trimLongestPrefix(s string, args ...string) string {
+	if len(args) != 0 {
+		s = trimLongest(s, args[0])
+	}
+	return s
+}
+
+func trimLongestSuffix(s string, args ...string) string {
+	if len(args) != 0 {
+		r := reverse(s)
+		rarg := reverse(args[0])
+		s = reverse(trimLongest(r, rarg))
+	}
+	return s
+}
+
+func trimShortest(s, arg string) string {
+	var shortestMatch string
+	for i := 0; i < len(s); i++ {
+		match, err := path.Match(arg, s[0:len(s)-i])
+
+		if err != nil {
+			return s
+		}
+
+		if match {
+			shortestMatch = s[0 : len(s)-i]
+		}
+	}
+
+	if shortestMatch != "" {
+		return strings.TrimPrefix(s, shortestMatch)
+	}
+
+	return s
+}
+
+func trimLongest(s, arg string) string {
+	for i := 0; i < len(s); i++ {
+		match, err := path.Match(arg, s[0:len(s)-i])
+
+		if err != nil {
+			return s
+		}
+
+		if match {
+			return strings.TrimPrefix(s, s[0:len(s)-i])
+		}
+	}
+
+	return s
+}
+
+func reverse(s string) string {
+	r := []rune(s)
+	for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
+		r[i], r[j] = r[j], r[i]
+	}
+	return string(r)
+}
diff --git a/third_party/github.com/drone/envsubst/parse/node.go b/third_party/github.com/drone/envsubst/parse/node.go
new file mode 100644
index 0000000..09787eb
--- /dev/null
+++ b/third_party/github.com/drone/envsubst/parse/node.go
@@ -0,0 +1,86 @@
+package parse
+
+// Node is an element in the parse tree.
+type Node interface {
+	node()
+}
+
+// empty string node
+var empty = new(TextNode)
+
+// a template is represented by a tree consisting of one
+// or more of the following nodes.
+type (
+	// TextNode represents a string of text.
+	TextNode struct {
+		Value string
+	}
+
+	// FuncNode represents a string function.
+	FuncNode struct {
+		Param string
+		Name  string
+		Args  []Node
+	}
+
+	// ListNode represents a list of nodes.
+	ListNode struct {
+		Nodes []Node
+	}
+
+	// ParamNode struct{
+	// 	Name string
+	// }
+	//
+	// CaseNode struct {
+	// 	Name string
+	// 	First bool
+	// }
+	//
+	// LowerNode struct {
+	// 	Name string
+	// 	First bool
+	// }
+	//
+	// SubstrNode struct {
+	// 	Name string
+	// 	Pos Node
+	// 	Len Node
+	// }
+	//
+	// ReplaceNode struct {
+	// 	Name string
+	// 	Substring Node
+	// 	Replacement Node
+	// }
+	//
+	// TrimNode struct{
+	//
+	// }
+	//
+	// DefaultNode struct {
+	// 	Name string
+	// 	Default Node
+	// }
+)
+
+// newTextNode returns a new TextNode.
+func newTextNode(text string) *TextNode {
+	return &TextNode{Value: text}
+}
+
+// newListNode returns a new ListNode.
+func newListNode(nodes ...Node) *ListNode {
+	return &ListNode{Nodes: nodes}
+}
+
+// newFuncNode returns a new FuncNode.
+func newFuncNode(name string) *FuncNode {
+	return &FuncNode{Param: name}
+}
+
+// node() defines the node in a parse tree
+
+func (*TextNode) node() {}
+func (*ListNode) node() {}
+func (*FuncNode) node() {}
diff --git a/third_party/github.com/drone/envsubst/parse/parse.go b/third_party/github.com/drone/envsubst/parse/parse.go
new file mode 100644
index 0000000..bc418fb
--- /dev/null
+++ b/third_party/github.com/drone/envsubst/parse/parse.go
@@ -0,0 +1,402 @@
+package parse
+
+import (
+	"errors"
+)
+
+var (
+	// ErrBadSubstitution represents a substitution parsing error.
+	ErrBadSubstitution = errors.New("bad substitution")
+
+	// ErrMissingClosingBrace represents a missing closing brace "}" error.
+	ErrMissingClosingBrace = errors.New("missing closing brace")
+
+	// ErrParseVariableName represents the error when unable to parse a
+	// variable name within a substitution.
+	ErrParseVariableName = errors.New("unable to parse variable name")
+
+	// ErrParseFuncSubstitution represents the error when unable to parse the
+	// substitution within a function parameter.
+	ErrParseFuncSubstitution = errors.New("unable to parse substitution within function")
+
+	// ErrParseDefaultFunction represent the error when unable to parse a
+	// default function.
+	ErrParseDefaultFunction = errors.New("unable to parse default function")
+)
+
+// Tree is the representation of a single parsed SQL statement.
+type Tree struct {
+	Root Node
+
+	// Parsing only; cleared after parse.
+	scanner *scanner
+}
+
+// Parse parses the string and returns a Tree.
+func Parse(buf string) (*Tree, error) {
+	t := new(Tree)
+	t.scanner = new(scanner)
+	return t.Parse(buf)
+}
+
+// Parse parses the string buffer to construct an ast
+// representation for expansion.
+func (t *Tree) Parse(buf string) (tree *Tree, err error) {
+	t.scanner.init(buf)
+	t.Root, err = t.parseAny()
+	return t, err
+}
+
+func (t *Tree) parseAny() (Node, error) {
+	t.scanner.accept = acceptRune
+	t.scanner.mode = scanIdent | scanLbrack | scanEscape
+	t.scanner.escapeChars = dollar
+
+	switch t.scanner.scan() {
+	case tokenIdent:
+		left := newTextNode(
+			t.scanner.string(),
+		)
+		right, err := t.parseAny()
+		switch {
+		case err != nil:
+			return nil, err
+		case right == empty:
+			return left, nil
+		}
+		return newListNode(left, right), nil
+	case tokenEOF:
+		return empty, nil
+	case tokenLbrack:
+		left, err := t.parseFunc()
+		if err != nil {
+			return nil, err
+		}
+
+		right, err := t.parseAny()
+		switch {
+		case err != nil:
+			return nil, err
+		case right == empty:
+			return left, nil
+		}
+		return newListNode(left, right), nil
+	}
+
+	return nil, ErrBadSubstitution
+}
+
+func (t *Tree) parseFunc() (Node, error) {
+	// Turn on all escape characters
+	t.scanner.escapeChars = escapeAll
+	switch t.scanner.peek() {
+	case '#':
+		return t.parseLenFunc()
+	}
+
+	var name string
+	t.scanner.accept = acceptIdent
+	t.scanner.mode = scanIdent
+
+	switch t.scanner.scan() {
+	case tokenIdent:
+		name = t.scanner.string()
+	default:
+		return nil, ErrParseVariableName
+	}
+
+	switch t.scanner.peek() {
+	case ':':
+		return t.parseDefaultOrSubstr(name)
+	case '=':
+		return t.parseDefaultFunc(name)
+	case ',', '^':
+		return t.parseCasingFunc(name)
+	case '/':
+		return t.parseReplaceFunc(name)
+	case '#':
+		return t.parseRemoveFunc(name, acceptHashFunc)
+	case '%':
+		return t.parseRemoveFunc(name, acceptPercentFunc)
+	}
+
+	t.scanner.accept = acceptIdent
+	t.scanner.mode = scanRbrack
+	switch t.scanner.scan() {
+	case tokenRbrack:
+		return newFuncNode(name), nil
+	default:
+		return nil, ErrMissingClosingBrace
+	}
+}
+
+// parse a substitution function parameter.
+func (t *Tree) parseParam(accept acceptFunc, mode byte) (Node, error) {
+	t.scanner.accept = accept
+	t.scanner.mode = mode | scanLbrack
+	switch t.scanner.scan() {
+	case tokenLbrack:
+		return t.parseFunc()
+	case tokenIdent:
+		return newTextNode(
+			t.scanner.string(),
+		), nil
+	case tokenRbrack:
+		return newTextNode(
+			t.scanner.string(),
+		), nil
+	default:
+		return nil, ErrParseFuncSubstitution
+	}
+}
+
+// parse either a default or substring substitution function.
+func (t *Tree) parseDefaultOrSubstr(name string) (Node, error) {
+	t.scanner.read()
+	r := t.scanner.peek()
+	t.scanner.unread()
+	switch r {
+	case '=', '-', '?', '+':
+		return t.parseDefaultFunc(name)
+	default:
+		return t.parseSubstrFunc(name)
+	}
+}
+
+// parses the ${param:offset} string function
+// parses the ${param:offset:length} string function
+func (t *Tree) parseSubstrFunc(name string) (Node, error) {
+	node := new(FuncNode)
+	node.Param = name
+
+	t.scanner.accept = acceptOneColon
+	t.scanner.mode = scanIdent
+	switch t.scanner.scan() {
+	case tokenIdent:
+		node.Name = t.scanner.string()
+	default:
+		return nil, ErrBadSubstitution
+	}
+
+	// scan arg[1]
+	{
+		param, err := t.parseParam(rejectColonClose, scanIdent)
+		if err != nil {
+			return nil, err
+		}
+
+		// param.Value = t.scanner.string()
+		node.Args = append(node.Args, param)
+	}
+
+	// expect delimiter or close
+	t.scanner.accept = acceptColon
+	t.scanner.mode = scanIdent | scanRbrack
+	switch t.scanner.scan() {
+	case tokenRbrack:
+		return node, nil
+	case tokenIdent:
+		// no-op
+	default:
+		return nil, ErrBadSubstitution
+	}
+
+	// scan arg[2]
+	{
+		param, err := t.parseParam(acceptNotClosing, scanIdent)
+		if err != nil {
+			return nil, err
+		}
+		node.Args = append(node.Args, param)
+	}
+
+	return node, t.consumeRbrack()
+}
+
+// parses the ${param%word} string function
+// parses the ${param%%word} string function
+// parses the ${param#word} string function
+// parses the ${param##word} string function
+func (t *Tree) parseRemoveFunc(name string, accept acceptFunc) (Node, error) {
+	node := new(FuncNode)
+	node.Param = name
+
+	t.scanner.accept = accept
+	t.scanner.mode = scanIdent
+	switch t.scanner.scan() {
+	case tokenIdent:
+		node.Name = t.scanner.string()
+	default:
+		return nil, ErrBadSubstitution
+	}
+
+	// scan arg[1]
+	{
+		param, err := t.parseParam(acceptNotClosing, scanIdent)
+		if err != nil {
+			return nil, err
+		}
+
+		// param.Value = t.scanner.string()
+		node.Args = append(node.Args, param)
+	}
+
+	return node, t.consumeRbrack()
+}
+
+// parses the ${param/pattern/string} string function
+// parses the ${param//pattern/string} string function
+// parses the ${param/#pattern/string} string function
+// parses the ${param/%pattern/string} string function
+func (t *Tree) parseReplaceFunc(name string) (Node, error) {
+	node := new(FuncNode)
+	node.Param = name
+
+	t.scanner.accept = acceptReplaceFunc
+	t.scanner.mode = scanIdent
+	switch t.scanner.scan() {
+	case tokenIdent:
+		node.Name = t.scanner.string()
+	default:
+		return nil, ErrBadSubstitution
+	}
+
+	// scan arg[1]
+	{
+		param, err := t.parseParam(acceptNotSlash, scanIdent|scanEscape)
+		if err != nil {
+			return nil, err
+		}
+		node.Args = append(node.Args, param)
+	}
+
+	// expect delimiter
+	t.scanner.accept = acceptSlash
+	t.scanner.mode = scanIdent
+	switch t.scanner.scan() {
+	case tokenIdent:
+		// no-op
+	default:
+		return nil, ErrBadSubstitution
+	}
+
+	// check for blank string
+	switch t.scanner.peek() {
+	case '}':
+		return node, t.consumeRbrack()
+	}
+
+	// scan arg[2]
+	{
+		param, err := t.parseParam(acceptNotClosing, scanIdent|scanEscape)
+		if err != nil {
+			return nil, err
+		}
+		node.Args = append(node.Args, param)
+	}
+
+	return node, t.consumeRbrack()
+}
+
+// parses the ${parameter=word} string function
+// parses the ${parameter:=word} string function
+// parses the ${parameter:-word} string function
+// parses the ${parameter:?word} string function
+// parses the ${parameter:+word} string function
+func (t *Tree) parseDefaultFunc(name string) (Node, error) {
+	node := new(FuncNode)
+	node.Param = name
+
+	t.scanner.accept = acceptDefaultFunc
+	if t.scanner.peek() == '=' {
+		t.scanner.accept = acceptOneEqual
+	}
+	t.scanner.mode = scanIdent
+	switch t.scanner.scan() {
+	case tokenIdent:
+		node.Name = t.scanner.string()
+	default:
+		return nil, ErrParseDefaultFunction
+	}
+
+	// loop through all possible runes in default param
+	for {
+		// this acts as the break condition. Peek to see if we reached the end
+		switch t.scanner.peek() {
+		case '}':
+			return node, t.consumeRbrack()
+		}
+		param, err := t.parseParam(acceptNotClosing, scanIdent)
+		if err != nil {
+			return nil, err
+		}
+
+		node.Args = append(node.Args, param)
+	}
+}
+
+// parses the ${param,} string function
+// parses the ${param,,} string function
+// parses the ${param^} string function
+// parses the ${param^^} string function
+func (t *Tree) parseCasingFunc(name string) (Node, error) {
+	node := new(FuncNode)
+	node.Param = name
+
+	t.scanner.accept = acceptCasingFunc
+	t.scanner.mode = scanIdent
+	switch t.scanner.scan() {
+	case tokenIdent:
+		node.Name = 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)
+
+	t.scanner.accept = acceptOneHash
+	t.scanner.mode = scanIdent
+	switch t.scanner.scan() {
+	case tokenIdent:
+		node.Name = t.scanner.string()
+	default:
+		return nil, ErrBadSubstitution
+	}
+
+	t.scanner.accept = acceptIdent
+	t.scanner.mode = scanIdent
+	switch t.scanner.scan() {
+	case tokenIdent:
+		node.Param = t.scanner.string()
+	default:
+		return nil, ErrBadSubstitution
+	}
+
+	return node, t.consumeRbrack()
+}
+
+// consumeRbrack consumes a right closing bracket. If a closing
+// bracket token is not consumed an ErrBadSubstitution is returned.
+func (t *Tree) consumeRbrack() error {
+	t.scanner.mode = scanRbrack
+	if t.scanner.scan() != tokenRbrack {
+		return ErrBadSubstitution
+	}
+	return nil
+}
+
+// consumeDelimiter consumes a function argument delimiter. If a
+// delimiter is not consumed an ErrBadSubstitution is returned.
+// func (t *Tree) consumeDelimiter(accept acceptFunc, mode uint) error {
+// 	t.scanner.accept = accept
+// 	t.scanner.mode = mode
+// 	if t.scanner.scan() != tokenRbrack {
+// 		return ErrBadSubstitution
+// 	}
+// 	return nil
+// }
diff --git a/third_party/github.com/drone/envsubst/parse/scan.go b/third_party/github.com/drone/envsubst/parse/scan.go
new file mode 100644
index 0000000..2710879
--- /dev/null
+++ b/third_party/github.com/drone/envsubst/parse/scan.go
@@ -0,0 +1,294 @@
+package parse
+
+import (
+	"unicode"
+	"unicode/utf8"
+)
+
+// eof rune sent when end of file is reached
+var eof = rune(0)
+
+// token is a lexical token.
+type token uint
+
+// list of lexical tokens.
+const (
+	// special tokens
+	tokenIllegal token = iota
+	tokenEOF
+
+	// identifiers and literals
+	tokenIdent
+
+	// operators and delimiters
+	tokenLbrack
+	tokenRbrack
+	tokenQuote
+)
+
+// predefined mode bits to control recognition of tokens.
+const (
+	scanIdent byte = 1 << iota
+	scanLbrack
+	scanRbrack
+	scanEscape
+)
+
+// predefined mode bits to control escape tokens.
+const (
+	dollar byte = 1 << iota
+	backslash
+	escapeAll = dollar | backslash
+)
+
+// returns true if rune is accepted.
+type acceptFunc func(r rune, i int) bool
+
+// scanner implements a lexical scanner that reads unicode
+// characters and tokens from a string buffer.
+type scanner struct {
+	buf         string
+	pos         int
+	start       int
+	width       int
+	mode        byte
+	escapeChars byte
+
+	accept acceptFunc
+}
+
+// init initializes a scanner with a new buffer.
+func (s *scanner) init(buf string) {
+	s.buf = buf
+	s.pos = 0
+	s.start = 0
+	s.width = 0
+	s.accept = nil
+}
+
+// read returns the next unicode character. It returns eof at
+// the end of the string buffer.
+func (s *scanner) read() rune {
+	if s.pos >= len(s.buf) {
+		s.width = 0
+		return eof
+	}
+	r, w := utf8.DecodeRuneInString(s.buf[s.pos:])
+	s.width = w
+	s.pos += s.width
+	return r
+}
+
+func (s *scanner) unread() {
+	s.pos -= s.width
+}
+
+// skip skips over the curring unicode character in the buffer
+// by slicing and removing from the buffer.
+func (s *scanner) skip() {
+	l := s.buf[:s.pos-1]
+	r := s.buf[s.pos:]
+	s.buf = l + r
+}
+
+// peek returns the next unicode character in the buffer without
+// advancing the scanner. It returns eof if the scanner's position
+// is at the last character of the source.
+func (s *scanner) peek() rune {
+	r := s.read()
+	s.unread()
+	return r
+}
+
+// string returns the string corresponding to the most recently
+// scanned token. Valid after calling scan().
+func (s *scanner) string() string {
+	return s.buf[s.start:s.pos]
+}
+
+// tests if the bit exists for a given character bit
+func (s *scanner) shouldEscape(character byte) bool {
+	return s.escapeChars&character != 0
+}
+
+// scan reads the next token or Unicode character from source and
+// returns it. It returns EOF at the end of the source.
+func (s *scanner) scan() token {
+	s.start = s.pos
+	r := s.read()
+	switch {
+	case r == eof:
+		return tokenEOF
+	case s.scanLbrack(r):
+		return tokenLbrack
+	case s.scanRbrack(r):
+		return tokenRbrack
+	case s.scanIdent(r):
+		return tokenIdent
+	}
+	return tokenIllegal
+}
+
+// scanIdent reads the next token or Unicode character from source
+// and returns true if the Ident character is accepted.
+func (s *scanner) scanIdent(r rune) bool {
+	if s.mode&scanIdent == 0 {
+		return false
+	}
+	if s.scanEscaped(r) {
+		s.skip()
+	} else if !s.accept(r, s.pos-s.start) {
+		return false
+	}
+loop:
+	for {
+		r := s.read()
+		switch {
+		case r == eof:
+			s.unread()
+			break loop
+		case s.scanLbrack(r):
+			s.unread()
+			s.unread()
+			break loop
+		}
+		if s.scanEscaped(r) {
+			s.skip()
+			continue
+		}
+		if !s.accept(r, s.pos-s.start) {
+			s.unread()
+			break loop
+		}
+	}
+	return true
+}
+
+// scanLbrack reads the next token or Unicode character from source
+// and returns true if the open bracket is encountered.
+func (s *scanner) scanLbrack(r rune) bool {
+	if s.mode&scanLbrack == 0 {
+		return false
+	}
+	if r == '$' {
+		if s.read() == '{' {
+			return true
+		}
+		s.unread()
+	}
+	return false
+}
+
+// scanRbrack reads the next token or Unicode character from source
+// and returns true if the closing bracket is encountered.
+func (s *scanner) scanRbrack(r rune) bool {
+	if s.mode&scanRbrack == 0 {
+		return false
+	}
+	return r == '}'
+}
+
+// scanEscaped reads the next token or Unicode character from source
+// and returns true if it being escaped and should be skipped.
+func (s *scanner) scanEscaped(r rune) bool {
+	if s.mode&scanEscape == 0 {
+		return false
+	}
+	if r == '$' && s.shouldEscape(dollar) {
+		if s.peek() == '$' {
+			return true
+		}
+	}
+	if r == '\\' && s.shouldEscape(backslash) {
+		switch s.peek() {
+		case '/', '\\':
+			return true
+		default:
+			return false
+		}
+	}
+
+	return false
+}
+
+//
+// scanner functions accept or reject runes.
+//
+
+func acceptRune(r rune, i int) bool {
+	return true
+}
+
+func acceptIdent(r rune, i int) bool {
+	return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_'
+}
+
+func acceptColon(r rune, i int) bool {
+	return r == ':'
+}
+
+func acceptOneHash(r rune, i int) bool {
+	return r == '#' && i == 1
+}
+
+func acceptNone(r rune, i int) bool {
+	return false
+}
+
+func acceptNotClosing(r rune, i int) bool {
+	return r != '}'
+}
+
+func acceptHashFunc(r rune, i int) bool {
+	return r == '#' && i < 3
+}
+
+func acceptPercentFunc(r rune, i int) bool {
+	return r == '%' && i < 3
+}
+
+func acceptDefaultFunc(r rune, i int) bool {
+	switch {
+	case i == 1 && r == ':':
+		return true
+	case i == 2 && (r == '=' || r == '-' || r == '?' || r == '+'):
+		return true
+	default:
+		return false
+	}
+}
+
+func acceptReplaceFunc(r rune, i int) bool {
+	switch {
+	case i == 1 && r == '/':
+		return true
+	case i == 2 && (r == '/' || r == '#' || r == '%'):
+		return true
+	default:
+		return false
+	}
+}
+
+func acceptOneEqual(r rune, i int) bool {
+	return i == 1 && r == '='
+}
+
+func acceptOneColon(r rune, i int) bool {
+	return i == 1 && r == ':'
+}
+
+func rejectColonClose(r rune, i int) bool {
+	return r != ':' && r != '}'
+}
+
+func acceptSlash(r rune, i int) bool {
+	return r == '/'
+}
+
+func acceptNotSlash(r rune, i int) bool {
+	return r != '/'
+}
+
+func acceptCasingFunc(r rune, i int) bool {
+	return (r == ',' || r == '^') && i < 3
+}
diff --git a/third_party/github.com/drone/envsubst/path/match.go b/third_party/github.com/drone/envsubst/path/match.go
new file mode 100644
index 0000000..9306b0c
--- /dev/null
+++ b/third_party/github.com/drone/envsubst/path/match.go
@@ -0,0 +1,207 @@
+// Copyright 2010 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package path
+
+import (
+	"errors"
+	"unicode/utf8"
+)
+
+// ErrBadPattern indicates a globbing pattern was malformed.
+var ErrBadPattern = errors.New("syntax error in pattern")
+
+// Match reports whether name matches the shell file name pattern.
+// The pattern syntax is:
+//
+//	pattern:
+//		{ term }
+//	term:
+//		'*'         matches any sequence of non-/ characters
+//		'?'         matches any single non-/ character
+//		'[' [ '^' ] { character-range } ']'
+//		            character class (must be non-empty)
+//		c           matches character c (c != '*', '?', '\\', '[')
+//		'\\' c      matches character c
+//
+//	character-range:
+//		c           matches character c (c != '\\', '-', ']')
+//		'\\' c      matches character c
+//		lo '-' hi   matches character c for lo <= c <= hi
+//
+// Match requires pattern to match all of name, not just a substring.
+// The only possible returned error is ErrBadPattern, when pattern
+// is malformed.
+//
+func Match(pattern, name string) (matched bool, err error) {
+Pattern:
+	for len(pattern) > 0 {
+		var star bool
+		var chunk string
+		star, chunk, pattern = scanChunk(pattern)
+		if star && chunk == "" {
+			// Trailing * matches rest of string unless it has a /.
+			// return !strings.Contains(name, "/"), nil
+
+			// Return rest of string
+			return true, nil
+		}
+		// Look for match at current position.
+		t, ok, err := matchChunk(chunk, name)
+		// if we're the last chunk, make sure we've exhausted the name
+		// otherwise we'll give a false result even if we could still match
+		// using the star
+		if ok && (len(t) == 0 || len(pattern) > 0) {
+			name = t
+			continue
+		}
+		if err != nil {
+			return false, err
+		}
+		if star {
+			// Look for match skipping i+1 bytes.
+			for i := 0; i < len(name); i++ {
+				t, ok, err := matchChunk(chunk, name[i+1:])
+				if ok {
+					// if we're the last chunk, make sure we exhausted the name
+					if len(pattern) == 0 && len(t) > 0 {
+						continue
+					}
+					name = t
+					continue Pattern
+				}
+				if err != nil {
+					return false, err
+				}
+			}
+		}
+		return false, nil
+	}
+	return len(name) == 0, nil
+}
+
+// scanChunk gets the next segment of pattern, which is a non-star string
+// possibly preceded by a star.
+func scanChunk(pattern string) (star bool, chunk, rest string) {
+	for len(pattern) > 0 && pattern[0] == '*' {
+		pattern = pattern[1:]
+		star = true
+	}
+	inrange := false
+	var i int
+Scan:
+	for i = 0; i < len(pattern); i++ {
+		switch pattern[i] {
+		case '\\':
+			// error check handled in matchChunk: bad pattern.
+			if i+1 < len(pattern) {
+				i++
+			}
+		case '[':
+			inrange = true
+		case ']':
+			inrange = false
+		case '*':
+			if !inrange {
+				break Scan
+			}
+		}
+	}
+	return star, pattern[0:i], pattern[i:]
+}
+
+// matchChunk checks whether chunk matches the beginning of s.
+// If so, it returns the remainder of s (after the match).
+// Chunk is all single-character operators: literals, char classes, and ?.
+func matchChunk(chunk, s string) (rest string, ok bool, err error) {
+	for len(chunk) > 0 {
+		if len(s) == 0 {
+			return
+		}
+		switch chunk[0] {
+		case '[':
+			// character class
+			r, n := utf8.DecodeRuneInString(s)
+			s = s[n:]
+			chunk = chunk[1:]
+			// possibly negated
+			notNegated := true
+			if len(chunk) > 0 && chunk[0] == '^' {
+				notNegated = false
+				chunk = chunk[1:]
+			}
+			// parse all ranges
+			match := false
+			nrange := 0
+			for {
+				if len(chunk) > 0 && chunk[0] == ']' && nrange > 0 {
+					chunk = chunk[1:]
+					break
+				}
+				var lo, hi rune
+				if lo, chunk, err = getEsc(chunk); err != nil {
+					return
+				}
+				hi = lo
+				if chunk[0] == '-' {
+					if hi, chunk, err = getEsc(chunk[1:]); err != nil {
+						return
+					}
+				}
+				if lo <= r && r <= hi {
+					match = true
+				}
+				nrange++
+			}
+			if match != notNegated {
+				return
+			}
+
+		case '?':
+			_, n := utf8.DecodeRuneInString(s)
+			s = s[n:]
+			chunk = chunk[1:]
+
+		case '\\':
+			chunk = chunk[1:]
+			if len(chunk) == 0 {
+				err = ErrBadPattern
+				return
+			}
+			fallthrough
+
+		default:
+			if chunk[0] != s[0] {
+				return
+			}
+			s = s[1:]
+			chunk = chunk[1:]
+		}
+	}
+	return s, true, nil
+}
+
+// getEsc gets a possibly-escaped character from chunk, for a character class.
+func getEsc(chunk string) (r rune, nchunk string, err error) {
+	if len(chunk) == 0 || chunk[0] == '-' || chunk[0] == ']' {
+		err = ErrBadPattern
+		return
+	}
+	if chunk[0] == '\\' {
+		chunk = chunk[1:]
+		if len(chunk) == 0 {
+			err = ErrBadPattern
+			return
+		}
+	}
+	r, n := utf8.DecodeRuneInString(chunk)
+	if r == utf8.RuneError && n == 1 {
+		err = ErrBadPattern
+	}
+	nchunk = chunk[n:]
+	if len(nchunk) == 0 {
+		err = ErrBadPattern
+	}
+	return
+}
diff --git a/third_party/github.com/drone/envsubst/template.go b/third_party/github.com/drone/envsubst/template.go
new file mode 100644
index 0000000..367e565
--- /dev/null
+++ b/third_party/github.com/drone/envsubst/template.go
@@ -0,0 +1,157 @@
+package envsubst
+
+import (
+	"bytes"
+	"io"
+	"io/ioutil"
+
+	"github.com/rsteube/carapace/third_party/github.com/drone/envsubst/parse"
+)
+
+// state represents the state of template execution. It is not part of the
+// template so that multiple executions can run in parallel.
+type state struct {
+	template *Template
+	writer   io.Writer
+	node     parse.Node // current node
+
+	// maps variable names to values
+	mapper func(string) string
+}
+
+// Template is the representation of a parsed shell format string.
+type Template struct {
+	tree *parse.Tree
+}
+
+// Parse creates a new shell format template and parses the template
+// definition from string s.
+func Parse(s string) (t *Template, err error) {
+	t = new(Template)
+	t.tree, err = parse.Parse(s)
+	if err != nil {
+		return nil, err
+	}
+	return t, nil
+}
+
+// ParseFile creates a new shell format template and parses the template
+// definition from the named file.
+func ParseFile(path string) (*Template, error) {
+	b, err := ioutil.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+	return Parse(string(b))
+}
+
+// Execute applies a parsed template to the specified data mapping.
+func (t *Template) Execute(mapping func(string) string) (str string, err error) {
+	b := new(bytes.Buffer)
+	s := new(state)
+	s.node = t.tree.Root
+	s.mapper = mapping
+	s.writer = b
+	err = t.eval(s)
+	if err != nil {
+		return
+	}
+	return b.String(), nil
+}
+
+func (t *Template) eval(s *state) (err error) {
+	switch node := s.node.(type) {
+	case *parse.TextNode:
+		err = t.evalText(s, node)
+	case *parse.FuncNode:
+		err = t.evalFunc(s, node)
+	case *parse.ListNode:
+		err = t.evalList(s, node)
+	}
+	return err
+}
+
+func (t *Template) evalText(s *state, node *parse.TextNode) error {
+	_, err := io.WriteString(s.writer, node.Value)
+	return err
+}
+
+func (t *Template) evalList(s *state, node *parse.ListNode) (err error) {
+	for _, n := range node.Nodes {
+		s.node = n
+		err = t.eval(s)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (t *Template) evalFunc(s *state, node *parse.FuncNode) error {
+	var w = s.writer
+	var buf bytes.Buffer
+	var args []string
+	for _, n := range node.Args {
+		buf.Reset()
+		s.writer = &buf
+		s.node = n
+		err := t.eval(s)
+		if err != nil {
+			return err
+		}
+		args = append(args, buf.String())
+	}
+
+	// restore the origin writer
+	s.writer = w
+	s.node = node
+
+	v := s.mapper(node.Param)
+
+	fn := lookupFunc(node.Name, len(args))
+
+	_, err := io.WriteString(s.writer, fn(v, args...))
+	return err
+}
+
+// lookupFunc returns the parameters substitution function by name. If the
+// named function does not exists, a default function is returned.
+func lookupFunc(name string, args int) substituteFunc {
+	switch name {
+	case ",":
+		return toLowerFirst
+	case ",,":
+		return toLower
+	case "^":
+		return toUpperFirst
+	case "^^":
+		return toUpper
+	case "#":
+		if args == 0 {
+			return toLen
+		}
+		return trimShortestPrefix
+	case "##":
+		return trimLongestPrefix
+	case "%":
+		return trimShortestSuffix
+	case "%%":
+		return trimLongestSuffix
+	case ":":
+		return toSubstr
+	case "/#":
+		return replacePrefix
+	case "/%":
+		return replaceSuffix
+	case "/":
+		return replaceFirst
+	case "//":
+		return replaceAll
+	case "=", ":=", ":-":
+		return toDefault
+	case ":?", ":+", "-", "+":
+		return toDefault
+	default:
+		return toDefault
+	}
+}


Test patch:
diff --git a/context_test.go b/context_test.go
index a7ff03c..18d35af 100644
--- a/context_test.go
+++ b/context_test.go
@@ -62,3 +62,50 @@ func TestContextAbs(t *testing.T) {
 	}
 
 }
+
+func TestEnv(t *testing.T) {
+	c := Context{}
+	if c.Getenv("example") != "" {
+		t.Fail()
+	}
+	if v, exist := c.LookupEnv("example"); v != "" || exist {
+		t.Fail()
+	}
+
+	c.Setenv("example", "value")
+	if c.Getenv("example") != "value" {
+		t.Fail()
+	}
+	if v, exist := c.LookupEnv("example"); v != "value" || !exist {
+		t.Fail()
+	}
+
+	c.Setenv("example", "newvalue")
+	if c.Getenv("example") != "newvalue" {
+		t.Fail()
+	}
+	if v, exist := c.LookupEnv("example"); v != "newvalue" || !exist {
+		t.Fail()
+	}
+}
+
+func TestEnvsubst(t *testing.T) {
+	c := Context{}
+
+	if s, err := c.Envsubst("start${example}end"); s != "startend" || err != nil {
+		t.Fail()
+	}
+
+	if s, err := c.Envsubst("start${example:-default}end"); s != "startdefaultend" || err != nil {
+		t.Fail()
+	}
+
+	c.Setenv("example", "value")
+	if s, err := c.Envsubst("start${example}end"); s != "startvalueend" || err != nil {
+		t.Fail()
+	}
+
+	if s, err := c.Envsubst("start${example:-default}end"); s != "startvalueend" || err != nil {
+		t.Fail()
+	}
+}
diff --git a/third_party/github.com/drone/envsubst/eval_test.go b/third_party/github.com/drone/envsubst/eval_test.go
new file mode 100644
index 0000000..f7867e5
--- /dev/null
+++ b/third_party/github.com/drone/envsubst/eval_test.go
@@ -0,0 +1,228 @@
+package envsubst
+
+import "testing"
+
+// test cases sourced from tldp.org
+// http://www.tldp.org/LDP/abs/html/parameter-substitution.html
+
+func TestExpand(t *testing.T) {
+	var expressions = []struct {
+		params map[string]string
+		input  string
+		output string
+	}{
+		// text-only
+		{
+			params: map[string]string{},
+			input:  "abcdEFGH28ij",
+			output: "abcdEFGH28ij",
+		},
+		// length
+		{
+			params: map[string]string{"var01": "abcdEFGH28ij"},
+			input:  "${#var01}",
+			output: "12",
+		},
+		// uppercase first
+		{
+			params: map[string]string{"var01": "abcdEFGH28ij"},
+			input:  "${var01^}",
+			output: "AbcdEFGH28ij",
+		},
+		// uppercase
+		{
+			params: map[string]string{"var01": "abcdEFGH28ij"},
+			input:  "${var01^^}",
+			output: "ABCDEFGH28IJ",
+		},
+		// lowercase first
+		{
+			params: map[string]string{"var01": "ABCDEFGH28IJ"},
+			input:  "${var01,}",
+			output: "aBCDEFGH28IJ",
+		},
+		// lowercase
+		{
+			params: map[string]string{"var01": "ABCDEFGH28IJ"},
+			input:  "${var01,,}",
+			output: "abcdefgh28ij",
+		},
+		// substring with position
+		{
+			params: map[string]string{"path_name": "/home/bozo/ideas/thoughts.for.today"},
+			input:  "${path_name:11}",
+			output: "ideas/thoughts.for.today",
+		},
+		// substring with position and length
+		{
+			params: map[string]string{"path_name": "/home/bozo/ideas/thoughts.for.today"},
+			input:  "${path_name:11:5}",
+			output: "ideas",
+		},
+		// default not used
+		{
+			params: map[string]string{"var": "abc"},
+			input:  "${var=xyz}",
+			output: "abc",
+		},
+		// default used
+		{
+			params: map[string]string{},
+			input:  "${var=xyz}",
+			output: "xyz",
+		},
+		{
+			params: map[string]string{"default_var": "foo"},
+			input:  "something ${var=${default_var}}",
+			output: "something foo",
+		},
+		{
+			params: map[string]string{"default_var": "foo1"},
+			input:  `foo: ${var=${default_var}-suffix}`,
+			output: "foo: foo1-suffix",
+		},
+		{
+			params: map[string]string{"default_var": "foo1"},
+			input:  `foo: ${var=prefix${default_var}-suffix}`,
+			output: "foo: prefixfoo1-suffix",
+		},
+		{
+			params: map[string]string{},
+			input:  "${var:=xyz}",
+			output: "xyz",
+		},
+		// replace suffix
+		{
+			params: map[string]string{"stringZ": "abcABC123ABCabc"},
+			input:  "${stringZ/%abc/XYZ}",
+			output: "abcABC123ABCXYZ",
+		},
+		// replace prefix
+		{
+			params: map[string]string{"stringZ": "abcABC123ABCabc"},
+			input:  "${stringZ/#abc/XYZ}",
+			output: "XYZABC123ABCabc",
+		},
+		// replace all
+		{
+			params: map[string]string{"stringZ": "abcABC123ABCabc"},
+			input:  "${stringZ//abc/xyz}",
+			output: "xyzABC123ABCxyz",
+		},
+		// replace first
+		{
+			params: map[string]string{"stringZ": "abcABC123ABCabc"},
+			input:  "${stringZ/abc/xyz}",
+			output: "xyzABC123ABCabc",
+		},
+		// delete shortest match prefix
+		{
+			params: map[string]string{"filename": "bash.string.txt"},
+			input:  "${filename#*.}",
+			output: "string.txt",
+		},
+		{
+			params: map[string]string{"filename": "path/to/file"},
+			input:  "${filename#*/}",
+			output: "to/file",
+		},
+		{
+			params: map[string]string{"filename": "/path/to/file"},
+			input:  "${filename#*/}",
+			output: "path/to/file",
+		},
+		// delete longest match prefix
+		{
+			params: map[string]string{"filename": "bash.string.txt"},
+			input:  "${filename##*.}",
+			output: "txt",
+		},
+		{
+			params: map[string]string{"filename": "path/to/file"},
+			input:  "${filename##*/}",
+			output: "file",
+		},
+		{
+			params: map[string]string{"filename": "/path/to/file"},
+			input:  "${filename##*/}",
+			output: "file",
+		},
+		// delete shortest match suffix
+		{
+			params: map[string]string{"filename": "bash.string.txt"},
+			input:  "${filename%.*}",
+			output: "bash.string",
+		},
+		// delete longest match suffix
+		{
+			params: map[string]string{"filename": "bash.string.txt"},
+			input:  "${filename%%.*}",
+			output: "bash",
+		},
+
+		// nested parameters
+		{
+			params: map[string]string{"var01": "abcdEFGH28ij"},
+			input:  "${var=${var01^^}}",
+			output: "ABCDEFGH28IJ",
+		},
+		// escaped
+		{
+			params: map[string]string{"var01": "abcdEFGH28ij"},
+			input:  "$${var01}",
+			output: "${var01}",
+		},
+		{
+			params: map[string]string{"var01": "abcdEFGH28ij"},
+			input:  "some text ${var01}$${var$${var01}$var01${var01}",
+			output: "some text abcdEFGH28ij${var${var01}$var01abcdEFGH28ij",
+		},
+		{
+			params: map[string]string{"default_var": "foo"},
+			input:  "something $${var=${default_var}}",
+			output: "something ${var=foo}",
+		},
+		// some common escaping use cases
+		{
+			params: map[string]string{"stringZ": "foo/bar"},
+			input:  `${stringZ/\//-}`,
+			output: "foo-bar",
+		},
+		{
+			params: map[string]string{"stringZ": "foo/bar/baz"},
+			input:  `${stringZ//\//-}`,
+			output: "foo-bar-baz",
+		},
+		// escape outside of expansion shouldn't be processed
+		{
+			params: map[string]string{"default_var": "foo"},
+			input:  "\\\\something ${var=${default_var}}",
+			output: "\\\\something foo",
+		},
+		// substitute with a blank string
+		{
+			params: map[string]string{"stringZ": "foo.bar"},
+			input:  `${stringZ/./}`,
+			output: "foobar",
+		},
+	}
+
+	for _, expr := range expressions {
+		t.Run(expr.input, func(t *testing.T) {
+			t.Logf(expr.input)
+			output, err := Eval(expr.input, func(s string) string {
+				return expr.params[s]
+			})
+			if err != nil {
+				t.Errorf("Want %q expanded but got error %q", expr.input, err)
+			}
+
+			if output != expr.output {
+				t.Errorf("Want %q expanded to %q, got %q",
+					expr.input,
+					expr.output,
+					output)
+			}
+		})
+	}
+}
diff --git a/third_party/github.com/drone/envsubst/funcs_test.go b/third_party/github.com/drone/envsubst/funcs_test.go
new file mode 100644
index 0000000..3668161
--- /dev/null
+++ b/third_party/github.com/drone/envsubst/funcs_test.go
@@ -0,0 +1,104 @@
+package envsubst
+
+import "testing"
+
+func Test_len(t *testing.T) {
+	got, want := toLen("Hello World"), "11"
+	if got != want {
+		t.Errorf("Expect len function to return %s, got %s", want, got)
+	}
+}
+
+func Test_lower(t *testing.T) {
+	got, want := toLower("Hello World"), "hello world"
+	if got != want {
+		t.Errorf("Expect lower function to return %s, got %s", want, got)
+	}
+}
+
+func Test_lowerFirst(t *testing.T) {
+	got, want := toLowerFirst("HELLO WORLD"), "hELLO WORLD"
+	if got != want {
+		t.Errorf("Expect lowerFirst function to return %s, got %s", want, got)
+	}
+	defer func() {
+		if recover() != nil {
+			t.Errorf("Expect empty string does not panic lowerFirst")
+		}
+	}()
+	toLowerFirst("")
+}
+
+func Test_upper(t *testing.T) {
+	got, want := toUpper("Hello World"), "HELLO WORLD"
+	if got != want {
+		t.Errorf("Expect upper function to return %s, got %s", want, got)
+	}
+}
+
+func Test_upperFirst(t *testing.T) {
+	got, want := toUpperFirst("hello world"), "Hello world"
+	if got != want {
+		t.Errorf("Expect upperFirst function to return %s, got %s", want, got)
+	}
+	defer func() {
+		if recover() != nil {
+			t.Errorf("Expect empty string does not panic upperFirst")
+		}
+	}()
+	toUpperFirst("")
+}
+
+func Test_default(t *testing.T) {
+	got, want := toDefault("Hello World", "Hola Mundo"), "Hello World"
+	if got != want {
+		t.Errorf("Expect default function uses variable value")
+	}
+
+	got, want = toDefault("", "Hola Mundo"), "Hola Mundo"
+	if got != want {
+		t.Errorf("Expect default function uses default value, when variable empty. Got %s, Want %s", got, want)
+	}
+
+	got, want = toDefault("", "Hola Mundo", "-Bonjour le monde", "-Halló heimur"), "Hola Mundo-Bonjour le monde-Halló heimur"
+	if got != want {
+		t.Errorf("Expect default function to use concatenated args when variable empty. Got %s, Want %s", got, want)
+	}
+}
+
+func Test_substr(t *testing.T) {
+	got, want := toSubstr("123456789123456789", "0", "8"), "12345678"
+	if got != want {
+		t.Errorf("Expect substr function to cut from beginning to length")
+	}
+
+	got, want = toSubstr("123456789123456789", "1", "8"), "23456789"
+	if got != want {
+		t.Errorf("Expect substr function to cut from offset to length")
+	}
+
+	got, want = toSubstr("123456789123456789", "9"), "123456789"
+	if got != want {
+		t.Errorf("Expect substr function to cut beginnging with offset")
+	}
+
+	got, want = toSubstr("123456789123456789", "9", "50"), "123456789"
+	if got != want {
+		t.Errorf("Expect substr function to ignore length if out of bound")
+	}
+
+	got, want = toSubstr("123456789123456789", "-3", "2"), "78"
+	if got != want {
+		t.Errorf("Expect substr function to count negative offsets from the end")
+	}
+
+	got, want = toSubstr("123456789123456789", "-300", "3"), "123"
+	if got != want {
+		t.Errorf("Expect substr function to cut from the beginning to length for negative offsets exceeding string length")
+	}
+
+	got, want = toSubstr("12345678", "9", "1"), ""
+	if got != want {
+		t.Errorf("Expect substr function to cut entire string if pos is itself out of bound")
+	}
+}
diff --git a/third_party/github.com/drone/envsubst/parse/scan_test.go b/third_party/github.com/drone/envsubst/parse/scan_test.go
new file mode 100644
index 0000000..fe2554d
--- /dev/null
+++ b/third_party/github.com/drone/envsubst/parse/scan_test.go
@@ -0,0 +1,1 @@
+package parse