diff options
| -rw-r--r-- | plugins.go | 77 | ||||
| -rw-r--r-- | plugins_test.go | 60 |
2 files changed, 109 insertions, 28 deletions
@@ -17,40 +17,81 @@ import ( "github.com/hairyhenderson/gomplate/v3/internal/config" ) +// bindPlugins creates custom plugin functions for each plugin specified by +// the config, and adds them to the given funcMap. Uses the configuration's +// PluginTimeout as the default plugin Timeout. Errors if a function name is +// duplicated. func bindPlugins(ctx context.Context, cfg *config.Config, funcMap template.FuncMap) error { for k, v := range cfg.Plugins { + if _, ok := funcMap[k]; ok { + return fmt.Errorf("function %q is already bound, and can not be overridden", k) + } + + // default the timeout to the one in the config timeout := cfg.PluginTimeout if v.Timeout != 0 { timeout = v.Timeout } - plugin := &plugin{ - ctx: ctx, - name: k, - path: v.Cmd, - timeout: timeout, - pipe: v.Pipe, - stderr: cfg.Stderr, - } - if _, ok := funcMap[plugin.name]; ok { - return fmt.Errorf("function %q is already bound, and can not be overridden", plugin.name) - } - funcMap[plugin.name] = plugin.run + funcMap[k] = PluginFunc(ctx, v.Cmd, PluginOpts{ + Timeout: timeout, + Pipe: v.Pipe, + Stderr: cfg.Stderr, + }) } + return nil } +// PluginOpts are options for controlling plugin function execution +type PluginOpts struct { + // Stderr can be set to redirect the plugin's stderr to a custom writer. + // Defaults to os.Stderr. + Stderr io.Writer + + // Timeout is the maximum amount of time to wait for the plugin to complete. + // Defaults to 5 seconds. + Timeout time.Duration + + // Pipe indicates whether the last argument should be piped to the plugin's + // stdin (true) or processed as a commandline argument (false) + Pipe bool +} + +// PluginFunc creates a template function that runs an external process - either +// a shell script or commandline executable. +func PluginFunc(ctx context.Context, cmd string, opts PluginOpts) func(...interface{}) (interface{}, error) { + timeout := opts.Timeout + if timeout == 0 { + timeout = 5 * time.Second + } + + stderr := opts.Stderr + if stderr == nil { + stderr = os.Stderr + } + + plugin := &plugin{ + ctx: ctx, + path: cmd, + timeout: timeout, + pipe: opts.Pipe, + stderr: stderr, + } + + return plugin.run +} + // plugin represents a custom function that binds to an external process to be executed type plugin struct { - ctx context.Context - stderr io.Writer - name, path string - timeout time.Duration - pipe bool + ctx context.Context + stderr io.Writer + path string + timeout time.Duration + pipe bool } // builds a command that's appropriate for running scripts -// nolint: gosec func (p *plugin) buildCommand(a []string) (name string, args []string) { switch filepath.Ext(p.path) { case ".ps1": diff --git a/plugins_test.go b/plugins_test.go index 1e00a908..d8442ecf 100644 --- a/plugins_test.go +++ b/plugins_test.go @@ -3,15 +3,15 @@ package gomplate import ( "bytes" "context" + "fmt" + "os" "strings" "testing" "text/template" "time" - "gotest.tools/v3/assert" - "gotest.tools/v3/assert/cmp" - "github.com/hairyhenderson/gomplate/v3/internal/config" + "github.com/stretchr/testify/assert" ) func TestBindPlugins(t *testing.T) { @@ -21,13 +21,13 @@ func TestBindPlugins(t *testing.T) { Plugins: map[string]config.PluginConfig{}, } err := bindPlugins(ctx, cfg, fm) - assert.NilError(t, err) - assert.DeepEqual(t, template.FuncMap{}, fm) + assert.NoError(t, err) + assert.EqualValues(t, template.FuncMap{}, fm) cfg.Plugins = map[string]config.PluginConfig{"foo": {Cmd: "bar"}} err = bindPlugins(ctx, cfg, fm) - assert.NilError(t, err) - assert.Check(t, cmp.Contains(fm, "foo")) + assert.NoError(t, err) + assert.Contains(t, fm, "foo") err = bindPlugins(ctx, cfg, fm) assert.ErrorContains(t, err, "already bound") @@ -49,12 +49,11 @@ func TestBuildCommand(t *testing.T) { for _, d := range data { p := &plugin{ ctx: ctx, - name: d.name, path: d.path, } name, args := p.buildCommand(d.args) actual := append([]string{name}, args...) - assert.DeepEqual(t, d.expected, actual) + assert.EqualValues(t, d.expected, actual) } } @@ -70,7 +69,48 @@ func TestRun(t *testing.T) { path: "echo", } out, err := p.run("foo") - assert.NilError(t, err) + assert.NoError(t, err) assert.Equal(t, "", stderr.String()) assert.Equal(t, "foo", strings.TrimSpace(out.(string))) } + +func ExamplePluginFunc() { + ctx := context.Background() + + // PluginFunc creates a template function that runs an arbitrary command. + f := PluginFunc(ctx, "echo", PluginOpts{}) + + // The function can be used in a template, but here we'll just run it + // directly. This is equivalent to running 'echo foo bar' + out, err := f("foo", "bar") + if err != nil { + panic(err) + } + fmt.Println(out) + + // Output: + // foo bar +} + +func ExamplePluginFunc_with_template() { + ctx := context.Background() + + f := PluginFunc(ctx, "echo", PluginOpts{}) + + // PluginFunc is intended for use with gomplate, but can be used in any + // text/template by adding it to the FuncMap. + tmpl := template.New("new").Funcs(template.FuncMap{"echo": f}) + + tmpl, err := tmpl.Parse(`{{ echo "baz" "qux" }}`) + if err != nil { + panic(err) + } + + err = tmpl.Execute(os.Stdout, nil) + if err != nil { + panic(err) + } + + // Output: + // baz qux +} |
