pax_global_header00006660000000000000000000000064147570331310014516gustar00rootroot0000000000000052 comment=2aa6422a5466ee701933dd838d9d36aaf16a0b32 envsubst-1.4.3/000077500000000000000000000000001475703313100133745ustar00rootroot00000000000000envsubst-1.4.3/.github/000077500000000000000000000000001475703313100147345ustar00rootroot00000000000000envsubst-1.4.3/.github/workflows/000077500000000000000000000000001475703313100167715ustar00rootroot00000000000000envsubst-1.4.3/.github/workflows/binaries.yml000066400000000000000000000041111475703313100213050ustar00rootroot00000000000000name: Release Binaries on: release: types: [created] permissions: contents: write packages: write jobs: release-linux-amd64: name: release linux/amd64 runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: wangyoucao577/go-release-action@v1.53 with: github_token: ${{ secrets.GITHUB_TOKEN }} goos: linux goarch: amd64 project_path: cmd/envsubst asset_name: envsubst-Linux-x86_64 compress_assets: OFF release-linux-arm64: name: release linux/arm64 runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: wangyoucao577/go-release-action@v1.53 with: github_token: ${{ secrets.GITHUB_TOKEN }} goos: linux goarch: arm64 project_path: cmd/envsubst asset_name: envsubst-Linux-arm64 compress_assets: OFF release-darwin-amd64: name: release darwin/amd64 runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: wangyoucao577/go-release-action@v1.53 with: github_token: ${{ secrets.GITHUB_TOKEN }} goos: darwin goarch: amd64 project_path: cmd/envsubst asset_name: envsubst-Darwin-x86_64 compress_assets: OFF release-darwin-arm64: name: release darwin/arm64 runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: wangyoucao577/go-release-action@v1.53 with: github_token: ${{ secrets.GITHUB_TOKEN }} goos: darwin goarch: arm64 project_path: cmd/envsubst asset_name: envsubst-Darwin-arm64 compress_assets: OFF release-windows: name: release windows runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: wangyoucao577/go-release-action@v1.53 with: github_token: ${{ secrets.GITHUB_TOKEN }} goos: windows goarch: amd64 project_path: cmd/envsubst binary_name: envsubst-windows #release fails if the binary name is the same as the asset name asset_name: envsubst compress_assets: OFF envsubst-1.4.3/.github/workflows/test.yml000066400000000000000000000005521475703313100204750ustar00rootroot00000000000000name: Test on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: go: [ '1.23', '1.24' ] name: Go ${{ matrix.go }} testing steps: - uses: actions/checkout@v2 - name: Set up Go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go }} - name: Test run: go test envsubst-1.4.3/.travis.yml000066400000000000000000000001001475703313100154740ustar00rootroot00000000000000language: go go: - 1.13 - 1.14 - tip scripts: - go test envsubst-1.4.3/LICENSE000066400000000000000000000020661475703313100144050ustar00rootroot00000000000000MIT License Copyright (c) 2017 envsubst contributors 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.4.3/README.md000066400000000000000000000103161475703313100146540ustar00rootroot00000000000000# envsubst [![GoDoc][godoc-img]][godoc-url] [![License][license-image]][license-url] [![Build status][travis-image]][travis-url] [![Github All Releases][releases-image]][releases] > Environment variables substitution for Go. see docs [below](#docs) #### Installation: ##### From binaries Latest stable `envsubst` [prebuilt binaries for 64-bit Linux, or Mac OS X][releases] are available via Github releases. ###### Linux and MacOS ```console curl -L https://github.com/a8m/envsubst/releases/download/v1.2.0/envsubst-`uname -s`-`uname -m` -o envsubst chmod +x envsubst sudo mv envsubst /usr/local/bin ``` ###### Windows Download the latest prebuilt binary from [releases page][releases], or if you have curl installed: ```console curl -L https://github.com/a8m/envsubst/releases/download/v1.2.0/envsubst.exe ``` ##### With go You can install via `go get` (provided you have installed go): ```console go get github.com/a8m/envsubst/cmd/envsubst ``` #### Using via cli ```sh envsubst < input.tmpl > output.text echo 'welcome $HOME ${USER:=a8m}' | envsubst envsubst -help ``` #### Imposing restrictions There are three command line flags with which you can cause the substitution to stop with an error code, should the restriction associated with the flag not be met. This can be handy if you want to avoid creating e.g. configuration files with unset or empty parameters. Setting a `-fail-fast` flag in conjunction with either no-unset or no-empty or both will result in a faster feedback loop, this can be especially useful when running through a large file or byte array input, otherwise a list of errors is returned. The flags and their restrictions are: |__Option__ | __Meaning__ | __Type__ | __Default__ | | ------------| -------------- | ------------ | ------------ | |`-i` | input file | `string \| stdin` | `stdin` |`-o` | output file | `string \| stdout` | `stdout` |`-no-digit` | do not replace variables starting with a digit, e.g. $1 and ${1} | `flag` | `false` |`-no-unset` | fail if a variable is not set | `flag` | `false` |`-no-empty` | fail if a variable is set but empty | `flag` | `false` |`-fail-fast` | fails at first occurrence of an error, if `-no-empty` or `-no-unset` flags were **not** specified this is ignored | `flag` | `false` These flags can be combined to form tighter restrictions. #### Using `envsubst` programmatically ? You can take a look on [`_example/main`](https://github.com/a8m/envsubst/blob/master/_example/main.go) or see the example below. ```go package main import ( "fmt" "github.com/a8m/envsubst" ) func main() { input := "welcome $HOME" str, err := envsubst.String(input) // ... buf, err := envsubst.Bytes([]byte(input)) // ... buf, err := envsubst.ReadFile("filename") } ``` ### Docs > api docs here: [![GoDoc][godoc-img]][godoc-url] |__Expression__ | __Meaning__ | | ----------------- | -------------- | |`${var}` | Value of var (same as `$var`) |`${var-$DEFAULT}` | If var not set, evaluate expression as $DEFAULT |`${var:-$DEFAULT}` | If var not set or is empty, evaluate expression as $DEFAULT |`${var=$DEFAULT}` | If var not set, evaluate expression as $DEFAULT |`${var:=$DEFAULT}` | If var not set or is empty, evaluate expression as $DEFAULT |`${var+$OTHER}` | If var set, evaluate expression as $OTHER, otherwise as empty string |`${var:+$OTHER}` | If var set, evaluate expression as $OTHER, otherwise as empty string |`$$var` | Escape expressions. Result will be `$var`. Most of the rows in this table were taken from [here](http://www.tldp.org/LDP/abs/html/refcards.html#AEN22728) ### See also * `os.ExpandEnv(s string) string` - only supports `$var` and `${var}` notations #### License MIT [releases]: https://github.com/a8m/envsubst/releases [releases-image]: https://img.shields.io/github/downloads/a8m/envsubst/total.svg?style=for-the-badge [godoc-url]: https://godoc.org/github.com/a8m/envsubst [godoc-img]: https://img.shields.io/badge/godoc-reference-blue.svg?style=for-the-badge [license-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge [license-url]: LICENSE [travis-image]: https://img.shields.io/travis/a8m/envsubst.svg?style=for-the-badge [travis-url]: https://travis-ci.org/a8m/envsubst envsubst-1.4.3/_example/000077500000000000000000000000001475703313100151665ustar00rootroot00000000000000envsubst-1.4.3/_example/config.yaml000066400000000000000000000001251475703313100173150ustar00rootroot00000000000000# ... env: ${ENV:=dev} host: ${HOST:=localhost} region: ${REGION:=us-east-1} # ... envsubst-1.4.3/_example/main.go000066400000000000000000000007041475703313100164420ustar00rootroot00000000000000package main import ( "fmt" "log" "github.com/a8m/envsubst" "gopkg.in/yaml.v3" ) type Config struct { Env string Host string Region string } func main() { data, err := envsubst.ReadFile("config.yaml") if err != nil { log.Fatalf("envsubst error: %v", err) } c := new(Config) err = yaml.Unmarshal(data, c) if err != nil { log.Fatalf("yaml error: %v", err) } fmt.Println(c.Env, c.Host, c.Region) // dev, localhost, us-east-1 } envsubst-1.4.3/cmd/000077500000000000000000000000001475703313100141375ustar00rootroot00000000000000envsubst-1.4.3/cmd/envsubst/000077500000000000000000000000001475703313100160105ustar00rootroot00000000000000envsubst-1.4.3/cmd/envsubst/main.go000066400000000000000000000050761475703313100172730ustar00rootroot00000000000000// envsubst command line tool package main import ( "bufio" "flag" "fmt" "io" "os" "github.com/a8m/envsubst/parse" ) var ( input = flag.String("i", "", "") output = flag.String("o", "", "") noDigit = flag.Bool("no-digit", false, "") noUnset = flag.Bool("no-unset", false, "") noEmpty = flag.Bool("no-empty", false, "") failFast = flag.Bool("fail-fast", false, "") ) var usage = `Usage: envsubst [options...] Options: -i Specify file input, otherwise use last argument as input file. If no input file is specified, read from stdin. -o Specify file output. If none is specified, write to stdout. -no-digit Do not replace variables starting with a digit. e.g. $1 and ${1} -no-unset Fail if a variable is not set. -no-empty Fail if a variable is set but empty. -fail-fast Fail on first error otherwise display all failures if restrictions are set. ` func main() { flag.Usage = func() { fmt.Fprint(os.Stderr, fmt.Sprintf(usage)) } flag.Parse() var reader *bufio.Reader if *input != "" { file, err := os.Open(*input) if err != nil { usageAndExit(fmt.Sprintf("Error to open file input: %s.", *input)) } defer file.Close() reader = bufio.NewReader(file) } else { stat, err := os.Stdin.Stat() if err != nil || (stat.Mode()&os.ModeCharDevice) != 0 { usageAndExit("") } reader = bufio.NewReader(os.Stdin) } // Collect input data. var data string for { line, err := reader.ReadString('\n') if err != nil { if err == io.EOF { data += line break } usageAndExit("Failed to read input.") } data += line } var ( err error file *os.File ) if *output != "" { file, err = os.Create(*output) if err != nil { usageAndExit("Error to create the wanted output file.") } } else { file = os.Stdout } // Parse input string parserMode := parse.AllErrors if *failFast { parserMode = parse.Quick } restrictions := &parse.Restrictions{*noUnset, *noEmpty, *noDigit} result, err := (&parse.Parser{Name: "string", Env: os.Environ(), Restrict: restrictions, Mode: parserMode}).Parse(data) if err != nil { errorAndExit(err) } if _, err := file.WriteString(result); err != nil { filename := *output if filename == "" { filename = "STDOUT" } usageAndExit(fmt.Sprintf("Error writing output to: %s.", filename)) } } func usageAndExit(msg string) { if msg != "" { fmt.Fprintf(os.Stderr, msg) fmt.Fprintf(os.Stderr, "\n\n") } flag.Usage() fmt.Fprintf(os.Stderr, "\n") os.Exit(1) } func errorAndExit(e error) { fmt.Fprintf(os.Stderr, "%v\n\n", e.Error()) os.Exit(1) } envsubst-1.4.3/envsubst.go000066400000000000000000000056421475703313100156030ustar00rootroot00000000000000package envsubst import ( "io/ioutil" "os" "github.com/a8m/envsubst/parse" ) // String returns the parsed template string after processing it. // If the parser encounters invalid input, it returns an error describing the failure. func String(s string) (string, error) { return StringRestricted(s, false, false) } // StringRestricted returns the parsed template string after processing it. // If the parser encounters invalid input, or a restriction is violated, it returns // an error describing the failure. // Errors on first failure or returns a collection of failures if failOnFirst is false func StringRestricted(s string, noUnset, noEmpty bool) (string, error) { return StringRestrictedNoDigit(s, noUnset, noEmpty , false) } // Like StringRestricted but additionally allows to ignore env variables which start with a digit. func StringRestrictedNoDigit(s string, noUnset, noEmpty bool, noDigit bool) (string, error) { return parse.New("string", os.Environ(), &parse.Restrictions{noUnset, noEmpty, noDigit}).Parse(s) } // Bytes returns the bytes represented by the parsed template after processing it. // If the parser encounters invalid input, it returns an error describing the failure. func Bytes(b []byte) ([]byte, error) { return BytesRestricted(b, false, false) } // BytesRestricted returns the bytes represented by the parsed template after processing it. // If the parser encounters invalid input, or a restriction is violated, it returns // an error describing the failure. func BytesRestricted(b []byte, noUnset, noEmpty bool) ([]byte, error) { return BytesRestrictedNoDigit(b, noUnset, noEmpty, false) } // Like BytesRestricted but additionally allows to ignore env variables which start with a digit. func BytesRestrictedNoDigit(b []byte, noUnset, noEmpty bool, noDigit bool) ([]byte, error) { s, err := parse.New("bytes", os.Environ(), &parse.Restrictions{noUnset, noEmpty, noDigit}).Parse(string(b)) if err != nil { return nil, err } return []byte(s), nil } // ReadFile call io.ReadFile with the given file name. // If the call to io.ReadFile failed it returns the error; otherwise it will // call envsubst.Bytes with the returned content. func ReadFile(filename string) ([]byte, error) { return ReadFileRestricted(filename, false, false) } // ReadFileRestricted calls io.ReadFile with the given file name. // If the call to io.ReadFile failed it returns the error; otherwise it will // call envsubst.Bytes with the returned content. func ReadFileRestricted(filename string, noUnset, noEmpty bool) ([]byte, error) { return ReadFileRestrictedNoDigit(filename, noUnset, noEmpty, false) } // Like ReadFileRestricted but additionally allows to ignore env variables which start with a digit. func ReadFileRestrictedNoDigit(filename string, noUnset, noEmpty bool, noDigit bool) ([]byte, error) { b, err := ioutil.ReadFile(filename) if err != nil { return nil, err } return BytesRestrictedNoDigit(b, noUnset, noEmpty, noDigit) } envsubst-1.4.3/envsubst_test.go000066400000000000000000000013571475703313100166410ustar00rootroot00000000000000package envsubst import ( "io/ioutil" "os" "testing" ) func init() { os.Setenv("BAR", "bar") } // Basic integration tests. because we already test the // templating processing in envsubst/parse; func TestIntegration(t *testing.T) { input, expected := "foo $BAR", "foo bar" str, err := String(input) if str != expected || err != nil { t.Error("Expect string integration test to pass") } bytes, err := Bytes([]byte(input)) if string(bytes) != expected || err != nil { t.Error("Expect bytes integration test to pass") } bytes, err = ReadFile("testdata/file.tmpl") fexpected, err := ioutil.ReadFile("testdata/file.out") if string(bytes) != string(fexpected) || err != nil { t.Error("Expect ReadFile integration test to pass") } } envsubst-1.4.3/go.mod000066400000000000000000000000501475703313100144750ustar00rootroot00000000000000module github.com/a8m/envsubst go 1.24 envsubst-1.4.3/go.sum000066400000000000000000000000001475703313100145150ustar00rootroot00000000000000envsubst-1.4.3/parse/000077500000000000000000000000001475703313100145065ustar00rootroot00000000000000envsubst-1.4.3/parse/env.go000066400000000000000000000006301475703313100156240ustar00rootroot00000000000000package parse import ( "strings" ) type Env []string func (e Env) Get(name string) string { v, _ := e.Lookup(name) return v } func (e Env) Has(name string) bool { _, ok := e.Lookup(name) return ok } func (e Env) Lookup(name string) (string, bool) { prefix := name + "=" for _, pair := range e { if strings.HasPrefix(pair, prefix) { return pair[len(prefix):], true } } return "", false } envsubst-1.4.3/parse/lex.go000066400000000000000000000146131475703313100156320ustar00rootroot00000000000000package parse import ( "fmt" "strings" "unicode" "unicode/utf8" ) // itemType identifies the type of lex items. type itemType int // Pos represents a byte position in the original input text from which // this template was parsed. type Pos int // item represents a token or text string returned from the scanner. type item struct { typ itemType // The type of this item. pos Pos // The starting position, in bytes, of this item in the input string. val string // The value of this item. } func (i item) String() string { typ := "OP" if t, ok := tokens[i.typ]; ok { typ = t } return fmt.Sprintf("%s: %.40q", typ, i.val) } const ( eof = -1 itemError itemType = iota // error occurred; value is text of error itemEOF itemText // plain text itemPlus // plus('+') itemDash // dash('-') itemEquals // equals itemColonEquals // colon-equals (':=') itemColonDash // colon-dash(':-') itemColonPlus // colon-plus(':+') itemVariable // variable starting with '$', such as '$hello' or '$1' itemLeftDelim // left action delimiter '${' itemRightDelim // right action delimiter '}' ) var tokens = map[itemType]string{ itemEOF: "EOF", itemError: "ERROR", itemText: "TEXT", itemVariable: "VAR", itemLeftDelim: "START EXP", itemRightDelim: "END EXP", } // stateFn represents the state of the lexer as a function that returns the next state. type stateFn func(*lexer) stateFn // lexer holds the state of the scanner type lexer struct { input string // the string being lexed state stateFn // the next lexing function to enter pos Pos // current position in the input start Pos // start position of this item width Pos // width of last rune read from input lastPos Pos // position of most recent item returned by nextItem items chan item // channel of lexed items subsDepth int // depth of substitution noDigit bool // if the lexer skips variables that start with a digit } // next returns the next rune in the input. func (l *lexer) next() rune { if int(l.pos) >= len(l.input) { l.width = 0 return eof } r, w := utf8.DecodeRuneInString(l.input[l.pos:]) l.width = Pos(w) l.pos += l.width return r } // peek returns but does not consume the next rune in the input. func (l *lexer) peek() rune { r := l.next() l.backup() return r } // backup steps back one rune. Can only be called once per call of next. func (l *lexer) backup() { l.pos -= l.width } // emit passes an item back to the client. func (l *lexer) emit(t itemType) { l.items <- item{t, l.start, l.input[l.start:l.pos]} l.lastPos = l.start l.start = l.pos } // ignore skips over the pending input before this point. func (l *lexer) ignore() { l.start = l.pos } // errorf returns an error token and terminates the scan by passing // back a nil pointer that will be the next state, terminating l.nextItem. func (l *lexer) errorf(format string, args ...interface{}) stateFn { l.items <- item{itemError, l.start, fmt.Sprintf(format, args...)} return nil } // nextItem returns the next item from the input. // Called by the parser, not in the lexing goroutine. func (l *lexer) nextItem() item { item := <-l.items return item } // lex creates a new scanner for the input string. func lex(input string, noDigit bool) *lexer { l := &lexer{ input: input, items: make(chan item), noDigit: noDigit, } go l.run() return l } // run runs the state machine for the lexer. func (l *lexer) run() { for l.state = lexText; l.state != nil; { l.state = l.state(l) } close(l.items) } // lexText scans until encountering with "$" or an opening action delimiter, "${". func lexText(l *lexer) stateFn { Loop: for { switch r := l.next(); r { case '$': l.pos-- // emit the text we've found until here, if any. if l.pos > l.start { l.emit(itemText) } l.pos++ switch r := l.peek(); { case l.noDigit && unicode.IsDigit(r): // ignore variable starting with digit like $1. l.next() l.emit(itemText) case r == '$': // ignore the previous '$'. l.ignore() l.next() l.emit(itemText) case r == '{': l.next() r2 := l.peek() if l.noDigit && unicode.IsDigit(r2) { // ignore variable starting with digit like ${1}. l.next() l.emit(itemText) break } l.subsDepth++ l.emit(itemLeftDelim) return lexSubstitutionOperator case isAlphaNumeric(r): return lexVariable } case eof: break Loop } } // Correctly reached EOF. if l.pos > l.start { l.emit(itemText) } l.emit(itemEOF) return nil } // lexVariable scans a Variable: $Alphanumeric. // The $ has been scanned. func lexVariable(l *lexer) stateFn { var r rune for { r = l.next() if !isAlphaNumeric(r) { l.backup() break } } if v := l.input[l.start:l.pos]; v == "_" || v == "$_" { return lexText } l.emit(itemVariable) if l.subsDepth > 0 { return lexSubstitutionOperator } return lexText } // lexSubstitutionOperator scans a starting substitution operator (if any) and continues with lexSubstitution func lexSubstitutionOperator(l *lexer) stateFn { switch r := l.next(); { case r == '}': l.subsDepth-- l.emit(itemRightDelim) return lexText case r == eof || isEndOfLine(r): return l.errorf("closing brace expected") case isAlphaNumeric(r) && strings.HasPrefix(l.input[l.lastPos:], "${"): return lexVariable case r == '+': l.emit(itemPlus) case r == '-': l.emit(itemDash) case r == '=': l.emit(itemEquals) case r == ':': switch l.next() { case '-': l.emit(itemColonDash) case '=': l.emit(itemColonEquals) case '+': l.emit(itemColonPlus) } } return lexSubstitution } // lexSubstitution scans the elements inside substitution delimiters. func lexSubstitution(l *lexer) stateFn { switch r := l.next(); { case r == '}': l.subsDepth-- l.emit(itemRightDelim) return lexText case r == eof || isEndOfLine(r): return l.errorf("closing brace expected") case isAlphaNumeric(r) && strings.HasPrefix(l.input[l.lastPos:], "${"): fallthrough case r == '$': return lexVariable default: l.emit(itemText) } return lexSubstitution } // isEndOfLine reports whether r is an end-of-line character. func isEndOfLine(r rune) bool { return r == '\r' || r == '\n' } // isAlphaNumeric reports whether r is an alphabetic, digit, or underscore. func isAlphaNumeric(r rune) bool { return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r) } envsubst-1.4.3/parse/lex_test.go000066400000000000000000000074321475703313100166720ustar00rootroot00000000000000package parse import ( "strings" "testing" ) type lexTest struct { name string input string items []item } var ( tEOF = item{itemEOF, 0, ""} tPlus = item{itemPlus, 0, ""} tDash = item{itemDash, 0, "-"} tEquals = item{itemEquals, 0, "="} tColEquals = item{itemColonEquals, 0, ":="} tColDash = item{itemColonDash, 0, ":-"} tColPlus = item{itemColonPlus, 0, ":+"} tLeft = item{itemLeftDelim, 0, "${"} tRight = item{itemRightDelim, 0, "}"} ) var lexTests = []lexTest{ {"empty", "", []item{tEOF}}, {"text", "hello", []item{ {itemText, 0, "hello"}, tEOF, }}, {"var", "$hello", []item{ {itemVariable, 0, "$hello"}, tEOF, }}, {"single char var", "${A}", []item{ tLeft, {itemVariable, 0, "A"}, tRight, tEOF, }}, {"2 vars", "$hello $world", []item{ {itemVariable, 0, "$hello"}, {itemText, 0, " "}, {itemVariable, 0, "$world"}, tEOF, }}, {"substitution-1", "bar ${BAR}", []item{ {itemText, 0, "bar "}, tLeft, {itemVariable, 0, "BAR"}, tRight, tEOF, }}, {"substitution-2", "bar ${BAR:=baz}", []item{ {itemText, 0, "bar "}, tLeft, {itemVariable, 0, "BAR"}, tColEquals, {itemText, 0, "b"}, {itemText, 0, "a"}, {itemText, 0, "z"}, tRight, tEOF, }}, {"substitution-3", "bar ${BAR:=$BAZ}", []item{ {itemText, 0, "bar "}, tLeft, {itemVariable, 0, "BAR"}, tColEquals, {itemVariable, 0, "$BAZ"}, tRight, tEOF, }}, {"substitution-4", "bar ${BAR:=$BAZ} foo", []item{ {itemText, 0, "bar "}, tLeft, {itemVariable, 0, "BAR"}, tColEquals, {itemVariable, 0, "$BAZ"}, tRight, {itemText, 0, " foo"}, tEOF, }}, {"substitution-leading-dash-1", "bar ${BAR:--1} foo", []item{ {itemText, 0, "bar "}, tLeft, {itemVariable, 0, "BAR"}, tColDash, {itemText, 0, "-"}, {itemText, 0, "1"}, tRight, {itemText, 0, " foo"}, tEOF, }}, {"substitution-leading-dash-2", "bar ${BAR:=-1} foo", []item{ {itemText, 0, "bar "}, tLeft, {itemVariable, 0, "BAR"}, tColEquals, {itemText, 0, "-"}, {itemText, 0, "1"}, tRight, {itemText, 0, " foo"}, tEOF, }}, {"closing brace error", "hello-${world", []item{ {itemText, 0, "hello-"}, tLeft, {itemVariable, 0, "world"}, {itemError, 0, "closing brace expected"}, }}, {"escaping $$var", "hello $$HOME", []item{ {itemText, 0, "hello "}, {itemText, 7, "$"}, {itemText, 8, "HOME"}, tEOF, }}, {"escaping $${subst}", "hello $${HOME}", []item{ {itemText, 0, "hello "}, {itemText, 7, "$"}, {itemText, 8, "{HOME}"}, tEOF, }}, {"no digit $1", "hello $1", []item{ {itemText, 0, "hello "}, {itemText, 7, "$1"}, tEOF, }}, {"no digit $1ABC", "hello $1ABC", []item{ {itemText, 0, "hello "}, {itemText, 7, "$1"}, {itemText, 9, "ABC"}, tEOF, }}, {"no digit ${2}", "hello ${2}", []item{ {itemText, 0, "hello "}, {itemText, 7, "${2"}, {itemText, 10, "}"}, tEOF, }}, {"no digit ${2ABC}", "hello ${2ABC}", []item{ {itemText, 0, "hello "}, {itemText, 7, "${2"}, {itemText, 10, "ABC}"}, tEOF, }}, } func TestLex(t *testing.T) { for _, test := range lexTests { items := collect(&test) if !equal(items, test.items, false) { t.Errorf("%s:\ninput\n\t%q\ngot\n\t%+v\nexpected\n\t%v", test.name, test.input, items, test.items) } } } // collect gathers the emitted items into a slice. func collect(t *lexTest) (items []item) { noDigit := strings.HasPrefix(t.name, "no digit") l := lex(t.input, noDigit) for { item := l.nextItem() items = append(items, item) if item.typ == itemEOF || item.typ == itemError { break } } return } func equal(i1, i2 []item, checkPos bool) bool { if len(i1) != len(i2) { return false } for k := range i1 { if i1[k].typ != i2[k].typ { return false } if i1[k].val != i2[k].val { return false } if checkPos && i1[k].pos != i2[k].pos { return false } } return true } envsubst-1.4.3/parse/node.go000066400000000000000000000041101475703313100157560ustar00rootroot00000000000000package parse import ( "fmt" ) type Node interface { Type() NodeType String() (string, error) } // NodeType identifies the type of a node. type NodeType int // Type returns itself and provides an easy default implementation // for embedding in a Node. Embedded in all non-trivial Nodes. func (t NodeType) Type() NodeType { return t } const ( NodeText NodeType = iota NodeSubstitution NodeVariable ) type TextNode struct { NodeType Text string } func NewText(text string) *TextNode { return &TextNode{NodeText, text} } func (t *TextNode) String() (string, error) { return t.Text, nil } type VariableNode struct { NodeType Ident string Env Env Restrict *Restrictions } func NewVariable(ident string, env Env, restrict *Restrictions) *VariableNode { return &VariableNode{NodeVariable, ident, env, restrict} } func (t *VariableNode) String() (string, error) { if err := t.validateNoUnset(); err != nil { return "", err } value := t.Env.Get(t.Ident) if err := t.validateNoEmpty(value); err != nil { return "", err } return value, nil } func (t *VariableNode) isSet() bool { return t.Env.Has(t.Ident) } func (t *VariableNode) validateNoUnset() error { if t.Restrict.NoUnset && !t.isSet() { return fmt.Errorf("variable ${%s} not set", t.Ident) } return nil } func (t *VariableNode) validateNoEmpty(value string) error { if t.Restrict.NoEmpty && value == "" && t.isSet() { return fmt.Errorf("variable ${%s} set but empty", t.Ident) } return nil } type SubstitutionNode struct { NodeType ExpType itemType Variable *VariableNode Default Node // Default could be variable or text } func (t *SubstitutionNode) String() (string, error) { if t.ExpType >= itemPlus && t.Default != nil { switch t.ExpType { case itemColonDash, itemColonEquals: if s, _ := t.Variable.String(); s != "" { return s, nil } return t.Default.String() case itemPlus, itemColonPlus: if t.Variable.isSet() { return t.Default.String() } return "", nil default: if !t.Variable.isSet() { return t.Default.String() } } } return t.Variable.String() } envsubst-1.4.3/parse/parse.go000066400000000000000000000077321475703313100161600ustar00rootroot00000000000000// Most of the code in this package taken from golang/text/template/parse package parse import ( "errors" "strings" ) // A mode value is a set of flags (or 0). They control parser behavior. type Mode int // Mode for parser behaviour const ( Quick Mode = iota // stop parsing after first error encountered and return AllErrors // report all errors ) // The restrictions option controls the parsing restriction. type Restrictions struct { NoUnset bool NoEmpty bool NoDigit bool } // Restrictions specifier var ( Relaxed = &Restrictions{false, false, false} NoEmpty = &Restrictions{false, true, false} NoUnset = &Restrictions{true, false, false} Strict = &Restrictions{true, true, false} ) // Parser type initializer type Parser struct { Name string // name of the processing template Env Env Restrict *Restrictions Mode Mode // parsing state; lex *lexer token [3]item // three-token lookahead peekCount int nodes []Node } // New allocates a new Parser with the given name. func New(name string, env []string, r *Restrictions) *Parser { return &Parser{ Name: name, Env: Env(env), Restrict: r, } } // Parse parses the given string. func (p *Parser) Parse(text string) (string, error) { p.lex = lex(text, p.Restrict.NoDigit) // Build internal array of all unset or empty vars here var errs []error // clean parse state p.nodes = make([]Node, 0) p.peekCount = 0 if err := p.parse(); err != nil { switch p.Mode { case Quick: return "", err case AllErrors: errs = append(errs, err) } } var out string for _, node := range p.nodes { s, err := node.String() if err != nil { switch p.Mode { case Quick: return "", err case AllErrors: errs = append(errs, err) } } out += s } if len(errs) > 0 { var b strings.Builder for i, err := range errs { if i > 0 { b.WriteByte('\n') } b.WriteString(err.Error()) } return "", errors.New(b.String()) } return out, nil } // parse is the top-level parser for the template. // It runs to EOF and return an error if something isn't right. func (p *Parser) parse() error { Loop: for { switch t := p.next(); t.typ { case itemEOF: break Loop case itemError: return p.errorf(t.val) case itemVariable: varNode := NewVariable(strings.TrimPrefix(t.val, "$"), p.Env, p.Restrict) p.nodes = append(p.nodes, varNode) case itemLeftDelim: if p.peek().typ == itemVariable { n, err := p.action() if err != nil { return err } p.nodes = append(p.nodes, n) continue } fallthrough default: textNode := NewText(t.val) p.nodes = append(p.nodes, textNode) } } return nil } // Parse substitution. first item is a variable. func (p *Parser) action() (Node, error) { var expType itemType var defaultNode Node varNode := NewVariable(p.next().val, p.Env, p.Restrict) Loop: for { switch t := p.next(); t.typ { case itemRightDelim: break Loop case itemError: return nil, p.errorf(t.val) case itemVariable: defaultNode = NewVariable(strings.TrimPrefix(t.val, "$"), p.Env, p.Restrict) case itemText: n := NewText(t.val) Text: for { switch p.peek().typ { case itemRightDelim, itemError, itemEOF: break Text default: // patch to accept all kind of chars n.Text += p.next().val } } defaultNode = n default: expType = t.typ } } return &SubstitutionNode{NodeSubstitution, expType, varNode, defaultNode}, nil } func (p *Parser) errorf(s string) error { return errors.New(s) } // next returns the next token. func (p *Parser) next() item { if p.peekCount > 0 { p.peekCount-- } else { p.token[0] = p.lex.nextItem() } return p.token[p.peekCount] } // backup backs the input stream up one token. func (p *Parser) backup() { p.peekCount++ } // peek returns but does not consume the next token. func (p *Parser) peek() item { if p.peekCount > 0 { return p.token[p.peekCount-1] } p.peekCount = 1 p.token[0] = p.lex.nextItem() return p.token[0] } envsubst-1.4.3/parse/parse_test.go000066400000000000000000000144451475703313100172160ustar00rootroot00000000000000package parse import ( "testing" ) var FakeEnv = []string{ "BAR=bar", "FOO=foo", "EMPTY=", "ALSO_EMPTY=", "A=AAA", } type mode int const ( relaxed mode = iota noUnset noEmpty strict ) var restrict = map[mode]*Restrictions{ relaxed: Relaxed, noUnset: NoUnset, noEmpty: NoEmpty, strict: Strict, } var errNone = map[mode]bool{} var errUnset = map[mode]bool{noUnset: true, strict: true} var errEmpty = map[mode]bool{noEmpty: true, strict: true} var errAll = map[mode]bool{relaxed: true, noUnset: true, noEmpty: true, strict: true} var errAllFull = map[mode]bool{relaxed: true, noUnset: true, noEmpty: true, strict: true} type parseTest struct { name string input string expected string hasErr map[mode]bool } var parseTests = []parseTest{ {"empty", "", "", errNone}, {"env only", "$BAR", "bar", errNone}, {"with text", "$BAR baz", "bar baz", errNone}, {"concatenated", "$BAR$FOO", "barfoo", errNone}, {"2 env var", "$BAR - $FOO", "bar - foo", errNone}, {"invalid var", "$_ bar", "$_ bar", errNone}, {"invalid subst var", "${_} bar", "${_} bar", errNone}, {"value of $var", "${BAR}baz", "barbaz", errNone}, {"$var not set -", "${NOTSET-$BAR}", "bar", errNone}, {"$var not set =", "${NOTSET=$BAR}", "bar", errNone}, {"$var set but empty -", "${EMPTY-$BAR}", "", errEmpty}, {"$var set but empty =", "${EMPTY=$BAR}", "", errEmpty}, {"$var not set or empty :-", "${EMPTY:-$BAR}", "bar", errNone}, {"$var not set or empty :=", "${EMPTY:=$BAR}", "bar", errNone}, {"if $var set evaluate expression as $other +", "${EMPTY+hello}", "hello", errNone}, {"if $var set evaluate expression as $other :+", "${EMPTY:+hello}", "hello", errNone}, {"if $var not set, use empty string +", "${NOTSET+hello}", "", errNone}, {"if $var not set, use empty string :+", "${NOTSET:+hello}", "", errNone}, {"multi line string", "hello $BAR\nhello ${EMPTY:=$FOO}", "hello bar\nhello foo", errNone}, {"issue #1", "${hello:=wo_rld} ${foo:=bar_baz}", "wo_rld bar_baz", errNone}, {"issue #2", "name: ${NAME:=foo_qux}, key: ${EMPTY:=baz_bar}", "name: foo_qux, key: baz_bar", errNone}, {"gh-issue-8", "prop=${HOME_URL-http://localhost:8080}", "prop=http://localhost:8080", errNone}, // operators as leading values {"gh-issue-41-1", "${NOTSET--1}", "-1", errNone}, {"gh-issue-41-2", "${NOTSET:--1}", "-1", errNone}, {"gh-issue-41-3", "${NOTSET=-1}", "-1", errNone}, {"gh-issue-41-4", "${NOTSET:==1}", "=1", errNone}, // single letter {"gh-issue-43-1", "${A}", "AAA", errNone}, // bad substitution {"closing brace expected", "hello ${", "", errAll}, // test specifically for failure modes {"$var not set", "${NOTSET}", "", errUnset}, {"$var set to empty", "${EMPTY}", "", errEmpty}, // restrictions for plain variables without braces {"gh-issue-9", "$NOTSET", "", errUnset}, {"gh-issue-9", "$EMPTY", "", errEmpty}, {"$var and $DEFAULT not set -", "${NOTSET-$ALSO_NOTSET}", "", errUnset}, {"$var and $DEFAULT not set :-", "${NOTSET:-$ALSO_NOTSET}", "", errUnset}, {"$var and $DEFAULT not set =", "${NOTSET=$ALSO_NOTSET}", "", errUnset}, {"$var and $DEFAULT not set :=", "${NOTSET:=$ALSO_NOTSET}", "", errUnset}, {"$var and $OTHER not set +", "${NOTSET+$ALSO_NOTSET}", "", errNone}, {"$var and $OTHER not set :+", "${NOTSET:+$ALSO_NOTSET}", "", errNone}, {"$var empty and $DEFAULT not set -", "${EMPTY-$NOTSET}", "", errEmpty}, {"$var empty and $DEFAULT not set :-", "${EMPTY:-$NOTSET}", "", errUnset}, {"$var empty and $DEFAULT not set =", "${EMPTY=$NOTSET}", "", errEmpty}, {"$var empty and $DEFAULT not set :=", "${EMPTY:=$NOTSET}", "", errUnset}, {"$var empty and $OTHER not set +", "${EMPTY+$NOTSET}", "", errUnset}, {"$var empty and $OTHER not set :+", "${EMPTY:+$NOTSET}", "", errUnset}, {"$var not set and $DEFAULT empty -", "${NOTSET-$EMPTY}", "", errEmpty}, {"$var not set and $DEFAULT empty :-", "${NOTSET:-$EMPTY}", "", errEmpty}, {"$var not set and $DEFAULT empty =", "${NOTSET=$EMPTY}", "", errEmpty}, {"$var not set and $DEFAULT empty :=", "${NOTSET:=$EMPTY}", "", errEmpty}, {"$var not set and $OTHER empty +", "${NOTSET+$EMPTY}", "", errNone}, {"$var not set and $OTHER empty :+", "${NOTSET:+$EMPTY}", "", errNone}, {"$var and $DEFAULT empty -", "${EMPTY-$ALSO_EMPTY}", "", errEmpty}, {"$var and $DEFAULT empty :-", "${EMPTY:-$ALSO_EMPTY}", "", errEmpty}, {"$var and $DEFAULT empty =", "${EMPTY=$ALSO_EMPTY}", "", errEmpty}, {"$var and $DEFAULT empty :=", "${EMPTY:=$ALSO_EMPTY}", "", errEmpty}, {"$var and $OTHER empty +", "${EMPTY+$ALSO_EMPTY}", "", errEmpty}, {"$var and $OTHER empty :+", "${EMPTY:+$ALSO_EMPTY}", "", errEmpty}, // escaping. {"escape $$var", "FOO $$BAR BAZ", "FOO $BAR BAZ", errNone}, {"escape $${subst}", "FOO $${BAR} BAZ", "FOO ${BAR} BAZ", errNone}, {"escape $$$var", "$$$BAR", "$bar", errNone}, {"escape $$${subst}", "$$${BAZ:-baz}", "$baz", errNone}, } var negativeParseTests = []parseTest{ {"$NOTSET and EMPTY are displayed as in full error output", "${NOTSET} and $EMPTY", "variable ${NOTSET} not set\nvariable ${EMPTY} set but empty", errAllFull}, } func TestParse(t *testing.T) { doTest(t, relaxed) } func TestParseNoUnset(t *testing.T) { doTest(t, noUnset) } func TestParseNoEmpty(t *testing.T) { doTest(t, noEmpty) } func TestParseStrict(t *testing.T) { doTest(t, strict) } func TestParseStrictNoFailFast(t *testing.T) { doNegativeAssertTest(t, strict) } func doTest(t *testing.T, m mode) { for _, test := range parseTests { result, err := New(test.name, FakeEnv, restrict[m]).Parse(test.input) hasErr := err != nil if hasErr != test.hasErr[m] { t.Errorf("%s=(error): got\n\t%v\nexpected\n\t%v\ninput: %s\nresult: %s\nerror: %v", test.name, hasErr, test.hasErr[m], test.input, result, err) } if result != test.expected { t.Errorf("%s=(%q): got\n\t%v\nexpected\n\t%v", test.name, test.input, result, test.expected) } } } func doNegativeAssertTest(t *testing.T, m mode) { for _, test := range negativeParseTests { result, err := (*&Parser{Name: test.name, Env: FakeEnv, Restrict: restrict[m], Mode: AllErrors}).Parse(test.input) hasErr := err != nil if hasErr != test.hasErr[m] { t.Errorf("%s=(error): got\n\t%v\nexpected\n\t%v\ninput: %s\nresult: %s\nerror: %v", test.name, hasErr, test.hasErr[m], test.input, result, err) } if err.Error() != test.expected { t.Errorf("%s=(%q): got\n\t%v\nexpected\n\t%v", test.name, test.input, err.Error(), test.expected) } } } envsubst-1.4.3/testdata/000077500000000000000000000000001475703313100152055ustar00rootroot00000000000000envsubst-1.4.3/testdata/file.out000066400000000000000000000001031475703313100166470ustar00rootroot00000000000000foo: bar baz: baz env: dev uri: http://bar.com/foo host: localhost envsubst-1.4.3/testdata/file.tmpl000066400000000000000000000001451475703313100170220ustar00rootroot00000000000000foo: $BAR baz: ${FOO:=baz} env: ${ENV:-dev} uri: http://${BAR:=$BAZ}.com/foo host: ${BAR:+localhost}