1
0
mirror of https://github.com/go-task/task.git synced 2024-12-04 10:24:45 +02:00

Add ability to set error_only: true on the group output mode

This commit is contained in:
Dennis Jekubczyk 2023-03-09 02:34:52 +01:00 committed by GitHub
parent 4b97d4f7f5
commit 88d644a7e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 135 additions and 16 deletions

View File

@ -94,6 +94,7 @@ func main() {
pflag.StringVarP(&output.Name, "output", "o", "", "sets output style: [interleaved|group|prefixed]") 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.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.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.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.IntVarP(&concurrency, "concurrency", "C", 0, "limit number tasks to run concurrently")
pflag.DurationVarP(&interval, "interval", "I", 0, "interval to watch for changes") 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") log.Fatal("task: You can't set --output-group-end without --output=group")
return return
} }
if output.Group.ErrorOnly {
log.Fatal("task: You can't set --output-group-error-only without --output=group")
return
}
} }
e := task.Executor{ e := task.Executor{

View File

@ -36,6 +36,7 @@ variable
| `-o` | `--output` | `string` | Default set in the Taskfile or `intervealed` | Sets output style: [`interleaved`/`group`/`prefixed`]. | | `-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-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-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. | | `-p` | `--parallel` | `bool` | `false` | Executes tasks provided on command line in parallel. |
| `-s` | `--silent` | `bool` | `false` | Disables echoing. | | `-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. | | | `--status` | `bool` | `false` | Exits with non-zero exit code if any of the given tasks is not up-to-date. |

View File

@ -1342,6 +1342,30 @@ Hello, World!
::endgroup:: ::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 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 `[task-name] ` as the prefix, but you can customize the prefix for a command
with the `prefix:` attribute: with the `prefix:` attribute:

View File

@ -331,6 +331,11 @@
}, },
"end": { "end": {
"type": "string" "type": "string"
},
"error_only": {
"description": "Swallows command output on zero exit code",
"type": "boolean",
"default": false
} }
} }
} }

View File

@ -7,6 +7,7 @@ import (
type Group struct { type Group struct {
Begin, End string Begin, End string
ErrorOnly bool
} }
func (g Group) WrapWriter(stdOut, _ io.Writer, _ string, tmpl Templater) (io.Writer, io.Writer, CloseFunc) { 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 != "" { if g.End != "" {
gw.end = tmpl.Replace(g.End) + "\n" 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 { type groupWriter struct {

View File

@ -7,5 +7,5 @@ import (
type Interleaved struct{} type Interleaved struct{}
func (Interleaved) WrapWriter(stdOut, stdErr io.Writer, _ string, _ Templater) (io.Writer, io.Writer, CloseFunc) { 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 }
} }

View File

@ -18,7 +18,7 @@ type Output interface {
WrapWriter(stdOut, stdErr io.Writer, prefix string, tmpl Templater) (io.Writer, io.Writer, CloseFunc) 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. // Build the Output for the requested taskfile.Output.
func BuildFor(o *taskfile.Output) (Output, error) { func BuildFor(o *taskfile.Output) (Output, error) {
@ -30,8 +30,9 @@ func BuildFor(o *taskfile.Output) (Output, error) {
return Interleaved{}, nil return Interleaved{}, nil
case "group": case "group":
return Group{ return Group{
Begin: o.Group.Begin, Begin: o.Group.Begin,
End: o.Group.End, End: o.Group.End,
ErrorOnly: o.Group.ErrorOnly,
}, nil }, nil
case "prefixed": case "prefixed":
if err := checkOutputGroupUnset(o); err != nil { if err := checkOutputGroupUnset(o); err != nil {

View File

@ -2,6 +2,7 @@ package output_test
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"io" "io"
"testing" "testing"
@ -38,7 +39,7 @@ func TestGroup(t *testing.T) {
fmt.Fprintln(stdErr, "err") fmt.Fprintln(stdErr, "err")
assert.Equal(t, "", b.String()) 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()) 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()) assert.Equal(t, "", b.String())
fmt.Fprintln(w, "baz") fmt.Fprintln(w, "baz")
assert.Equal(t, "", b.String()) 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()) assert.Equal(t, "::group::example-value\nfoo\nbar\nbaz\n::endgroup::\n", b.String())
}) })
t.Run("no output", func(t *testing.T) { t.Run("no output", func(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer
var _, _, cleanup = o.WrapWriter(&b, io.Discard, "", &tmpl) var _, _, cleanup = o.WrapWriter(&b, io.Discard, "", &tmpl)
assert.NoError(t, cleanup()) assert.NoError(t, cleanup(nil))
assert.Equal(t, "", b.String()) 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) { func TestPrefixed(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer
var o output.Output = output.Prefixed{} 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()) assert.Equal(t, "[prefix] foo\n[prefix] bar\n", b.String())
fmt.Fprintln(w, "baz") fmt.Fprintln(w, "baz")
assert.Equal(t, "[prefix] foo\n[prefix] bar\n[prefix] baz\n", b.String()) 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) { 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.Equal(t, "", b.String())
} }
assert.NoError(t, cleanup()) assert.NoError(t, cleanup(nil))
assert.Equal(t, "[prefix] Test!\n", b.String()) assert.Equal(t, "[prefix] Test!\n", b.String())
}) })
} }

View File

@ -11,7 +11,7 @@ type Prefixed struct{}
func (Prefixed) WrapWriter(stdOut, _ io.Writer, prefix string, _ Templater) (io.Writer, io.Writer, CloseFunc) { func (Prefixed) WrapWriter(stdOut, _ io.Writer, prefix string, _ Templater) (io.Writer, io.Writer, CloseFunc) {
pw := &prefixWriter{writer: stdOut, prefix: prefix} 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 { type prefixWriter struct {

10
task.go
View File

@ -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) return fmt.Errorf("task: failed to get variables: %w", err)
} }
stdOut, stdErr, close := outputWrapper.WrapWriter(e.Stdout, e.Stderr, t.Prefix, outputTemplater) 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{ err = execext.RunCommand(ctx, &execext.RunCommandOptions{
Command: cmd.Cmd, Command: cmd.Cmd,
@ -298,6 +293,11 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
Stdout: stdOut, Stdout: stdOut,
Stderr: stdErr, 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 { if execext.IsExitError(err) && cmd.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v", t.Name(), err) e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v", t.Name(), err)
return nil return nil

View File

@ -1576,6 +1576,36 @@ Bye!
t.Log(buff.String()) t.Log(buff.String())
assert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder) 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) { func TestIncludedVars(t *testing.T) {
const dir = "testdata/include_with_vars" const dir = "testdata/include_with_vars"

View File

@ -53,6 +53,7 @@ func (s *Output) UnmarshalYAML(node *yaml.Node) error {
// OutputGroup is the style options specific to the Group style. // OutputGroup is the style options specific to the Group style.
type OutputGroup struct { type OutputGroup struct {
Begin, End string Begin, End string
ErrorOnly bool `yaml:"error_only"`
} }
// IsSet returns true if and only if a custom output style is set. // IsSet returns true if and only if a custom output style is set.

View File

@ -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