Envsubst params (#1147)

* Can give envsubst optional arguments
This commit is contained in:
Mike Farah 2022-03-20 10:56:50 +11:00 committed by GitHub
parent 1a964c5055
commit 78e9cc7998
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 238 additions and 15 deletions

View File

@ -8,6 +8,17 @@ There are three operators:
- `envsubst` which you pipe strings into and it interpolates environment variables in strings using [envsubst](https://github.com/a8m/envsubst). - `envsubst` which you pipe strings into and it interpolates environment variables in strings using [envsubst](https://github.com/a8m/envsubst).
## EnvSubst Options
You can optionally pass envsubst any of the following options:
- nu: NoUnset, this will fail if there are any referenced variables that are not set
- ne: NoEmpty, this will fail if there are any referenced variables that are empty
- ff: FailFast, this will abort on the first failure (rather than collect all the errors)
E.g:
`envsubst(ne, ff)` will fail on the first empty variable.
See [Imposing Restrictions](https://github.com/a8m/envsubst#imposing-restrictions) in the `envsubst` documentation for more information, and below for examples.
## Tip ## Tip
To replace environment variables across all values in a document, `envsubst` can be used with the recursive descent operator To replace environment variables across all values in a document, `envsubst` can be used with the recursive descent operator
as follows: as follows:
@ -133,23 +144,83 @@ the cat meows
## Replace strings with envsubst, missing variables ## Replace strings with envsubst, missing variables
Running Running
```bash ```bash
myenv="cat" yq --null-input '"the ${myenvnonexisting} meows" | envsubst' yq --null-input '"the ${myenvnonexisting} meows" | envsubst'
``` ```
will output will output
```yaml ```yaml
the meows the meows
``` ```
## Replace strings with envsubst(nu), missing variables
(nu) not unset, will fail if there are unset (missing) variables
Running
```bash
yq --null-input '"the ${myenvnonexisting} meows" | envsubst(nu)'
```
will output
```bash
Error: variable ${myenvnonexisting} not set
```
## Replace strings with envsubst(ne), missing variables
(ne) not empty, only validates set variables
Running
```bash
yq --null-input '"the ${myenvnonexisting} meows" | envsubst(ne)'
```
will output
```yaml
the meows
```
## Replace strings with envsubst(ne), empty variable
(ne) not empty, will fail if a references variable is empty
Running
```bash
myenv="" yq --null-input '"the ${myenv} meows" | envsubst(ne)'
```
will output
```bash
Error: variable ${myenv} set but empty
```
## Replace strings with envsubst, missing variables with defaults ## Replace strings with envsubst, missing variables with defaults
Running Running
```bash ```bash
myenv="cat" yq --null-input '"the ${myenvnonexisting-dog} meows" | envsubst' yq --null-input '"the ${myenvnonexisting-dog} meows" | envsubst'
``` ```
will output will output
```yaml ```yaml
the dog meows the dog meows
``` ```
## Replace strings with envsubst(nu), missing variables with defaults
Having a default specified skips over the missing variable.
Running
```bash
yq --null-input '"the ${myenvnonexisting-dog} meows" | envsubst(nu)'
```
will output
```yaml
the dog meows
```
## Replace strings with envsubst(ne), missing variables with defaults
Fails, because the variable is explicitly set to blank.
Running
```bash
myEmptyEnv="" yq --null-input '"the ${myEmptyEnv-dog} meows" | envsubst(ne)'
```
will output
```bash
Error: variable ${myEmptyEnv} set but empty
```
## Replace string environment variable in document ## Replace string environment variable in document
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
@ -164,3 +235,26 @@ will output
v: cat meow v: cat meow
``` ```
## (Default) Return all envsubst errors
By default, all errors are returned at once.
Running
```bash
yq --null-input '"the ${notThere} ${alsoNotThere}" | envsubst(nu)'
```
will output
```bash
Error: variable ${notThere} not set
variable ${alsoNotThere} not set
```
## Fail fast, return the first envsubst error (and abort)
Running
```bash
yq --null-input '"the ${notThere} ${alsoNotThere}" | envsubst(nu,ff)'
```
will output
```bash
Error: variable ${notThere} not set
```

View File

@ -8,6 +8,17 @@ There are three operators:
- `envsubst` which you pipe strings into and it interpolates environment variables in strings using [envsubst](https://github.com/a8m/envsubst). - `envsubst` which you pipe strings into and it interpolates environment variables in strings using [envsubst](https://github.com/a8m/envsubst).
## EnvSubst Options
You can optionally pass envsubst any of the following options:
- nu: NoUnset, this will fail if there are any referenced variables that are not set
- ne: NoEmpty, this will fail if there are any referenced variables that are empty
- ff: FailFast, this will abort on the first failure (rather than collect all the errors)
E.g:
`envsubst(ne, ff)` will fail on the first empty variable.
See [Imposing Restrictions](https://github.com/a8m/envsubst#imposing-restrictions) in the `envsubst` documentation for more information, and below for examples.
## Tip ## Tip
To replace environment variables across all values in a document, `envsubst` can be used with the recursive descent operator To replace environment variables across all values in a document, `envsubst` can be used with the recursive descent operator
as follows: as follows:

View File

@ -15,6 +15,26 @@ var pathTests = []struct {
expectedTokens []interface{} expectedTokens []interface{}
expectedPostFix []interface{} expectedPostFix []interface{}
}{ }{
{
`envsubst(ne)`,
append(make([]interface{}, 0), "ENVSUBST_NO_EMPTY"),
append(make([]interface{}, 0), "ENVSUBST_NO_EMPTY"),
},
{
`envsubst(nu)`,
append(make([]interface{}, 0), "ENVSUBST_NO_UNSET"),
append(make([]interface{}, 0), "ENVSUBST_NO_UNSET"),
},
{
`envsubst(nu, ne)`,
append(make([]interface{}, 0), "ENVSUBST_NO_EMPTY_NO_UNSET"),
append(make([]interface{}, 0), "ENVSUBST_NO_EMPTY_NO_UNSET"),
},
{
`envsubst(ne, nu)`,
append(make([]interface{}, 0), "ENVSUBST_NO_EMPTY_NO_UNSET"),
append(make([]interface{}, 0), "ENVSUBST_NO_EMPTY_NO_UNSET"),
},
{ {
`[.a, .b]`, `[.a, .b]`,
append(make([]interface{}, 0), "[", "a", "UNION", "b", "]"), append(make([]interface{}, 0), "[", "a", "UNION", "b", "]"),

View File

@ -135,7 +135,18 @@ func opTokenWithPrefs(op *operationType, assignOpType *operationType, preference
} }
} }
func extractNumberParamter(value string) (int, error) { func hasOptionParameter(value string, option string) bool {
parameterParser := regexp.MustCompile(`.*\([^\)]*\)`)
matches := parameterParser.FindStringSubmatch(value)
if len(matches) == 0 {
return false
}
parameterString := matches[0]
optionParser := regexp.MustCompile(fmt.Sprintf("\\b%v\\b", option))
return len(optionParser.FindStringSubmatch(parameterString)) > 0
}
func extractNumberParameter(value string) (int, error) {
parameterParser := regexp.MustCompile(`.*\(([0-9]+)\)`) parameterParser := regexp.MustCompile(`.*\(([0-9]+)\)`)
matches := parameterParser.FindStringSubmatch(value) matches := parameterParser.FindStringSubmatch(value)
var indent, errParsingInt = strconv.ParseInt(matches[1], 10, 32) var indent, errParsingInt = strconv.ParseInt(matches[1], 10, 32)
@ -145,10 +156,30 @@ func extractNumberParamter(value string) (int, error) {
return int(indent), nil return int(indent), nil
} }
func envSubstWithOptions() lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
value := string(m.Bytes)
noEmpty := hasOptionParameter(value, "ne")
noUnset := hasOptionParameter(value, "nu")
failFast := hasOptionParameter(value, "ff")
envsubstOpType.Type = "ENVSUBST"
prefs := envOpPreferences{NoUnset: noUnset, NoEmpty: noEmpty, FailFast: failFast}
if noEmpty {
envsubstOpType.Type = envsubstOpType.Type + "_NO_EMPTY"
}
if noUnset {
envsubstOpType.Type = envsubstOpType.Type + "_NO_UNSET"
}
op := &Operation{OperationType: envsubstOpType, Value: envsubstOpType.Type, StringValue: value, Preferences: prefs}
return &token{TokenType: operationToken, Operation: op}, nil
}
}
func flattenWithDepth() lex.Action { func flattenWithDepth() lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) { return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
value := string(m.Bytes) value := string(m.Bytes)
var depth, errParsingInt = extractNumberParamter(value) var depth, errParsingInt = extractNumberParameter(value)
if errParsingInt != nil { if errParsingInt != nil {
return nil, errParsingInt return nil, errParsingInt
} }
@ -162,7 +193,7 @@ func flattenWithDepth() lex.Action {
func encodeWithIndent(outputFormat PrinterOutputFormat) lex.Action { func encodeWithIndent(outputFormat PrinterOutputFormat) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) { return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
value := string(m.Bytes) value := string(m.Bytes)
var indent, errParsingInt = extractNumberParamter(value) var indent, errParsingInt = extractNumberParameter(value)
if errParsingInt != nil { if errParsingInt != nil {
return nil, errParsingInt return nil, errParsingInt
} }
@ -507,6 +538,7 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`strenv\([^\)]+\)`), envOp(true)) lexer.Add([]byte(`strenv\([^\)]+\)`), envOp(true))
lexer.Add([]byte(`env\([^\)]+\)`), envOp(false)) lexer.Add([]byte(`env\([^\)]+\)`), envOp(false))
lexer.Add([]byte(`envsubst\((ne|nu|ff| |,)+\)`), envSubstWithOptions())
lexer.Add([]byte(`envsubst`), opToken(envsubstOpType)) lexer.Add([]byte(`envsubst`), opToken(envsubstOpType))
lexer.Add([]byte(`\[`), literalToken(openCollect, false)) lexer.Add([]byte(`\[`), literalToken(openCollect, false))

View File

@ -6,12 +6,15 @@ import (
"os" "os"
"strings" "strings"
envsubst "github.com/a8m/envsubst" parse "github.com/a8m/envsubst/parse"
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
type envOpPreferences struct { type envOpPreferences struct {
StringValue bool StringValue bool
NoUnset bool
NoEmpty bool
FailFast bool
} }
func envOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { func envOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
@ -52,6 +55,19 @@ func envOperator(d *dataTreeNavigator, context Context, expressionNode *Expressi
func envsubstOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { func envsubstOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
var results = list.New() var results = list.New()
preferences := envOpPreferences{}
if expressionNode.Operation.Preferences != nil {
preferences = expressionNode.Operation.Preferences.(envOpPreferences)
}
parser := parse.New("string", os.Environ(),
&parse.Restrictions{NoUnset: preferences.NoUnset, NoEmpty: preferences.NoEmpty})
if preferences.FailFast {
parser.Mode = parse.Quick
} else {
parser.Mode = parse.AllErrors
}
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
@ -61,7 +77,7 @@ func envsubstOperator(d *dataTreeNavigator, context Context, expressionNode *Exp
return Context{}, fmt.Errorf("cannot substitute with %v, can only substitute strings. Hint: Most often you'll want to use '|=' over '=' for this operation", node.Tag) return Context{}, fmt.Errorf("cannot substitute with %v, can only substitute strings. Hint: Most often you'll want to use '|=' over '=' for this operation", node.Tag)
} }
value, err := envsubst.String(node.Value) value, err := parser.Parse(node.Value)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }

View File

@ -82,20 +82,54 @@ var envOperatorScenarios = []expressionScenario{
}, },
{ {
description: "Replace strings with envsubst, missing variables", description: "Replace strings with envsubst, missing variables",
environmentVariables: map[string]string{"myenv": "cat"},
expression: `"the ${myenvnonexisting} meows" | envsubst`, expression: `"the ${myenvnonexisting} meows" | envsubst`,
expected: []string{ expected: []string{
"D0, P[], (!!str)::the meows\n", "D0, P[], (!!str)::the meows\n",
}, },
}, },
{
description: "Replace strings with envsubst(nu), missing variables",
subdescription: "(nu) not unset, will fail if there are unset (missing) variables",
expression: `"the ${myenvnonexisting} meows" | envsubst(nu)`,
expectedError: "variable ${myenvnonexisting} not set",
},
{
description: "Replace strings with envsubst(ne), missing variables",
subdescription: "(ne) not empty, only validates set variables",
expression: `"the ${myenvnonexisting} meows" | envsubst(ne)`,
expected: []string{
"D0, P[], (!!str)::the meows\n",
},
},
{
description: "Replace strings with envsubst(ne), empty variable",
subdescription: "(ne) not empty, will fail if a references variable is empty",
environmentVariables: map[string]string{"myenv": ""},
expression: `"the ${myenv} meows" | envsubst(ne)`,
expectedError: "variable ${myenv} set but empty",
},
{ {
description: "Replace strings with envsubst, missing variables with defaults", description: "Replace strings with envsubst, missing variables with defaults",
environmentVariables: map[string]string{"myenv": "cat"},
expression: `"the ${myenvnonexisting-dog} meows" | envsubst`, expression: `"the ${myenvnonexisting-dog} meows" | envsubst`,
expected: []string{ expected: []string{
"D0, P[], (!!str)::the dog meows\n", "D0, P[], (!!str)::the dog meows\n",
}, },
}, },
{
description: "Replace strings with envsubst(nu), missing variables with defaults",
subdescription: "Having a default specified skips over the missing variable.",
expression: `"the ${myenvnonexisting-dog} meows" | envsubst(nu)`,
expected: []string{
"D0, P[], (!!str)::the dog meows\n",
},
},
{
description: "Replace strings with envsubst(ne), missing variables with defaults",
subdescription: "Fails, because the variable is explicitly set to blank.",
environmentVariables: map[string]string{"myEmptyEnv": ""},
expression: `"the ${myEmptyEnv-dog} meows" | envsubst(ne)`,
expectedError: "variable ${myEmptyEnv} set but empty",
},
{ {
description: "Replace string environment variable in document", description: "Replace string environment variable in document",
environmentVariables: map[string]string{"myenv": "cat meow"}, environmentVariables: map[string]string{"myenv": "cat meow"},
@ -105,6 +139,17 @@ var envOperatorScenarios = []expressionScenario{
"D0, P[], (doc)::{v: \"cat meow\"}\n", "D0, P[], (doc)::{v: \"cat meow\"}\n",
}, },
}, },
{
description: "(Default) Return all envsubst errors",
subdescription: "By default, all errors are returned at once.",
expression: `"the ${notThere} ${alsoNotThere}" | envsubst(nu)`,
expectedError: "variable ${notThere} not set\nvariable ${alsoNotThere} not set",
},
{
description: "Fail fast, return the first envsubst error (and abort)",
expression: `"the ${notThere} ${alsoNotThere}" | envsubst(nu,ff)`,
expectedError: "variable ${notThere} not set",
},
} }
func TestEnvOperatorScenarios(t *testing.T) { func TestEnvOperatorScenarios(t *testing.T) {

View File

@ -105,7 +105,7 @@ func testScenario(t *testing.T, s *expressionScenario) {
} }
if err != nil { if err != nil {
t.Error(fmt.Errorf("%w: %v", err, s.expression)) t.Error(fmt.Errorf("%w: %v: %v", err, s.description, s.expression))
return return
} }
test.AssertResultComplexWithContext(t, s.expected, resultsToString(t, context.MatchingNodes), fmt.Sprintf("desc: %v\nexp: %v\ndoc: %v", s.description, s.expression, s.document)) test.AssertResultComplexWithContext(t, s.expected, resultsToString(t, context.MatchingNodes), fmt.Sprintf("desc: %v\nexp: %v\ndoc: %v", s.description, s.expression, s.document))
@ -343,8 +343,13 @@ func documentOutput(t *testing.T, w *bufio.Writer, s expressionScenario, formatt
} }
context, err := NewDataTreeNavigator().GetMatchingNodes(Context{MatchingNodes: inputs}, node) context, err := NewDataTreeNavigator().GetMatchingNodes(Context{MatchingNodes: inputs}, node)
if err != nil {
if s.expectedError != "" && err != nil {
writeOrPanic(w, fmt.Sprintf("```bash\nError: %v\n```\n\n", err.Error()))
return
} else if err != nil {
t.Error(err, s.expression) t.Error(err, s.expression)
return
} }
err = printer.PrintResults(context.MatchingNodes) err = printer.PrintResults(context.MatchingNodes)