diff --git a/README.md b/README.md index 4a3012a..aa3e678 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,12 @@ Code generator and script runner. -## Wishlist (if time allows) +## Wishlist -- When prompting again, reuse existing values as defaults -- Per-template/module scripts in `bin` dir, which are automatically included in `PATH` - - `jen exec` and `jen export` should alter `PATH` to include `bin` dir(s) - `jen do` alone to prompt for action - `jen export` to list env variables - Reusable modules - Add `set` step to set multiple variables (are those saved to `jen.yaml`?) -- Override values at command-line level (`--set myValue=value`) -- Exceptionally escape within any file, using `{{{` and `}}}` to represent `{{` and `}}` - `confirm` step (similar to `if`, but `confirm` property contains message to display and `then` the steps to execute) - Custom placeholders: diff --git a/src/internal/evaluation/evaluation.go b/src/internal/evaluation/evaluation.go index 4458757..3019e53 100644 --- a/src/internal/evaluation/evaluation.go +++ b/src/internal/evaluation/evaluation.go @@ -40,6 +40,14 @@ func EvalBoolExpression(values model.Values, expression string) (bool, error) { // EvalPromptValueTemplate interpolates a choice or default value string that will be presented to // user via a prompt, by evaluating both go template expressions and $... shell expressions func EvalPromptValueTemplate(values model.Values, binDirs []string, text string) (string, error) { + // Escape triple braces + doubleOpen := strings.Repeat("{", 2) + doubleClose := strings.Repeat("}", 2) + tripleOpen := strings.Repeat("{", 3) + tripleClose := strings.Repeat("}", 3) + text = strings.ReplaceAll(text, tripleOpen, doubleOpen+"`"+doubleOpen+"`"+doubleClose) + text = strings.ReplaceAll(text, tripleClose, doubleOpen+"`"+doubleClose+"`"+doubleClose) + // Interpolate go templating str, err := EvalTemplate(values, text) if err != nil { diff --git a/src/internal/evaluation/evaluation_test.go b/src/internal/evaluation/evaluation_test.go index f4985af..a248f0c 100644 --- a/src/internal/evaluation/evaluation_test.go +++ b/src/internal/evaluation/evaluation_test.go @@ -238,11 +238,16 @@ func TestEvalPromptValueTemplate(t *testing.T) { Value: `Hello {{"$VAR1"}} World`, Expected: `Hello value1 World`, }, + { + Name: "Ignore escaped triple braces", + Value: `Hello {{{ .VAR }}} World`, + Expected: `Hello {{ .VAR }} World`, + }, } for _, f := range fixtures { t.Run(f.Name, func(t *testing.T) { - actual, err := EvalPromptValueTemplate(values, "", f.Value) + actual, err := EvalPromptValueTemplate(values, nil, f.Value) assert.NoError(t, err) assert.Equal(t, f.Expected, actual) }) diff --git a/src/internal/persist/loadHelpers.go b/src/internal/persist/loadHelpers.go index b13d4b0..6bdb140 100644 --- a/src/internal/persist/loadHelpers.go +++ b/src/internal/persist/loadHelpers.go @@ -40,11 +40,11 @@ func getOptionalMap(node yaml.Node, key string) (yaml.Map, bool, error) { return m, true, nil } -// getOptionalMapOrRawString retrieves the child map with given key or, if child is a raw string, it returns a map with +// getOptionalMapOrRawStringOrRawStrings retrieves the child map with given key or, if child is a raw string, it returns a map with // raw string stored in a property keyed with defaultSubKey. This is to support steps that have two alternate syntaxes, // a long-hand syntax using a map with multiple properties and a short-hand syntax with a raw string that specifies // only the value of defaultSubKey. If defaultSubKey is an empty string, then only the long-hand map syntax is tried. -func getOptionalMapOrRawString(node yaml.Node, key, defaultSubKey string) (yaml.Map, bool, error) { +func getOptionalMapOrRawStringOrRawStrings(node yaml.Node, key, defaultSubKey string) (yaml.Map, bool, error) { _map, ok := node.(yaml.Map) if !ok { return nil, false, nil @@ -63,6 +63,15 @@ func getOptionalMapOrRawString(node yaml.Node, key, defaultSubKey string) (yaml. } return _map, true, nil } + + // Try list of raw strings + list := getOptionalListOfScalar(child) + if list != nil { + _map = yaml.Map{ + defaultSubKey: list, + } + return _map, true, nil + } } // Try map @@ -172,6 +181,22 @@ func getString(node yaml.Node) (string, bool) { return str, true } +func getOptionalListOfScalar(node yaml.Node) yaml.List { + list, ok := node.(yaml.List) + if !ok { + return nil + } + + // Ensure all list children are scalars + for _, item := range list { + if _, ok := item.(yaml.Scalar); !ok { + return nil + } + } + + return list +} + func getOptionalBool(_map yaml.Map, key string, defaultValue bool) (bool, error) { value, ok, err := getStringInternal(_map, key) if err != nil { diff --git a/src/internal/persist/loadSpec.go b/src/internal/persist/loadSpec.go index ea2a106..ed6eafd 100644 --- a/src/internal/persist/loadSpec.go +++ b/src/internal/persist/loadSpec.go @@ -153,7 +153,7 @@ func loadExecutable(node yaml.Node) (model.Executable, error) { } for _, x := range items { - _map, ok, err := getOptionalMapOrRawString(node, x.name, x.defaultSubKey) + _map, ok, err := getOptionalMapOrRawStringOrRawStrings(node, x.name, x.defaultSubKey) if err != nil { return nil, err }