Skip to content

Commit 618cd89

Browse files
authored
feat: wildcard match aliases (#2234)
1 parent b53e5da commit 618cd89

5 files changed

Lines changed: 87 additions & 50 deletions

File tree

task.go

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -404,19 +404,40 @@ func (e *Executor) startExecution(ctx context.Context, t *ast.Task, execute func
404404
}
405405

406406
// FindMatchingTasks returns a list of tasks that match the given call. A task
407-
// matches a call if its name is equal to the call's task name or if it matches
407+
// matches a call if its name is equal to the call's task name, or one of aliases, or if it matches
408408
// a wildcard pattern. The function returns a list of MatchingTask structs, each
409409
// containing a task and a list of wildcards that were matched.
410-
func (e *Executor) FindMatchingTasks(call *Call) []*MatchingTask {
410+
// If multiple tasks match due to aliases, a TaskNameConflictError is returned.
411+
func (e *Executor) FindMatchingTasks(call *Call) ([]*MatchingTask, error) {
411412
if call == nil {
412-
return nil
413+
return nil, nil
413414
}
414415
var matchingTasks []*MatchingTask
415416
// If there is a direct match, return it
416417
if task, ok := e.Taskfile.Tasks.Get(call.Task); ok {
417418
matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil})
418-
return matchingTasks
419+
return matchingTasks, nil
420+
}
421+
var aliasedTasks []string
422+
for task := range e.Taskfile.Tasks.Values(nil) {
423+
if slices.Contains(task.Aliases, call.Task) {
424+
aliasedTasks = append(aliasedTasks, task.Task)
425+
matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil})
426+
}
419427
}
428+
429+
if len(aliasedTasks) == 1 {
430+
return matchingTasks, nil
431+
}
432+
433+
// If we found multiple tasks
434+
if len(aliasedTasks) > 1 {
435+
return nil, &errors.TaskNameConflictError{
436+
Call: call.Task,
437+
TaskNames: aliasedTasks,
438+
}
439+
}
440+
420441
// Attempt a wildcard match
421442
for _, value := range e.Taskfile.Tasks.All(nil) {
422443
if match, wildcards := value.WildcardMatch(call.Task); match {
@@ -426,15 +447,19 @@ func (e *Executor) FindMatchingTasks(call *Call) []*MatchingTask {
426447
})
427448
}
428449
}
429-
return matchingTasks
450+
return matchingTasks, nil
430451
}
431452

