pax_global_header 0000666 0000000 0000000 00000000064 14757033131 0014516 g ustar 00root root 0000000 0000000 52 comment=2aa6422a5466ee701933dd838d9d36aaf16a0b32
envsubst-1.4.3/ 0000775 0000000 0000000 00000000000 14757033131 0013374 5 ustar 00root root 0000000 0000000 envsubst-1.4.3/.github/ 0000775 0000000 0000000 00000000000 14757033131 0014734 5 ustar 00root root 0000000 0000000 envsubst-1.4.3/.github/workflows/ 0000775 0000000 0000000 00000000000 14757033131 0016771 5 ustar 00root root 0000000 0000000 envsubst-1.4.3/.github/workflows/binaries.yml 0000664 0000000 0000000 00000004111 14757033131 0021305 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000000552 14757033131 0020475 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000000100 14757033131 0015474 0 ustar 00root root 0000000 0000000 language: go
go:
- 1.13
- 1.14
- tip
scripts:
- go test
envsubst-1.4.3/LICENSE 0000664 0000000 0000000 00000002066 14757033131 0014405 0 ustar 00root root 0000000 0000000 MIT 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.md 0000664 0000000 0000000 00000010316 14757033131 0014654 0 ustar 00root root 0000000 0000000 # 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/ 0000775 0000000 0000000 00000000000 14757033131 0015166 5 ustar 00root root 0000000 0000000 envsubst-1.4.3/_example/config.yaml 0000664 0000000 0000000 00000000125 14757033131 0017315 0 ustar 00root root 0000000 0000000 # ...
env: ${ENV:=dev}
host: ${HOST:=localhost}
region: ${REGION:=us-east-1}
# ...
envsubst-1.4.3/_example/main.go 0000664 0000000 0000000 00000000704 14757033131 0016442 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14757033131 0014137 5 ustar 00root root 0000000 0000000 envsubst-1.4.3/cmd/envsubst/ 0000775 0000000 0000000 00000000000 14757033131 0016010 5 ustar 00root root 0000000 0000000 envsubst-1.4.3/cmd/envsubst/main.go 0000664 0000000 0000000 00000005076 14757033131 0017273 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000005642 14757033131 0015603 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001357 14757033131 0016641 0 ustar 00root root 0000000 0000000 package 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.mod 0000664 0000000 0000000 00000000050 14757033131 0014475 0 ustar 00root root 0000000 0000000 module github.com/a8m/envsubst
go 1.24
envsubst-1.4.3/go.sum 0000664 0000000 0000000 00000000000 14757033131 0014515 0 ustar 00root root 0000000 0000000 envsubst-1.4.3/parse/ 0000775 0000000 0000000 00000000000 14757033131 0014506 5 ustar 00root root 0000000 0000000 envsubst-1.4.3/parse/env.go 0000664 0000000 0000000 00000000630 14757033131 0015624 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000014613 14757033131 0015632 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000007432 14757033131 0016672 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000004110 14757033131 0015756 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000007732 14757033131 0016160 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000014445 14757033131 0017216 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14757033131 0015205 5 ustar 00root root 0000000 0000000 envsubst-1.4.3/testdata/file.out 0000664 0000000 0000000 00000000103 14757033131 0016647 0 ustar 00root root 0000000 0000000 foo: bar
baz: baz
env: dev
uri: http://bar.com/foo
host: localhost
envsubst-1.4.3/testdata/file.tmpl 0000664 0000000 0000000 00000000145 14757033131 0017022 0 ustar 00root root 0000000 0000000 foo: $BAR
baz: ${FOO:=baz}
env: ${ENV:-dev}
uri: http://${BAR:=$BAZ}.com/foo
host: ${BAR:+localhost}