Skip to content

Commit 460b89c

Browse files
committed
Merge branch 'feat/suggest-similar-task'
2 parents a4ec6e5 + 3e5ee23 commit 460b89c

File tree

7 files changed

+51
-5
lines changed

7 files changed

+51
-5
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
- Add a "Did you mean ...?" suggestion when a task does not exits another one
6+
with a similar name is found
7+
([#867](https://github.com/go-task/task/issues/867), [#880](https://github.com/go-task/task/pull/880)).
58
- Now YAML parse errors will print which Taskfile failed to parse
69
([#885](https://github.com/go-task/task/issues/885), [#887](https://github.com/go-task/task/pull/887)).
710
- Add ability to set `aliases` for tasks and namespaces ([#268](https://github.com/go-task/task/pull/268), [#340](https://github.com/go-task/task/pull/340), [#879](https://github.com/go-task/task/pull/879)).

errors.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,20 @@ var (
1414
)
1515

1616
type taskNotFoundError struct {
17-
taskName string
17+
taskName string
18+
didYouMean string
1819
}
1920

2021
func (err *taskNotFoundError) Error() string {
21-
return fmt.Sprintf(`task: Task %q not found`, err.taskName)
22+
if err.didYouMean != "" {
23+
return fmt.Sprintf(
24+
`task: Task %q does not exist. Did you mean %q?`,
25+
err.taskName,
26+
err.didYouMean,
27+
)
28+
}
29+
30+
return fmt.Sprintf(`task: Task %q does not exist`, err.taskName)
2231
}
2332

2433
type multipleTasksWithAliasError struct {

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/mattn/go-zglob v0.0.3
88
github.com/mitchellh/hashstructure/v2 v2.0.2
99
github.com/radovskyb/watcher v1.0.7
10+
github.com/sajari/fuzzy v1.0.0
1011
github.com/spf13/pflag v1.0.5
1112
github.com/stretchr/testify v1.8.0
1213
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
2626
github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
2727
github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
2828
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
29+
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
30+
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
2931
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
3032
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
3133
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

help.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func (e *Executor) tasksWithDesc() (tasks []*taskfile.Task) {
8181
return
8282
}
8383

84-
// PrintTaskNames prints only the task names in a Taskfile.
84+
// ListTaskNames prints only the task names in a Taskfile.
8585
// Only tasks with a non-empty description are printed if allTasks is false.
8686
// Otherwise, all task names are printed.
8787
func (e *Executor) ListTaskNames(allTasks bool) {

setup.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
"github.com/go-task/task/v3/internal/output"
1818
"github.com/go-task/task/v3/taskfile"
1919
"github.com/go-task/task/v3/taskfile/read"
20+
21+
"github.com/sajari/fuzzy"
2022
)
2123

2224
func (e *Executor) Setup() error {
@@ -28,6 +30,8 @@ func (e *Executor) Setup() error {
2830
return err
2931
}
3032

33+
e.setupFuzzyModel()
34+
3135
v, err := e.Taskfile.ParsedVersion()
3236
if err != nil {
3337
return err
@@ -85,6 +89,25 @@ func (e *Executor) readTaskfile() error {
8589
return err
8690
}
8791

92+
func (e *Executor) setupFuzzyModel() {
93+
if e.Taskfile != nil {
94+
model := fuzzy.NewModel()
95+
model.SetThreshold(1) // because we want to build grammar based on every task name
96+
97+
var words []string
98+
for taskName := range e.Taskfile.Tasks {
99+
words = append(words, taskName)
100+
101+
for _, task := range e.Taskfile.Tasks {
102+
words = append(words, task.Aliases...)
103+
}
104+
}
105+
106+
model.Train(words)
107+
e.fuzzyModel = model
108+
}
109+
}
110+
88111
func (e *Executor) setupTempDir() error {
89112
if e.TempDir != "" {
90113
return nil

task.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/go-task/task/v3/internal/templater"
1717
"github.com/go-task/task/v3/taskfile"
1818

19+
"github.com/sajari/fuzzy"
1920
"golang.org/x/exp/slices"
2021
"golang.org/x/sync/errgroup"
2122
)
@@ -53,7 +54,8 @@ type Executor struct {
5354
Output output.Output
5455
OutputStyle taskfile.Output
5556

56-
taskvars *taskfile.Vars
57+
taskvars *taskfile.Vars
58+
fuzzyModel *fuzzy.Model
5759

5860
concurrencySemaphore chan struct{}
5961
taskCallCount map[string]*int32
@@ -71,6 +73,7 @@ func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error {
7173
e.ListTasksWithDesc()
7274
return err
7375
}
76+
7477
if task.Internal {
7578
e.ListTasksWithDesc()
7679
return &taskInternalError{taskName: call.Task}
@@ -359,8 +362,13 @@ func (e *Executor) GetTask(call taskfile.Call) (*taskfile.Task, error) {
359362
}
360363
// If we found no tasks
361364
if len(aliasedTasks) == 0 {
365+
didYouMean := ""
366+
if e.fuzzyModel != nil {
367+
didYouMean = e.fuzzyModel.SpellCheck(call.Task)
368+
}
362369
return nil, &taskNotFoundError{
363-
taskName: call.Task,
370+
taskName: call.Task,
371+
didYouMean: didYouMean,
364372
}
365373
}
366374

0 commit comments

Comments
 (0)