From 7a1263ced6dfbea47b1a99f07b97d45cf03b4a36 Mon Sep 17 00:00:00 2001 From: Dave Henderson Date: Tue, 27 Nov 2018 23:08:08 -0500 Subject: [PATCH] Adding option to suppress empty output files Signed-off-by: Dave Henderson --- docs/content/usage.md | 11 +++++ template.go | 83 +++++++++++++++++++++++++++++++++ template_test.go | 57 ++++++++++++++++++++++ tests/integration/basic_test.go | 16 +++++++ 4 files changed, 167 insertions(+) diff --git a/docs/content/usage.md b/docs/content/usage.md index bdbc42499..05d798ffc 100644 --- a/docs/content/usage.md +++ b/docs/content/usage.md @@ -139,3 +139,14 @@ add the command to the command-line after a `--` argument: $ gomplate -i 'hello world' -o out.txt -- cat out.txt hello world ``` + +## 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: + +```console +$ export GOMPLATE_SUPPRESS_EMPTY=true +$ gomplate -i '{{ print " \n" }}' -o out +$ cat out +cat: out: No such file or directory +``` diff --git a/template.go b/template.go index 86fbfb252..1741de107 100644 --- a/template.go +++ b/template.go @@ -1,6 +1,7 @@ package gomplate import ( + "bytes" "fmt" "io" "io/ioutil" @@ -8,6 +9,10 @@ import ( "path/filepath" "text/template" + "github.com/hairyhenderson/gomplate/conv" + "github.com/hairyhenderson/gomplate/env" + "github.com/pkg/errors" + "github.com/spf13/afero" ) @@ -219,9 +224,23 @@ func inList(list []string, entry string) bool { } func openOutFile(filename string, mode os.FileMode, modeOverride bool) (out io.WriteCloser, err error) { + if conv.ToBool(env.Getenv("GOMPLATE_SUPPRESS_EMPTY", "false")) { + out = newEmptySkipper(func() (io.WriteCloser, error) { + if filename == "-" { + return Stdout, nil + } + return createOutFile(filename, mode, modeOverride) + }) + return out, nil + } + if filename == "-" { return Stdout, nil } + return createOutFile(filename, mode, modeOverride) +} + +func createOutFile(filename string, mode os.FileMode, modeOverride bool) (out io.WriteCloser, err error) { out, err = fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm()) if err != nil { return out, err @@ -268,3 +287,67 @@ func executeCombinedGlob(globArray []string) ([]string, error) { return combinedExcludes, nil } + +// emptySkipper is a io.WriteCloser wrapper that will only start writing once a +// non-whitespace byte has been encountered. The writer must be provided by the +// `open` func +type emptySkipper struct { + open func() (io.WriteCloser, error) + + // internal + w io.WriteCloser + buf *bytes.Buffer + nw bool +} + +func newEmptySkipper(open func() (io.WriteCloser, error)) *emptySkipper { + return &emptySkipper{ + w: nil, + buf: &bytes.Buffer{}, + nw: false, + open: open, + } +} + +func (f *emptySkipper) Write(p []byte) (n int, err error) { + if !f.nw { + if allWhitespace(p) { + // buffer the whitespace + return f.buf.Write(p) + } + + // first time around, so open the writer + f.nw = true + f.w, err = f.open() + if err != nil { + return 0, err + } + if f.w == nil { + return 0, errors.New("nil writer returned by open") + } + // empty the buffer into the wrapped writer + _, err = f.buf.WriteTo(f.w) + if err != nil { + return 0, err + } + } + + return f.w.Write(p) +} + +func (f *emptySkipper) Close() error { + if f.w != nil { + return f.w.Close() + } + return nil +} + +func allWhitespace(p []byte) bool { + for _, b := range p { + if b == ' ' || b == '\t' || b == '\n' || b == '\r' || b == '\v' { + continue + } + return false + } + return true +} diff --git a/template_test.go b/template_test.go index 190fb35f8..fb71ecea5 100644 --- a/template_test.go +++ b/template_test.go @@ -295,3 +295,60 @@ func TestProcessTemplates(t *testing.T) { fs.Remove("out") } } + +func TestAllWhitespace(t *testing.T) { + testdata := []struct { + in []byte + expected bool + }{ + {[]byte(" "), true}, + {[]byte("foo"), false}, + {[]byte(" \t\n\n\v\r\n"), true}, + {[]byte(" foo "), false}, + } + + for _, d := range testdata { + assert.Equal(t, d.expected, allWhitespace(d.in)) + } +} + +func TestEmptySkipper(t *testing.T) { + testdata := []struct { + in []byte + empty bool + }{ + {[]byte(" "), true}, + {[]byte("foo"), false}, + {[]byte(" \t\n\n\v\r\n"), true}, + {[]byte(" foo "), false}, + } + + for _, d := range testdata { + w := &bufferCloser{&bytes.Buffer{}} + opened := false + f := newEmptySkipper(func() (io.WriteCloser, error) { + t.Logf("I got called %#v", w) + opened = true + return w, nil + }) + n, err := f.Write(d.in) + assert.NoError(t, err) + assert.Equal(t, len(d.in), n) + if d.empty { + assert.Nil(t, f.w) + assert.False(t, opened) + } else { + assert.NotNil(t, f.w) + assert.True(t, opened) + assert.EqualValues(t, d.in, w.Bytes()) + } + } +} + +type bufferCloser struct { + *bytes.Buffer +} + +func (b *bufferCloser) Close() error { + return nil +} diff --git a/tests/integration/basic_test.go b/tests/integration/basic_test.go index 1dc52b857..7e2f031ed 100644 --- a/tests/integration/basic_test.go +++ b/tests/integration/basic_test.go @@ -233,3 +233,19 @@ func (s *BasicSuite) TestExecCommand(c *C) { Out: "hello world", }) } + +func (s *BasicSuite) TestEmptyOutputSuppression(c *C) { + out := s.tmpDir.Join("out") + result := icmd.RunCmd(icmd.Command(GomplateBin, + "-i", + `{{print "\t \n\n\r\n\t\t \v\n"}}`, + "-o", out), + func(cmd *icmd.Cmd) { + cmd.Env = []string{ + "GOMPLATE_SUPPRESS_EMPTY=true", + } + }) + result.Assert(c, icmd.Expected{ExitCode: 0}) + _, err := os.Stat(out) + assert.Equal(c, true, os.IsNotExist(err)) +}