1
0
mirror of https://github.com/go-task/task.git synced 2025-04-25 12:25:07 +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
- 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
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

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. |
| `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). |
| `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
@ -140,6 +142,8 @@ includes:
| `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`. |
| `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
@ -191,6 +195,8 @@ tasks:
| `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`. |
| `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

View File

@ -1420,6 +1420,31 @@ tasks:
- ./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
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.",
"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": {
"description": "A set of variables that can be used in the task.",
"$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": {
"type": "object",
"patternProperties": {
@ -233,6 +255,20 @@
"description": "Silent mode disables echoing of command before Task runs it",
"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": {
"description": "Prevent command from aborting the execution of task even after receiving a status code of 1",
"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.",
"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": {
"type": "array",
"description": "A list of `.env` file paths to be parsed.",

View File

@ -3,6 +3,7 @@ package execext
import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
@ -20,6 +21,8 @@ type RunCommandOptions struct {
Command string
Dir string
Env []string
PosixOpts []string
BashOpts []string
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
@ -36,9 +39,18 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
return ErrNilOptions
}
p, err := syntax.NewParser().Parse(strings.NewReader(opts.Command), "")
if err != nil {
return err
// Set "-e" or "errexit" by default
opts.PosixOpts = append(opts.PosixOpts, "e")
// 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
@ -47,7 +59,7 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
}
r, err := interp.New(
interp.Params("-e"),
interp.Params(params...),
interp.Env(expand.ListEnviron(environ...)),
interp.ExecHandler(interp.DefaultExecHandler(15*time.Second)),
interp.OpenHandler(openHandler),
@ -58,6 +70,25 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
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)
}

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/logger"
"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/templater"
"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)
defer func() {
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,
Dir: t.Dir,
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,
Stdout: stdOut,
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.Equal(t, fmt.Sprintf("%s\n", wd), buff.String())
}
func TestPlatforms(t *testing.T) {
var buff bytes.Buffer
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.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
Silent bool
Task string
Set []string
Shopt []string
Vars *Vars
IgnoreError bool
Defer bool
@ -40,12 +42,16 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
var cmdStruct struct {
Cmd string
Silent bool
Set []string
Shopt []string
IgnoreError bool `yaml:"ignore_error"`
Platforms []*Platform
}
if err := node.Decode(&cmdStruct); err == nil && cmdStruct.Cmd != "" {
c.Cmd = cmdStruct.Cmd
c.Silent = cmdStruct.Silent
c.Set = cmdStruct.Set
c.Shopt = cmdStruct.Shopt
c.IgnoreError = cmdStruct.IgnoreError
c.Platforms = cmdStruct.Platforms
return nil

View File

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

View File

@ -15,6 +15,8 @@ type Taskfile struct {
Output Output
Method string
Includes *IncludedTaskfiles
Set []string
Shopt []string
Vars *Vars
Env *Vars
Tasks Tasks
@ -34,6 +36,8 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
Output Output
Method string
Includes *IncludedTaskfiles
Set []string
Shopt []string
Vars *Vars
Env *Vars
Tasks Tasks
@ -50,6 +54,8 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
tf.Output = taskfile.Output
tf.Method = taskfile.Method
tf.Includes = taskfile.Includes
tf.Set = taskfile.Set
tf.Shopt = taskfile.Shopt
tf.Vars = taskfile.Vars
tf.Env = taskfile.Env
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),
Generates: r.ReplaceSlice(origTask.Generates),
Dir: r.Replace(origTask.Dir),
Set: origTask.Set,
Shopt: origTask.Shopt,
Vars: nil,
Env: nil,
Dotenv: r.ReplaceSlice(origTask.Dotenv),
@ -125,9 +127,11 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
continue
}
new.Cmds = append(new.Cmds, &taskfile.Cmd{
Task: r.Replace(cmd.Task),
Silent: cmd.Silent,
Cmd: r.Replace(cmd.Cmd),
Silent: cmd.Silent,
Task: r.Replace(cmd.Task),
Set: cmd.Set,
Shopt: cmd.Shopt,
Vars: r.ReplaceVars(cmd.Vars),
IgnoreError: cmd.IgnoreError,
Defer: cmd.Defer,