From b7def2695d8650fddbd3749e218bbeea7a782566 Mon Sep 17 00:00:00 2001 From: David Alpert Date: Tue, 4 Oct 2022 21:55:55 -0500 Subject: [PATCH 1/4] refactor: introduce a ListOptions struct to collect list options in preparation for adding a third list option, let's first collect the existing two list options into a struct to avoid expanding parameter lists this also lets us simplify the multiple ListTask* functions into a single one which accepts options and switches internally; this is more consistent with the tell, don't ask pattern of pushing implementation below the level where clients care. --- cmd/task/task.go | 24 +++++++++++------------- help.go | 47 ++++++++++++++++++++++++++++++++--------------- task.go | 4 ++-- task_test.go | 6 +++--- 4 files changed, 48 insertions(+), 33 deletions(-) diff --git a/cmd/task/task.go b/cmd/task/task.go index 5a3893b89c..b624f0a76e 100644 --- a/cmd/task/task.go +++ b/cmd/task/task.go @@ -57,8 +57,7 @@ func main() { versionFlag bool helpFlag bool init bool - list bool - listAll bool + listOptions task.ListOptions status bool force bool watch bool @@ -78,8 +77,8 @@ func main() { pflag.BoolVar(&versionFlag, "version", false, "show Task version") pflag.BoolVarP(&helpFlag, "help", "h", false, "shows Task usage") pflag.BoolVarP(&init, "init", "i", false, "creates a new Taskfile.yaml in the current folder") - pflag.BoolVarP(&list, "list", "l", false, "lists tasks with description of current Taskfile") - pflag.BoolVarP(&listAll, "list-all", "a", false, "lists tasks with or without a description") + pflag.BoolVarP(&listOptions.ListWithDescriptionsOnly, "list", "l", false, "lists tasks with description of current Taskfile") + pflag.BoolVarP(&listOptions.ListAll, "list-all", "a", false, "lists tasks with or without a description") pflag.BoolVar(&status, "status", false, "exits with non-zero exit code if any of the given tasks is not up-to-date") pflag.BoolVarP(&force, "force", "f", false, "forces execution even when the task is up-to-date") pflag.BoolVarP(&watch, "watch", "w", false, "enables watch of the given task") @@ -159,8 +158,12 @@ func main() { OutputStyle: output, } - if (list || listAll) && silent { - e.ListTaskNames(listAll) + if err := listOptions.Validate(); err != nil { + log.Fatal(err) + } + + if (listOptions.ShowList()) && silent { + e.ListTaskNames(listOptions) return } @@ -173,13 +176,8 @@ func main() { return } - if list { - e.ListTasksWithDesc() - return - } - - if listAll { - e.ListAllTasks() + if listOptions.ShowList() { + e.ListTasks(listOptions) return } diff --git a/help.go b/help.go index 85ce0ce71d..10eb8d3b3e 100644 --- a/help.go +++ b/help.go @@ -13,26 +13,43 @@ import ( "github.com/go-task/task/v3/taskfile" ) -// ListTasksWithDesc reports tasks that have a description spec. -func (e *Executor) ListTasksWithDesc() { - e.printTasks(false) +type ListOptions struct { + ListWithDescriptionsOnly bool + ListAll bool } -// ListAllTasks reports all tasks, with or without a description spec. -func (e *Executor) ListAllTasks() { - e.printTasks(true) +func (o ListOptions) Validate() error { + if o.ListWithDescriptionsOnly && o.ListAll { + return fmt.Errorf("cannot use --list and --list-all at the same time") + } + return nil +} + +func (o ListOptions) ShowList() bool { + return o.ListWithDescriptionsOnly || o.ListAll } -func (e *Executor) printTasks(listAll bool) { +// ListTasks reports tasks. +func (e *Executor) ListTasks(o ListOptions) { + e.printTasks(o) +} + +func (e *Executor) printTasks(o ListOptions) { var tasks []*taskfile.Task - if listAll { + if o.ListAll { tasks = e.allTaskNames() } else { tasks = e.tasksWithDesc() } + // use stdout if no output defined + var w io.Writer = os.Stdout + if e.Stdout != nil { + w = e.Stdout + } + if len(tasks) == 0 { - if listAll { + if o.ListAll { e.Logger.Outf(logger.Yellow, "task: No tasks available") } else { e.Logger.Outf(logger.Yellow, "task: No tasks with description available. Try --list-all to list all tasks") @@ -42,11 +59,11 @@ func (e *Executor) printTasks(listAll bool) { e.Logger.Outf(logger.Default, "task: Available tasks for this project:") // Format in tab-separated columns with a tab stop of 8. - w := tabwriter.NewWriter(e.Stdout, 0, 8, 0, '\t', 0) + tw := tabwriter.NewWriter(w, 0, 8, 0, '\t', 0) for _, task := range tasks { - fmt.Fprintf(w, "* %s: \t%s\n", task.Name(), task.Desc) + fmt.Fprintf(tw, "* %s: \t%s\n", task.Name(), task.Desc) } - w.Flush() + tw.Flush() } func (e *Executor) allTaskNames() (tasks []*taskfile.Task) { @@ -75,10 +92,10 @@ 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) { +func (e *Executor) ListTaskNames(o ListOptions) { // if called from cmd/task.go, e.Taskfile has not yet been parsed if e.Taskfile == nil { if err := e.readTaskfile(); err != nil { @@ -94,7 +111,7 @@ func (e *Executor) ListTaskNames(allTasks bool) { // create a string slice from all map values (*taskfile.Task) s := make([]string, 0, len(e.Taskfile.Tasks)) for _, t := range e.Taskfile.Tasks { - if (allTasks || t.Desc != "") && !t.Internal { + if (o.ListAll || t.Desc != "") && !t.Internal { s = append(s, strings.TrimRight(t.Task, ":")) } } diff --git a/task.go b/task.go index 59cde1f150..b84ca8dbe1 100644 --- a/task.go +++ b/task.go @@ -67,11 +67,11 @@ func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error { t, ok := e.Taskfile.Tasks[c.Task] if !ok { // FIXME: move to the main package - e.ListTasksWithDesc() + e.ListTasks(ListOptions{ListWithDescriptionsOnly: true}) return &taskNotFoundError{taskName: c.Task} } if t.Internal { - e.ListTasksWithDesc() + e.ListTasks(ListOptions{ListWithDescriptionsOnly: true}) return &taskInternalError{taskName: c.Task} } } diff --git a/task_test.go b/task_test.go index 7235709541..76b9f18ed1 100644 --- a/task_test.go +++ b/task_test.go @@ -556,7 +556,7 @@ func TestLabelInList(t *testing.T) { Stderr: &buff, } assert.NoError(t, e.Setup()) - e.ListTasksWithDesc() + e.ListTasks(task.ListOptions{ListWithDescriptionsOnly: true, ListAll: false}) assert.Contains(t, buff.String(), "foobar") } @@ -574,7 +574,7 @@ func TestListAllShowsNoDesc(t *testing.T) { assert.NoError(t, e.Setup()) var title string - e.ListAllTasks() + e.ListTasks(task.ListOptions{ListWithDescriptionsOnly: false, ListAll: true}) for _, title = range []string{ "foo", "voo", @@ -596,7 +596,7 @@ func TestListCanListDescOnly(t *testing.T) { } assert.NoError(t, e.Setup()) - e.ListTasksWithDesc() + e.ListTasks(task.ListOptions{ListWithDescriptionsOnly: true, ListAll: false}) var title string assert.Contains(t, buff.String(), "foo") From 84889b2bb08d7f81b8f7b4ff895fd0cf6684ea7f Mon Sep 17 00:00:00 2001 From: David Alpert Date: Tue, 4 Oct 2022 21:57:14 -0500 Subject: [PATCH 2/4] feat: add a new --json to better supports interactivity in scripts resolves: #764 --- cmd/task/task.go | 1 + help.go | 22 +++++++++++++++-- taskfile/included_taskfile.go | 14 +++++------ taskfile/precondition.go | 4 +-- taskfile/task.go | 46 +++++++++++++++++------------------ taskfile/var.go | 12 ++++----- 6 files changed, 59 insertions(+), 40 deletions(-) diff --git a/cmd/task/task.go b/cmd/task/task.go index b624f0a76e..f456fb107e 100644 --- a/cmd/task/task.go +++ b/cmd/task/task.go @@ -79,6 +79,7 @@ func main() { pflag.BoolVarP(&init, "init", "i", false, "creates a new Taskfile.yaml in the current folder") pflag.BoolVarP(&listOptions.ListWithDescriptionsOnly, "list", "l", false, "lists tasks with description of current Taskfile") pflag.BoolVarP(&listOptions.ListAll, "list-all", "a", false, "lists tasks with or without a description") + pflag.BoolVarP(&listOptions.AsJson, "json", "j", false, "lists tasks as JSON") pflag.BoolVar(&status, "status", false, "exits with non-zero exit code if any of the given tasks is not up-to-date") pflag.BoolVarP(&force, "force", "f", false, "forces execution even when the task is up-to-date") pflag.BoolVarP(&watch, "watch", "w", false, "enables watch of the given task") diff --git a/help.go b/help.go index 10eb8d3b3e..d8d3ec437b 100644 --- a/help.go +++ b/help.go @@ -1,6 +1,7 @@ package task import ( + "encoding/json" "fmt" "io" "log" @@ -16,12 +17,16 @@ import ( type ListOptions struct { ListWithDescriptionsOnly bool ListAll bool + AsJson bool } func (o ListOptions) Validate() error { if o.ListWithDescriptionsOnly && o.ListAll { return fmt.Errorf("cannot use --list and --list-all at the same time") } + if o.AsJson && !(o.ListWithDescriptionsOnly || o.ListAll) { + return fmt.Errorf("cannot use --json without --list or --list-all") + } return nil } @@ -48,6 +53,13 @@ func (e *Executor) printTasks(o ListOptions) { w = e.Stdout } + if o.AsJson { + if err := json.NewEncoder(w).Encode(tasks); err != nil { + log.Fatal(err) + } + return + } + if len(tasks) == 0 { if o.ListAll { e.Logger.Outf(logger.Yellow, "task: No tasks available") @@ -117,7 +129,13 @@ func (e *Executor) ListTaskNames(o ListOptions) { } // sort and print all task names sort.Strings(s) - for _, t := range s { - fmt.Fprintln(w, t) + if o.AsJson { + if err := json.NewEncoder(w).Encode(s); err != nil { + log.Fatal(err) + } + } else { + for _, t := range s { + fmt.Fprintln(w, t) + } } } diff --git a/taskfile/included_taskfile.go b/taskfile/included_taskfile.go index fe83bd7d03..975cf5da30 100644 --- a/taskfile/included_taskfile.go +++ b/taskfile/included_taskfile.go @@ -13,13 +13,13 @@ import ( // IncludedTaskfile represents information about included taskfiles type IncludedTaskfile struct { - Taskfile string - Dir string - Optional bool - Internal bool - AdvancedImport bool - Vars *Vars - BaseDir string // The directory from which the including taskfile was loaded; used to resolve relative paths + Taskfile string `json:"taskfile"` + Dir string `json:"dir"` + Optional bool `json:"optional"` + Internal bool `json:"internal"` + AdvancedImport bool `json:"advanced_import"` + Vars *Vars `json:"vars"` + BaseDir string `json:"base_dir"` // The directory from which the including taskfile was loaded; used to resolve relative paths } // IncludedTaskfiles represents information about included tasksfiles diff --git a/taskfile/precondition.go b/taskfile/precondition.go index 04c1e53295..67e28cd75c 100644 --- a/taskfile/precondition.go +++ b/taskfile/precondition.go @@ -12,8 +12,8 @@ var ( // Precondition represents a precondition necessary for a task to run type Precondition struct { - Sh string - Msg string + Sh string `json:"sh"` + Msg string `json:"msg"` } // UnmarshalYAML implements yaml.Unmarshaler interface. diff --git a/taskfile/task.go b/taskfile/task.go index 46548bbf3f..bf09bfeeca 100644 --- a/taskfile/task.go +++ b/taskfile/task.go @@ -5,29 +5,29 @@ type Tasks map[string]*Task // Task represents a task type Task struct { - Task string - Cmds []*Cmd - Deps []*Dep - Label string - Desc string - Summary string - Sources []string - Generates []string - Status []string - Preconditions []*Precondition - Dir string - Vars *Vars - Env *Vars - Silent bool - Interactive bool - Internal bool - Method string - Prefix string - IgnoreError bool - Run string - IncludeVars *Vars - IncludedTaskfileVars *Vars - IncludedTaskfile *IncludedTaskfile + Task string `json:"task"` + Cmds []*Cmd `json:"cmds"` + Deps []*Dep `json:"deps"` + Label string `json:"label"` + Desc string `json:"desc"` + Summary string `json:"summary"` + Sources []string `json:"sources"` + Generates []string `json:"generates"` + Status []string `json:"status"` + Preconditions []*Precondition `json:"preconditions"` + Dir string `json:"dir"` + Vars *Vars `json:"vars"` + Env *Vars `json:"env"` + Silent bool `json:"silent"` + Interactive bool `json:"interactive"` + Internal bool `json:"internal"` + Method string `json:"method"` + Prefix string `json:"prefix"` + IgnoreError bool `json:"ignore_error"` + Run string `json:"run"` + IncludeVars *Vars `json:"include_vars"` + IncludedTaskfileVars *Vars `json:"included_taskfile_vars"` + IncludedTaskfile *IncludedTaskfile `json:"included_taskfile"` } func (t *Task) Name() string { diff --git a/taskfile/var.go b/taskfile/var.go index 2693444f71..5ca5e3cea4 100644 --- a/taskfile/var.go +++ b/taskfile/var.go @@ -8,8 +8,8 @@ import ( // Vars is a string[string] variables map. type Vars struct { - Keys []string - Mapping map[string]Var + Keys []string `json:"keys"` + Mapping map[string]Var `json:"mapping"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. @@ -97,10 +97,10 @@ func (vs *Vars) Len() int { // Var represents either a static or dynamic variable. type Var struct { - Static string - Live interface{} - Sh string - Dir string + Static string `json:"static"` + Live interface{} `json:"live"` + Sh string `json:"sh"` + Dir string `json:"dir"` } // UnmarshalYAML implements yaml.Unmarshaler interface. From 575105704ced5cda65b7a783c8a667d621823aac Mon Sep 17 00:00:00 2001 From: David Alpert Date: Wed, 5 Oct 2022 11:59:10 -0500 Subject: [PATCH 3/4] refactor: add json tags to Cmd struct --- taskfile/cmd.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/taskfile/cmd.go b/taskfile/cmd.go index 9a5de8401c..f991021098 100644 --- a/taskfile/cmd.go +++ b/taskfile/cmd.go @@ -2,18 +2,18 @@ package taskfile // Cmd is a task command type Cmd struct { - Cmd string - Silent bool - Task string - Vars *Vars - IgnoreError bool - Defer bool + Cmd string `json:"cmd"` + Silent bool `json:"silent"` + Task string `json:"task"` + Vars *Vars `json:"vars"` + IgnoreError bool `json:"ignore_error"` + Defer bool `json:"defer"` } // Dep is a task dependency type Dep struct { - Task string - Vars *Vars + Task string `json:"task"` + Vars *Vars `json:"vars"` } // UnmarshalYAML implements yaml.Unmarshaler interface From a8ad1f35a4f0a445e1f60763f7fb0676ef041d55 Mon Sep 17 00:00:00 2001 From: David Alpert Date: Wed, 5 Oct 2022 11:59:18 -0500 Subject: [PATCH 4/4] refactor: add json tags to more structs --- taskfile/call.go | 4 ++-- taskfile/included_taskfile.go | 4 ++-- taskfile/output.go | 4 ++-- taskfile/taskfile.go | 22 +++++++++++----------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/taskfile/call.go b/taskfile/call.go index c1ca108356..9a39b2c2bc 100644 --- a/taskfile/call.go +++ b/taskfile/call.go @@ -2,6 +2,6 @@ package taskfile // Call is the parameters to a task call type Call struct { - Task string - Vars *Vars + Task string `json:"task"` + Vars *Vars `json:"vars"` } diff --git a/taskfile/included_taskfile.go b/taskfile/included_taskfile.go index 975cf5da30..f912ae952e 100644 --- a/taskfile/included_taskfile.go +++ b/taskfile/included_taskfile.go @@ -24,8 +24,8 @@ type IncludedTaskfile struct { // IncludedTaskfiles represents information about included tasksfiles type IncludedTaskfiles struct { - Keys []string - Mapping map[string]IncludedTaskfile + Keys []string `json:"keys"` + Mapping map[string]IncludedTaskfile `json:"mapping"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. diff --git a/taskfile/output.go b/taskfile/output.go index d3c7b54cd6..0185a92091 100644 --- a/taskfile/output.go +++ b/taskfile/output.go @@ -7,9 +7,9 @@ import ( // Output of the Task output type Output struct { // Name of the Output. - Name string `yaml:"-"` + Name string `yaml:"-" json:"name"` // Group specific style - Group OutputGroup + Group OutputGroup `json:"group"` } // IsSet returns true if and only if a custom output style is set. diff --git a/taskfile/taskfile.go b/taskfile/taskfile.go index ff7b1bbff7..9a10af0935 100644 --- a/taskfile/taskfile.go +++ b/taskfile/taskfile.go @@ -7,17 +7,17 @@ import ( // Taskfile represents a Taskfile.yml type Taskfile struct { - Version string - Expansions int - Output Output - Method string - Includes *IncludedTaskfiles - Vars *Vars - Env *Vars - Tasks Tasks - Silent bool - Dotenv []string - Run string + Version string `json:"version"` + Expansions int `json:"expansions"` + Output Output `json:"output"` + Method string `json:"method"` + Includes *IncludedTaskfiles `json:"includes"` + Vars *Vars `json:"vars"` + Env *Vars `json:"env"` + Tasks Tasks `json:"tasks"` + Silent bool `json:"silent"` + Dotenv []string `json:"dotenv"` + Run string `json:"run"` } // UnmarshalYAML implements yaml.Unmarshaler interface