Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2758,6 +2758,67 @@ func TestBashShellOptsCommandLevel(t *testing.T) {
assert.Equal(t, "globstar\ton\n", buff.String())
}

func TestShellOptsInvalidShoptGlobalLevel(t *testing.T) {
t.Parallel()

var buff bytes.Buffer
e := task.NewExecutor(
task.WithDir("testdata/shopts/invalid_global_shopt"),
task.WithStdout(&buff),
task.WithStderr(&buff),
)

err := e.Setup()
require.Error(t, err)
assert.Contains(t, err.Error(), `invalid shopt option "pipefail" (did you mean to put it in "set"?)`)
}

func TestShellOptsInvalidSetTaskLevel(t *testing.T) {
t.Parallel()

var buff bytes.Buffer
e := task.NewExecutor(
task.WithDir("testdata/shopts/invalid_task_set"),
task.WithStdout(&buff),
task.WithStderr(&buff),
)

err := e.Setup()
require.Error(t, err)
assert.Contains(t, err.Error(), `invalid set option "globstar" (did you mean to put it in "shopt"?)`)
}

func TestShellOptsInvalidShoptCommandLevel(t *testing.T) {
t.Parallel()

var buff bytes.Buffer
e := task.NewExecutor(
task.WithDir("testdata/shopts/invalid_command_shopt"),
task.WithStdout(&buff),
task.WithStderr(&buff),
)

err := e.Setup()
require.Error(t, err)
assert.Contains(t, err.Error(), `invalid shopt option "not_a_real_option"`)
}

func TestShellOptsSetOptionsViaShopt(t *testing.T) {
t.Parallel()

var buff bytes.Buffer
e := task.NewExecutor(
task.WithDir("testdata/shopts/set_via_shopt"),
task.WithStdout(&buff),
task.WithStderr(&buff),
)
require.NoError(t, e.Setup())

err := e.Run(t.Context(), &task.Call{Task: "default"})
require.NoError(t, err)
assert.Equal(t, "hello\n", buff.String())
}

