diff --git a/README.md b/README.md index 02412c4..52e6463 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ The flags and their restrictions are: | ------------| -------------- | ------------ | ------------ | |`-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 occurence of an error, if `-no-empty` or `-no-unset` flags were **not** specified this is ignored | `flag` | `false` diff --git a/cmd/envsubst/main.go b/cmd/envsubst/main.go index add36f6..8c0a003 100644 --- a/cmd/envsubst/main.go +++ b/cmd/envsubst/main.go @@ -14,6 +14,7 @@ import ( 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, "") @@ -24,6 +25,7 @@ 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. @@ -79,7 +81,7 @@ func main() { if *failFast { parserMode = parse.Quick } - restrictions := &parse.Restrictions{*noUnset, *noEmpty} + 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) diff --git a/envsubst.go b/envsubst.go index bbab7a6..22b5bb8 100644 --- a/envsubst.go +++ b/envsubst.go @@ -18,8 +18,13 @@ func String(s string) (string, error) { // 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}).Parse(s) + &parse.Restrictions{noUnset, noEmpty, noDigit}).Parse(s) } // Bytes returns the bytes represented by the parsed template after processing it. @@ -32,8 +37,13 @@ func Bytes(b []byte) ([]byte, error) { // 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}).Parse(string(b)) + &parse.Restrictions{noUnset, noEmpty, noDigit}).Parse(string(b)) if err != nil { return nil, err } @@ -51,9 +61,14 @@ func ReadFile(filename string) ([]byte, error) { // 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 BytesRestricted(b, noUnset, noEmpty) + return BytesRestrictedNoDigit(b, noUnset, noEmpty, noDigit) } diff --git a/parse/lex.go b/parse/lex.go index f7fd4df..263b528 100644 --- a/parse/lex.go +++ b/parse/lex.go @@ -67,6 +67,7 @@ type lexer struct { 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. @@ -120,10 +121,11 @@ func (l *lexer) nextItem() item { } // lex creates a new scanner for the input string. -func lex(input string) *lexer { +func lex(input string, noDigit bool) *lexer { l := &lexer{ input: input, items: make(chan item), + noDigit: noDigit, } go l.run() return l @@ -150,6 +152,10 @@ Loop: } 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() @@ -157,6 +163,13 @@ Loop: 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 lexSubstitution diff --git a/parse/lex_test.go b/parse/lex_test.go index b8f8ed5..86c4f21 100644 --- a/parse/lex_test.go +++ b/parse/lex_test.go @@ -2,6 +2,7 @@ package parse import ( "testing" + "strings" ) type lexTest struct { @@ -93,6 +94,29 @@ var lexTests = []lexTest{ {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) { @@ -106,7 +130,8 @@ func TestLex(t *testing.T) { // collect gathers the emitted items into a slice. func collect(t *lexTest) (items []item) { - l := lex(t.input) + noDigit := strings.HasPrefix(t.name, "no digit") + l := lex(t.input, noDigit) for { item := l.nextItem() items = append(items, item) diff --git a/parse/parse.go b/parse/parse.go index 0ce4b0c..8b9b427 100644 --- a/parse/parse.go +++ b/parse/parse.go @@ -19,14 +19,15 @@ const ( type Restrictions struct { NoUnset bool NoEmpty bool + NoDigit bool } // Restrictions specifier var ( - Relaxed = &Restrictions{false, false} - NoEmpty = &Restrictions{false, true} - NoUnset = &Restrictions{true, false} - Strict = &Restrictions{true, true} + Relaxed = &Restrictions{false, false, false} + NoEmpty = &Restrictions{false, true, false} + NoUnset = &Restrictions{true, false, false} + Strict = &Restrictions{true, true, false} ) // Parser type initializer @@ -53,7 +54,7 @@ func New(name string, env []string, r *Restrictions) *Parser { // Parse parses the given string. func (p *Parser) Parse(text string) (string, error) { - p.lex = lex(text) + p.lex = lex(text, p.Restrict.NoDigit) // Build internal array of all unset or empty vars here var errs []error // clean parse state