From 1deb1d8195ea788e83567adec8263f2b41208d2e Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Thu, 20 Nov 2025 19:49:02 +0100 Subject: [PATCH 1/7] fix: disable fuzzy --- setup.go | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/setup.go b/setup.go index 6234ceefa0..29e4335275 100644 --- a/setup.go +++ b/setup.go @@ -103,24 +103,25 @@ func (e *Executor) readTaskfile(node taskfile.Node) error { } func (e *Executor) setupFuzzyModel() { + if e.Taskfile == nil { return } - - model := fuzzy.NewModel() - model.SetThreshold(1) // because we want to build grammar based on every task name - - var words []string - for name, task := range e.Taskfile.Tasks.All(nil) { - if task.Internal { - continue - } - words = append(words, name) - words = slices.Concat(words, task.Aliases) - } - - model.Train(words) - e.fuzzyModel = model + // + //model := fuzzy.NewModel() + //model.SetThreshold(1) // because we want to build grammar based on every task name + // + //var words []string + //for name, task := range e.Taskfile.Tasks.All(nil) { + // if task.Internal { + // continue + // } + // words = append(words, name) + // words = slices.Concat(words, task.Aliases) + //} + // + //model.Train(words) + //e.fuzzyModel = model } func (e *Executor) setupTempDir() error { From dcc23cf1c7cb9e535a806ae0e31a029b4a744340 Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Thu, 20 Nov 2025 19:57:02 +0100 Subject: [PATCH 2/7] fix unused import --- setup.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.go b/setup.go index 29e4335275..14ffc3b3df 100644 --- a/setup.go +++ b/setup.go @@ -5,12 +5,10 @@ import ( "fmt" "os" "path/filepath" - "slices" "strings" "sync" "github.com/Masterminds/semver/v3" - "github.com/sajari/fuzzy" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/env" From 3ce9cc689a17044db45f7a292fbde707e113974c Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Sun, 23 Nov 2025 21:12:25 +0100 Subject: [PATCH 3/7] refactor: lazy load fuzzy model & ability to disable it --- executor.go | 14 ++++++++++++ internal/flags/flags.go | 3 +++ setup.go | 33 ++++++++++++++------------- task.go | 10 ++++++-- taskrc/ast/taskrc.go | 12 ++++++---- website/src/docs/reference/cli.md | 10 ++++++++ website/src/docs/reference/config.md | 12 ++++++++++ website/src/public/schema-taskrc.json | 4 ++++ 8 files changed, 75 insertions(+), 23 deletions(-) diff --git a/executor.go b/executor.go index 6ecf910a5b..84dd2e39ac 100644 --- a/executor.go +++ b/executor.go @@ -39,6 +39,7 @@ type ( Watch bool Verbose bool Silent bool + DisableFuzzy bool AssumeYes bool AssumeTerm bool // Used for testing Dry bool @@ -296,6 +297,19 @@ func (o *silentOption) ApplyToExecutor(e *Executor) { e.Silent = o.silent } +// WithDisableFuzzy tells the [Executor] to disable fuzzy matching for task names. +func WithDisableFuzzy(disableFuzzy bool) ExecutorOption { + return &disableFuzzyOption{disableFuzzy} +} + +type disableFuzzyOption struct { + disableFuzzy bool +} + +func (o *disableFuzzyOption) ApplyToExecutor(e *Executor) { + e.DisableFuzzy = o.disableFuzzy +} + // WithAssumeYes tells the [Executor] to assume "yes" for all prompts. func WithAssumeYes(assumeYes bool) ExecutorOption { return &assumeYesOption{assumeYes} diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 5787e17a6d..b1ef0e36eb 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -58,6 +58,7 @@ var ( Watch bool Verbose bool Silent bool + DisableFuzzy bool AssumeYes bool Dry bool Summary bool @@ -123,6 +124,7 @@ func init() { pflag.BoolVarP(&Watch, "watch", "w", false, "Enables watch of the given task.") pflag.BoolVarP(&Verbose, "verbose", "v", getConfig(config, func() *bool { return config.Verbose }, false), "Enables verbose mode.") pflag.BoolVarP(&Silent, "silent", "s", false, "Disables echoing.") + pflag.BoolVar(&DisableFuzzy, "disable-fuzzy", getConfig(config, func() *bool { return config.DisableFuzzy }, false), "Disables fuzzy matching for task names.") pflag.BoolVarP(&AssumeYes, "yes", "y", false, "Assume \"yes\" as answer to all prompts.") pflag.BoolVarP(&Parallel, "parallel", "p", false, "Executes tasks provided on command line in parallel.") pflag.BoolVarP(&Dry, "dry", "n", false, "Compiles and prints tasks in the order that they would be run, without executing them.") @@ -243,6 +245,7 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) { task.WithWatch(Watch), task.WithVerbose(Verbose), task.WithSilent(Silent), + task.WithDisableFuzzy(DisableFuzzy), task.WithAssumeYes(AssumeYes), task.WithDry(Dry || Status), task.WithSummary(Summary), diff --git a/setup.go b/setup.go index 14ffc3b3df..df236f5e16 100644 --- a/setup.go +++ b/setup.go @@ -5,10 +5,12 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" "sync" "github.com/Masterminds/semver/v3" + "github.com/sajari/fuzzy" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/env" @@ -34,7 +36,6 @@ func (e *Executor) Setup() error { if err := e.readTaskfile(node); err != nil { return err } - e.setupFuzzyModel() e.setupStdFiles() if err := e.setupOutput(); err != nil { return err @@ -105,21 +106,21 @@ func (e *Executor) setupFuzzyModel() { if e.Taskfile == nil { return } - // - //model := fuzzy.NewModel() - //model.SetThreshold(1) // because we want to build grammar based on every task name - // - //var words []string - //for name, task := range e.Taskfile.Tasks.All(nil) { - // if task.Internal { - // continue - // } - // words = append(words, name) - // words = slices.Concat(words, task.Aliases) - //} - // - //model.Train(words) - //e.fuzzyModel = model + + model := fuzzy.NewModel() + model.SetThreshold(1) // because we want to build grammar based on every task name + + var words []string + for name, task := range e.Taskfile.Tasks.All(nil) { + if task.Internal { + continue + } + words = append(words, name) + words = slices.Concat(words, task.Aliases) + } + + model.Train(words) + e.fuzzyModel = model } func (e *Executor) setupTempDir() error { diff --git a/task.go b/task.go index 79bc36ac59..95eeea56c8 100644 --- a/task.go +++ b/task.go @@ -450,8 +450,14 @@ func (e *Executor) GetTask(call *Call) (*ast.Task, error) { // If we found no tasks if len(aliasedTasks) == 0 { didYouMean := "" - if e.fuzzyModel != nil { - didYouMean = e.fuzzyModel.SpellCheck(call.Task) + if !e.DisableFuzzy { + // Lazy initialization of fuzzy model + if e.fuzzyModel == nil { + e.setupFuzzyModel() + } + if e.fuzzyModel != nil { + didYouMean = e.fuzzyModel.SpellCheck(call.Task) + } } return nil, &errors.TaskNotFoundError{ TaskName: call.Task, diff --git a/taskrc/ast/taskrc.go b/taskrc/ast/taskrc.go index 9410ed1738..e625616fbe 100644 --- a/taskrc/ast/taskrc.go +++ b/taskrc/ast/taskrc.go @@ -9,11 +9,12 @@ import ( ) type TaskRC struct { - Version *semver.Version `yaml:"version"` - Verbose *bool `yaml:"verbose"` - Concurrency *int `yaml:"concurrency"` - Remote Remote `yaml:"remote"` - Experiments map[string]int `yaml:"experiments"` + Version *semver.Version `yaml:"version"` + Verbose *bool `yaml:"verbose"` + DisableFuzzy *bool `yaml:"disable-fuzzy"` + Concurrency *int `yaml:"concurrency"` + Remote Remote `yaml:"remote"` + Experiments map[string]int `yaml:"experiments"` } type Remote struct { @@ -44,5 +45,6 @@ func (t *TaskRC) Merge(other *TaskRC) { t.Remote.CacheExpiry = cmp.Or(other.Remote.CacheExpiry, t.Remote.CacheExpiry) t.Verbose = cmp.Or(other.Verbose, t.Verbose) + t.DisableFuzzy = cmp.Or(other.DisableFuzzy, t.DisableFuzzy) t.Concurrency = cmp.Or(other.Concurrency, t.Concurrency) } diff --git a/website/src/docs/reference/cli.md b/website/src/docs/reference/cli.md index 848f492cf1..6756592e04 100644 --- a/website/src/docs/reference/cli.md +++ b/website/src/docs/reference/cli.md @@ -108,6 +108,16 @@ Disable command echoing. task deploy --silent ``` +#### `--disable-fuzzy` + +Disable fuzzy matching for task names. When enabled, Task will not suggest similar task names when you mistype a task name. + +```bash +task buidl --disable-fuzzy +# Output: Task "buidl" does not exist +# (without "Did you mean 'build'?" suggestion) +``` + ### Execution Control #### `-f, --force` diff --git a/website/src/docs/reference/config.md b/website/src/docs/reference/config.md index 73bb152512..e4ec78a878 100644 --- a/website/src/docs/reference/config.md +++ b/website/src/docs/reference/config.md @@ -91,6 +91,17 @@ experiments: verbose: true ``` +### `disable-fuzzy` + +- **Type**: `boolean` +- **Default**: `false` +- **Description**: Disable fuzzy matching for task names. When enabled, Task will not suggest similar task names when you mistype a task name. +- **CLI equivalent**: [`--disable-fuzzy`](./cli.md#--disable-fuzzy) + +```yaml +disable-fuzzy: true +``` + ### `concurrency` - **Type**: `integer` @@ -109,6 +120,7 @@ Here's a complete example of a `.taskrc.yml` file with all available options: ```yaml # Global settings verbose: true +disable-fuzzy: false concurrency: 2 # Enable experimental features diff --git a/website/src/public/schema-taskrc.json b/website/src/public/schema-taskrc.json index ac101acfb8..813a1ebb03 100644 --- a/website/src/public/schema-taskrc.json +++ b/website/src/public/schema-taskrc.json @@ -50,6 +50,10 @@ "type": "boolean", "description": "Enable verbose output" }, + "disable-fuzzy": { + "type": "boolean", + "description": "Disable fuzzy matching for task names" + }, "concurrency": { "type": "integer", "description": "Number of concurrent tasks to run", From bd881e89fff1fd6ac4b43957eb0873cd2cff4a0c Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Sun, 23 Nov 2025 21:14:17 +0100 Subject: [PATCH 4/7] format --- setup.go | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.go b/setup.go index df236f5e16..01429d93a9 100644 --- a/setup.go +++ b/setup.go @@ -102,7 +102,6 @@ func (e *Executor) readTaskfile(node taskfile.Node) error { } func (e *Executor) setupFuzzyModel() { - if e.Taskfile == nil { return } From 2a88d5cc7afffa925fab06a9921680904015f16b Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Sun, 23 Nov 2025 21:38:50 +0100 Subject: [PATCH 5/7] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d5120d474..58d508f8fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ @vmaerten). - Improved shell completion scripts (Zsh, Fish, PowerShell) by adding missing flags and dynamic experimental feature detection (#2532 by @vmaerten). +- Improved performance of fuzzy task name matching by implementing lazy + initialization. Added `--disable-fuzzy` flag and `disable-fuzzy` taskrc option + to allow disabling fuzzy matching entirely (#2521, #2523 by @vmaerten). ## v3.45.5 - 2025-11-11 From 7ed6637a843012d7ff20b038fb7b700a3cbd9932 Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Sat, 29 Nov 2025 12:25:58 +0100 Subject: [PATCH 6/7] use Once --- executor.go | 3 ++- task.go | 5 +---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/executor.go b/executor.go index 84dd2e39ac..11cdac66ea 100644 --- a/executor.go +++ b/executor.go @@ -64,7 +64,8 @@ type ( UserWorkingDir string EnableVersionCheck bool - fuzzyModel *fuzzy.Model + fuzzyModel *fuzzy.Model + fuzzyModelOnce sync.Once concurrencySemaphore chan struct{} taskCallCount map[string]*int32 diff --git a/task.go b/task.go index 95eeea56c8..b04cd12e48 100644 --- a/task.go +++ b/task.go @@ -451,10 +451,7 @@ func (e *Executor) GetTask(call *Call) (*ast.Task, error) { if len(aliasedTasks) == 0 { didYouMean := "" if !e.DisableFuzzy { - // Lazy initialization of fuzzy model - if e.fuzzyModel == nil { - e.setupFuzzyModel() - } + e.fuzzyModelOnce.Do(e.setupFuzzyModel) if e.fuzzyModel != nil { didYouMean = e.fuzzyModel.SpellCheck(call.Task) } From b74a4081458b79bb0984b2eadfb0725e9a585959 Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Sat, 29 Nov 2025 12:30:01 +0100 Subject: [PATCH 7/7] completion --- completion/fish/task.fish | 1 + completion/ps/task.ps1 | 1 + completion/zsh/_task | 1 + 3 files changed, 3 insertions(+) diff --git a/completion/fish/task.fish b/completion/fish/task.fish index 3b7683fe02..7cc9223a1d 100644 --- a/completion/fish/task.fish +++ b/completion/fish/task.fish @@ -69,6 +69,7 @@ complete -c $GO_TASK_PROGNAME -s c -l color -d 'colored outp complete -c $GO_TASK_PROGNAME -s C -l concurrency -d 'limit number of concurrent tasks' complete -c $GO_TASK_PROGNAME -l completion -d 'generate shell completion script' -xa "bash zsh fish powershell" complete -c $GO_TASK_PROGNAME -s d -l dir -d 'set directory of execution' +complete -c $GO_TASK_PROGNAME -l disable-fuzzy -d 'disable fuzzy matching for task names' complete -c $GO_TASK_PROGNAME -s n -l dry -d 'compile and print tasks without executing' complete -c $GO_TASK_PROGNAME -s x -l exit-code -d 'pass-through exit code of task command' complete -c $GO_TASK_PROGNAME -l experiments -d 'list available experiments' diff --git a/completion/ps/task.ps1 b/completion/ps/task.ps1 index 156faa443e..d322eb7263 100644 --- a/completion/ps/task.ps1 +++ b/completion/ps/task.ps1 @@ -15,6 +15,7 @@ Register-ArgumentCompleter -CommandName task -ScriptBlock { [CompletionResult]::new('--completion', '--completion', [CompletionResultType]::ParameterName, 'generate shell completion'), [CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'set directory'), [CompletionResult]::new('--dir', '--dir', [CompletionResultType]::ParameterName, 'set directory'), + [CompletionResult]::new('--disable-fuzzy', '--disable-fuzzy', [CompletionResultType]::ParameterName, 'disable fuzzy matching'), [CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'dry run'), [CompletionResult]::new('--dry', '--dry', [CompletionResultType]::ParameterName, 'dry run'), [CompletionResult]::new('-x', '-x', [CompletionResultType]::ParameterName, 'pass-through exit code'), diff --git a/completion/zsh/_task b/completion/zsh/_task index 08670531a9..bcc259ece6 100755 --- a/completion/zsh/_task +++ b/completion/zsh/_task @@ -51,6 +51,7 @@ _task() { '(-c --color)'{-c,--color}'[colored output]' '(--completion)--completion[generate shell completion script]:shell:(bash zsh fish powershell)' '(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs' + '(--disable-fuzzy)--disable-fuzzy[disable fuzzy matching for task names]' '(-n --dry)'{-n,--dry}'[compiles and prints tasks without executing]' '(--dry)--dry[dry-run mode, compile and print tasks only]' '(-x --exit-code)'{-x,--exit-code}'[pass-through exit code of task command]'