Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: wildcard matching for task names #1489

Merged
merged 5 commits into from
Feb 22, 2024
Merged
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
6 changes: 3 additions & 3 deletions args/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import (
)

// Parse parses command line argument: tasks and global variables
func Parse(args ...string) ([]ast.Call, *ast.Vars) {
calls := []ast.Call{}
func Parse(args ...string) ([]*ast.Call, *ast.Vars) {
calls := []*ast.Call{}
globals := &ast.Vars{}

for _, arg := range args {
if !strings.Contains(arg, "=") {
calls = append(calls, ast.Call{Task: arg})
calls = append(calls, &ast.Call{Task: arg})
continue
}

Expand Down
16 changes: 8 additions & 8 deletions args/args_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,20 @@ import (
func TestArgs(t *testing.T) {
tests := []struct {
Args []string
ExpectedCalls []ast.Call
ExpectedCalls []*ast.Call
ExpectedGlobals *ast.Vars
}{
{
Args: []string{"task-a", "task-b", "task-c"},
ExpectedCalls: []ast.Call{
ExpectedCalls: []*ast.Call{
{Task: "task-a"},
{Task: "task-b"},
{Task: "task-c"},
},
},
{
Args: []string{"task-a", "FOO=bar", "task-b", "task-c", "BAR=baz", "BAZ=foo"},
ExpectedCalls: []ast.Call{
ExpectedCalls: []*ast.Call{
{Task: "task-a"},
{Task: "task-b"},
{Task: "task-c"},
Expand All @@ -45,7 +45,7 @@ func TestArgs(t *testing.T) {
},
{
Args: []string{"task-a", "CONTENT=with some spaces"},
ExpectedCalls: []ast.Call{
ExpectedCalls: []*ast.Call{
{Task: "task-a"},
},
ExpectedGlobals: &ast.Vars{
Expand All @@ -59,7 +59,7 @@ func TestArgs(t *testing.T) {
},
{
Args: []string{"FOO=bar", "task-a", "task-b"},
ExpectedCalls: []ast.Call{
ExpectedCalls: []*ast.Call{
{Task: "task-a"},
{Task: "task-b"},
},
Expand All @@ -74,15 +74,15 @@ func TestArgs(t *testing.T) {
},
{
Args: nil,
ExpectedCalls: []ast.Call{},
ExpectedCalls: []*ast.Call{},
},
{
Args: []string{},
ExpectedCalls: []ast.Call{},
ExpectedCalls: []*ast.Call{},
},
{
Args: []string{"FOO=bar", "BAR=baz"},
ExpectedCalls: []ast.Call{},
ExpectedCalls: []*ast.Call{},
ExpectedGlobals: &ast.Vars{
OrderedMap: omap.FromMapWithOrder(
map[string]ast.Var{
Expand Down
4 changes: 2 additions & 2 deletions cmd/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ func run() error {
}

var (
calls []ast.Call
calls []*ast.Call
globals *ast.Vars
)

Expand All @@ -304,7 +304,7 @@ func run() error {

// If there are no calls, run the default task instead
if len(calls) == 0 {
calls = append(calls, ast.Call{Task: "default"})
calls = append(calls, &ast.Call{Task: "default"})
}

globals.Set("CLI_ARGS", ast.Var{Value: cliArgs})
Expand Down
47 changes: 47 additions & 0 deletions docs/docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -1205,6 +1205,53 @@ tasks:
- yarn {{.CLI_ARGS}}
```

## Wildcard arguments

Another way to parse arguments into a task is to use a wildcard in your task's
name. Wildcards are denoted by an asterisk (`*`) and can be used multiple times
in a task's name to pass in multiple arguments.

Matching arguments will be captured and stored in the `.MATCH` variable and can
then be used in your task's commands like any other variable. This variable is
an array of strings and so will need to be indexed to access the individual
arguments. We suggest creating a named variable for each argument to make it
clear what they contain:

```yaml
version: '3'

tasks:
echo-*:
vars:
TEXT: '{{index .MATCH 0}}'
cmds:
- echo {{.TEXT}}

run-*-*:
vars:
ARG_1: '{{index .MATCH 0}}'
ARG_2: '{{index .MATCH 1}}'
cmds:
- echo {{.ARG_1}} {{.ARG_2}}
```

```shell
# This call matches the "echo-*" task and the string "hello" is captured by the
# wildcard and stored in the .MATCH variable. We then index the .MATCH array and
# store the result in the .TEXT variable which is then echoed out in the cmds.
$ task echo-hello
hello
# You can use whitespace in your arguments as long as you quote the task name
$ task "echo-hello world"
hello world
# And you can pass multiple arguments
$ task run-foo-bar
foo bar
```

If multiple matching tasks are found, an error occurs. If you are using included
Taskfiles, tasks in parent files will be considered first.

## Doing task cleanup with `defer`

With the `defer` keyword, it's possible to schedule cleanup to be run once the
Expand Down
6 changes: 3 additions & 3 deletions errors/errors_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ func (err *TaskInternalError) Code() int {
return CodeTaskInternal
}

// TaskNameConflictError is returned when multiple tasks with the same name or
// TaskNameConflictError is returned when multiple tasks with a matching name or
// alias are found.
type TaskNameConflictError struct {
AliasName string
Call string
TaskNames []string
}

func (err *TaskNameConflictError) Error() string {
return fmt.Sprintf(`task: Multiple tasks (%s) with alias %q found`, strings.Join(err.TaskNames, ", "), err.AliasName)
return fmt.Sprintf(`task: Found multiple tasks (%s) that match %q`, strings.Join(err.TaskNames, ", "), err.Call)
}

func (err *TaskNameConflictError) Code() int {
Expand Down
8 changes: 4 additions & 4 deletions internal/compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ func (c *Compiler) GetTaskfileVariables() (*ast.Vars, error) {
return c.getVariables(nil, nil, true)
}

func (c *Compiler) GetVariables(t *ast.Task, call ast.Call) (*ast.Vars, error) {
return c.getVariables(t, &call, true)
func (c *Compiler) GetVariables(t *ast.Task, call *ast.Call) (*ast.Vars, error) {
return c.getVariables(t, call, true)
}

func (c *Compiler) FastGetVariables(t *ast.Task, call ast.Call) (*ast.Vars, error) {
return c.getVariables(t, &call, false)
func (c *Compiler) FastGetVariables(t *ast.Task, call *ast.Call) (*ast.Vars, error) {
return c.getVariables(t, call, false)
}

func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool) (*ast.Vars, error) {
Expand Down
2 changes: 1 addition & 1 deletion internal/summary/summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/go-task/task/v3/taskfile/ast"
)

func PrintTasks(l *logger.Logger, t *ast.Taskfile, c []ast.Call) {
func PrintTasks(l *logger.Logger, t *ast.Taskfile, c []*ast.Call) {
for i, call := range c {
PrintSpaceBetweenSummaries(l, i)
PrintTask(l, t.Tasks.Get(call.Task))
Expand Down
2 changes: 1 addition & 1 deletion internal/summary/summary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ func TestPrintAllWithSpaces(t *testing.T) {

summary.PrintTasks(&l,
&ast.Taskfile{Tasks: tasks},
[]ast.Call{{Task: "t1"}, {Task: "t2"}, {Task: "t3"}})
[]*ast.Call{{Task: "t1"}, {Task: "t2"}, {Task: "t3"}})

assert.True(t, strings.HasPrefix(buffer.String(), "task: t1"))
assert.Contains(t, buffer.String(), "\n(task does not have description or summary)\n\n\ntask: t2")
Expand Down
2 changes: 1 addition & 1 deletion requires.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/go-task/task/v3/taskfile/ast"
)

func (e *Executor) areTaskRequiredVarsSet(ctx context.Context, t *ast.Task, call ast.Call) error {
func (e *Executor) areTaskRequiredVarsSet(ctx context.Context, t *ast.Task, call *ast.Call) error {
if t.Requires == nil || len(t.Requires.Vars) == 0 {
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion status.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

// Status returns an error if any the of given tasks is not up-to-date
func (e *Executor) Status(ctx context.Context, calls ...ast.Call) error {
func (e *Executor) Status(ctx context.Context, calls ...*ast.Call) error {
for _, call := range calls {

// Compile the task
Expand Down
42 changes: 29 additions & 13 deletions task.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ type Executor struct {
}

// Run runs Task
func (e *Executor) Run(ctx context.Context, calls ...ast.Call) error {
func (e *Executor) Run(ctx context.Context, calls ...*ast.Call) error {
// check if given tasks exist
for _, call := range calls {
task, err := e.GetTask(call)
Expand Down Expand Up @@ -142,7 +142,7 @@ func (e *Executor) Run(ctx context.Context, calls ...ast.Call) error {
return nil
}

func (e *Executor) splitRegularAndWatchCalls(calls ...ast.Call) (regularCalls []ast.Call, watchCalls []ast.Call, err error) {
func (e *Executor) splitRegularAndWatchCalls(calls ...*ast.Call) (regularCalls []*ast.Call, watchCalls []*ast.Call, err error) {
for _, c := range calls {
t, err := e.GetTask(c)
if err != nil {
Expand All @@ -159,7 +159,7 @@ func (e *Executor) splitRegularAndWatchCalls(calls ...ast.Call) (regularCalls []
}

// RunTask runs a task by its name
func (e *Executor) RunTask(ctx context.Context, call ast.Call) error {
func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
t, err := e.FastCompiledTask(call)
if err != nil {
return err
Expand Down Expand Up @@ -296,7 +296,7 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
for _, d := range t.Deps {
d := d
g.Go(func() error {
err := e.RunTask(ctx, ast.Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true})
err := e.RunTask(ctx, &ast.Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true})
if err != nil {
return err
}
Expand All @@ -307,7 +307,7 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
return g.Wait()
}

func (e *Executor) runDeferred(t *ast.Task, call ast.Call, i int) {
func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

Expand All @@ -316,15 +316,15 @@ func (e *Executor) runDeferred(t *ast.Task, call ast.Call, i int) {
}
}

func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call ast.Call, i int) error {
func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *ast.Call, i int) error {
cmd := t.Cmds[i]

switch {
case cmd.Task != "":
reacquire := e.releaseConcurrencyLimit()
defer reacquire()

err := e.RunTask(ctx, ast.Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true})
err := e.RunTask(ctx, &ast.Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true})
if err != nil {
return err
}
Expand Down Expand Up @@ -413,14 +413,30 @@ func (e *Executor) startExecution(ctx context.Context, t *ast.Task, execute func
// GetTask will return the task with the name matching the given call from the taskfile.
// If no task is found, it will search for tasks with a matching alias.
// If multiple tasks contain the same alias or no matches are found an error is returned.
func (e *Executor) GetTask(call ast.Call) (*ast.Task, error) {
func (e *Executor) GetTask(call *ast.Call) (*ast.Task, error) {
// Search for a matching task
matchingTask := e.Taskfile.Tasks.Get(call.Task)
if matchingTask != nil {
return matchingTask, nil
matchingTasks := e.Taskfile.Tasks.FindMatchingTasks(call)
switch len(matchingTasks) {
case 0: // Carry on
case 1:
if call.Vars == nil {
call.Vars = &ast.Vars{}
}
call.Vars.Set("MATCH", ast.Var{Value: matchingTasks[0].Wildcards})
return matchingTasks[0].Task, nil
default:
taskNames := make([]string, len(matchingTasks))
for i, matchingTask := range matchingTasks {
taskNames[i] = matchingTask.Task.Task
}
return nil, &errors.TaskNameConflictError{
Call: call.Task,
TaskNames: taskNames,
}
}

// If didn't find one, search for a task with a matching alias
var matchingTask *ast.Task
var aliasedTasks []string
for _, task := range e.Taskfile.Tasks.Values() {
if slices.Contains(task.Aliases, call.Task) {
Expand All @@ -431,7 +447,7 @@ func (e *Executor) GetTask(call ast.Call) (*ast.Task, error) {
// If we found multiple tasks
if len(aliasedTasks) > 1 {
return nil, &errors.TaskNameConflictError{
AliasName: call.Task,
Call: call.Task,
TaskNames: aliasedTasks,
}
}
Expand Down Expand Up @@ -476,7 +492,7 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
idx := i
task := tasks[idx]
g.Go(func() error {
compiledTask, err := e.FastCompiledTask(ast.Call{Task: task.Task})
compiledTask, err := e.FastCompiledTask(&ast.Call{Task: task.Task})
if err != nil {
return err
}
Expand Down
Loading
Loading