diff --git a/cmd/gomplate/main.go b/cmd/gomplate/main.go index a11ea9318..8a0dffb7d 100644 --- a/cmd/gomplate/main.go +++ b/cmd/gomplate/main.go @@ -5,6 +5,7 @@ The gomplate command package main import ( + "bytes" "fmt" "os" "os/exec" @@ -19,8 +20,11 @@ import ( var ( printVer bool verbose bool + execPipe bool opts gomplate.Config includes []string + + postRunInput *bytes.Buffer ) func printVersion(name string) { @@ -34,7 +38,11 @@ func postRunExec(cmd *cobra.Command, args []string) error { args = args[1:] // nolint: gosec c := exec.Command(name, args...) - c.Stdin = os.Stdin + if execPipe { + c.Stdin = postRunInput + } else { + c.Stdin = os.Stdin + } c.Stderr = os.Stderr c.Stdout = os.Stdout @@ -101,6 +109,10 @@ func newGomplateCmd() *cobra.Command { // support --include opts.ExcludeGlob = processIncludes(includes, opts.ExcludeGlob) + if execPipe { + postRunInput = &bytes.Buffer{} + opts.Out = postRunInput + } err := gomplate.RunTemplates(&opts) cmd.SilenceErrors = true cmd.SilenceUsage = true @@ -140,6 +152,8 @@ func initFlags(command *cobra.Command) { command.Flags().StringVar(&opts.OutputMap, "output-map", "", "Template `string` to map the input file to an output path") command.Flags().StringVar(&opts.OutMode, "chmod", "", "set the mode for output file(s). Omit to inherit from input file(s)") + command.Flags().BoolVar(&execPipe, "exec-pipe", false, "pipe the output to the post-run exec command") + ldDefault := env.Getenv("GOMPLATE_LEFT_DELIM", "{{") rdDefault := env.Getenv("GOMPLATE_RIGHT_DELIM", "}}") command.Flags().StringVar(&opts.LDelim, "left-delim", ldDefault, "override the default left-`delimiter` [$GOMPLATE_LEFT_DELIM]") diff --git a/cmd/gomplate/validate.go b/cmd/gomplate/validate.go index 68c132b87..f4c73133f 100644 --- a/cmd/gomplate/validate.go +++ b/cmd/gomplate/validate.go @@ -47,6 +47,10 @@ func validateOpts(cmd *cobra.Command, args []string) (err error) { err = fmt.Errorf("must provide same number of --out (%d) as --file (%d) options", len(opts.OutputFiles), len(opts.InputFiles)) } + if err == nil && cmd.Flag("exec-pipe").Changed && len(args) == 0 { + err = fmt.Errorf("--exec-pipe may only be used with a post-exec command after --") + } + if err == nil { err = mustTogether(cmd, "output-dir", "input-dir") } diff --git a/cmd/gomplate/validate_test.go b/cmd/gomplate/validate_test.go index 91f167679..54f4ba017 100644 --- a/cmd/gomplate/validate_test.go +++ b/cmd/gomplate/validate_test.go @@ -42,6 +42,41 @@ func TestValidateOpts(t *testing.T) { )) assert.Error(t, err) + err = validateOpts(parseFlags("--exec-pipe")) + assert.Error(t, err) + + err = validateOpts(parseFlags("--exec-pipe", "--")) + assert.Error(t, err) + + err = validateOpts(parseFlags( + "--exec-pipe", + "--", "echo", "foo", + )) + assert.NoError(t, err) + + err = validateOpts(parseFlags( + "--exec-pipe", + "--out", "foo", + "--", "echo", + )) + assert.Error(t, err) + + err = validateOpts(parseFlags( + "--input-dir", "in", + "--exec-pipe", + "--output-dir", "foo", + "--", "echo", + )) + assert.Error(t, err) + + err = validateOpts(parseFlags( + "--input-dir", "in", + "--exec-pipe", + "--output-map", "foo", + "--", "echo", + )) + assert.Error(t, err) + err = validateOpts(parseFlags( "--input-dir", "in", "--output-map", "bar", diff --git a/config.go b/config.go index b1f3c453f..18bd5546a 100644 --- a/config.go +++ b/config.go @@ -1,6 +1,7 @@ package gomplate import ( + "io" "os" "strconv" "strings" @@ -17,6 +18,7 @@ type Config struct { OutputDir string OutputMap string OutMode string + Out io.Writer DataSources []string DataSourceHeaders []string diff --git a/docs/content/usage.md b/docs/content/usage.md index 229058596..e5dc7d92e 100644 --- a/docs/content/usage.md +++ b/docs/content/usage.md @@ -232,6 +232,21 @@ By default, plugins will time out after 5 seconds. To adjust this, set the `GOMPLATE_PLUGIN_TIMEOUT` environment variable to a valid [duration](../functions/time/#time-parseduration) such as `10s` or `3m`. +### `--exec-pipe` + +When using [post-template command execution](#post-template-command-execution), +it may be useful to pipe gomplate's rendered output directly into the command's +standard input. + +To do this, simply use `--exec-pipe` instead of `--out` or any other output flag: + +```console +$ gomplate -i 'hello world' --exec-pipe -- tr a-z A-Z +HELLO WORLD +``` + +Note that multiple inputs are not yet supported when using this option. + ## Post-template command execution Gomplate can launch other commands when template execution is successful. Simply @@ -242,6 +257,9 @@ $ gomplate -i 'hello world' -o out.txt -- cat out.txt hello world ``` +See also [`--exec-pipe`](#exec-pipe) for piping output directly into the +post-exec command. + ## Suppressing empty output Sometimes it can be desirable to suppress empty output (i.e. output consisting of only whitespace). To do so, set `GOMPLATE_SUPPRESS_EMPTY=true` in your environment: diff --git a/gomplate_test.go b/gomplate_test.go index 83e47adc0..9cd8362ff 100644 --- a/gomplate_test.go +++ b/gomplate_test.go @@ -2,7 +2,6 @@ package gomplate import ( "bytes" - "io" "net/http/httptest" "os" "path/filepath" @@ -19,15 +18,6 @@ import ( "github.com/stretchr/testify/assert" ) -// like ioutil.NopCloser(), except for io.WriteClosers... -type nopWCloser struct { - io.Writer -} - -func (n *nopWCloser) Close() error { - return nil -} - func testTemplate(g *gomplate, tmpl string) string { var out bytes.Buffer err := g.runTemplate(&tplate{name: "testtemplate", contents: tmpl, target: &out}) diff --git a/template.go b/template.go index f2a021c7f..df7d604f0 100644 --- a/template.go +++ b/template.go @@ -103,6 +103,11 @@ func gatherTemplates(o *Config, outFileNamer func(string) (string, error)) (temp return nil, err } + // --exec-pipe redirects standard out to the out pipe + if o.Out != nil { + Stdout = &nopWCloser{o.Out} + } + switch { // the arg-provided input string gets a special name case o.Input != "": @@ -336,3 +341,12 @@ func allWhitespace(p []byte) bool { } return true } + +// like ioutil.NopCloser(), except for io.WriteClosers... +type nopWCloser struct { + io.Writer +} + +func (n *nopWCloser) Close() error { + return nil +} diff --git a/tests/integration/basic_test.go b/tests/integration/basic_test.go index 0d44eb0f9..39ce824d8 100644 --- a/tests/integration/basic_test.go +++ b/tests/integration/basic_test.go @@ -184,6 +184,17 @@ func (s *BasicSuite) TestExecCommand(c *C) { }) } +func (s *BasicSuite) TestPostRunExecPipe(c *C) { + result := icmd.RunCmd(icmd.Command(GomplateBin, + "-i", `{{print "hello world"}}`, + "--exec-pipe", + "--", "tr", "a-z", "A-Z")) + result.Assert(c, icmd.Expected{ + ExitCode: 0, + Out: "HELLO WORLD", + }) +} + func (s *BasicSuite) TestEmptyOutputSuppression(c *C) { out := s.tmpDir.Join("out") result := icmd.RunCmd(icmd.Command(GomplateBin,