diff --git a/README.md b/README.md index 919c7e5d..46d76157 100644 --- a/README.md +++ b/README.md @@ -640,6 +640,39 @@ tasks: Dry run mode (`--dry`) compiles and steps through each task, printing the commands that would be run without executing them. This is useful for debugging your Taskfiles. +## Ignore errors + +You have the option to ignore errors during command execution. +Given the following Taskfile: + +```yml +version: '2' + +tasks: + echo: + cmds: + - exit 1 + - echo "Hello World" +``` + +Task will abort the execution after running `exit 1` because the status code `1` stands for `EXIT_FAILURE`. +However it is possible to continue with execution using `ignore_error`: + +```yml +version: '2' + +tasks: + echo: + cmds: + - cmd: exit 1 + ignore_error: true + - echo "Hello World" +``` + +`ignore_error` can also be set for a task, which mean errors will be supressed +for all commands. But keep in mind this option won't propagate to other tasks +called either by `deps` or `cmds`! + ## Output syntax By default, Task just redirect the STDOUT and STDERR of the running commands diff --git a/internal/taskfile/cmd.go b/internal/taskfile/cmd.go index f2bae2fc..ea267811 100644 --- a/internal/taskfile/cmd.go +++ b/internal/taskfile/cmd.go @@ -7,10 +7,11 @@ import ( // Cmd is a task command type Cmd struct { - Cmd string - Silent bool - Task string - Vars Vars + Cmd string + Silent bool + Task string + Vars Vars + IgnoreError bool } // Dep is a task dependency @@ -38,12 +39,14 @@ func (c *Cmd) UnmarshalYAML(unmarshal func(interface{}) error) error { return nil } var cmdStruct struct { - Cmd string - Silent bool + Cmd string + Silent bool + IgnoreError bool `yaml:"ignore_error"` } if err := unmarshal(&cmdStruct); err == nil && cmdStruct.Cmd != "" { c.Cmd = cmdStruct.Cmd c.Silent = cmdStruct.Silent + c.IgnoreError = cmdStruct.IgnoreError return nil } var taskCall struct { diff --git a/internal/taskfile/task.go b/internal/taskfile/task.go index cf15ea8a..4ebfd5e5 100644 --- a/internal/taskfile/task.go +++ b/internal/taskfile/task.go @@ -5,17 +5,18 @@ type Tasks map[string]*Task // Task represents a task type Task struct { - Task string - Cmds []*Cmd - Deps []*Dep - Desc string - Sources []string - Generates []string - Status []string - Dir string - Vars Vars - Env Vars - Silent bool - Method string - Prefix string + Task string + Cmds []*Cmd + Deps []*Dep + Desc string + Sources []string + Generates []string + Status []string + Dir string + Vars Vars + Env Vars + Silent bool + Method string + Prefix string + IgnoreError bool `yaml:"ignore_error"` } diff --git a/task.go b/task.go index 6f5dd434..71c7644f 100644 --- a/task.go +++ b/task.go @@ -19,6 +19,7 @@ import ( "github.com/Masterminds/semver" "golang.org/x/sync/errgroup" + "mvdan.cc/sh/interp" ) const ( @@ -181,6 +182,12 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error { if err2 := statusOnError(t); err2 != nil { e.Logger.VerboseErrf("task: error cleaning status on error: %v", err2) } + + if _, ok := err.(interp.ExitCode); ok && t.IgnoreError { + e.Logger.VerboseErrf("task: task error ignored: %v", err) + continue + } + return &taskRunError{t.Task, err} } } @@ -221,7 +228,7 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi defer stdOut.Close() defer stdErr.Close() - return execext.RunCommand(&execext.RunCommandOptions{ + err := execext.RunCommand(&execext.RunCommandOptions{ Context: ctx, Command: cmd.Cmd, Dir: t.Dir, @@ -230,6 +237,11 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi Stdout: stdOut, Stderr: stdErr, }) + if _, ok := err.(interp.ExitCode); ok && cmd.IgnoreError { + e.Logger.VerboseErrf("task: command error ignored: %v", err) + return nil + } + return err default: return nil } diff --git a/task_test.go b/task_test.go index 24d8c00e..58687cab 100644 --- a/task_test.go +++ b/task_test.go @@ -53,7 +53,6 @@ func (fct fileContentTest) Run(t *testing.T) { assert.Equal(t, expectContent, s, "unexpected file content") }) } - } func TestEnv(t *testing.T) { @@ -414,6 +413,22 @@ func TestTaskVersion(t *testing.T) { } } +func TestTaskIgnoreErrors(t *testing.T) { + const dir = "testdata/ignore_errors" + + e := task.Executor{ + Dir: dir, + Stdout: ioutil.Discard, + Stderr: ioutil.Discard, + } + assert.NoError(t, e.Setup()) + + assert.NoError(t, e.Run(taskfile.Call{Task: "task-should-pass"})) + assert.Error(t, e.Run(taskfile.Call{Task: "task-should-fail"})) + assert.NoError(t, e.Run(taskfile.Call{Task: "cmd-should-pass"})) + assert.Error(t, e.Run(taskfile.Call{Task: "cmd-should-fail"})) +} + func TestExpand(t *testing.T) { const dir = "testdata/expand" diff --git a/testdata/ignore_errors/Taskfile.yml b/testdata/ignore_errors/Taskfile.yml new file mode 100644 index 00000000..1d7dc8b5 --- /dev/null +++ b/testdata/ignore_errors/Taskfile.yml @@ -0,0 +1,20 @@ +version: '2' + +tasks: + task-should-pass: + cmds: + - exit 1 + ignore_error: true + + task-should-fail: + cmds: + - exit 1 + + cmd-should-pass: + cmds: + - cmd: exit 1 + ignore_error: true + + cmd-should-fail: + cmds: + - cmd: exit 1 diff --git a/variables.go b/variables.go index 3e8947d6..5968fb7b 100644 --- a/variables.go +++ b/variables.go @@ -24,17 +24,18 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) { r := templater.Templater{Vars: vars} new := taskfile.Task{ - Task: origTask.Task, - Desc: r.Replace(origTask.Desc), - Sources: r.ReplaceSlice(origTask.Sources), - Generates: r.ReplaceSlice(origTask.Generates), - Status: r.ReplaceSlice(origTask.Status), - Dir: r.Replace(origTask.Dir), - Vars: nil, - Env: r.ReplaceVars(origTask.Env), - Silent: origTask.Silent, - Method: r.Replace(origTask.Method), - Prefix: r.Replace(origTask.Prefix), + Task: origTask.Task, + Desc: r.Replace(origTask.Desc), + Sources: r.ReplaceSlice(origTask.Sources), + Generates: r.ReplaceSlice(origTask.Generates), + Status: r.ReplaceSlice(origTask.Status), + Dir: r.Replace(origTask.Dir), + Vars: nil, + Env: r.ReplaceVars(origTask.Env), + Silent: origTask.Silent, + Method: r.Replace(origTask.Method), + Prefix: r.Replace(origTask.Prefix), + IgnoreError: origTask.IgnoreError, } new.Dir, err = shell.Expand(new.Dir, nil) if err != nil { @@ -58,12 +59,12 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) { new.Cmds = make([]*taskfile.Cmd, len(origTask.Cmds)) for i, cmd := range origTask.Cmds { new.Cmds[i] = &taskfile.Cmd{ - Task: r.Replace(cmd.Task), - Silent: cmd.Silent, - Cmd: r.Replace(cmd.Cmd), - Vars: r.ReplaceVars(cmd.Vars), + Task: r.Replace(cmd.Task), + Silent: cmd.Silent, + Cmd: r.Replace(cmd.Cmd), + Vars: r.ReplaceVars(cmd.Vars), + IgnoreError: cmd.IgnoreError, } - } } if len(origTask.Deps) > 0 {