pax_global_header00006660000000000000000000000064140623514500014512gustar00rootroot0000000000000052 comment=0351a447dc0531882e15ce0a2367c9f797927a10 envsubst-1.0.3/000077500000000000000000000000001406235145000133645ustar00rootroot00000000000000envsubst-1.0.3/.drone.yml000066400000000000000000000001511406235145000152710ustar00rootroot00000000000000kind: pipeline name: default steps: - name: build image: golang:1.11 commands: - go test -v ./... envsubst-1.0.3/.gitignore000066400000000000000000000000271406235145000153530ustar00rootroot00000000000000/envsubst coverage.out envsubst-1.0.3/LICENSE000066400000000000000000000020511406235145000143670ustar00rootroot00000000000000MIT 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. envsubst-1.0.3/cmd/000077500000000000000000000000001406235145000141275ustar00rootroot00000000000000envsubst-1.0.3/cmd/envsubst/000077500000000000000000000000001406235145000160005ustar00rootroot00000000000000envsubst-1.0.3/cmd/envsubst/main.go000066400000000000000000000007011406235145000172510ustar00rootroot00000000000000package main import ( "bufio" "os" "log" "fmt" "github.com/drone/envsubst" ) func main() { stdin := bufio.NewScanner(os.Stdin) stdout := bufio.NewWriter(os.Stdout) for stdin.Scan() { line, err := envsubst.EvalEnv(stdin.Text()) if err != nil { log.Fatalf("Error while envsubst: %v", err) } _, err = fmt.Fprintln(stdout, line) if err != nil { log.Fatalf("Error while writing to stdout: %v", err) } stdout.Flush() } } envsubst-1.0.3/eval.go000066400000000000000000000007641406235145000146510ustar00rootroot00000000000000package 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) } envsubst-1.0.3/eval_test.go000066400000000000000000000121471406235145000157060ustar00rootroot00000000000000package 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", }, // substitute with a blank string { params: map[string]string{"stringZ": "foo.bar"}, input: `${stringZ/./}`, output: "foobar", }, } for _, expr := range expressions { 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) } } } envsubst-1.0.3/funcs.go000066400000000000000000000122351406235145000150340ustar00rootroot00000000000000package envsubst import ( "strconv" "strings" "unicode" "unicode/utf8" "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) } envsubst-1.0.3/funcs_test.go000066400000000000000000000056571406235145000161050ustar00rootroot00000000000000package 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") } } envsubst-1.0.3/go.mod000066400000000000000000000001231406235145000144660ustar00rootroot00000000000000module github.com/drone/envsubst require github.com/google/go-cmp v0.2.0 go 1.13 envsubst-1.0.3/go.sum000066400000000000000000000002471406235145000145220ustar00rootroot00000000000000github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= envsubst-1.0.3/parse/000077500000000000000000000000001406235145000144765ustar00rootroot00000000000000envsubst-1.0.3/parse/node.go000066400000000000000000000026341406235145000157570ustar00rootroot00000000000000package 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() {} envsubst-1.0.3/parse/parse.go000066400000000000000000000220211406235145000161340ustar00rootroot00000000000000package 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 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) { 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 // } envsubst-1.0.3/parse/parse_test.go000066400000000000000000000165411406235145000172050ustar00rootroot00000000000000package parse import ( "testing" "github.com/google/go-cmp/cmp" ) var tests = []struct { Text string Node Node }{ // // text only // { Text: "text", Node: &TextNode{Value: "text"}, }, { Text: "}text", Node: &TextNode{Value: "}text"}, }, { Text: "http://github.com", Node: &TextNode{Value: "http://github.com"}, // should not escape double slash }, { Text: "$${string}", Node: &TextNode{Value: "${string}"}, // should not escape double dollar }, { Text: "$$string", Node: &TextNode{Value: "$string"}, // should not escape double dollar }, // // variable only // { Text: "${string}", Node: &FuncNode{Param: "string"}, }, // // text transform functions // { Text: "${string,}", Node: &FuncNode{ Param: "string", Name: ",", Args: nil, }, }, { Text: "${string,,}", Node: &FuncNode{ Param: "string", Name: ",,", Args: nil, }, }, { Text: "${string^}", Node: &FuncNode{ Param: "string", Name: "^", Args: nil, }, }, { Text: "${string^^}", Node: &FuncNode{ Param: "string", Name: "^^", Args: nil, }, }, // // substring functions // { Text: "${string:position}", Node: &FuncNode{ Param: "string", Name: ":", Args: []Node{ &TextNode{Value: "position"}, }, }, }, { Text: "${string:position:length}", Node: &FuncNode{ Param: "string", Name: ":", Args: []Node{ &TextNode{Value: "position"}, &TextNode{Value: "length"}, }, }, }, // // string removal functions // { Text: "${string#substring}", Node: &FuncNode{ Param: "string", Name: "#", Args: []Node{ &TextNode{Value: "substring"}, }, }, }, { Text: "${string##substring}", Node: &FuncNode{ Param: "string", Name: "##", Args: []Node{ &TextNode{Value: "substring"}, }, }, }, { Text: "${string%substring}", Node: &FuncNode{ Param: "string", Name: "%", Args: []Node{ &TextNode{Value: "substring"}, }, }, }, { Text: "${string%%substring}", Node: &FuncNode{ Param: "string", Name: "%%", Args: []Node{ &TextNode{Value: "substring"}, }, }, }, // // string replace functions // { Text: "${string/substring/replacement}", Node: &FuncNode{ Param: "string", Name: "/", Args: []Node{ &TextNode{Value: "substring"}, &TextNode{Value: "replacement"}, }, }, }, { Text: "${string//substring/replacement}", Node: &FuncNode{ Param: "string", Name: "//", Args: []Node{ &TextNode{Value: "substring"}, &TextNode{Value: "replacement"}, }, }, }, { Text: "${string/#substring/replacement}", Node: &FuncNode{ Param: "string", Name: "/#", Args: []Node{ &TextNode{Value: "substring"}, &TextNode{Value: "replacement"}, }, }, }, { Text: "${string/%substring/replacement}", Node: &FuncNode{ Param: "string", Name: "/%", Args: []Node{ &TextNode{Value: "substring"}, &TextNode{Value: "replacement"}, }, }, }, // // default value functions // { Text: "${string=default}", Node: &FuncNode{ Param: "string", Name: "=", Args: []Node{ &TextNode{Value: "default"}, }, }, }, { Text: "${string:=default}", Node: &FuncNode{ Param: "string", Name: ":=", Args: []Node{ &TextNode{Value: "default"}, }, }, }, { Text: "${string:-default}", Node: &FuncNode{ Param: "string", Name: ":-", Args: []Node{ &TextNode{Value: "default"}, }, }, }, { Text: "${string:?default}", Node: &FuncNode{ Param: "string", Name: ":?", Args: []Node{ &TextNode{Value: "default"}, }, }, }, { Text: "${string:+default}", Node: &FuncNode{ Param: "string", Name: ":+", Args: []Node{ &TextNode{Value: "default"}, }, }, }, // // length function // { Text: "${#string}", Node: &FuncNode{ Param: "string", Name: "#", }, }, // // special characters in argument // { Text: "${string#$%:*{}", Node: &FuncNode{ Param: "string", Name: "#", Args: []Node{ &TextNode{Value: "$%:*{"}, }, }, }, // text before and after function { Text: "hello ${#string} world", Node: &ListNode{ Nodes: []Node{ &TextNode{ Value: "hello ", }, &ListNode{ Nodes: []Node{ &FuncNode{ Param: "string", Name: "#", }, &TextNode{ Value: " world", }, }, }, }, }, }, // escaped function arguments { Text: `${string/\/position/length}`, Node: &FuncNode{ Param: "string", Name: "/", Args: []Node{ &TextNode{ Value: "/position", }, &TextNode{ Value: "length", }, }, }, }, { Text: `${string/\/position\\/length}`, Node: &FuncNode{ Param: "string", Name: "/", Args: []Node{ &TextNode{ Value: "/position\\", }, &TextNode{ Value: "length", }, }, }, }, { Text: `${string/position/\/length}`, Node: &FuncNode{ Param: "string", Name: "/", Args: []Node{ &TextNode{ Value: "position", }, &TextNode{ Value: "/length", }, }, }, }, { Text: `${string/position/\/length\\}`, Node: &FuncNode{ Param: "string", Name: "/", Args: []Node{ &TextNode{ Value: "position", }, &TextNode{ Value: "/length\\", }, }, }, }, // functions in functions { Text: "${string:${position}}", Node: &FuncNode{ Param: "string", Name: ":", Args: []Node{ &FuncNode{ Param: "position", }, }, }, }, { Text: "${string:${stringy:position:length}:${stringz,,}}", Node: &FuncNode{ Param: "string", Name: ":", Args: []Node{ &FuncNode{ Param: "stringy", Name: ":", Args: []Node{ &TextNode{Value: "position"}, &TextNode{Value: "length"}, }, }, &FuncNode{ Param: "stringz", Name: ",,", }, }, }, }, { Text: "${string#${stringz}}", Node: &FuncNode{ Param: "string", Name: "#", Args: []Node{ &FuncNode{Param: "stringz"}, }, }, }, { Text: "${string=${stringz}}", Node: &FuncNode{ Param: "string", Name: "=", Args: []Node{ &FuncNode{Param: "stringz"}, }, }, }, { Text: "${string=prefix-${var}}", Node: &FuncNode{ Param: "string", Name: "=", Args: []Node{ &TextNode{Value: "prefix-"}, &FuncNode{Param: "var"}, }, }, }, { Text: "${string=${var}-suffix}", Node: &FuncNode{ Param: "string", Name: "=", Args: []Node{ &FuncNode{Param: "var"}, &TextNode{Value: "-suffix"}, }, }, }, { Text: "${string=prefix-${var}-suffix}", Node: &FuncNode{ Param: "string", Name: "=", Args: []Node{ &TextNode{Value: "prefix-"}, &FuncNode{Param: "var"}, &TextNode{Value: "-suffix"}, }, }, }, { Text: "${string=prefix${var} suffix}", Node: &FuncNode{ Param: "string", Name: "=", Args: []Node{ &TextNode{Value: "prefix"}, &FuncNode{Param: "var"}, &TextNode{Value: " suffix"}, }, }, }, { Text: "${string//${stringy}/${stringz}}", Node: &FuncNode{ Param: "string", Name: "//", Args: []Node{ &FuncNode{Param: "stringy"}, &FuncNode{Param: "stringz"}, }, }, }, } func TestParse(t *testing.T) { for _, test := range tests { t.Log(test.Text) got, err := Parse(test.Text) if err != nil { t.Error(err) } if diff := cmp.Diff(test.Node, got.Root); diff != "" { t.Errorf(diff) } } } envsubst-1.0.3/parse/scan.go000066400000000000000000000121731406235145000157550ustar00rootroot00000000000000package 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 ) // 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 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] } // 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 sipped. func (s *scanner) scanEscaped(r rune) bool { if s.mode&scanEscape == 0 { return false } if r == '$' { if s.peek() == '$' { return true } } if r != '\\' { return false } switch s.peek() { case '/', '\\': return true default: 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 } envsubst-1.0.3/parse/scan_test.go000066400000000000000000000000161406235145000170050ustar00rootroot00000000000000package parse envsubst-1.0.3/path/000077500000000000000000000000001406235145000143205ustar00rootroot00000000000000envsubst-1.0.3/path/match.go000066400000000000000000000112321406235145000157420ustar00rootroot00000000000000// 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 } envsubst-1.0.3/readme.md000066400000000000000000000014241406235145000151440ustar00rootroot00000000000000# envsubst `envsubst` is a Go package for expanding variables in a string using `${var}` syntax. Includes support for bash string replacement functions. ## Documentation [Documentation can be found on GoDoc][doc]. ## Supported Functions * `${var^}` * `${var^^}` * `${var,}` * `${var,,}` * `${var:position}` * `${var:position:length}` * `${var#substring}` * `${var##substring}` * `${var%substring}` * `${var%%substring}` * `${var/substring/replacement}` * `${var//substring/replacement}` * `${var/#substring/replacement}` * `${var/%substring/replacement}` * `${#var}` * `${var=default}` * `${var:=default}` * `${var:-default}` ## Unsupported Functions * `${var-default}` * `${var+default}` * `${var:?default}` * `${var:+default}` [doc]: http://godoc.org/github.com/drone/envsubstenvsubst-1.0.3/template.go000066400000000000000000000062171406235145000155340ustar00rootroot00000000000000package envsubst import ( "bytes" "io" "io/ioutil" "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 } }