diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eb6f46b..9ca82fd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- Added task location data to the `--json` flag output ([#1056](https://github.com/go-task/task/pull/1056) by @pd93) + +## Unreleased + - Change the name of the file generated by `task --init` from `Taskfile.yaml` to `Taskfile.yml` ([#1062](https://github.com/go-task/task/pull/1062) by @misitebao). @@ -9,6 +13,7 @@ (`{{splitArgs "foo bar 'foo bar baz'"}}`) to ensure string is splitted as arguments not whitespaces ([#1040](https://github.com/go-task/task/issues/1040), [#1059](https://github.com/go-task/task/pull/1059) by @dhanusaputra). +- Added task location data to the `--json` flag output ([#1056](https://github.com/go-task/task/pull/1056) by @pd93) ## v3.22.0 - 2023-03-10 diff --git a/docs/docs/api_reference.md b/docs/docs/api_reference.md index cd3b2350..349d1404 100644 --- a/docs/docs/api_reference.md +++ b/docs/docs/api_reference.md @@ -34,6 +34,7 @@ variable | `-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. | +| | `--json` | `bool` | `false` | See [JSON Output](#json-output) | | `-o` | `--output` | `string` | Default set in the Taskfile or `intervealed` | Sets output style: [`interleaved`/`group`/`prefixed`]. | | | `--output-group-begin` | `string` | | Message template to print before a task's grouped output. | | | `--output-group-end` | `string` | | Message template to print after a task's grouped output. | @@ -47,6 +48,30 @@ variable | | `--version` | `bool` | `false` | Show Task version. | | `-w` | `--watch` | `bool` | `false` | Enables watch of the given task. | +## JSON Output + +When using the `--json` flag in combination with either the `--list` or `--list-all` flags, the output will be a JSON object with the following structure: + +```jsonc +{ + "tasks": [ + { + "name": "", + "desc": "", + "summary": "", + "up_to_date": false, + "location": { + "line": 54, + "column": 3, + "taskfile": "/path/to/Taskfile.yml" + } + }, + // ... + ], + "location": "/path/to/Taskfile.yml" +} +``` + ## Special Variables There are some special variables that is available on the templating system: diff --git a/help.go b/help.go index 31ff1771..1c1fc7cf 100644 --- a/help.go +++ b/help.go @@ -11,6 +11,8 @@ import ( "strings" "text/tabwriter" + "golang.org/x/sync/errgroup" + "github.com/go-task/task/v3/internal/editors" "github.com/go-task/task/v3/internal/fingerprint" "github.com/go-task/task/v3/internal/logger" @@ -144,31 +146,43 @@ func (e *Executor) ListTaskNames(allTasks bool) { } } -func (e *Executor) ToEditorOutput(tasks []*taskfile.Task) (*editors.Output, error) { - o := &editors.Output{ - Tasks: make([]editors.Task, len(tasks)), +func (e *Executor) ToEditorOutput(tasks []*taskfile.Task) (*editors.Taskfile, error) { + o := &editors.Taskfile{ + Tasks: make([]editors.Task, len(tasks)), + Location: e.Taskfile.Location, } - for i, t := range tasks { - // Get the fingerprinting method to use - method := e.Taskfile.Method - if t.Method != "" { - method = t.Method - } - upToDate, err := fingerprint.IsTaskUpToDate(context.Background(), t, - fingerprint.WithMethod(method), - fingerprint.WithTempDir(e.TempDir), - fingerprint.WithDry(e.Dry), - fingerprint.WithLogger(e.Logger), - ) - if err != nil { - return nil, err - } - o.Tasks[i] = editors.Task{ - Name: t.Name(), - Desc: t.Desc, - Summary: t.Summary, - UpToDate: upToDate, - } + var g errgroup.Group + for i := range tasks { + task := tasks[i] + j := i + g.Go(func() error { + // Get the fingerprinting method to use + method := e.Taskfile.Method + if task.Method != "" { + method = task.Method + } + upToDate, err := fingerprint.IsTaskUpToDate(context.Background(), task, + fingerprint.WithMethod(method), + fingerprint.WithTempDir(e.TempDir), + fingerprint.WithDry(e.Dry), + fingerprint.WithLogger(e.Logger), + ) + if err != nil { + return err + } + o.Tasks[j] = editors.Task{ + Name: task.Name(), + Desc: task.Desc, + Summary: task.Summary, + UpToDate: upToDate, + Location: &editors.Location{ + Line: task.Location.Line, + Column: task.Location.Column, + Taskfile: task.Location.Taskfile, + }, + } + return nil + }) } - return o, nil + return o, g.Wait() } diff --git a/internal/editors/output.go b/internal/editors/output.go index 18997304..f2bab68a 100644 --- a/internal/editors/output.go +++ b/internal/editors/output.go @@ -1,14 +1,23 @@ package editors -// Output wraps task list output for use in editor integrations (e.g. VSCode, etc) -type Output struct { - Tasks []Task `json:"tasks"` -} - -// Task describes a single task -type Task struct { - Name string `json:"name"` - Desc string `json:"desc"` - Summary string `json:"summary"` - UpToDate bool `json:"up_to_date"` -} +type ( + // Taskfile wraps task list output for use in editor integrations (e.g. VSCode, etc) + Taskfile struct { + Tasks []Task `json:"tasks"` + Location string `json:"location"` + } + // Task describes a single task + Task struct { + Name string `json:"name"` + Desc string `json:"desc"` + Summary string `json:"summary"` + UpToDate bool `json:"up_to_date"` + Location *Location `json:"location"` + } + // Location describes a task's location in a taskfile + Location struct { + Line int `json:"line"` + Column int `json:"column"` + Taskfile string `json:"taskfile"` + } +) diff --git a/taskfile/location.go b/taskfile/location.go new file mode 100644 index 00000000..fa469278 --- /dev/null +++ b/taskfile/location.go @@ -0,0 +1,18 @@ +package taskfile + +type Location struct { + Line int + Column int + Taskfile string +} + +func (l *Location) DeepCopy() *Location { + if l == nil { + return nil + } + return &Location{ + Line: l.Line, + Column: l.Column, + Taskfile: l.Taskfile, + } +} diff --git a/taskfile/merge.go b/taskfile/merge.go index e74d9da4..c4d872da 100644 --- a/taskfile/merge.go +++ b/taskfile/merge.go @@ -65,7 +65,9 @@ func Merge(t1, t2 *Taskfile, includedTaskfile *IncludedTaskfile, namespaces ...s } // Add the task to the merged taskfile - t1.Tasks[taskNameWithNamespace(k, namespaces...)] = task + taskNameWithNamespace := taskNameWithNamespace(k, namespaces...) + task.Task = taskNameWithNamespace + t1.Tasks[taskNameWithNamespace] = task } return nil diff --git a/taskfile/read/taskfile.go b/taskfile/read/taskfile.go index 0f58f41a..58197b1c 100644 --- a/taskfile/read/taskfile.go +++ b/taskfile/read/taskfile.go @@ -176,12 +176,18 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, string, error) { } } - for name, task := range t.Tasks { + // Set the location of the Taskfile + t.Location = path + + for _, task := range t.Tasks { + // If the task is not defined, create a new one if task == nil { task = &taskfile.Task{} - t.Tasks[name] = task } - task.Task = name + // Set the location of the taskfile for each task + if task.Location.Taskfile == "" { + task.Location.Taskfile = path + } } return t, readerNode.Dir, nil diff --git a/taskfile/task.go b/taskfile/task.go index 95b4c1b6..7cfd7523 100644 --- a/taskfile/task.go +++ b/taskfile/task.go @@ -6,9 +6,6 @@ import ( "gopkg.in/yaml.v3" ) -// Tasks represents a group of tasks -type Tasks map[string]*Task - // Task represents a task type Task struct { Task string @@ -39,6 +36,7 @@ type Task struct { IncludedTaskfileVars *Vars IncludedTaskfile *IncludedTaskfile Platforms []*Platform + Location *Location } func (t *Task) Name() string { @@ -162,6 +160,7 @@ func (t *Task) DeepCopy() *Task { IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(), IncludedTaskfile: t.IncludedTaskfile.DeepCopy(), Platforms: deepCopySlice(t.Platforms), + Location: t.Location.DeepCopy(), } return c } diff --git a/taskfile/taskfile.go b/taskfile/taskfile.go index 9f2c1777..babe1c3e 100644 --- a/taskfile/taskfile.go +++ b/taskfile/taskfile.go @@ -15,6 +15,7 @@ var ( // Taskfile represents a Taskfile.yml type Taskfile struct { + Location string Version *semver.Version Expansions int Output Output diff --git a/taskfile/tasks.go b/taskfile/tasks.go new file mode 100644 index 00000000..975744bf --- /dev/null +++ b/taskfile/tasks.go @@ -0,0 +1,45 @@ +package taskfile + +import ( + "fmt" + + "gopkg.in/yaml.v3" +) + +// Tasks represents a group of tasks +type Tasks map[string]*Task + +func (t *Tasks) UnmarshalYAML(node *yaml.Node) error { + switch node.Kind { + case yaml.MappingNode: + tasks := map[string]*Task{} + if err := node.Decode(tasks); err != nil { + return err + } + + for name := range tasks { + // Set the task's name + if tasks[name] == nil { + tasks[name] = &Task{ + Task: name, + } + } + tasks[name].Task = name + + // Set the task's location + for _, keys := range node.Content { + if keys.Value == name { + tasks[name].Location = &Location{ + Line: keys.Line, + Column: keys.Column, + } + } + } + } + + *t = Tasks(tasks) + return nil + } + + return fmt.Errorf("yaml: line %d: cannot unmarshal %s into tasks", node.Line, node.ShortTag()) +} diff --git a/variables.go b/variables.go index 77c8fc43..0885d849 100644 --- a/variables.go +++ b/variables.go @@ -66,6 +66,7 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf IncludeVars: origTask.IncludeVars, IncludedTaskfileVars: origTask.IncludedTaskfileVars, Platforms: origTask.Platforms, + Location: origTask.Location, } new.Dir, err = execext.Expand(new.Dir) if err != nil {