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.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{ | ||||
|   | ||||
| @@ -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. | | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
							
								
								
									
										5
									
								
								docs/static/schema.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								docs/static/schema.json
									
									
									
									
										vendored
									
									
								
							| @@ -331,6 +331,11 @@ | ||||
|               }, | ||||
|               "end": { | ||||
|                 "type": "string" | ||||
|               }, | ||||
|               "error_only": { | ||||
|                 "description": "Swallows command output on zero exit code", | ||||
|                 "type": "boolean", | ||||
|                 "default": false | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 } | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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()) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
							
								
								
									
										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) | ||||
| 		} | ||||
| 		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 | ||||
|   | ||||
							
								
								
									
										30
									
								
								task_test.go
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								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" | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
							
								
								
									
										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