1
0
mirror of https://github.com/go-task/task.git synced 2025-04-27 12:32:25 +02:00

feat: set and shopt directives (#929)

Co-authored-by: Andrey Nering <andrey@nering.com.br>
This commit is contained in:
Pete Davison 2023-01-14 13:41:56 -06:00 committed by GitHub
parent 4be1050234
commit 1c1be683ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 308 additions and 19 deletions

View File

@ -2,6 +2,9 @@
## Unreleased ## Unreleased
- Add ability to configure options for the [`set`](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html)
and [`shopt`](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html) builtins
([#908](https://github.com/go-task/task/issues/908), [#929](https://github.com/go-task/task/pull/929) by @pd93, [Documentation](http://taskfile.dev/usage/#set-and-shopt)).
- Add new `platforms:` attribute to `task` and `cmd`, so it's now possible to - Add new `platforms:` attribute to `task` and `cmd`, so it's now possible to
choose in which platforms that given task or command will be run on. Possible choose in which platforms that given task or command will be run on. Possible
values are operating system (GOOS), architecture (GOARCH) or a combination of values are operating system (GOOS), architecture (GOARCH) or a combination of

View File

@ -91,6 +91,8 @@ Some environment variables can be overriden to adjust Task behavior.
| `dotenv` | `[]string` | | A list of `.env` file paths to be parsed. | | `dotenv` | `[]string` | | A list of `.env` file paths to be parsed. |
| `run` | `string` | `always` | Default 'run' option for this Taskfile. Available options: `always`, `once` and `when_changed`. | | `run` | `string` | `always` | Default 'run' option for this Taskfile. Available options: `always`, `once` and `when_changed`. |
| `interval` | `string` | `5s` | Sets a different watch interval when using `--watch`, the default being 5 seconds. This string should be a valid [Go Duration](https://pkg.go.dev/time#ParseDuration). | | `interval` | `string` | `5s` | Sets a different watch interval when using `--watch`, the default being 5 seconds. This string should be a valid [Go Duration](https://pkg.go.dev/time#ParseDuration). |
| `set` | `[]string` | | Specify options for the [`set` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html). |
| `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html). |
### Include ### Include
@ -140,6 +142,8 @@ includes:
| `ignore_error` | `bool` | `false` | Continue execution if errors happen while executing commands. | | `ignore_error` | `bool` | `false` | Continue execution if errors happen while executing commands. |
| `run` | `string` | The one declared globally in the Taskfile or `always` | Specifies whether the task should run again or not if called more than once. Available options: `always`, `once` and `when_changed`. | | `run` | `string` | The one declared globally in the Taskfile or `always` | Specifies whether the task should run again or not if called more than once. Available options: `always`, `once` and `when_changed`. |
| `platforms` | `[]string` | All platforms | Specifies which platforms the task should be run on. [Valid GOOS and GOARCH values allowed](https://github.com/golang/go/blob/master/src/go/build/syslist.go). Task will be skipped otherwise. | | `platforms` | `[]string` | All platforms | Specifies which platforms the task should be run on. [Valid GOOS and GOARCH values allowed](https://github.com/golang/go/blob/master/src/go/build/syslist.go). Task will be skipped otherwise. |
| `set` | `[]string` | | Specify options for the [`set` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html). |
| `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html). |
:::info :::info
@ -191,6 +195,8 @@ tasks:
| `ignore_error` | `bool` | `false` | Continue execution if errors happen while executing the command. | | `ignore_error` | `bool` | `false` | Continue execution if errors happen while executing the command. |
| `defer` | `string` | | Alternative to `cmd`, but schedules the command to be executed at the end of this task instead of immediately. This cannot be used together with `cmd`. | | `defer` | `string` | | Alternative to `cmd`, but schedules the command to be executed at the end of this task instead of immediately. This cannot be used together with `cmd`. |
| `platforms` | `[]string` | All platforms | Specifies which platforms the command should be run on. [Valid GOOS and GOARCH values allowed](https://github.com/golang/go/blob/master/src/go/build/syslist.go). Command will be skipped otherwise. | | `platforms` | `[]string` | All platforms | Specifies which platforms the command should be run on. [Valid GOOS and GOARCH values allowed](https://github.com/golang/go/blob/master/src/go/build/syslist.go). Command will be skipped otherwise. |
| `set` | `[]string` | | Specify options for the [`set` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html). |
| `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html). |
:::info :::info

View File

@ -1420,6 +1420,31 @@ tasks:
- ./app{{exeExt}} -h localhost -p 8080 - ./app{{exeExt}} -h localhost -p 8080
``` ```
## `set` and `shopt`
It's possible to specify options to the
[`set`](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html)
and [`shopt`](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html)
builtins. This can be added at global, task or command level.
```yaml
version: '2'
set: [pipefail]
shopt: [globstar]
tasks:
# `globstar` required for double star globs to work
default: echo **/*.go
```
:::info
Keep in mind that not all options are available in the
[shell interpreter library](https://github.com/mvdan/sh) that Task uses.
:::
## Watch tasks ## Watch tasks
With the flags `--watch` or `-w` task will watch for file changes With the flags `--watch` or `-w` task will watch for file changes

View File

@ -108,6 +108,20 @@
"description": "The directory in which this task should run. Defaults to the current working directory.", "description": "The directory in which this task should run. Defaults to the current working directory.",
"type": "string" "type": "string"
}, },
"set": {
"description": "Enables POSIX shell options for all of a task's commands. See https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/3/set"
}
},
"shopt": {
"description": "Enables Bash shell options for all of a task's commands. See https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/3/shopt"
}
},
"vars": { "vars": {
"description": "A set of variables that can be used in the task.", "description": "A set of variables that can be used in the task.",
"$ref": "#/definitions/3/vars" "$ref": "#/definitions/3/vars"
@ -184,6 +198,14 @@
} }
] ]
}, },
"set": {
"type": "string",
"enum": ["allexport", "a", "errexit", "e", "noexec", "n", "noglob", "f", "nounset", "u", "xtrace", "x", "pipefail"]
},
"shopt": {
"type": "string",
"enum": ["expand_aliases", "globstar", "nullglob"]
},
"vars": { "vars": {
"type": "object", "type": "object",
"patternProperties": { "patternProperties": {
@ -233,6 +255,20 @@
"description": "Silent mode disables echoing of command before Task runs it", "description": "Silent mode disables echoing of command before Task runs it",
"type": "boolean" "type": "boolean"
}, },
"set": {
"description": "Enables POSIX shell options for this command. See https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/3/set"
}
},
"shopt": {
"description": "Enables Bash shell options for this command. See https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/3/shopt"
}
},
"ignore_error": { "ignore_error": {
"description": "Prevent command from aborting the execution of task even after receiving a status code of 1", "description": "Prevent command from aborting the execution of task even after receiving a status code of 1",
"type": "boolean" "type": "boolean"
@ -371,6 +407,20 @@
"description": "Default 'silent' options for this Taskfile. If `false`, can be overidden with `true` in a task by task basis.", "description": "Default 'silent' options for this Taskfile. If `false`, can be overidden with `true` in a task by task basis.",
"type": "boolean" "type": "boolean"
}, },
"set": {
"description": "Enables POSIX shell options for all commands in the Taskfile. See https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/3/set"
}
},
"shopt": {
"description": "Enables Bash shell options for all commands in the Taskfile. See https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/3/shopt"
}
},
"dotenv": { "dotenv": {
"type": "array", "type": "array",
"description": "A list of `.env` file paths to be parsed.", "description": "A list of `.env` file paths to be parsed.",

View File

@ -3,6 +3,7 @@ package execext
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
@ -20,6 +21,8 @@ type RunCommandOptions struct {
Command string Command string
Dir string Dir string
Env []string Env []string
PosixOpts []string
BashOpts []string
Stdin io.Reader Stdin io.Reader
Stdout io.Writer Stdout io.Writer
Stderr io.Writer Stderr io.Writer
@ -36,9 +39,18 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
return ErrNilOptions return ErrNilOptions
} }
p, err := syntax.NewParser().Parse(strings.NewReader(opts.Command), "") // Set "-e" or "errexit" by default
if err != nil { opts.PosixOpts = append(opts.PosixOpts, "e")
return err
// Format POSIX options into a slice that mvdan/sh understands
var params []string
for _, opt := range opts.PosixOpts {
if len(opt) == 1 {
params = append(params, fmt.Sprintf("-%s", opt))
} else {
params = append(params, "-o")
params = append(params, opt)
}
} }
environ := opts.Env environ := opts.Env
@ -47,7 +59,7 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
} }
r, err := interp.New( r, err := interp.New(
interp.Params("-e"), interp.Params(params...),
interp.Env(expand.ListEnviron(environ...)), interp.Env(expand.ListEnviron(environ...)),
interp.ExecHandler(interp.DefaultExecHandler(15*time.Second)), interp.ExecHandler(interp.DefaultExecHandler(15*time.Second)),
interp.OpenHandler(openHandler), interp.OpenHandler(openHandler),
@ -58,6 +70,25 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
return err return err
} }
parser := syntax.NewParser()
// Run any shopt commands
if len(opts.BashOpts) > 0 {
shoptCmdStr := fmt.Sprintf("shopt -s %s", strings.Join(opts.BashOpts, " "))
shoptCmd, err := parser.Parse(strings.NewReader(shoptCmdStr), "")
if err != nil {
return err
}
if err := r.Run(ctx, shoptCmd); err != nil {
return err
}
}
// Run the user-defined command
p, err := parser.Parse(strings.NewReader(opts.Command), "")
if err != nil {
return err
}
return r.Run(ctx, p) return r.Run(ctx, p)
} }

View File

@ -0,0 +1,20 @@
package slicesext
import (
"golang.org/x/exp/constraints"
"golang.org/x/exp/slices"
)
func UniqueJoin[T constraints.Ordered](ss ...[]T) []T {
var length int
for _, s := range ss {
length += len(s)
}
r := make([]T, length)
var i int
for _, s := range ss {
i += copy(r[i:], s)
}
slices.Sort(r)
return slices.Compact(r)
}

View File

@ -16,6 +16,7 @@ import (
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/internal/output"
"github.com/go-task/task/v3/internal/slicesext"
"github.com/go-task/task/v3/internal/summary" "github.com/go-task/task/v3/internal/summary"
"github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile" "github.com/go-task/task/v3/taskfile"
@ -283,7 +284,7 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
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() { defer func() {
if err := close(); err != nil { if err := close(); err != nil {
e.Logger.Errf(logger.Red, "task: unable to close writter: %v", err) e.Logger.Errf(logger.Red, "task: unable to close writer: %v", err)
} }
}() }()
@ -291,6 +292,8 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
Command: cmd.Cmd, Command: cmd.Cmd,
Dir: t.Dir, Dir: t.Dir,
Env: getEnviron(t), Env: getEnviron(t),
PosixOpts: slicesext.UniqueJoin(e.Taskfile.Set, t.Set, cmd.Set),
BashOpts: slicesext.UniqueJoin(e.Taskfile.Shopt, t.Shopt, cmd.Shopt),
Stdin: e.Stdin, Stdin: e.Stdin,
Stdout: stdOut, Stdout: stdOut,
Stderr: stdErr, Stderr: stdErr,

View File

@ -1696,6 +1696,7 @@ func TestUserWorkingDirectory(t *testing.T) {
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"})) assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"}))
assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String()) assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String())
} }
func TestPlatforms(t *testing.T) { func TestPlatforms(t *testing.T) {
var buff bytes.Buffer var buff bytes.Buffer
e := task.Executor{ e := task.Executor{
@ -1707,3 +1708,87 @@ func TestPlatforms(t *testing.T) {
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build-" + runtime.GOOS})) assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build-" + runtime.GOOS}))
assert.Equal(t, fmt.Sprintf("task: [build-%s] echo 'Running task on %s'\nRunning task on %s\n", runtime.GOOS, runtime.GOOS, runtime.GOOS), buff.String()) assert.Equal(t, fmt.Sprintf("task: [build-%s] echo 'Running task on %s'\nRunning task on %s\n", runtime.GOOS, runtime.GOOS, runtime.GOOS), buff.String())
} }
func TestPOSIXShellOptsGlobalLevel(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/shopts/global_level",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
err := e.Run(context.Background(), taskfile.Call{Task: "pipefail"})
assert.NoError(t, err)
assert.Equal(t, "pipefail\ton\n", buff.String())
}
func TestPOSIXShellOptsTaskLevel(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/shopts/task_level",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
err := e.Run(context.Background(), taskfile.Call{Task: "pipefail"})
assert.NoError(t, err)
assert.Equal(t, "pipefail\ton\n", buff.String())
}
func TestPOSIXShellOptsCommandLevel(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/shopts/command_level",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
err := e.Run(context.Background(), taskfile.Call{Task: "pipefail"})
assert.NoError(t, err)
assert.Equal(t, "pipefail\ton\n", buff.String())
}
func TestBashShellOptsGlobalLevel(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/shopts/global_level",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
err := e.Run(context.Background(), taskfile.Call{Task: "globstar"})
assert.NoError(t, err)
assert.Equal(t, "globstar\ton\n", buff.String())
}
func TestBashShellOptsTaskLevel(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/shopts/task_level",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
err := e.Run(context.Background(), taskfile.Call{Task: "globstar"})
assert.NoError(t, err)
assert.Equal(t, "globstar\ton\n", buff.String())
}
func TestBashShellOptsCommandLevel(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/shopts/command_level",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
err := e.Run(context.Background(), taskfile.Call{Task: "globstar"})
assert.NoError(t, err)
assert.Equal(t, "globstar\ton\n", buff.String())
}

View File

@ -11,6 +11,8 @@ type Cmd struct {
Cmd string Cmd string
Silent bool Silent bool
Task string Task string
Set []string
Shopt []string
Vars *Vars Vars *Vars
IgnoreError bool IgnoreError bool
Defer bool Defer bool
@ -40,12 +42,16 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
var cmdStruct struct { var cmdStruct struct {
Cmd string Cmd string
Silent bool Silent bool
Set []string
Shopt []string
IgnoreError bool `yaml:"ignore_error"` IgnoreError bool `yaml:"ignore_error"`
Platforms []*Platform Platforms []*Platform
} }
if err := node.Decode(&cmdStruct); err == nil && cmdStruct.Cmd != "" { if err := node.Decode(&cmdStruct); err == nil && cmdStruct.Cmd != "" {
c.Cmd = cmdStruct.Cmd c.Cmd = cmdStruct.Cmd
c.Silent = cmdStruct.Silent c.Silent = cmdStruct.Silent
c.Set = cmdStruct.Set
c.Shopt = cmdStruct.Shopt
c.IgnoreError = cmdStruct.IgnoreError c.IgnoreError = cmdStruct.IgnoreError
c.Platforms = cmdStruct.Platforms c.Platforms = cmdStruct.Platforms
return nil return nil

View File

@ -23,6 +23,8 @@ type Task struct {
Status []string Status []string
Preconditions []*Precondition Preconditions []*Precondition
Dir string Dir string
Set []string
Shopt []string
Vars *Vars Vars *Vars
Env *Vars Env *Vars
Dotenv []string Dotenv []string
@ -81,6 +83,8 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
Status []string Status []string
Preconditions []*Precondition Preconditions []*Precondition
Dir string Dir string
Set []string
Shopt []string
Vars *Vars Vars *Vars
Env *Vars Env *Vars
Dotenv []string Dotenv []string
@ -107,6 +111,8 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
t.Status = task.Status t.Status = task.Status
t.Preconditions = task.Preconditions t.Preconditions = task.Preconditions
t.Dir = task.Dir t.Dir = task.Dir
t.Set = task.Set
t.Shopt = task.Shopt
t.Vars = task.Vars t.Vars = task.Vars
t.Env = task.Env t.Env = task.Env
t.Dotenv = task.Dotenv t.Dotenv = task.Dotenv
@ -140,6 +146,8 @@ func (t *Task) DeepCopy() *Task {
Status: deepCopySlice(t.Status), Status: deepCopySlice(t.Status),
Preconditions: deepCopySlice(t.Preconditions), Preconditions: deepCopySlice(t.Preconditions),
Dir: t.Dir, Dir: t.Dir,
Set: deepCopySlice(t.Set),
Shopt: deepCopySlice(t.Shopt),
Vars: t.Vars.DeepCopy(), Vars: t.Vars.DeepCopy(),
Env: t.Env.DeepCopy(), Env: t.Env.DeepCopy(),
Dotenv: deepCopySlice(t.Dotenv), Dotenv: deepCopySlice(t.Dotenv),

View File

@ -15,6 +15,8 @@ type Taskfile struct {
Output Output Output Output
Method string Method string
Includes *IncludedTaskfiles Includes *IncludedTaskfiles
Set []string
Shopt []string
Vars *Vars Vars *Vars
Env *Vars Env *Vars
Tasks Tasks Tasks Tasks
@ -34,6 +36,8 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
Output Output Output Output
Method string Method string
Includes *IncludedTaskfiles Includes *IncludedTaskfiles
Set []string
Shopt []string
Vars *Vars Vars *Vars
Env *Vars Env *Vars
Tasks Tasks Tasks Tasks
@ -50,6 +54,8 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
tf.Output = taskfile.Output tf.Output = taskfile.Output
tf.Method = taskfile.Method tf.Method = taskfile.Method
tf.Includes = taskfile.Includes tf.Includes = taskfile.Includes
tf.Set = taskfile.Set
tf.Shopt = taskfile.Shopt
tf.Vars = taskfile.Vars tf.Vars = taskfile.Vars
tf.Env = taskfile.Env tf.Env = taskfile.Env
tf.Tasks = taskfile.Tasks tf.Tasks = taskfile.Tasks

View File

@ -0,0 +1,14 @@
version: '3'
silent: true
tasks:
pipefail:
cmds:
- cmd: set -o | grep pipefail
set: [pipefail]
globstar:
cmds:
- cmd: shopt | grep globstar
shopt: [globstar]

View File

@ -0,0 +1,14 @@
version: '3'
silent: true
set: [pipefail]
shopt: [globstar]
tasks:
pipefail:
cmds:
- set -o | grep pipefail
globstar:
cmds:
- shopt | grep globstar

14
testdata/shopts/task_level/Taskfile.yml vendored Normal file
View File

@ -0,0 +1,14 @@
version: '3'
silent: true
tasks:
pipefail:
set: [pipefail]
cmds:
- set -o | grep pipefail
globstar:
shopt: [globstar]
cmds:
- shopt | grep globstar

View File

@ -56,6 +56,8 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
Sources: r.ReplaceSlice(origTask.Sources), Sources: r.ReplaceSlice(origTask.Sources),
Generates: r.ReplaceSlice(origTask.Generates), Generates: r.ReplaceSlice(origTask.Generates),
Dir: r.Replace(origTask.Dir), Dir: r.Replace(origTask.Dir),
Set: origTask.Set,
Shopt: origTask.Shopt,
Vars: nil, Vars: nil,
Env: nil, Env: nil,
Dotenv: r.ReplaceSlice(origTask.Dotenv), Dotenv: r.ReplaceSlice(origTask.Dotenv),
@ -125,9 +127,11 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
continue continue
} }
new.Cmds = append(new.Cmds, &taskfile.Cmd{ new.Cmds = append(new.Cmds, &taskfile.Cmd{
Task: r.Replace(cmd.Task),
Silent: cmd.Silent,
Cmd: r.Replace(cmd.Cmd), Cmd: r.Replace(cmd.Cmd),
Silent: cmd.Silent,
Task: r.Replace(cmd.Task),
Set: cmd.Set,
Shopt: cmd.Shopt,
Vars: r.ReplaceVars(cmd.Vars), Vars: r.ReplaceVars(cmd.Vars),
IgnoreError: cmd.IgnoreError, IgnoreError: cmd.IgnoreError,
Defer: cmd.Defer, Defer: cmd.Defer,