mirror of
				https://github.com/go-task/task.git
				synced 2025-10-30 23:58:01 +02:00 
			
		
		
		
	Add ability to set error_only: true on the group output mode
				
					
				
			This commit is contained in:
		| @@ -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{ | ||||||
|   | |||||||
| @@ -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. | | ||||||
|   | |||||||
| @@ -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: | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								docs/static/schema.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								docs/static/schema.json
									
									
									
									
										vendored
									
									
								
							| @@ -331,6 +331,11 @@ | |||||||
|               }, |               }, | ||||||
|               "end": { |               "end": { | ||||||
|                 "type": "string" |                 "type": "string" | ||||||
|  |               }, | ||||||
|  |               "error_only": { | ||||||
|  |                 "description": "Swallows command output on zero exit code", | ||||||
|  |                 "type": "boolean", | ||||||
|  |                 "default": false | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|   | |||||||
| @@ -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 { | ||||||
|   | |||||||
| @@ -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 } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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) { | ||||||
| @@ -32,6 +32,7 @@ func BuildFor(o *taskfile.Output) (Output, error) { | |||||||
| 		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 { | ||||||
|   | |||||||
| @@ -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()) | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								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) | 			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 | ||||||
|   | |||||||
							
								
								
									
										30
									
								
								task_test.go
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								task_test.go
									
									
									
									
									
								
							| @@ -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" | ||||||
|   | |||||||
| @@ -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. | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								testdata/output_group_error_only/Taskfile.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								testdata/output_group_error_only/Taskfile.yml
									
									
									
									
										vendored
									
									
										Normal 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 | ||||||
		Reference in New Issue
	
	Block a user