diff --git a/.editorconfig b/.editorconfig index 8ef8a237..d37c349a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,6 +8,6 @@ charset = utf-8 trim_trailing_whitespace = true indent_style = tab -[*.{md,yml,yaml,json,toml,htm,html,js,css,svg,sh,bash}] +[*.{md,yml,yaml,json,toml,htm,html,js,css,svg,sh,bash,fish}] indent_style = space indent_size = 2 diff --git a/.gitignore b/.gitignore index 9827564c..35da8d54 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ dist/ # editors .idea/ .vscode/ +.fleet/ # exuberant ctags tags diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c05fbd8..3a0ef886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ ## Unreleased - 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)). +- Improvements to Fish shell completion + ([#897](https://github.com/go-task/task/pull/897)). +- Added ability to set a different watch interval by setting + `interval: '500ms'` or using the `--interval=500ms` flag + ([#813](https://github.com/go-task/task/issues/813), [#865](https://github.com/go-task/task/pull/865)). - Add colored output to `--list`, `--list-all` and `--summary` flags ([#845](https://github.com/go-task/task/pull/845), [#874](https://github.com/go-task/task/pull/874)). - Fix unexpected behavior where `label:` was being shown instead of the task name on `--list` diff --git a/cmd/task/task.go b/cmd/task/task.go index 5a3893b8..c7c25dee 100644 --- a/cmd/task/task.go +++ b/cmd/task/task.go @@ -73,6 +73,7 @@ func main() { entrypoint string output taskfile.Output color bool + interval string ) pflag.BoolVar(&versionFlag, "version", false, "show Task version") @@ -96,6 +97,7 @@ func main() { pflag.StringVar(&output.Group.End, "output-group-end", "", "message template to print after a task's grouped output") pflag.BoolVarP(&color, "color", "c", true, "colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable") pflag.IntVarP(&concurrency, "concurrency", "C", 0, "limit number tasks to run concurrently") + pflag.StringVarP(&interval, "interval", "I", "5s", "interval to watch for changes") pflag.Parse() if versionFlag { @@ -151,6 +153,7 @@ func main() { Parallel: parallel, Color: color, Concurrency: concurrency, + Interval: interval, Stdin: os.Stdin, Stdout: os.Stdout, diff --git a/completion/fish/task.fish b/completion/fish/task.fish index e0d9c051..45a9746d 100644 --- a/completion/fish/task.fish +++ b/completion/fish/task.fish @@ -1,16 +1,24 @@ set GO_TASK_PROGNAME task function __task_get_tasks --description "Prints all available tasks with their description" - set -l output ($GO_TASK_PROGNAME --list-all | sed '1d; s/\* \(.*\):\s*\(.*\)/\1\t\2/' | string split0) + # Read the list of tasks (and potential errors) + $GO_TASK_PROGNAME --list-all 2>&1 | read -lz rawOutput + + # Return on non-zero exit code (for cases when there is no Taskfile found or etc.) + if test $status -ne 0 + return + end + + # Grab names and descriptions (if any) of the tasks + set -l output (echo $rawOutput | sed '1d; s/\* \(.*\):\s*\(.*\)/\1\t\2/' | string split0) if test $output - echo $output + echo $output end end complete -c $GO_TASK_PROGNAME -d 'Runs the specified task(s). Falls back to the "default" task if no task name was specified, or lists all tasks if an unknown task name was specified.' -xa "(__task_get_tasks)" - complete -c $GO_TASK_PROGNAME -s c -l color -d 'colored output (default true)' complete -c $GO_TASK_PROGNAME -s d -l dir -d 'sets directory of execution' complete -c $GO_TASK_PROGNAME -l dry -d 'compiles and prints tasks in the order that they would be run, without executing them' diff --git a/docs/docs/api_reference.md b/docs/docs/api_reference.md index 50cc0aec..87c39b71 100644 --- a/docs/docs/api_reference.md +++ b/docs/docs/api_reference.md @@ -30,6 +30,7 @@ variable | `-f` | `--force` | `bool` | `false` | Forces execution even when the task is up-to-date. | | `-h` | `--help` | `bool` | `false` | Shows Task usage. | | `-i` | `--init` | `bool` | `false` | Creates a new Taskfile.yaml in the current folder. | +| `-I` | `--interval` | `string` | `5s` | Sets a different watch interval when using `--watch`, the default being 5 seconds. This string should be a valid [Go Duration](https://pkg.go.dev/time#ParseDuration). | | `-l` | `--list` | `bool` | `false` | Lists tasks with description of current Taskfile. | | `-a` | `--list-all` | `bool` | `false` | Lists tasks with or without a description. | | `-o` | `--output` | `string` | Default set in the Taskfile or `intervealed` | Sets output style: [`interleaved`/`group`/`prefixed`]. | @@ -84,6 +85,7 @@ Some environment variables can be overriden to adjust Task behavior. | `method` | `string` | `checksum` | Default method in this Taskfile. Can be overriden in a task by task basis. Available options: `checksum`, `timestamp` and `none`. | | `silent` | `bool` | `false` | Default "silent" options for this Taskfile. If `false`, can be overidden with `true` in a task by task basis. | | `run` | `string` | `always` | Default "run" option for this Taskfile. Available options: `always`, `once` and `when_changed`. | +| `interval` | `string` | `5s` | Sets a different watch interval when using `--watch`, the default being 5 seconds. This string should be a valid [Go Duration](https://pkg.go.dev/time#ParseDuration). | | `vars` | [`map[string]Variable`](#variable) | | Global variables. | | `env` | [`map[string]Variable`](#variable) | | Global environment. | | `dotenv` | `[]string` | | A list of `.env` file paths to be parsed. | diff --git a/docs/docs/usage.md b/docs/docs/usage.md index d96b4454..c58a5119 100644 --- a/docs/docs/usage.md +++ b/docs/docs/usage.md @@ -232,7 +232,9 @@ includes: ### Namespace aliases -When including a taskfile, you can give the namespace a list of `aliases`. This works in the same way as [task aliases](#task-aliases) and can be used together to create shorter and easier-to-type commands. +When including a Taskfile, you can give the namespace a list of `aliases`. +This works in the same way as [task aliases](#task-aliases) and can be used +together to create shorter and easier-to-type commands. ```yaml version: '3' @@ -1284,5 +1286,9 @@ With the flags `--watch` or `-w` task will watch for file changes and run the task again. This requires the `sources` attribute to be given, so task knows which files to watch. +The default watch interval is 5 seconds, but it's possible to change it by +either setting `interval: '500ms'` in the root of the Taskfile passing it +as an argument like `--interval=500ms`. + [gotemplate]: https://golang.org/pkg/text/template/ [minify]: https://github.com/tdewolff/minify/tree/master/cmd/minify diff --git a/task.go b/task.go index d2cca73a..b5dbcce9 100644 --- a/task.go +++ b/task.go @@ -42,6 +42,7 @@ type Executor struct { Parallel bool Color bool Concurrency int + Interval string Stdin io.Reader Stdout io.Writer diff --git a/task_test.go b/task_test.go index 9a48e206..9fc018b1 100644 --- a/task_test.go +++ b/task_test.go @@ -10,6 +10,7 @@ import ( "runtime" "strings" "testing" + "time" "github.com/stretchr/testify/assert" @@ -1532,3 +1533,64 @@ func TestEvaluateSymlinksInPaths(t *testing.T) { err = os.RemoveAll(dir + "/.task") assert.NoError(t, err) } + +func TestFileWatcherInterval(t *testing.T) { + const dir = "testdata/watcher_interval" + expectedOutput := strings.TrimSpace(` +task: Started watching for tasks: default +task: [default] echo "Hello, World!" +Hello, World! +task: [default] echo "Hello, World!" +Hello, World! + `) + + var buff bytes.Buffer + e := &task.Executor{ + Dir: dir, + Stdout: &buff, + Stderr: &buff, + Watch: true, + } + + assert.NoError(t, e.Setup()) + buff.Reset() + + err := os.MkdirAll(filepathext.SmartJoin(dir, "src"), 0755) + assert.NoError(t, err) + + err = os.WriteFile(filepathext.SmartJoin(dir, "src/a"), []byte("test"), 0644) + if err != nil { + t.Fatal(err) + } + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + + go func(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + default: + err := e.Run(ctx, taskfile.Call{Task: "default"}) + if err != nil { + return + } + } + } + }(ctx) + + time.Sleep(10 * time.Millisecond) + err = os.WriteFile(filepathext.SmartJoin(dir, "src/a"), []byte("test updated"), 0644) + if err != nil { + t.Fatal(err) + } + time.Sleep(700 * time.Millisecond) + cancel() + assert.Equal(t, expectedOutput, strings.TrimSpace(buff.String())) + buff.Reset() + err = os.RemoveAll(filepathext.SmartJoin(dir, ".task")) + assert.NoError(t, err) + err = os.RemoveAll(filepathext.SmartJoin(dir, "src")) + assert.NoError(t, err) +} diff --git a/taskfile/included_taskfile.go b/taskfile/included_taskfile.go index 3ee8822c..c305d0ea 100644 --- a/taskfile/included_taskfile.go +++ b/taskfile/included_taskfile.go @@ -7,8 +7,8 @@ import ( "github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/filepathext" - "golang.org/x/exp/slices" + "golang.org/x/exp/slices" "gopkg.in/yaml.v3" ) diff --git a/taskfile/taskfile.go b/taskfile/taskfile.go index ff7b1bbf..90f274ed 100644 --- a/taskfile/taskfile.go +++ b/taskfile/taskfile.go @@ -18,6 +18,7 @@ type Taskfile struct { Silent bool Dotenv []string Run string + Interval string } // UnmarshalYAML implements yaml.Unmarshaler interface @@ -34,10 +35,13 @@ func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error { Silent bool Dotenv []string Run string + Interval string } + if err := unmarshal(&taskfile); err != nil { return err } + tf.Version = taskfile.Version tf.Expansions = taskfile.Expansions tf.Output = taskfile.Output @@ -49,6 +53,8 @@ func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error { tf.Silent = taskfile.Silent tf.Dotenv = taskfile.Dotenv tf.Run = taskfile.Run + tf.Interval = taskfile.Interval + if tf.Expansions <= 0 { tf.Expansions = 2 } diff --git a/testdata/watcher_interval/.gitignore b/testdata/watcher_interval/.gitignore new file mode 100644 index 00000000..71bcfc6c --- /dev/null +++ b/testdata/watcher_interval/.gitignore @@ -0,0 +1 @@ +src/* diff --git a/testdata/watcher_interval/Taskfile.yaml b/testdata/watcher_interval/Taskfile.yaml new file mode 100644 index 00000000..110eff27 --- /dev/null +++ b/testdata/watcher_interval/Taskfile.yaml @@ -0,0 +1,16 @@ +# https://taskfile.dev + +version: '3' + +vars: + GREETING: Hello, World! + +interval: "500ms" + +tasks: + default: + sources: + - "src/*" + cmds: + - echo "{{.GREETING}}" + silent: false diff --git a/watch.go b/watch.go index b93e2d30..6af7dbc6 100644 --- a/watch.go +++ b/watch.go @@ -16,7 +16,7 @@ import ( "github.com/radovskyb/watcher" ) -const watchInterval = 5 * time.Second +const defaultWatchInterval = 5 * time.Second // watchTasks start watching the given tasks func (e *Executor) watchTasks(calls ...taskfile.Call) error { @@ -24,6 +24,7 @@ func (e *Executor) watchTasks(calls ...taskfile.Call) error { for i, c := range calls { tasks[i] = c.Task } + e.Logger.Errf(logger.Green, "task: Started watching for tasks: %s", strings.Join(tasks, ", ")) ctx, cancel := context.WithCancel(context.Background()) @@ -36,6 +37,27 @@ func (e *Executor) watchTasks(calls ...taskfile.Call) error { }() } + var watchIntervalString string + + if e.Interval != "" { + watchIntervalString = e.Interval + } else if e.Taskfile.Interval != "" { + watchIntervalString = e.Taskfile.Interval + } + + watchInterval := defaultWatchInterval + + if watchIntervalString != "" { + var err error + watchInterval, err = parseWatchInterval(watchIntervalString) + if err != nil { + cancel() + return err + } + } + + e.Logger.VerboseOutf(logger.Green, "task: Watching for changes every %v", watchInterval) + w := watcher.New() defer w.Close() w.SetMaxEvents(1) @@ -163,3 +185,11 @@ func (e *Executor) registerWatchedFiles(w *watcher.Watcher, calls ...taskfile.Ca func shouldIgnoreFile(path string) bool { return strings.Contains(path, "/.git") || strings.Contains(path, "/.task") || strings.Contains(path, "/node_modules") } + +func parseWatchInterval(watchInterval string) (time.Duration, error) { + v, err := time.ParseDuration(watchInterval) + if err != nil { + return 0, fmt.Errorf(`task: Could not parse watch interval "%s": %v`, watchInterval, err) + } + return v, nil +}