diff --git a/README.md b/README.md index d6f7794..0e36a85 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,9 @@ after inputs are collected but before script execution. Along with inputs, templates can access environment variables that are present and regardless whether `pure` is enabled or not. +Nested commands can include the run scripts from their parent commands. +ie. `{{template ""}}` + ### .Input. The expression to reference an input value. ie. '{{ .Input.my_input }}' diff --git a/command_set.go b/command_set.go index a3b997f..55a8886 100644 --- a/command_set.go +++ b/command_set.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "strings" + "text/template" ) var ( @@ -95,15 +96,30 @@ func (cs CommandSet) RenderEnv(data TemplateData) ([]string, error) { } func (cs CommandSet) RenderScript(data TemplateData) (string, error) { - for i := len(cs.Commands) - 1; i >= 0; { - command := cs.Commands[i] - if script, err := RenderTemplate(command.Run, data); err != nil { - return script, fmt.Errorf("template error for command: '%s' - %v", command.Name, err) + var tmpl *template.Template + for _, command := range cs.Commands { + var err error + if command.Run == "" { + continue + } + if tmpl == nil { + tmpl = template.New(command.Name) } else { - return script, nil + tmpl = tmpl.New(command.Name) } + tmpl, err = tmpl.Parse(command.Run) + if err != nil { + return "", fmt.Errorf("template error: %v", err) + } + } + if tmpl == nil { + return "", fmt.Errorf("no script present") + } + if script, err := RenderTemplate(tmpl, data); err != nil { + return script, fmt.Errorf("script error: %v", err) + } else { + return script, err } - return "", fmt.Errorf("no script present") } func (cs CommandSet) RenderScriptToTemp(data TemplateData) (string, error) { diff --git a/command_set_test.go b/command_set_test.go index e7a9144..afb2603 100644 --- a/command_set_test.go +++ b/command_set_test.go @@ -197,60 +197,135 @@ func TestCommandSetRenderEnv_TemplateError(t *testing.T) { assertEqual(t, expected, actual, "CommandSet.RenderEnv() returned unexpected error") } -func TestCommandSetRenderScript_Empty(t *testing.T) { - data := TemplateData{} - cs := CommandSet{} - _, err := cs.RenderScript(data) - expected := "no script present" - actual := fmt.Sprintf("%s", err) - assertEqual(t, expected, actual, "CommandSet.RenderScript() returned unexpected error") -} - -func TestCommandSetRenderScript_TemplateError(t *testing.T) { - data := TemplateData{ - Input: map[string]any{ - "A": "a", - }, - } - cs := CommandSet{ - Commands: []ConfigCommand{ - { - Name: "foobar", - Run: "{{.Input.A}", +func TestCommandSetRenderScript(t *testing.T) { + t.Run("empty command set", func(t *testing.T) { + data := TemplateData{} + cs := CommandSet{} + _, err := cs.RenderScript(data) + expected := "no script present" + actual := fmt.Sprintf("%s", err) + assertEqual(t, expected, actual, "CommandSet.RenderScript() returned unexpected error") + }) + t.Run("template error", func(t *testing.T) { + data := TemplateData{ + Input: map[string]any{ + "A": "a", }, - }, - } - _, err := cs.RenderScript(data) - expected := "template error for command: 'foobar' - template: :1: bad character U+007D '}'" - actual := fmt.Sprintf("%s", err) - assertEqual(t, expected, actual, "CommandSet.RenderScript() returned unexpected error") -} - -func TestCommandSetRenderScript_NonError(t *testing.T) { - data := TemplateData{ - Input: map[string]any{ - "A": "a", - "B": "b", - }, - } - cs := CommandSet{ - Commands: []ConfigCommand{ - { - Name: "foobaz", - Run: "echo {{.Input.B}}", + } + cs := CommandSet{ + Commands: []ConfigCommand{ + { + Name: "foobar", + Run: "{{.Input.A}", + }, }, - { - Name: "foobar", - Run: "echo {{.Input.A}}", + } + _, err := cs.RenderScript(data) + expected := "template error: template: foobar:1: bad character U+007D '}'" + actual := fmt.Sprintf("%s", err) + assertEqual(t, expected, actual, "CommandSet.RenderScript() returned unexpected error") + }) + t.Run("script error", func(t *testing.T) { + data := TemplateData{ + Input: map[string]any{ + "A": "a", }, - }, - } - expected := "echo a" - actual, err := cs.RenderScript(data) - if err != nil { - t.Fatalf("CommandSet.RenderScript() returned an unexpected error: %v", err) - } - assertEqual(t, expected, actual, "CommandSet.RenderScript() returned unexpected result") + } + cs := CommandSet{ + Commands: []ConfigCommand{ + { + Name: "foobar", + Run: "{{template \"foobaz\"}}", + }, + }, + } + _, err := cs.RenderScript(data) + expected := "script error: template: foobar:1:11: executing \"foobar\" at <{{template \"foobaz\"}}>: template \"foobaz\" not defined" + actual := fmt.Sprintf("%s", err) + assertEqual(t, expected, actual, "CommandSet.RenderScript() returned unexpected error") + }) + t.Run("render single template", func(t *testing.T) { + data := TemplateData{ + Input: map[string]any{ + "A": "a", + "B": "b", + }, + } + cs := CommandSet{ + Commands: []ConfigCommand{ + { + Name: "foobaz", + Run: "echo {{.Input.B}}", + }, + { + Name: "foobar", + Run: "echo {{.Input.A}}", + }, + }, + } + expected := "echo a" + actual, err := cs.RenderScript(data) + if err != nil { + t.Fatalf("CommandSet.RenderScript() returned an unexpected error: %v", err) + } + assertEqual(t, expected, actual, "CommandSet.RenderScript() returned unexpected result") + }) + t.Run("render multiple templates", func(t *testing.T) { + data := TemplateData{ + Input: map[string]any{ + "A": "a", + "B": "b", + }, + } + cs := CommandSet{ + Commands: []ConfigCommand{ + { + Name: "foobaz", + Run: "echo {{.Input.B}}", + }, + { + Name: "foobar", + Run: "echo {{.Input.A}} {{template \"foobaz\" .}}", + }, + }, + } + expected := "echo a echo b" + actual, err := cs.RenderScript(data) + if err != nil { + t.Fatalf("CommandSet.RenderScript() returned an unexpected error: %v", err) + } + assertEqual(t, expected, actual, "CommandSet.RenderScript() returned unexpected result") + }) + t.Run("latest command overrides existing template", func(t *testing.T) { + data := TemplateData{ + Input: map[string]any{ + "A": "a", + "B": "b", + }, + } + cs := CommandSet{ + Commands: []ConfigCommand{ + { + Name: "foobar", + Run: "echo {{.Input.A}}", + }, + { + Name: "foobaz", + Run: "echo {{.Input.B}}", + }, + { + Name: "foobar", + Run: "echo {{.Input.A}} {{template \"foobaz\" .}}", + }, + }, + } + expected := "echo a echo b" + actual, err := cs.RenderScript(data) + if err != nil { + t.Fatalf("CommandSet.RenderScript() returned an unexpected error: %v", err) + } + assertEqual(t, expected, actual, "CommandSet.RenderScript() returned unexpected result") + }) } func TestCommandSetRenderScriptToTemp(t *testing.T) { diff --git a/config.go b/config.go index a4eaa92..1360255 100644 --- a/config.go +++ b/config.go @@ -184,8 +184,6 @@ func (x *ConfigCommands) UnmarshalYAML(value *yaml.Node) error { } if numCommands := len(command.Commands); command.Run == "" && numCommands == 0 { return fmt.Errorf("line %d: command '%s' must have either 'run' or 'commands' attribute", keyNode.Line, keyNode.Value) - } else if command.Run != "" && numCommands > 0 { - return fmt.Errorf("line %d: command '%s' must only have 'run' or 'commands' attribute", keyNode.Line, keyNode.Value) } command.Name = keyNode.Value } diff --git a/config_test.go b/config_test.go index 3ddd42c..23846b2 100644 --- a/config_test.go +++ b/config_test.go @@ -106,20 +106,6 @@ commands: assertEqual(t, expected, actual, "ParseConfig() returned unexpected error") } -func TestParseConfig_CommandWithRunAndSubcommands(t *testing.T) { - content := ` -commands: - invalidCommand: - run: ooops - commands: - shouldNotExist: bugger -` - expected := "line 3: command 'invalidCommand' must only have 'run' or 'commands' attribute" - _, err := ParseConfig([]byte(content)) - actual := fmt.Sprintf("%s", err) - assertEqual(t, expected, actual, "ParseConfig() returned unexpected error") -} - func TestParseConfig_InputOptionsIsMap(t *testing.T) { content := ` run: ok diff --git a/utils.go b/utils.go index 32cde00..f509bbe 100644 --- a/utils.go +++ b/utils.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "strings" "text/template" ) @@ -37,11 +38,22 @@ func EnvMap(env []string) map[string]string { return m } -func RenderTemplate(text string, data TemplateData) (string, error) { +func RenderTemplate(text interface{}, data TemplateData) (string, error) { + var tmpl *template.Template + var err error b := strings.Builder{} - if tmpl, err := renderTemplate.Parse(text); err != nil { - return "", err - } else if tmpl.Execute(&b, data); err != nil { + switch t := text.(type) { + case *template.Template: + tmpl = t + case string: + tmpl, err = renderTemplate.Parse(t) + if err != nil { + return "", err + } + default: + return "", fmt.Errorf("unsupported type: %v", t) + } + if err = tmpl.Execute(&b, data); err != nil { return "", err } else { return b.String(), nil diff --git a/utils_test.go b/utils_test.go index 82e1eb4..40b10c4 100644 --- a/utils_test.go +++ b/utils_test.go @@ -1,7 +1,9 @@ package main import ( + "fmt" "testing" + "text/template" ) func TestEnvMap(t *testing.T) { @@ -36,18 +38,39 @@ func TestNewTemplateData(t *testing.T) { } func TestRenderTemplate(t *testing.T) { - template := "Input: {{ .Input.foobar }}, Input: {{ .Input.foobaz }}, Env: {{ .Env.FOOBAR }}, Env: {{ .Env.FOOBAZ }}" + text := "Input: {{ .Input.foobar }}, Input: {{ .Input.foobaz }}, Env: {{ .Env.FOOBAR }}, Env: {{ .Env.FOOBAZ }}" data := TemplateData{ Input: map[string]any{"foobar": "a"}, Env: map[string]string{"FOOBAR": "b"}, } expected := "Input: a, Input: , Env: b, Env: " - actual, err := RenderTemplate(template, data) - if err != nil { - t.Fatalf("RenderTemplate() returned unexpected error: %v", err) - } + t.Run("given a string", func(t *testing.T) { + actual, err := RenderTemplate(text, data) + if err != nil { + t.Fatalf("RenderTemplate() returned unexpected error: %v", err) + } + + assertEqual(t, expected, actual, "RenderTemplate() returned unexpected results") + }) + + t.Run("given a template object", func(t *testing.T) { + tmpl := template.New("") + if _, err := tmpl.Parse(text); err != nil { + t.Fatalf("Could not render template: %v", err) + } + actual, err := RenderTemplate(tmpl, data) + if err != nil { + t.Fatalf("RenderTemplate() returned unexpected error: %v", err) + } + + assertEqual(t, expected, actual, "RenderTemplate() returned unexpected results") + }) - assertEqual(t, expected, actual, "RenderTemplate() returned unexpected results") + t.Run("given other", func(t *testing.T) { + _, err := RenderTemplate(nil, data) + actual := fmt.Sprintf("%s", err) + assertEqual(t, "unsupported type: ", actual, "RenderTemplate() returned unexpected error") + }) } func TestDiffStrings(t *testing.T) {