432453
// GetTask will return the task with the name matching the given call from the taskfile.
433454
// If no task is found, it will search for tasks with a matching alias.
434455
// If multiple tasks contain the same alias or no matches are found an error is returned.
435456
func (e *Executor) GetTask(call *Call) (*ast.Task, error) {
436457
// Search for a matching task
437-
matchingTasks := e.FindMatchingTasks(call)
458+
matchingTasks, err := e.FindMatchingTasks(call)
459+
if err != nil {
460+
return nil, err
461+
}
462+
438463
if len(matchingTasks) > 0 {
439464
if call.Vars == nil {
440465
call.Vars = ast.NewVars()
@@ -443,37 +468,18 @@ func (e *Executor) GetTask(call *Call) (*ast.Task, error) {
443468
return matchingTasks[0].Task, nil
444469
}
445470

446-
// If didn't find one, search for a task with a matching alias
447-
var matchingTask *ast.Task
448-
var aliasedTasks []string
449-
for task := range e.Taskfile.Tasks.Values(nil) {
450-
if slices.Contains(task.Aliases, call.Task) {
451-
aliasedTasks = append(aliasedTasks, task.Task)
452-
matchingTask = task
453-
}
454-
}
455-
// If we found multiple tasks
456-
if len(aliasedTasks) > 1 {
457-
return nil, &errors.TaskNameConflictError{
458-
Call: call.Task,
459-
TaskNames: aliasedTasks,
460-
}
461-
}
462471
// If we found no tasks
463-
if len(aliasedTasks) == 0 {
464-
didYouMean := ""
465-
if !e.DisableFuzzy {
466-
e.fuzzyModelOnce.Do(e.setupFuzzyModel)
467-
if e.fuzzyModel != nil {
468-
didYouMean = e.fuzzyModel.SpellCheck(call.Task)
469-
}
470-
}
471-
return nil, &errors.TaskNotFoundError{
472-
TaskName: call.Task,
473-
DidYouMean: didYouMean,
472+
didYouMean := ""
473+
if !e.DisableFuzzy {
474+
e.fuzzyModelOnce.Do(e.setupFuzzyModel)
475+
if e.fuzzyModel != nil {
476+
didYouMean = e.fuzzyModel.SpellCheck(call.Task)
474477
}
475478
}
476-
return matchingTask, nil
479+
return nil, &errors.TaskNotFoundError{
480+
TaskName: call.Task,
481+
DidYouMean: didYouMean,
482+
}
477483
}
478484

479485
type FilterFunc func(task *ast.Task) bool

task_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2612,6 +2612,11 @@ func TestWildcard(t *testing.T) {
26122612
call: "start-foo",
26132613
expectedOutput: "Starting foo\n",
26142614
},
2615+
{
2616+
name: "alias",
2617+
call: "s-foo",
2618+
expectedOutput: "Starting foo\n",
2619+
},
26152620
{
26162621
name: "matches exactly",
26172622
call: "matches-exactly-*",

taskfile/ast/task.go

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -70,26 +70,29 @@ func (t *Task) LocalName() string {
7070

7171
// WildcardMatch will check if the given string matches the name of the Task and returns any wildcard values.
7272
func (t *Task) WildcardMatch(name string) (bool, []string) {
73-
// Convert the name into a regex string
74-
regexStr := fmt.Sprintf("^%s$", strings.ReplaceAll(t.Task, "*", "(.*)"))
75-
regex := regexp.MustCompile(regexStr)
76-
wildcards := regex.FindStringSubmatch(name)
77-
wildcardCount := strings.Count(t.Task, "*")
78-
79-
// If there are no wildcards, return false
80-
if len(wildcards) == 0 {
81-
return false, nil
82-
}
73+
names := append([]string{t.Task}, t.Aliases...)
74+
75+
for _, taskName := range names {
76+
regexStr := fmt.Sprintf("^%s$", strings.ReplaceAll(taskName, "*", "(.*)"))
77+
regex := regexp.MustCompile(regexStr)
78+
wildcards := regex.FindStringSubmatch(name)
79+
80+
if len(wildcards) == 0 {
81+
continue
82+
}
83+
84+
// Remove the first match, which is the full string
85+
wildcards = wildcards[1:]
86+
wildcardCount := strings.Count(taskName, "*")
8387

84-
// Remove the first match, which is the full string
85-
wildcards = wildcards[1:]
88+
if len(wildcards) != wildcardCount {
89+
continue
90+
}
8691

87-
// If there are more/less wildcards than matches, return false
88-
if len(wildcards) != wildcardCount {
89-
return false, wildcards
92+
return true, wildcards
9093
}
9194

92-
return true, wildcards
95+
return false, nil
9396
}
9497

9598
func (t *Task) UnmarshalYAML(node *yaml.Node) error {

testdata/wildcards/Taskfile.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ tasks:
1919
- "echo \"I don't consume matches: {{.MATCH}}\""
2020

2121
start-*:
22+
aliases:
23+
- s-*
2224
vars:
2325
SERVICE: "{{index .MATCH 0}}"
2426
cmds:

website/src/docs/guide.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1792,6 +1792,27 @@ $ task start:foo:3
17921792
Starting foo with 3 replicas
17931793
```
17941794

1795+
Using wildcards with aliases
1796+
Wildcards also work with aliases. If a task has an alias, you can use the alias name with wildcards to capture arguments. For example:
1797+
1798+
```yaml
1799+
version: '3'
1800+
1801+
tasks:
1802+
start:*:
1803+
aliases: [run:*]
1804+
vars:
1805+
SERVICE: "{{index .MATCH 0}}"
1806+
cmds:
1807+
- echo "Running {{.SERVICE}}"
1808+
```
1809+
In this example, you can call the task using the alias run:*:
1810+
1811+
```shell
1812+
$ task run:foo
1813+
Running foo
1814+
```
1815+
17951816
## Doing task cleanup with `defer`
17961817

17971818
With the `defer` keyword, it's possible to schedule cleanup to be run once the

0 commit comments

Comments
 (0)