func TestSplitArgs(t *testing.T) {
t.Parallel()

Expand Down
6 changes: 6 additions & 0 deletions taskfile/ast/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
if err := node.Decode(&cmdStruct); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
if err := checkSetOptions(node, cmdStruct.Set); err != nil {
return err
}
if err := checkShoptOptions(node, cmdStruct.Shopt); err != nil {
return err
}
if cmdStruct.Defer != nil {

// A deferred command
Expand Down
75 changes: 75 additions & 0 deletions taskfile/ast/shellopts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package ast

import (
"slices"

"go.yaml.in/yaml/v3"

"github.com/go-task/task/v3/errors"
)

// The tables below mirror the unexported posixOptsTable and bashOptsTable in
// mvdan.cc/sh/v3/interp (see interp/api.go:
// https://github.com/mvdan/sh/blob/master/interp/api.go). They must be kept
// in sync whenever the mvdan.cc/sh dependency is updated.

// validSetOptions contains the one-character flag and full name forms of the
// POSIX shell options accepted by "set" (posixOptsTable).
var validSetOptions = []string{
"a", "allexport",
"e", "errexit",
"n", "noexec",
"f", "noglob",
"u", "nounset",
"x", "xtrace",
"pipefail",
}

// validShoptOptions contains the Bash shell options accepted by "shopt" (the
// entries of bashOptsTable with supported: true).
var validShoptOptions = []string{
"dotglob",
"expand_aliases",
"extglob",
"globstar",
"nocaseglob",
"nullglob",
}

// checkSetOptions checks that every entry of a "set" list is a POSIX shell
// option supported by the interpreter so that typos and unsupported options
// are reported when the Taskfile is parsed instead of when a command runs.
func checkSetOptions(node *yaml.Node, opts []string) error {
for _, opt := range opts {
if slices.Contains(validSetOptions, opt) {
continue
}
if slices.Contains(validShoptOptions, opt) {
return errors.NewTaskfileDecodeError(nil, node).WithMessage(`invalid set option %q (did you mean to put it in "shopt"?)`, opt)
}
return errors.NewTaskfileDecodeError(nil, node).WithMessage("invalid set option %q", opt)
}
return nil
}

// checkShoptOptions checks that every entry of a "shopt" list is a Bash shell
// option supported by the interpreter so that typos and unsupported options
// are reported when the Taskfile is parsed instead of when a command runs.
func checkShoptOptions(node *yaml.Node, opts []string) error {
for i, opt := range opts {
// A "-o" entry makes shopt address the "set" builtin options instead
// (e.g. `shopt: ["-o", "pipefail"]` runs `shopt -s -o pipefail`), so
// the remaining entries are validated as set options.
if opt == "-o" {
return checkSetOptions(node, opts[i+1:])
}
if slices.Contains(validShoptOptions, opt) {
continue
}
if slices.Contains(validSetOptions, opt) {
return errors.NewTaskfileDecodeError(nil, node).WithMessage(`invalid shopt option %q (did you mean to put it in "set"?)`, opt)
}
return errors.NewTaskfileDecodeError(nil, node).WithMessage("invalid shopt option %q", opt)
}
return nil
}
6 changes: 6 additions & 0 deletions taskfile/ast/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
if err := node.Decode(&task); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
if err := checkSetOptions(node, task.Set); err != nil {
return err
}
if err := checkShoptOptions(node, task.Shopt); err != nil {
return err
}
if task.Cmd != nil {
if task.Cmds != nil {
return errors.NewTaskfileDecodeError(nil, node).WithMessage("task cannot have both cmd and cmds")
Expand Down
6 changes: 6 additions & 0 deletions taskfile/ast/taskfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
if err := node.Decode(&taskfile); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
if err := checkSetOptions(node, taskfile.Set); err != nil {
return err
}
if err := checkShoptOptions(node, taskfile.Shopt); err != nil {
return err
}
tf.Version = taskfile.Version
tf.Output = taskfile.Output
tf.Method = taskfile.Method
Expand Down
9 changes: 9 additions & 0 deletions testdata/shopts/invalid_command_shopt/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: '3'

silent: true

tasks:
default:
cmds:
- cmd: echo hello
shopt: [not_a_real_option]
9 changes: 9 additions & 0 deletions testdata/shopts/invalid_global_shopt/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: '3'

silent: true
shopt: [pipefail]

tasks:
default:
cmds:
- echo hello
9 changes: 9 additions & 0 deletions testdata/shopts/invalid_task_set/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: '3'

silent: true

tasks:
default:
set: [globstar]
cmds:
- echo hello
9 changes: 9 additions & 0 deletions testdata/shopts/set_via_shopt/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: '3'

silent: true
shopt: ['-o', 'pipefail']

tasks:
default:
cmds:
- echo hello
11 changes: 10 additions & 1 deletion website/src/docs/reference/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,13 +201,17 @@ set: [errexit, nounset, pipefail]
### `shopt`

- **Type**: `[]string`
- **Options**: `expand_aliases`, `globstar`, `nullglob`
- **Options**: `dotglob`, `expand_aliases`, `extglob`, `globstar`,
`nocaseglob`, `nullglob`
- **Description**: Bash shell options for all commands

```yaml
shopt: [globstar]
```

Options given to `set` and `shopt` are validated when the Taskfile is parsed
and an error is returned if an option is not in the lists above.

## Include

Configuration for including external Taskfiles.
Expand Down Expand Up @@ -946,10 +950,15 @@ tasks:

Available `shopt` options for Bash features:

- `dotglob` - Include hidden files in glob expansion
- `expand_aliases` - Enable alias expansion
- `extglob` - Enable extended pattern matching
- `globstar` - Enable `**` recursive globbing
- `nocaseglob` - Case-insensitive glob expansion
- `nullglob` - Null glob expansion

Any other option is rejected with an error when the Taskfile is parsed.

```yaml
# Global level
shopt: [globstar]
Expand Down
9 changes: 8 additions & 1 deletion website/src/public/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,14 @@
},
"shopt": {
"type": "string",
"enum": ["expand_aliases", "globstar", "nullglob"]
"enum": [
"dotglob",
"expand_aliases",
"extglob",
"globstar",
"nocaseglob",
"nullglob"
]
},
"vars": {
"type": "object",
Expand Down