From 88d644a7e9ff41db452508b03ad1fe44c1d4cf3e Mon Sep 17 00:00:00 2001 From: Dennis Jekubczyk Date: Thu, 9 Mar 2023 02:34:52 +0100 Subject: [PATCH] Add ability to set `error_only: true` on the `group` output mode --- cmd/task/task.go | 5 +++ docs/docs/api_reference.md | 1 + docs/docs/usage.md | 24 ++++++++++++ docs/static/schema.json | 5 +++ internal/output/group.go | 9 ++++- internal/output/interleaved.go | 2 +- internal/output/output.go | 7 ++-- internal/output/output_test.go | 38 ++++++++++++++++--- internal/output/prefixed.go | 2 +- task.go | 10 ++--- task_test.go | 30 +++++++++++++++ taskfile/output.go | 1 + testdata/output_group_error_only/Taskfile.yml | 17 +++++++++ 13 files changed, 135 insertions(+), 16 deletions(-) create mode 100644 testdata/output_group_error_only/Taskfile.yml diff --git a/cmd/task/task.go b/cmd/task/task.go index cf5347ba..1f4eba9a 100644 --- a/cmd/task/task.go +++ b/cmd/task/task.go @@ -94,6 +94,7 @@ func main() { pflag.StringVarP(&output.Name, "output", "o", "", "sets output style: [interleaved|group|prefixed]") pflag.StringVar(&output.Group.Begin, "output-group-begin", "", "message template to print before a task's grouped output") pflag.StringVar(&output.Group.End, "output-group-end", "", "message template to print after a task's grouped output") + pflag.BoolVar(&output.Group.ErrorOnly, "output-group-error-only", false, "swallow output from successful tasks") pflag.BoolVarP(&color, "color", "c", true, "colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable") pflag.IntVarP(&concurrency, "concurrency", "C", 0, "limit number tasks to run concurrently") pflag.DurationVarP(&interval, "interval", "I", 0, "interval to watch for changes") @@ -138,6 +139,10 @@ func main() { log.Fatal("task: You can't set --output-group-end without --output=group") return } + if output.Group.ErrorOnly { + log.Fatal("task: You can't set --output-group-error-only without --output=group") + return + } } e := task.Executor{ diff --git a/docs/docs/api_reference.md b/docs/docs/api_reference.md index 43b580df..a47d3253 100644 --- a/docs/docs/api_reference.md +++ b/docs/docs/api_reference.md @@ -36,6 +36,7 @@ variable | `-o` | `--output` | `string` | Default set in the Taskfile or `intervealed` | Sets output style: [`interleaved`/`group`/`prefixed`]. | | | `--output-group-begin` | `string` | | Message template to print before a task's grouped output. | | | `--output-group-end` | `string` | | Message template to print after a task's grouped output. | +| | `--output-group-error-only` | `bool` | `false` | Swallow command output on zero exit code. | | `-p` | `--parallel` | `bool` | `false` | Executes tasks provided on command line in parallel. | | `-s` | `--silent` | `bool` | `false` | Disables echoing. | | | `--status` | `bool` | `false` | Exits with non-zero exit code if any of the given tasks is not up-to-date. | diff --git a/docs/docs/usage.md b/docs/docs/usage.md index 1963c654..0e8f162f 100644 --- a/docs/docs/usage.md +++ b/docs/docs/usage.md @@ -1342,6 +1342,30 @@ Hello, World! ::endgroup:: ``` +When using the `group` output, you may swallow the output of the executed command +on standard output and standard error if it does not fail (zero exit code). + +```yaml +version: '3' + +silent: true + +output: + group: + error_only: true + +tasks: + passes: echo 'output-of-passes' + errors: echo 'output-of-errors' && exit 1 +``` + +```bash +$ task passes +$ task errors +output-of-errors +task: Failed to run task "errors": exit status 1 +``` + The `prefix` output will prefix every line printed by a command with `[task-name] ` as the prefix, but you can customize the prefix for a command with the `prefix:` attribute: diff --git a/docs/static/schema.json b/docs/static/schema.json index 43436e9e..0ac75eb2 100644 --- a/docs/static/schema.json +++ b/docs/static/schema.json @@ -331,6 +331,11 @@ }, "end": { "type": "string" + }, + "error_only": { + "description": "Swallows command output on zero exit code", + "type": "boolean", + "default": false } } } diff --git a/internal/output/group.go b/internal/output/group.go index 290fdab9..c602cd17 100644 --- a/internal/output/group.go +++ b/internal/output/group.go @@ -7,6 +7,7 @@ import ( type Group struct { Begin, End string + ErrorOnly bool } func (g Group) WrapWriter(stdOut, _ io.Writer, _ string, tmpl Templater) (io.Writer, io.Writer, CloseFunc) { @@ -17,7 +18,13 @@ func (g Group) WrapWriter(stdOut, _ io.Writer, _ string, tmpl Templater) (io.Wri if g.End != "" { gw.end = tmpl.Replace(g.End) + "\n" } - return gw, gw, func() error { return gw.close() } + return gw, gw, func(err error) error { + if g.ErrorOnly && err == nil { + return nil + } + + return gw.close() + } } type groupWriter struct { diff --git a/internal/output/interleaved.go b/internal/output/interleaved.go index ceeb789e..0bdd1640 100644 --- a/internal/output/interleaved.go +++ b/internal/output/interleaved.go @@ -7,5 +7,5 @@ import ( type Interleaved struct{} func (Interleaved) WrapWriter(stdOut, stdErr io.Writer, _ string, _ Templater) (io.Writer, io.Writer, CloseFunc) { - return stdOut, stdErr, func() error { return nil } + return stdOut, stdErr, func(error) error { return nil } } diff --git a/internal/output/output.go b/internal/output/output.go index 206b8c32..a7de44fb 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -18,7 +18,7 @@ type Output interface { WrapWriter(stdOut, stdErr io.Writer, prefix string, tmpl Templater) (io.Writer, io.Writer, CloseFunc) } -type CloseFunc func() error +type CloseFunc func(err error) error // Build the Output for the requested taskfile.Output. func BuildFor(o *taskfile.Output) (Output, error) { @@ -30,8 +30,9 @@ func BuildFor(o *taskfile.Output) (Output, error) { return Interleaved{}, nil case "group": return Group{ - Begin: o.Group.Begin, - End: o.Group.End, + Begin: o.Group.Begin, + End: o.Group.End, + ErrorOnly: o.Group.ErrorOnly, }, nil case "prefixed": if err := checkOutputGroupUnset(o); err != nil { diff --git a/internal/output/output_test.go b/internal/output/output_test.go index 4630de08..e3a32ef7 100644 --- a/internal/output/output_test.go +++ b/internal/output/output_test.go @@ -2,6 +2,7 @@ package output_test import ( "bytes" + "errors" "fmt" "io" "testing" @@ -38,7 +39,7 @@ func TestGroup(t *testing.T) { fmt.Fprintln(stdErr, "err") assert.Equal(t, "", b.String()) - assert.NoError(t, cleanup()) + assert.NoError(t, cleanup(nil)) assert.Equal(t, "out\nout\nerr\nerr\nout\nerr\n", b.String()) } @@ -64,17 +65,44 @@ func TestGroupWithBeginEnd(t *testing.T) { assert.Equal(t, "", b.String()) fmt.Fprintln(w, "baz") assert.Equal(t, "", b.String()) - assert.NoError(t, cleanup()) + assert.NoError(t, cleanup(nil)) assert.Equal(t, "::group::example-value\nfoo\nbar\nbaz\n::endgroup::\n", b.String()) }) t.Run("no output", func(t *testing.T) { var b bytes.Buffer var _, _, cleanup = o.WrapWriter(&b, io.Discard, "", &tmpl) - assert.NoError(t, cleanup()) + assert.NoError(t, cleanup(nil)) assert.Equal(t, "", b.String()) }) } +func TestGroupErrorOnlySwallowsOutputOnNoError(t *testing.T) { + var b bytes.Buffer + var o output.Output = output.Group{ + ErrorOnly: true, + } + var stdOut, stdErr, cleanup = o.WrapWriter(&b, io.Discard, "", nil) + + _, _ = fmt.Fprintln(stdOut, "std-out") + _, _ = fmt.Fprintln(stdErr, "std-err") + + assert.NoError(t, cleanup(nil)) + assert.Empty(t, b.String()) +} +func TestGroupErrorOnlyShowsOutputOnError(t *testing.T) { + var b bytes.Buffer + var o output.Output = output.Group{ + ErrorOnly: true, + } + var stdOut, stdErr, cleanup = o.WrapWriter(&b, io.Discard, "", nil) + + _, _ = fmt.Fprintln(stdOut, "std-out") + _, _ = fmt.Fprintln(stdErr, "std-err") + + assert.NoError(t, cleanup(errors.New("any-error"))) + assert.Equal(t, "std-out\nstd-err\n", b.String()) +} + func TestPrefixed(t *testing.T) { var b bytes.Buffer var o output.Output = output.Prefixed{} @@ -87,7 +115,7 @@ func TestPrefixed(t *testing.T) { assert.Equal(t, "[prefix] foo\n[prefix] bar\n", b.String()) fmt.Fprintln(w, "baz") assert.Equal(t, "[prefix] foo\n[prefix] bar\n[prefix] baz\n", b.String()) - assert.NoError(t, cleanup()) + assert.NoError(t, cleanup(nil)) }) t.Run("multiple writes for a single line", func(t *testing.T) { @@ -98,7 +126,7 @@ func TestPrefixed(t *testing.T) { assert.Equal(t, "", b.String()) } - assert.NoError(t, cleanup()) + assert.NoError(t, cleanup(nil)) assert.Equal(t, "[prefix] Test!\n", b.String()) }) } diff --git a/internal/output/prefixed.go b/internal/output/prefixed.go index badfef3f..da6d5e6b 100644 --- a/internal/output/prefixed.go +++ b/internal/output/prefixed.go @@ -11,7 +11,7 @@ type Prefixed struct{} func (Prefixed) WrapWriter(stdOut, _ io.Writer, prefix string, _ Templater) (io.Writer, io.Writer, CloseFunc) { pw := &prefixWriter{writer: stdOut, prefix: prefix} - return pw, pw, func() error { return pw.close() } + return pw, pw, func(error) error { return pw.close() } } type prefixWriter struct { diff --git a/task.go b/task.go index 24adb1ba..b2db3079 100644 --- a/task.go +++ b/task.go @@ -282,11 +282,6 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi return fmt.Errorf("task: failed to get variables: %w", err) } stdOut, stdErr, close := outputWrapper.WrapWriter(e.Stdout, e.Stderr, t.Prefix, outputTemplater) - defer func() { - if err := close(); err != nil { - e.Logger.Errf(logger.Red, "task: unable to close writer: %v", err) - } - }() err = execext.RunCommand(ctx, &execext.RunCommandOptions{ Command: cmd.Cmd, @@ -298,6 +293,11 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi Stdout: stdOut, Stderr: stdErr, }) + defer func() { + if err := close(err); err != nil { + e.Logger.Errf(logger.Red, "task: unable to close writer: %v", err) + } + }() if execext.IsExitError(err) && cmd.IgnoreError { e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v", t.Name(), err) return nil diff --git a/task_test.go b/task_test.go index d26592f7..beebdc5b 100644 --- a/task_test.go +++ b/task_test.go @@ -1576,6 +1576,36 @@ Bye! t.Log(buff.String()) assert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder) } +func TestOutputGroupErrorOnlySwallowsOutputOnSuccess(t *testing.T) { + const dir = "testdata/output_group_error_only" + var buff bytes.Buffer + e := task.Executor{ + Dir: dir, + Stdout: &buff, + Stderr: &buff, + } + assert.NoError(t, e.Setup()) + + assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "passing"})) + t.Log(buff.String()) + assert.Empty(t, buff.String()) +} + +func TestOutputGroupErrorOnlyShowsOutputOnFailure(t *testing.T) { + const dir = "testdata/output_group_error_only" + var buff bytes.Buffer + e := task.Executor{ + Dir: dir, + Stdout: &buff, + Stderr: &buff, + } + assert.NoError(t, e.Setup()) + + assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "failing"})) + t.Log(buff.String()) + assert.Contains(t, "failing-output", strings.TrimSpace(buff.String())) + assert.NotContains(t, "passing", strings.TrimSpace(buff.String())) +} func TestIncludedVars(t *testing.T) { const dir = "testdata/include_with_vars" diff --git a/taskfile/output.go b/taskfile/output.go index 7833e114..ef0edd14 100644 --- a/taskfile/output.go +++ b/taskfile/output.go @@ -53,6 +53,7 @@ func (s *Output) UnmarshalYAML(node *yaml.Node) error { // OutputGroup is the style options specific to the Group style. type OutputGroup struct { Begin, End string + ErrorOnly bool `yaml:"error_only"` } // IsSet returns true if and only if a custom output style is set. diff --git a/testdata/output_group_error_only/Taskfile.yml b/testdata/output_group_error_only/Taskfile.yml new file mode 100644 index 00000000..1f8751b6 --- /dev/null +++ b/testdata/output_group_error_only/Taskfile.yml @@ -0,0 +1,17 @@ +version: '3' + +silent: true + +output: + group: + error_only: true + +tasks: + passing: echo 'passing-output' + + failing: + cmds: + - task: passing + - echo 'passing-output-2' + - echo 'passing-output-3' + - echo 'failing-output' && exit 1