From f22389a82440286eb0e2457ac0b272d47c3cfead Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Thu, 6 Apr 2023 12:07:57 +0100 Subject: [PATCH] feat: implement task sorting with `--sort` flag (#1105) * refactor: move deepcopy into its own package * feat: add generic orderedmap implementation * refactor: implement tasks with orderedmap * feat: implement sort flag for all task outputs * refactor: implement vars with orderedmap * chore: docs * fix: linting issues * fix: non deterministic behavior in tests --- CHANGELOG.md | 6 +- args/args_test.go | 104 ++++++----- cmd/task/task.go | 12 ++ docs/docs/api_reference.md | 1 + help.go | 30 ++-- .../copy.go => internal/deepcopy/deepcopy.go | 12 +- internal/orderedmap/orderedmap.go | 164 ++++++++++++++++++ internal/orderedmap/orderedmap_test.go | 121 +++++++++++++ internal/output/output_test.go | 6 +- internal/sort/sorter.go | 44 +++++ internal/sort/sorter_test.go | 77 ++++++++ internal/summary/summary.go | 2 +- internal/summary/summary_test.go | 8 +- setup.go | 18 +- task.go | 58 +++---- task_test.go | 2 +- taskfile/cmd.go | 8 +- taskfile/merge.go | 11 +- taskfile/read/dotenv.go | 2 +- taskfile/read/taskfile.go | 28 +-- taskfile/task.go | 24 +-- taskfile/taskfile_test.go | 48 +++-- taskfile/tasks.go | 29 ++-- taskfile/var.go | 103 ++++------- variables.go | 2 +- 25 files changed, 678 insertions(+), 242 deletions(-) rename taskfile/copy.go => internal/deepcopy/deepcopy.go (57%) create mode 100644 internal/orderedmap/orderedmap.go create mode 100644 internal/orderedmap/orderedmap_test.go create mode 100644 internal/sort/sorter.go create mode 100644 internal/sort/sorter_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b71ec9a..d726cfca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,12 @@ - Fix bug where "up-to-date" logs were not being omitted for silent tasks (#546, #1107 by @danquah). -- Add `.hg` (Mercurial) to the list of ignored directories when using - `--watch` (#1098 by @misery). +- Add `.hg` (Mercurial) to the list of ignored directories when using `--watch` + (#1098 by @misery). - More improvements to the release tool (#1096 by @pd93) - Enforce [gofumpt](https://github.com/mvdan/gofumpt) linter (#1099 by @pd93) +- Add `--sort` flag for use with `--list` and `--list-all` (#946, #1105 by + @pd93) ## v3.23.0 - 2023-03-26 diff --git a/args/args_test.go b/args/args_test.go index 22768704..4404a4e9 100644 --- a/args/args_test.go +++ b/args/args_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/go-task/task/v3/args" + "github.com/go-task/task/v3/internal/orderedmap" "github.com/go-task/task/v3/taskfile" ) @@ -32,12 +33,14 @@ func TestArgsV3(t *testing.T) { {Task: "task-c"}, }, ExpectedGlobals: &taskfile.Vars{ - Keys: []string{"FOO", "BAR", "BAZ"}, - Mapping: map[string]taskfile.Var{ - "FOO": {Static: "bar"}, - "BAR": {Static: "baz"}, - "BAZ": {Static: "foo"}, - }, + OrderedMap: orderedmap.FromMapWithOrder( + map[string]taskfile.Var{ + "FOO": {Static: "bar"}, + "BAR": {Static: "baz"}, + "BAZ": {Static: "foo"}, + }, + []string{"FOO", "BAR", "BAZ"}, + ), }, }, { @@ -46,10 +49,12 @@ func TestArgsV3(t *testing.T) { {Task: "task-a"}, }, ExpectedGlobals: &taskfile.Vars{ - Keys: []string{"CONTENT"}, - Mapping: map[string]taskfile.Var{ - "CONTENT": {Static: "with some spaces"}, - }, + OrderedMap: orderedmap.FromMapWithOrder( + map[string]taskfile.Var{ + "CONTENT": {Static: "with some spaces"}, + }, + []string{"CONTENT"}, + ), }, }, { @@ -59,10 +64,12 @@ func TestArgsV3(t *testing.T) { {Task: "task-b"}, }, ExpectedGlobals: &taskfile.Vars{ - Keys: []string{"FOO"}, - Mapping: map[string]taskfile.Var{ - "FOO": {Static: "bar"}, - }, + OrderedMap: orderedmap.FromMapWithOrder( + map[string]taskfile.Var{ + "FOO": {Static: "bar"}, + }, + []string{"FOO"}, + ), }, }, { @@ -83,11 +90,13 @@ func TestArgsV3(t *testing.T) { {Task: "default"}, }, ExpectedGlobals: &taskfile.Vars{ - Keys: []string{"FOO", "BAR"}, - Mapping: map[string]taskfile.Var{ - "FOO": {Static: "bar"}, - "BAR": {Static: "baz"}, - }, + OrderedMap: orderedmap.FromMapWithOrder( + map[string]taskfile.Var{ + "FOO": {Static: "bar"}, + "BAR": {Static: "baz"}, + }, + []string{"FOO", "BAR"}, + ), }, }, } @@ -97,7 +106,8 @@ func TestArgsV3(t *testing.T) { calls, globals := args.ParseV3(test.Args...) assert.Equal(t, test.ExpectedCalls, calls) if test.ExpectedGlobals.Len() > 0 || globals.Len() > 0 { - assert.Equal(t, test.ExpectedGlobals, globals) + assert.Equal(t, test.ExpectedGlobals.Keys(), globals.Keys()) + assert.Equal(t, test.ExpectedGlobals.Values(), globals.Values()) } }) } @@ -123,21 +133,25 @@ func TestArgsV2(t *testing.T) { { Task: "task-a", Vars: &taskfile.Vars{ - Keys: []string{"FOO"}, - Mapping: map[string]taskfile.Var{ - "FOO": {Static: "bar"}, - }, + OrderedMap: orderedmap.FromMapWithOrder( + map[string]taskfile.Var{ + "FOO": {Static: "bar"}, + }, + []string{"FOO"}, + ), }, }, {Task: "task-b"}, { Task: "task-c", Vars: &taskfile.Vars{ - Keys: []string{"BAR", "BAZ"}, - Mapping: map[string]taskfile.Var{ - "BAR": {Static: "baz"}, - "BAZ": {Static: "foo"}, - }, + OrderedMap: orderedmap.FromMapWithOrder( + map[string]taskfile.Var{ + "BAR": {Static: "baz"}, + "BAZ": {Static: "foo"}, + }, + []string{"BAR", "BAZ"}, + ), }, }, }, @@ -148,10 +162,12 @@ func TestArgsV2(t *testing.T) { { Task: "task-a", Vars: &taskfile.Vars{ - Keys: []string{"CONTENT"}, - Mapping: map[string]taskfile.Var{ - "CONTENT": {Static: "with some spaces"}, - }, + OrderedMap: orderedmap.FromMapWithOrder( + map[string]taskfile.Var{ + "CONTENT": {Static: "with some spaces"}, + }, + []string{"CONTENT"}, + ), }, }, }, @@ -163,10 +179,12 @@ func TestArgsV2(t *testing.T) { {Task: "task-b"}, }, ExpectedGlobals: &taskfile.Vars{ - Keys: []string{"FOO"}, - Mapping: map[string]taskfile.Var{ - "FOO": {Static: "bar"}, - }, + OrderedMap: orderedmap.FromMapWithOrder( + map[string]taskfile.Var{ + "FOO": {Static: "bar"}, + }, + []string{"FOO"}, + ), }, }, { @@ -187,11 +205,13 @@ func TestArgsV2(t *testing.T) { {Task: "default"}, }, ExpectedGlobals: &taskfile.Vars{ - Keys: []string{"FOO", "BAR"}, - Mapping: map[string]taskfile.Var{ - "FOO": {Static: "bar"}, - "BAR": {Static: "baz"}, - }, + OrderedMap: orderedmap.FromMapWithOrder( + map[string]taskfile.Var{ + "FOO": {Static: "bar"}, + "BAR": {Static: "baz"}, + }, + []string{"FOO", "BAR"}, + ), }, }, } diff --git a/cmd/task/task.go b/cmd/task/task.go index 25abeaa8..b2183268 100644 --- a/cmd/task/task.go +++ b/cmd/task/task.go @@ -15,6 +15,7 @@ import ( "github.com/go-task/task/v3" "github.com/go-task/task/v3/args" "github.com/go-task/task/v3/internal/logger" + "github.com/go-task/task/v3/internal/sort" ver "github.com/go-task/task/v3/internal/version" "github.com/go-task/task/v3/taskfile" ) @@ -57,6 +58,7 @@ func main() { list bool listAll bool listJson bool + taskSort string status bool force bool watch bool @@ -81,6 +83,7 @@ func main() { 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(&listJson, "json", "j", false, "Formats task list as JSON.") + pflag.StringVar(&taskSort, "sort", "", "Changes the order of the tasks when listed.") 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.") @@ -160,6 +163,14 @@ func main() { } } + var taskSorter sort.TaskSorter + switch taskSort { + case "none": + taskSorter = &sort.Noop{} + case "alphanumeric": + taskSorter = &sort.AlphaNumeric{} + } + e := task.Executor{ Force: force, Watch: watch, @@ -179,6 +190,7 @@ func main() { Stderr: os.Stderr, OutputStyle: output, + TaskSorter: taskSorter, } listOptions := task.NewListOptions(list, listAll, listJson) diff --git a/docs/docs/api_reference.md b/docs/docs/api_reference.md index 5836e8f5..21de4743 100644 --- a/docs/docs/api_reference.md +++ b/docs/docs/api_reference.md @@ -36,6 +36,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. | +| | `--sort` | `string` | `default` | Changes the order of the tasks when listed. | | | `--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. | diff --git a/help.go b/help.go index 1c1fc7cf..598b6516 100644 --- a/help.go +++ b/help.go @@ -7,7 +7,6 @@ import ( "io" "log" "os" - "sort" "strings" "text/tabwriter" @@ -16,6 +15,7 @@ import ( "github.com/go-task/task/v3/internal/editors" "github.com/go-task/task/v3/internal/fingerprint" "github.com/go-task/task/v3/internal/logger" + "github.com/go-task/task/v3/internal/sort" "github.com/go-task/task/v3/taskfile" ) @@ -129,19 +129,27 @@ func (e *Executor) ListTaskNames(allTasks bool) { if e.Stdout != nil { w = e.Stdout } - // 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 { - s = append(s, strings.TrimRight(t.Task, ":")) - for _, alias := range t.Aliases { - s = append(s, strings.TrimRight(alias, ":")) + + // Get the list of tasks and sort them + tasks := e.Taskfile.Tasks.Values() + + // Sort the tasks + if e.TaskSorter == nil { + e.TaskSorter = &sort.AlphaNumericWithRootTasksFirst{} + } + e.TaskSorter.Sort(tasks) + + // Create a list of task names + taskNames := make([]string, 0, e.Taskfile.Tasks.Len()) + for _, task := range tasks { + if (allTasks || task.Desc != "") && !task.Internal { + taskNames = append(taskNames, strings.TrimRight(task.Task, ":")) + for _, alias := range task.Aliases { + taskNames = append(taskNames, strings.TrimRight(alias, ":")) } } } - // sort and print all task names - sort.Strings(s) - for _, t := range s { + for _, t := range taskNames { fmt.Fprintln(w, t) } } diff --git a/taskfile/copy.go b/internal/deepcopy/deepcopy.go similarity index 57% rename from taskfile/copy.go rename to internal/deepcopy/deepcopy.go index d2c12f67..e378f9fd 100644 --- a/taskfile/copy.go +++ b/internal/deepcopy/deepcopy.go @@ -1,16 +1,16 @@ -package taskfile +package deepcopy -type DeepCopier[T any] interface { +type Copier[T any] interface { DeepCopy() T } -func deepCopySlice[T any](orig []T) []T { +func Slice[T any](orig []T) []T { if orig == nil { return nil } c := make([]T, len(orig)) for i, v := range orig { - if copyable, ok := any(v).(DeepCopier[T]); ok { + if copyable, ok := any(v).(Copier[T]); ok { c[i] = copyable.DeepCopy() } else { c[i] = v @@ -19,13 +19,13 @@ func deepCopySlice[T any](orig []T) []T { return c } -func deepCopyMap[K comparable, V any](orig map[K]V) map[K]V { +func Map[K comparable, V any](orig map[K]V) map[K]V { if orig == nil { return nil } c := make(map[K]V, len(orig)) for k, v := range orig { - if copyable, ok := any(v).(DeepCopier[V]); ok { + if copyable, ok := any(v).(Copier[V]); ok { c[k] = copyable.DeepCopy() } else { c[k] = v diff --git a/internal/orderedmap/orderedmap.go b/internal/orderedmap/orderedmap.go new file mode 100644 index 00000000..f34ad925 --- /dev/null +++ b/internal/orderedmap/orderedmap.go @@ -0,0 +1,164 @@ +package orderedmap + +import ( + "fmt" + + "golang.org/x/exp/constraints" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" + "gopkg.in/yaml.v3" + + "github.com/go-task/task/v3/internal/deepcopy" +) + +// An OrderedMap is a wrapper around a regular map that maintains an ordered +// list of the map's keys. This allows you to run deterministic and ordered +// operations on the map such as printing/serializing/iterating. +type OrderedMap[K constraints.Ordered, V any] struct { + s []K + m map[K]V +} + +// New will create a new OrderedMap of the given type and return it. +func New[K constraints.Ordered, V any]() OrderedMap[K, V] { + return OrderedMap[K, V]{ + s: make([]K, 0), + m: make(map[K]V), + } +} + +// FromMap will create a new OrderedMap from the given map. Since Golang maps +// are unordered, the order of the created OrderedMap will be random. +func FromMap[K constraints.Ordered, V any](m map[K]V) OrderedMap[K, V] { + om := New[K, V]() + om.m = m + om.s = maps.Keys(m) + return om +} + +func FromMapWithOrder[K constraints.Ordered, V any](m map[K]V, order []K) OrderedMap[K, V] { + om := New[K, V]() + if len(m) != len(order) { + panic("length of map and order must be equal") + } + om.m = m + om.s = order + for key := range om.m { + if !slices.Contains(om.s, key) { + panic("order keys must match map keys") + } + } + return om +} + +// Len will return the number of items in the map. +func (om *OrderedMap[K, V]) Len() int { + return len(om.s) +} + +// Set will set the value for a given key. +func (om *OrderedMap[K, V]) Set(key K, value V) { + if om.m == nil { + om.m = make(map[K]V) + } + if _, ok := om.m[key]; !ok { + om.s = append(om.s, key) + } + om.m[key] = value +} + +// Get will return the value for a given key. +// If the key does not exist, it will return the zero value of the value type. +func (om *OrderedMap[K, V]) Get(key K) V { + value, ok := om.m[key] + if !ok { + var zero V + return zero + } + return value +} + +// Exists will return whether or not the given key exists. +func (om *OrderedMap[K, V]) Exists(key K) bool { + _, ok := om.m[key] + return ok +} + +// Sort will sort the map. +func (om *OrderedMap[K, V]) Sort() { + slices.Sort(om.s) +} + +// SortFunc will sort the map using the given function. +func (om *OrderedMap[K, V]) SortFunc(less func(i, j K) bool) { + slices.SortFunc(om.s, less) +} + +// Keys will return a slice of the map's keys in order. +func (om *OrderedMap[K, V]) Keys() []K { + return om.s +} + +// Values will return a slice of the map's values in order. +func (om *OrderedMap[K, V]) Values() []V { + var values []V + for _, key := range om.s { + values = append(values, om.m[key]) + } + return values +} + +// Range will iterate over the map and call the given function for each key/value. +func (om *OrderedMap[K, V]) Range(fn func(key K, value V) error) error { + for _, key := range om.s { + if err := fn(key, om.m[key]); err != nil { + return err + } + } + return nil +} + +// Merge merges the given Vars into the caller one +func (om *OrderedMap[K, V]) Merge(other OrderedMap[K, V]) { + // nolint: errcheck + other.Range(func(key K, value V) error { + om.Set(key, value) + return nil + }) +} + +func (om *OrderedMap[K, V]) DeepCopy() OrderedMap[K, V] { + return OrderedMap[K, V]{ + s: deepcopy.Slice(om.s), + m: deepcopy.Map(om.m), + } +} + +func (om *OrderedMap[K, V]) UnmarshalYAML(node *yaml.Node) error { + switch node.Kind { + // Even numbers contain the keys + // Odd numbers contain the values + case yaml.MappingNode: + for i := 0; i < len(node.Content); i += 2 { + // Decode the key + keyNode := node.Content[i] + var k K + if err := keyNode.Decode(&k); err != nil { + return err + } + + // Decode the value + valueNode := node.Content[i+1] + var v V + if err := valueNode.Decode(&v); err != nil { + return err + } + + // Set the key and value + om.Set(k, v) + } + return nil + } + + return fmt.Errorf("yaml: line %d: cannot unmarshal %s into variables", node.Line, node.ShortTag()) +} diff --git a/internal/orderedmap/orderedmap_test.go b/internal/orderedmap/orderedmap_test.go new file mode 100644 index 00000000..374b34da --- /dev/null +++ b/internal/orderedmap/orderedmap_test.go @@ -0,0 +1,121 @@ +package orderedmap + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func TestFromMap(t *testing.T) { + m := map[int]string{3: "three", 1: "one", 2: "two"} + om := FromMap(m) + assert.Len(t, om.m, 3) + assert.Len(t, om.s, 3) + assert.ElementsMatch(t, []int{1, 2, 3}, om.s) + for key, value := range m { + assert.Equal(t, om.Get(key), value) + } +} + +func TestSetGetExists(t *testing.T) { + om := New[int, string]() + assert.False(t, om.Exists(1)) + assert.Equal(t, "", om.Get(1)) + om.Set(1, "one") + assert.True(t, om.Exists(1)) + assert.Equal(t, "one", om.Get(1)) +} + +func TestSort(t *testing.T) { + om := New[int, string]() + om.Set(3, "three") + om.Set(1, "one") + om.Set(2, "two") + om.Sort() + assert.Equal(t, []int{1, 2, 3}, om.s) +} + +func TestSortFunc(t *testing.T) { + om := New[int, string]() + om.Set(3, "three") + om.Set(1, "one") + om.Set(2, "two") + om.SortFunc(func(i, j int) bool { + return i > j + }) + assert.Equal(t, []int{3, 2, 1}, om.s) +} + +func TestKeysValues(t *testing.T) { + om := New[int, string]() + om.Set(3, "three") + om.Set(1, "one") + om.Set(2, "two") + assert.Equal(t, []int{3, 1, 2}, om.Keys()) + assert.Equal(t, []string{"three", "one", "two"}, om.Values()) +} + +func Range(t *testing.T) { + om := New[int, string]() + om.Set(3, "three") + om.Set(1, "one") + om.Set(2, "two") + + expectedKeys := []int{3, 1, 2} + expectedValues := []string{"three", "one", "two"} + + keys := make([]int, 0, len(expectedKeys)) + values := make([]string, 0, len(expectedValues)) + + err := om.Range(func(key int, value string) error { + keys = append(keys, key) + values = append(values, value) + return nil + }) + + assert.NoError(t, err) + assert.ElementsMatch(t, expectedKeys, keys) + assert.ElementsMatch(t, expectedValues, values) +} + +func TestOrderedMapMerge(t *testing.T) { + om1 := New[string, int]() + om1.Set("a", 1) + om1.Set("b", 2) + + om2 := New[string, int]() + om2.Set("b", 3) + om2.Set("c", 4) + + om1.Merge(om2) + + expectedKeys := []string{"a", "b", "c"} + expectedValues := []int{1, 3, 4} + + assert.Equal(t, len(expectedKeys), len(om1.s)) + assert.Equal(t, len(expectedKeys), len(om1.m)) + + for i, key := range expectedKeys { + assert.True(t, om1.Exists(key)) + assert.Equal(t, expectedValues[i], om1.Get(key)) + } +} + +func TestUnmarshalYAML(t *testing.T) { + yamlString := ` +3: three +1: one +2: two +` + var om OrderedMap[int, string] + err := yaml.Unmarshal([]byte(yamlString), &om) + require.NoError(t, err) + + expectedKeys := []int{3, 1, 2} + expectedValues := []string{"three", "one", "two"} + + assert.Equal(t, expectedKeys, om.Keys()) + assert.Equal(t, expectedValues, om.Values()) +} diff --git a/internal/output/output_test.go b/internal/output/output_test.go index d5a5fab0..5d099381 100644 --- a/internal/output/output_test.go +++ b/internal/output/output_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/go-task/task/v3/internal/orderedmap" "github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/taskfile" @@ -47,10 +48,9 @@ func TestGroup(t *testing.T) { func TestGroupWithBeginEnd(t *testing.T) { tmpl := templater.Templater{ Vars: &taskfile.Vars{ - Keys: []string{"VAR1"}, - Mapping: map[string]taskfile.Var{ + OrderedMap: orderedmap.FromMap(map[string]taskfile.Var{ "VAR1": {Static: "example-value"}, - }, + }), }, } diff --git a/internal/sort/sorter.go b/internal/sort/sorter.go new file mode 100644 index 00000000..f26105eb --- /dev/null +++ b/internal/sort/sorter.go @@ -0,0 +1,44 @@ +package sort + +import ( + "sort" + "strings" + + "github.com/go-task/task/v3/taskfile" +) + +type TaskSorter interface { + Sort([]*taskfile.Task) +} + +type Noop struct{} + +func (s *Noop) Sort(tasks []*taskfile.Task) {} + +type AlphaNumeric struct{} + +// Tasks that are not namespaced should be listed before tasks that are. +// We detect this by searching for a ':' in the task name. +func (s *AlphaNumeric) Sort(tasks []*taskfile.Task) { + sort.Slice(tasks, func(i, j int) bool { + return tasks[i].Task < tasks[j].Task + }) +} + +type AlphaNumericWithRootTasksFirst struct{} + +// Tasks that are not namespaced should be listed before tasks that are. +// We detect this by searching for a ':' in the task name. +func (s *AlphaNumericWithRootTasksFirst) Sort(tasks []*taskfile.Task) { + sort.Slice(tasks, func(i, j int) bool { + iContainsColon := strings.Contains(tasks[i].Task, ":") + jContainsColon := strings.Contains(tasks[j].Task, ":") + if iContainsColon == jContainsColon { + return tasks[i].Task < tasks[j].Task + } + if !iContainsColon && jContainsColon { + return true + } + return false + }) +} diff --git a/internal/sort/sorter_test.go b/internal/sort/sorter_test.go new file mode 100644 index 00000000..4d06d86d --- /dev/null +++ b/internal/sort/sorter_test.go @@ -0,0 +1,77 @@ +package sort + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/go-task/task/v3/taskfile" +) + +func TestAlphaNumericWithRootTasksFirst_Sort(t *testing.T) { + task1 := &taskfile.Task{Task: "task1"} + task2 := &taskfile.Task{Task: "task2"} + task3 := &taskfile.Task{Task: "ns1:task3"} + task4 := &taskfile.Task{Task: "ns2:task4"} + task5 := &taskfile.Task{Task: "task5"} + task6 := &taskfile.Task{Task: "ns3:task6"} + + tests := []struct { + name string + tasks []*taskfile.Task + want []*taskfile.Task + }{ + { + name: "no namespace tasks sorted alphabetically first", + tasks: []*taskfile.Task{task3, task2, task1}, + want: []*taskfile.Task{task1, task2, task3}, + }, + { + name: "namespace tasks sorted alphabetically after non-namespaced tasks", + tasks: []*taskfile.Task{task3, task4, task5}, + want: []*taskfile.Task{task5, task3, task4}, + }, + { + name: "all tasks sorted alphabetically with root tasks first", + tasks: []*taskfile.Task{task6, task5, task4, task3, task2, task1}, + want: []*taskfile.Task{task1, task2, task5, task3, task4, task6}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &AlphaNumericWithRootTasksFirst{} + s.Sort(tt.tasks) + assert.Equal(t, tt.want, tt.tasks) + }) + } +} + +func TestAlphaNumeric_Sort(t *testing.T) { + task1 := &taskfile.Task{Task: "task1"} + task2 := &taskfile.Task{Task: "task2"} + task3 := &taskfile.Task{Task: "ns1:task3"} + task4 := &taskfile.Task{Task: "ns2:task4"} + task5 := &taskfile.Task{Task: "task5"} + task6 := &taskfile.Task{Task: "ns3:task6"} + + tests := []struct { + name string + tasks []*taskfile.Task + want []*taskfile.Task + }{ + { + name: "all tasks sorted alphabetically", + tasks: []*taskfile.Task{task3, task2, task5, task1, task4, task6}, + want: []*taskfile.Task{task3, task4, task6, task1, task2, task5}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &AlphaNumeric{} + s.Sort(tt.tasks) + assert.Equal(t, tt.tasks, tt.want) + }) + } +} diff --git a/internal/summary/summary.go b/internal/summary/summary.go index 0e58831c..5080c9ac 100644 --- a/internal/summary/summary.go +++ b/internal/summary/summary.go @@ -10,7 +10,7 @@ import ( func PrintTasks(l *logger.Logger, t *taskfile.Taskfile, c []taskfile.Call) { for i, call := range c { PrintSpaceBetweenSummaries(l, i) - PrintTask(l, t.Tasks[call.Task]) + PrintTask(l, t.Tasks.Get(call.Task)) } } diff --git a/internal/summary/summary_test.go b/internal/summary/summary_test.go index cb142cf4..51752638 100644 --- a/internal/summary/summary_test.go +++ b/internal/summary/summary_test.go @@ -156,10 +156,10 @@ func TestPrintAllWithSpaces(t *testing.T) { t2 := &taskfile.Task{Task: "t2"} t3 := &taskfile.Task{Task: "t3"} - tasks := make(taskfile.Tasks, 3) - tasks["t1"] = t1 - tasks["t2"] = t2 - tasks["t3"] = t3 + tasks := taskfile.Tasks{} + tasks.Set("t1", t1) + tasks.Set("t2", t2) + tasks.Set("t3", t3) summary.PrintTasks(&l, &taskfile.Taskfile{Tasks: tasks}, diff --git a/setup.go b/setup.go index fed76081..2eb72a87 100644 --- a/setup.go +++ b/setup.go @@ -94,10 +94,10 @@ func (e *Executor) setupFuzzyModel() { model.SetThreshold(1) // because we want to build grammar based on every task name var words []string - for taskName := range e.Taskfile.Tasks { + for _, taskName := range e.Taskfile.Tasks.Keys() { words = append(words, taskName) - for _, task := range e.Taskfile.Tasks { + for _, task := range e.Taskfile.Tasks.Values() { words = append(words, task.Aliases...) } } @@ -202,7 +202,7 @@ func (e *Executor) readDotEnvFiles() error { } err = env.Range(func(key string, value taskfile.Var) error { - if _, ok := e.Taskfile.Env.Mapping[key]; !ok { + if ok := e.Taskfile.Env.Exists(key); !ok { e.Taskfile.Env.Set(key, value) } return nil @@ -232,9 +232,9 @@ func (e *Executor) setupDefaults() { func (e *Executor) setupConcurrencyState() { e.executionHashes = make(map[string]context.Context) - e.taskCallCount = make(map[string]*int32, len(e.Taskfile.Tasks)) - e.mkdirMutexMap = make(map[string]*sync.Mutex, len(e.Taskfile.Tasks)) - for k := range e.Taskfile.Tasks { + e.taskCallCount = make(map[string]*int32, e.Taskfile.Tasks.Len()) + e.mkdirMutexMap = make(map[string]*sync.Mutex, e.Taskfile.Tasks.Len()) + for _, k := range e.Taskfile.Tasks.Keys() { e.taskCallCount[k] = new(int32) e.mkdirMutexMap[k] = &sync.Mutex{} } @@ -281,7 +281,7 @@ func (e *Executor) doVersionChecks() error { if v.Compare(semver.MustParse("2.1")) <= 0 { err := errors.New(`task: Taskfile option "ignore_error" is only available starting on Taskfile version v2.1`) - for _, task := range e.Taskfile.Tasks { + for _, task := range e.Taskfile.Tasks.Values() { if task.IgnoreError { return err } @@ -294,7 +294,7 @@ func (e *Executor) doVersionChecks() error { } if v.LessThan(semver.MustParse("2.6")) { - for _, task := range e.Taskfile.Tasks { + for _, task := range e.Taskfile.Tasks.Values() { if len(task.Preconditions) > 0 { return errors.New(`task: Task option "preconditions" is only available starting on Taskfile version v2.6`) } @@ -318,7 +318,7 @@ func (e *Executor) doVersionChecks() error { return errors.New(`task: Setting the "run" type is only available starting on Taskfile version v3.7`) } - for _, task := range e.Taskfile.Tasks { + for _, task := range e.Taskfile.Tasks.Values() { if task.Run != "" { return errors.New(`task: Setting the "run" type is only available starting on Taskfile version v3.7`) } diff --git a/task.go b/task.go index fb039729..98f6aca3 100644 --- a/task.go +++ b/task.go @@ -6,8 +6,6 @@ import ( "io" "os" "runtime" - "sort" - "strings" "sync" "sync/atomic" "time" @@ -19,6 +17,7 @@ import ( "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/internal/slicesext" + "github.com/go-task/task/v3/internal/sort" "github.com/go-task/task/v3/internal/summary" "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/taskfile" @@ -60,6 +59,7 @@ type Executor struct { Compiler compiler.Compiler Output output.Output OutputStyle taskfile.Output + TaskSorter sort.TaskSorter taskvars *taskfile.Vars fuzzyModel *fuzzy.Model @@ -357,14 +357,14 @@ func (e *Executor) startExecution(ctx context.Context, t *taskfile.Task, execute // If multiple tasks contain the same alias or no matches are found an error is returned. func (e *Executor) GetTask(call taskfile.Call) (*taskfile.Task, error) { // Search for a matching task - matchingTask, ok := e.Taskfile.Tasks[call.Task] - if ok { + matchingTask := e.Taskfile.Tasks.Get(call.Task) + if matchingTask != nil { return matchingTask, nil } // If didn't find one, search for a task with a matching alias var aliasedTasks []string - for _, task := range e.Taskfile.Tasks { + for _, task := range e.Taskfile.Tasks.Values() { if slices.Contains(task.Aliases, call.Task) { aliasedTasks = append(aliasedTasks, task.Task) matchingTask = task @@ -395,28 +395,33 @@ func (e *Executor) GetTask(call taskfile.Call) (*taskfile.Task, error) { type FilterFunc func(task *taskfile.Task) bool func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*taskfile.Task, error) { - tasks := make([]*taskfile.Task, 0, len(e.Taskfile.Tasks)) + tasks := make([]*taskfile.Task, 0, e.Taskfile.Tasks.Len()) // Create an error group to wait for each task to be compiled var g errgroup.Group - // Fetch and compile the list of tasks - for key := range e.Taskfile.Tasks { - task := e.Taskfile.Tasks[key] - g.Go(func() error { - // Check if we should filter the task - for _, filter := range filters { - if filter(task) { - return nil - } + // Filter tasks based on the given filter functions + for _, task := range e.Taskfile.Tasks.Values() { + var shouldFilter bool + for _, filter := range filters { + if filter(task) { + shouldFilter = true } + } + if !shouldFilter { + tasks = append(tasks, task) + } + } - // Compile the task + // Compile the list of tasks + for i := range tasks { + task := tasks[i] + g.Go(func() error { compiledTask, err := e.FastCompiledTask(taskfile.Call{Task: task.Task}) if err == nil { task = compiledTask } - tasks = append(tasks, task) + task = compiledTask return nil }) } @@ -426,20 +431,11 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*taskfile.Task, error) return nil, err } - // Sort the tasks. - // Tasks that are not namespaced should be listed before tasks that are. - // We detect this by searching for a ':' in the task name. - sort.Slice(tasks, func(i, j int) bool { - iContainsColon := strings.Contains(tasks[i].Task, ":") - jContainsColon := strings.Contains(tasks[j].Task, ":") - if iContainsColon == jContainsColon { - return tasks[i].Task < tasks[j].Task - } - if !iContainsColon && jContainsColon { - return true - } - return false - }) + // Sort the tasks + if e.TaskSorter == nil { + e.TaskSorter = &sort.AlphaNumericWithRootTasksFirst{} + } + e.TaskSorter.Sort(tasks) return tasks, nil } diff --git a/task_test.go b/task_test.go index ed16ae07..7ca97713 100644 --- a/task_test.go +++ b/task_test.go @@ -790,7 +790,7 @@ func TestTaskVersion(t *testing.T) { } require.NoError(t, e.Setup()) assert.Equal(t, test.Version, e.Taskfile.Version) - assert.Equal(t, 2, len(e.Taskfile.Tasks)) + assert.Equal(t, 2, e.Taskfile.Tasks.Len()) }) } } diff --git a/taskfile/cmd.go b/taskfile/cmd.go index 326a0287..f0f0ff7b 100644 --- a/taskfile/cmd.go +++ b/taskfile/cmd.go @@ -4,6 +4,8 @@ import ( "fmt" "gopkg.in/yaml.v3" + + "github.com/go-task/task/v3/internal/deepcopy" ) // Cmd is a task command @@ -27,12 +29,12 @@ func (c *Cmd) DeepCopy() *Cmd { Cmd: c.Cmd, Silent: c.Silent, Task: c.Task, - Set: deepCopySlice(c.Set), - Shopt: deepCopySlice(c.Shopt), + Set: deepcopy.Slice(c.Set), + Shopt: deepcopy.Slice(c.Shopt), Vars: c.Vars.DeepCopy(), IgnoreError: c.IgnoreError, Defer: c.Defer, - Platforms: deepCopySlice(c.Platforms), + Platforms: deepcopy.Slice(c.Platforms), } } diff --git a/taskfile/merge.go b/taskfile/merge.go index c4d872da..92a79c58 100644 --- a/taskfile/merge.go +++ b/taskfile/merge.go @@ -30,10 +30,7 @@ func Merge(t1, t2 *Taskfile, includedTaskfile *IncludedTaskfile, namespaces ...s t1.Vars.Merge(t2.Vars) t1.Env.Merge(t2.Env) - if t1.Tasks == nil { - t1.Tasks = make(Tasks) - } - for k, v := range t2.Tasks { + return t2.Tasks.Range(func(k string, v *Task) error { // We do a deep copy of the task struct here to ensure that no data can // be changed elsewhere once the taskfile is merged. task := v.DeepCopy() @@ -67,10 +64,10 @@ func Merge(t1, t2 *Taskfile, includedTaskfile *IncludedTaskfile, namespaces ...s // Add the task to the merged taskfile taskNameWithNamespace := taskNameWithNamespace(k, namespaces...) task.Task = taskNameWithNamespace - t1.Tasks[taskNameWithNamespace] = task - } + t1.Tasks.Set(taskNameWithNamespace, task) - return nil + return nil + }) } func taskNameWithNamespace(taskName string, namespaces ...string) string { diff --git a/taskfile/read/dotenv.go b/taskfile/read/dotenv.go index caed7b1c..8aa4da65 100644 --- a/taskfile/read/dotenv.go +++ b/taskfile/read/dotenv.go @@ -41,7 +41,7 @@ func Dotenv(c compiler.Compiler, tf *taskfile.Taskfile, dir string) (*taskfile.V return nil, err } for key, value := range envs { - if _, ok := env.Mapping[key]; !ok { + if ok := env.Exists(key); !ok { env.Set(key, taskfile.Var{Static: value}) } } diff --git a/taskfile/read/taskfile.go b/taskfile/read/taskfile.go index ecf00e3b..08552a0f 100644 --- a/taskfile/read/taskfile.go +++ b/taskfile/read/taskfile.go @@ -128,18 +128,22 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, string, error) { return err } - for k, v := range includedTaskfile.Vars.Mapping { + // nolint: errcheck + includedTaskfile.Vars.Range(func(k string, v taskfile.Var) error { o := v o.Dir = dir - includedTaskfile.Vars.Mapping[k] = o - } - for k, v := range includedTaskfile.Env.Mapping { + includedTaskfile.Vars.Set(k, o) + return nil + }) + // nolint: errcheck + includedTaskfile.Env.Range(func(k string, v taskfile.Var) error { o := v o.Dir = dir - includedTaskfile.Env.Mapping[k] = o - } + includedTaskfile.Env.Set(k, o) + return nil + }) - for _, task := range includedTaskfile.Tasks { + for _, task := range includedTaskfile.Tasks.Values() { task.Dir = filepathext.SmartJoin(dir, task.Dir) task.IncludeVars = includedTask.Vars task.IncludedTaskfileVars = includedTaskfile.Vars @@ -151,10 +155,12 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, string, error) { return err } - if includedTaskfile.Tasks["default"] != nil && t.Tasks[namespace] == nil { + if includedTaskfile.Tasks.Get("default") != nil && t.Tasks.Get(namespace) == nil { defaultTaskName := fmt.Sprintf("%s:default", namespace) - t.Tasks[defaultTaskName].Aliases = append(t.Tasks[defaultTaskName].Aliases, namespace) - t.Tasks[defaultTaskName].Aliases = append(t.Tasks[defaultTaskName].Aliases, includedTask.Aliases...) + task := t.Tasks.Get(defaultTaskName) + task.Aliases = append(task.Aliases, namespace) + task.Aliases = append(task.Aliases, includedTask.Aliases...) + t.Tasks.Set(defaultTaskName, task) } return nil @@ -179,7 +185,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, string, error) { // Set the location of the Taskfile t.Location = path - for _, task := range t.Tasks { + for _, task := range t.Tasks.Values() { // If the task is not defined, create a new one if task == nil { task = &taskfile.Task{} diff --git a/taskfile/task.go b/taskfile/task.go index d5621751..d02e4b84 100644 --- a/taskfile/task.go +++ b/taskfile/task.go @@ -4,6 +4,8 @@ import ( "fmt" "gopkg.in/yaml.v3" + + "github.com/go-task/task/v3/internal/deepcopy" ) // Task represents a task @@ -136,22 +138,22 @@ func (t *Task) DeepCopy() *Task { } c := &Task{ Task: t.Task, - Cmds: deepCopySlice(t.Cmds), - Deps: deepCopySlice(t.Deps), + Cmds: deepcopy.Slice(t.Cmds), + Deps: deepcopy.Slice(t.Deps), Label: t.Label, Desc: t.Desc, Summary: t.Summary, - Aliases: deepCopySlice(t.Aliases), - Sources: deepCopySlice(t.Sources), - Generates: deepCopySlice(t.Generates), - Status: deepCopySlice(t.Status), - Preconditions: deepCopySlice(t.Preconditions), + Aliases: deepcopy.Slice(t.Aliases), + Sources: deepcopy.Slice(t.Sources), + Generates: deepcopy.Slice(t.Generates), + Status: deepcopy.Slice(t.Status), + Preconditions: deepcopy.Slice(t.Preconditions), Dir: t.Dir, - Set: deepCopySlice(t.Set), - Shopt: deepCopySlice(t.Shopt), + Set: deepcopy.Slice(t.Set), + Shopt: deepcopy.Slice(t.Shopt), Vars: t.Vars.DeepCopy(), Env: t.Env.DeepCopy(), - Dotenv: deepCopySlice(t.Dotenv), + Dotenv: deepcopy.Slice(t.Dotenv), Silent: t.Silent, Interactive: t.Interactive, Internal: t.Internal, @@ -162,7 +164,7 @@ func (t *Task) DeepCopy() *Task { IncludeVars: t.IncludeVars.DeepCopy(), IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(), IncludedTaskfile: t.IncludedTaskfile.DeepCopy(), - Platforms: deepCopySlice(t.Platforms), + Platforms: deepcopy.Slice(t.Platforms), Location: t.Location.DeepCopy(), } return c diff --git a/taskfile/taskfile_test.go b/taskfile/taskfile_test.go index 5d086fb8..17542ecc 100644 --- a/taskfile/taskfile_test.go +++ b/taskfile/taskfile_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" + "github.com/go-task/task/v3/internal/orderedmap" "github.com/go-task/task/v3/taskfile" ) @@ -36,13 +37,17 @@ vars: { yamlTaskCall, &taskfile.Cmd{}, - &taskfile.Cmd{Task: "another-task", Vars: &taskfile.Vars{ - Keys: []string{"PARAM1", "PARAM2"}, - Mapping: map[string]taskfile.Var{ - "PARAM1": {Static: "VALUE1"}, - "PARAM2": {Static: "VALUE2"}, + &taskfile.Cmd{ + Task: "another-task", Vars: &taskfile.Vars{ + OrderedMap: orderedmap.FromMapWithOrder( + map[string]taskfile.Var{ + "PARAM1": {Static: "VALUE1"}, + "PARAM2": {Static: "VALUE2"}, + }, + []string{"PARAM1", "PARAM2"}, + ), }, - }}, + }, }, { yamlDeferredCmd, @@ -52,12 +57,17 @@ vars: { yamlDeferredCall, &taskfile.Cmd{}, - &taskfile.Cmd{Task: "some_task", Vars: &taskfile.Vars{ - Keys: []string{"PARAM1"}, - Mapping: map[string]taskfile.Var{ - "PARAM1": {Static: "var"}, + &taskfile.Cmd{ + Task: "some_task", Vars: &taskfile.Vars{ + OrderedMap: orderedmap.FromMapWithOrder( + map[string]taskfile.Var{ + "PARAM1": {Static: "var"}, + }, + []string{"PARAM1"}, + ), }, - }, Defer: true}, + Defer: true, + }, }, { yamlDep, @@ -67,13 +77,17 @@ vars: { yamlTaskCall, &taskfile.Dep{}, - &taskfile.Dep{Task: "another-task", Vars: &taskfile.Vars{ - Keys: []string{"PARAM1", "PARAM2"}, - Mapping: map[string]taskfile.Var{ - "PARAM1": {Static: "VALUE1"}, - "PARAM2": {Static: "VALUE2"}, + &taskfile.Dep{ + Task: "another-task", Vars: &taskfile.Vars{ + OrderedMap: orderedmap.FromMapWithOrder( + map[string]taskfile.Var{ + "PARAM1": {Static: "VALUE1"}, + "PARAM2": {Static: "VALUE2"}, + }, + []string{"PARAM1", "PARAM2"}, + ), }, - }}, + }, }, } for _, test := range tests { diff --git a/taskfile/tasks.go b/taskfile/tasks.go index 975744bf..b027e62b 100644 --- a/taskfile/tasks.go +++ b/taskfile/tasks.go @@ -4,40 +4,49 @@ import ( "fmt" "gopkg.in/yaml.v3" + + "github.com/go-task/task/v3/internal/orderedmap" ) // Tasks represents a group of tasks -type Tasks map[string]*Task +type Tasks struct { + orderedmap.OrderedMap[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 { + tasks := orderedmap.New[string, *Task]() + if err := node.Decode(&tasks); err != nil { return err } - for name := range tasks { + // nolint: errcheck + tasks.Range(func(name string, task *Task) error { // Set the task's name - if tasks[name] == nil { - tasks[name] = &Task{ + if task == nil { + task = &Task{ Task: name, } } - tasks[name].Task = name + task.Task = name // Set the task's location for _, keys := range node.Content { if keys.Value == name { - tasks[name].Location = &Location{ + task.Location = &Location{ Line: keys.Line, Column: keys.Column, } } } - } + tasks.Set(name, task) + return nil + }) - *t = Tasks(tasks) + *t = Tasks{ + OrderedMap: tasks, + } return nil } diff --git a/taskfile/var.go b/taskfile/var.go index aa446441..ba1c2d26 100644 --- a/taskfile/var.go +++ b/taskfile/var.go @@ -3,80 +3,14 @@ package taskfile import ( "fmt" - "golang.org/x/exp/slices" "gopkg.in/yaml.v3" + + "github.com/go-task/task/v3/internal/orderedmap" ) // Vars is a string[string] variables map. type Vars struct { - Keys []string - Mapping map[string]Var -} - -func (vs *Vars) UnmarshalYAML(node *yaml.Node) error { - switch node.Kind { - case yaml.MappingNode: - // NOTE(@andreynering): on this style of custom unmarshalling, - // even number contains the keys, while odd numbers contains - // the values. - for i := 0; i < len(node.Content); i += 2 { - keyNode := node.Content[i] - valueNode := node.Content[i+1] - - var v Var - if err := valueNode.Decode(&v); err != nil { - return err - } - vs.Set(keyNode.Value, v) - } - return nil - } - - return fmt.Errorf("yaml: line %d: cannot unmarshal %s into variables", node.Line, node.ShortTag()) -} - -// DeepCopy creates a new instance of Vars and copies -// data by value from the source struct. -func (vs *Vars) DeepCopy() *Vars { - if vs == nil { - return nil - } - return &Vars{ - Keys: deepCopySlice(vs.Keys), - Mapping: deepCopyMap(vs.Mapping), - } -} - -// Merge merges the given Vars into the caller one -func (vs *Vars) Merge(other *Vars) { - _ = other.Range(func(key string, value Var) error { - vs.Set(key, value) - return nil - }) -} - -// Set sets a value to a given key -func (vs *Vars) Set(key string, value Var) { - if vs.Mapping == nil { - vs.Mapping = make(map[string]Var, 1) - } - if !slices.Contains(vs.Keys, key) { - vs.Keys = append(vs.Keys, key) - } - vs.Mapping[key] = value -} - -// Range allows you to loop into the vars in its right order -func (vs *Vars) Range(yield func(key string, value Var) error) error { - if vs == nil { - return nil - } - for _, k := range vs.Keys { - if err := yield(k, vs.Mapping[k]); err != nil { - return err - } - } - return nil + orderedmap.OrderedMap[string, Var] } // ToCacheMap converts Vars to a map containing only the static @@ -100,12 +34,39 @@ func (vs *Vars) ToCacheMap() (m map[string]any) { return } -// Len returns the size of the map +// Wrapper around OrderedMap.Set to ensure we don't get nil pointer errors +func (vs *Vars) Range(f func(k string, v Var) error) error { + if vs == nil { + return nil + } + return vs.OrderedMap.Range(f) +} + +// Wrapper around OrderedMap.Merge to ensure we don't get nil pointer errors +func (vs *Vars) Merge(other *Vars) { + if vs == nil || other == nil { + return + } + vs.OrderedMap.Merge(other.OrderedMap) +} + +// Wrapper around OrderedMap.Len to ensure we don't get nil pointer errors func (vs *Vars) Len() int { if vs == nil { return 0 } - return len(vs.Keys) + return vs.OrderedMap.Len() +} + +// DeepCopy creates a new instance of Vars and copies +// data by value from the source struct. +func (vs *Vars) DeepCopy() *Vars { + if vs == nil { + return nil + } + return &Vars{ + OrderedMap: vs.OrderedMap.DeepCopy(), + } } // Var represents either a static or dynamic variable. diff --git a/variables.go b/variables.go index 0885d849..75667822 100644 --- a/variables.go +++ b/variables.go @@ -91,7 +91,7 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf return nil, err } for key, value := range envs { - if _, ok := dotenvEnvs.Mapping[key]; !ok { + if ok := dotenvEnvs.Exists(key); !ok { dotenvEnvs.Set(key, taskfile.Var{Static: value}) } }