Skip to content

Commit

Permalink
feat: suggest the most similar task name when a given task does not e…
Browse files Browse the repository at this point in the history
…xist
  • Loading branch information
maxpushka committed Oct 10, 2022
1 parent d2061ec commit 3e5ee23
Show file tree
Hide file tree
Showing 7 changed files with 48 additions and 6 deletions.
13 changes: 11 additions & 2 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,20 @@ var (
)

type taskNotFoundError struct {
taskName string
taskName string
didYouMean string
}

func (err *taskNotFoundError) Error() string {
return fmt.Sprintf(`task: Task %q not found`, err.taskName)
if err.didYouMean != "" {
return fmt.Sprintf(
`task: Task %q does not exist. Did you mean %q?`,
err.taskName,
err.didYouMean,
)
}

return fmt.Sprintf(`task: Task %q does not exist`, err.taskName)
}

type taskInternalError struct {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/mattn/go-zglob v0.0.3
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/radovskyb/watcher v1.0.7
github.com/sajari/fuzzy v1.0.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
2 changes: 1 addition & 1 deletion help.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (e *Executor) tasksWithDesc() (tasks []*taskfile.Task) {
return
}

// PrintTaskNames prints only the task names in a Taskfile.
// ListTaskNames prints only the task names in a Taskfile.
// Only tasks with a non-empty description are printed if allTasks is false.
// Otherwise, all task names are printed.
func (e *Executor) ListTaskNames(allTasks bool) {
Expand Down
19 changes: 19 additions & 0 deletions setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"github.com/go-task/task/v3/internal/output"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/read"

"github.com/sajari/fuzzy"
)

func (e *Executor) Setup() error {
Expand All @@ -28,6 +30,8 @@ func (e *Executor) Setup() error {
return err
}

e.setupFuzzyModel()

v, err := e.Taskfile.ParsedVersion()
if err != nil {
return err
Expand Down Expand Up @@ -85,6 +89,21 @@ func (e *Executor) readTaskfile() error {
return err
}

func (e *Executor) setupFuzzyModel() {
if e.Taskfile != nil {
model := fuzzy.NewModel()
model.SetThreshold(1) // because we want to build grammar based on every task name

var words []string
for taskName := range e.Taskfile.Tasks {
words = append(words, taskName)
}

model.Train(words)
e.fuzzyModel = model
}
}

func (e *Executor) setupTempDir() error {
if e.TempDir != "" {
return nil
Expand Down
11 changes: 9 additions & 2 deletions task.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile"

"github.com/sajari/fuzzy"
"golang.org/x/sync/errgroup"
)

Expand Down Expand Up @@ -51,7 +52,8 @@ type Executor struct {
Output output.Output
OutputStyle taskfile.Output

taskvars *taskfile.Vars
taskvars *taskfile.Vars
fuzzyModel *fuzzy.Model

concurrencySemaphore chan struct{}
taskCallCount map[string]*int32
Expand All @@ -68,7 +70,12 @@ func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error {
if !ok {
// FIXME: move to the main package
e.ListTasksWithDesc()
return &taskNotFoundError{taskName: c.Task}

didYouMean := ""
if e.fuzzyModel != nil {
didYouMean = e.fuzzyModel.SpellCheck(c.Task)
}
return &taskNotFoundError{taskName: c.Task, didYouMean: didYouMean}
}
if t.Internal {
e.ListTasksWithDesc()
Expand Down
6 changes: 5 additions & 1 deletion variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ func (e *Executor) FastCompiledTask(call taskfile.Call) (*taskfile.Task, error)
func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskfile.Task, error) {
origTask, ok := e.Taskfile.Tasks[call.Task]
if !ok {
return nil, &taskNotFoundError{call.Task}
didYouMean := ""
if e.fuzzyModel != nil {
didYouMean = e.fuzzyModel.SpellCheck(call.Task)
}
return nil, &taskNotFoundError{taskName: call.Task, didYouMean: didYouMean}
}

var vars *taskfile.Vars
Expand Down

0 comments on commit 3e5ee23

Please sign in to comment.