summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--plugins.go77
-rw-r--r--plugins_test.go60
2 files changed, 109 insertions, 28 deletions
diff --git a/plugins.go b/plugins.go
index 3e6e1f04..b7349567 100644
--- a/plugins.go
+++ b/plugins.go
@@ -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
+}