diff --git a/CHANGELOG.md b/CHANGELOG.md index 7370445584..3a0ef886a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 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 diff --git a/Taskfile.yml b/Taskfile.yml index b0ed62486e..9145fe5733 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -2,6 +2,7 @@ version: '3' includes: docs: + aliases: [d] taskfile: ./docs dir: ./docs @@ -23,6 +24,7 @@ tasks: install: desc: Installs Task + aliases: [i] sources: - './**/*.go' cmds: @@ -42,6 +44,7 @@ tasks: lint: desc: Runs golangci-lint + aliases: [l] sources: - './**/*.go' cmds: @@ -65,6 +68,7 @@ tasks: test: desc: Runs test suite + aliases: [t] deps: [install] cmds: - go test {{catLines .GO_PACKAGES}} diff --git a/docs/Taskfile.yml b/docs/Taskfile.yml index 60ddf422d5..929705a805 100644 --- a/docs/Taskfile.yml +++ b/docs/Taskfile.yml @@ -8,6 +8,7 @@ tasks: start: desc: Start website + aliases: [s] vars: HOST: '{{default "localhost" .HOST}}' PORT: '{{default "3001" .PORT}}' @@ -25,6 +26,7 @@ tasks: - rm -rf ./build deploy: - desc: Build and deploy Docusaurus. Requires GIT_USER and GIT_PASS envs to be previous set + desc: Build and deploy Docusaurus + summary: Requires GIT_USER and GIT_PASS envs to be previous set cmds: - npx docusaurus deploy diff --git a/docs/docs/api_reference.md b/docs/docs/api_reference.md index 5a10c700bb..87c39b714a 100644 --- a/docs/docs/api_reference.md +++ b/docs/docs/api_reference.md @@ -91,7 +91,6 @@ Some environment variables can be overriden to adjust Task behavior. | `dotenv` | `[]string` | | A list of `.env` file paths to be parsed. | | `tasks` | [`map[string]Task`](#task) | | The task definitions. | - ### Include | Attribute | Type | Default | Description | @@ -99,7 +98,8 @@ Some environment variables can be overriden to adjust Task behavior. | `taskfile` | `string` | | The path for the Taskfile or directory to be included. If a directory, Task will look for files named `Taskfile.yml` or `Taskfile.yaml` inside that directory. If a relative path, resolved relative to the directory containing the including Taskfile. | | `dir` | `string` | The parent Taskfile directory | The working directory of the included tasks when run. | | `optional` | `bool` | `false` | If `true`, no errors will be thrown if the specified file does not exist. | -| `internal` | `bool` | `false` | If `true`, tasks will be omitted from both `--list` and `--list-all`. | +| `internal` | `bool` | `false` | If `true`, tasks will not be callable from the command line and will be omitted from both `--list` and `--list-all`. | +| `aliases` | `[]string` | | Alternative names for the namespace of the included Taskfile. | :::info @@ -118,11 +118,13 @@ includes: | - | - | - | - | | `desc` | `string` | | A short description of the task. This is listed when calling `task --list`. | | `summary` | `string` | | A longer description of the task. This is listed when calling `task --summary [task]`. | +| `aliases` | `[]string` | | Alternative names for the task. | +| `label` | `string` | | Overrides the name of the task in the output when a task is run. Supports variables. | | `sources` | `[]string` | | List of sources to check before running this task. Relevant for `checksum` and `timestamp` methods. Can be file paths or star globs. | | `dir` | `string` | | The current directory which this task should run. | | `method` | `string` | `checksum` | Method used by this task. Default to the one declared globally or `checksum`. Available options: `checksum`, `timestamp` and `none` | | `silent` | `bool` | `false` | Skips some output for this task. Note that STDOUT and STDERR of the commands will still be redirected. | -| `internal` | `bool` | `false` | If `true`, omit this task from both `--list` and `--list-all`. | +| `internal` | `bool` | `false` | If `true`, this task will not be callable from the command line and will be omitted from both `--list` and `--list-all`. | | `run` | `string` | The one declared globally in the Taskfile or `always` | Specifies whether the task should run again or not if called more than once. Available options: `always`, `once` and `when_changed`. | | `prefix` | `string` | | Allows to override the prefix print before the STDOUT. Only relevant when using the `prefixed` output mode. | | `ignore_error` | `bool` | `false` | Continue execution if errors happen while executing the commands. | diff --git a/docs/docs/usage.md b/docs/docs/usage.md index 10f2eab355..c58a511938 100644 --- a/docs/docs/usage.md +++ b/docs/docs/usage.md @@ -230,6 +230,21 @@ includes: DOCKER_IMAGE: frontend_image ``` +### 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. + +```yaml +version: '3' + +includes: + generate: + taskfile: ./taskfiles/Generate.yml + aliases: [gen] +``` + :::info Vars declared in the included Taskfile have preference over the @@ -965,6 +980,30 @@ If the task does not have a summary or a description, a warning is printed. Please note: *showing the summary will not execute the command*. +## Task aliases + +Aliases are alternative names for tasks. They can be used to make it easier and +quicker to run tasks with long or hard-to-type names. You can use them on the +command line, when [calling sub-tasks](#calling-another-task) in your Taskfile +and when [including tasks](#including-other-taskfiles) with aliases from another +Taskfile. They can also be used together with [namespace +aliases](#namespace-aliases). + +```yaml +version: '3' + +tasks: + generate: + aliases: [gen] + cmds: + - task: gen-mocks + + generate-mocks: + aliases: [gen-mocks] + cmds: + - echo "generating..." +``` + ## Overriding task name Sometimes you may want to override the task name printed on the summary, up-to-date diff --git a/errors.go b/errors.go index cc2e65ed1a..1be4c15ee6 100644 --- a/errors.go +++ b/errors.go @@ -3,6 +3,7 @@ package task import ( "errors" "fmt" + "strings" "mvdan.cc/sh/v3/interp" ) @@ -20,6 +21,15 @@ func (err *taskNotFoundError) Error() string { return fmt.Sprintf(`task: Task %q not found`, err.taskName) } +type multipleTasksWithAliasError struct { + aliasName string + taskNames []string +} + +func (err *multipleTasksWithAliasError) Error() string { + return fmt.Sprintf(`task: Multiple tasks (%s) with alias %q found`, strings.Join(err.taskNames, ", "), err.aliasName) +} + type taskInternalError struct { taskName string } diff --git a/go.mod b/go.mod index acb991e651..e8cec71fd6 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/radovskyb/watcher v1.0.7 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.0 + golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c gopkg.in/yaml.v3 v3.0.1 mvdan.cc/sh/v3 v3.6.0-0.dev.0.20220704111049-a6e3029cd899 @@ -19,7 +20,7 @@ require ( github.com/mattn/go-colorable v0.1.9 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/go.sum b/go.sum index e6ab020a92..7a4d3b13a3 100644 --- a/go.sum +++ b/go.sum @@ -7,7 +7,7 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= @@ -34,16 +34,17 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 h1:RjggHMcaTVp0LOVZcW0bo8alwHrOaCrGUDgfWUHhnN4= +golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/help.go b/help.go index b47350e2dc..e1ae8e6d1f 100644 --- a/help.go +++ b/help.go @@ -47,6 +47,9 @@ func (e *Executor) printTasks(listAll bool) { e.Logger.FOutf(w, logger.Yellow, "* ") e.Logger.FOutf(w, logger.Green, task.Task) e.Logger.FOutf(w, logger.Default, ": \t%s", task.Desc) + if len(task.Aliases) > 0 { + e.Logger.FOutf(w, logger.Cyan, "\t(aliases: %s)", strings.Join(task.Aliases, ", ")) + } fmt.Fprint(w, "\n") } w.Flush() diff --git a/internal/summary/summary.go b/internal/summary/summary.go index 13e19f0143..0e58831cfe 100644 --- a/internal/summary/summary.go +++ b/internal/summary/summary.go @@ -28,6 +28,7 @@ func PrintTask(l *logger.Logger, t *taskfile.Task) { printTaskName(l, t) printTaskDescribingText(t, l) printTaskDependencies(l, t) + printTaskAliases(l, t) printTaskCommands(l, t) } @@ -61,6 +62,18 @@ func printTaskName(l *logger.Logger, t *taskfile.Task) { l.Outf(logger.Default, "") } +func printTaskAliases(l *logger.Logger, t *taskfile.Task) { + if len(t.Aliases) == 0 { + return + } + l.Outf(logger.Default, "") + l.Outf(logger.Default, "aliases:") + for _, alias := range t.Aliases { + l.FOutf(l.Stdout, logger.Default, " - ") + l.Outf(logger.Cyan, alias) + } +} + func hasDescription(t *taskfile.Task) bool { return t.Desc != "" } diff --git a/task.go b/task.go index 0a0751618e..b5dbcce978 100644 --- a/task.go +++ b/task.go @@ -16,6 +16,7 @@ import ( "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/taskfile" + "golang.org/x/exp/slices" "golang.org/x/sync/errgroup" ) @@ -64,16 +65,15 @@ type Executor struct { // Run runs Task func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error { // check if given tasks exist - for _, c := range calls { - t, ok := e.Taskfile.Tasks[c.Task] - if !ok { - // FIXME: move to the main package + for _, call := range calls { + task, err := e.GetTask(call) + if err != nil { e.ListTasksWithDesc() - return &taskNotFoundError{taskName: c.Task} + return err } - if t.Internal { + if task.Internal { e.ListTasksWithDesc() - return &taskInternalError{taskName: c.Task} + return &taskInternalError{taskName: call.Task} } } @@ -113,8 +113,8 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error { if err != nil { return err } - if !e.Watch && atomic.AddInt32(e.taskCallCount[call.Task], 1) >= MaximumTaskCall { - return &MaximumTaskCallExceededError{task: call.Task} + if !e.Watch && atomic.AddInt32(e.taskCallCount[t.Task], 1) >= MaximumTaskCall { + return &MaximumTaskCallExceededError{task: t.Task} } release := e.acquireConcurrencyLimit() @@ -331,3 +331,38 @@ func (e *Executor) startExecution(ctx context.Context, t *taskfile.Task, execute return execute(ctx) } + +// GetTask will return the task with the name matching the given call from the taskfile. +// If no task is found, it will search for tasks with a matching alias. +// 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 { + 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 { + if slices.Contains(task.Aliases, call.Task) { + aliasedTasks = append(aliasedTasks, task.Task) + matchingTask = task + } + } + // If we found multiple tasks + if len(aliasedTasks) > 1 { + return nil, &multipleTasksWithAliasError{ + aliasName: call.Task, + taskNames: aliasedTasks, + } + } + // If we found no tasks + if len(aliasedTasks) == 0 { + return nil, &taskNotFoundError{ + taskName: call.Task, + } + } + + return matchingTask, nil +} diff --git a/task_test.go b/task_test.go index 96d5a4bbce..9fc018b16a 100644 --- a/task_test.go +++ b/task_test.go @@ -477,6 +477,58 @@ func TestStatusChecksum(t *testing.T) { assert.Equal(t, `task: Task "build" is up to date`+"\n", buff.String()) } +func TestAlias(t *testing.T) { + const dir = "testdata/alias" + + data, err := os.ReadFile(filepathext.SmartJoin(dir, "alias.txt")) + assert.NoError(t, err) + + var buff bytes.Buffer + e := task.Executor{ + Dir: dir, + Stdout: &buff, + Stderr: &buff, + } + assert.NoError(t, e.Setup()) + assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "f"})) + assert.Equal(t, string(data), buff.String()) +} + +func TestDuplicateAlias(t *testing.T) { + const dir = "testdata/alias" + + data, err := os.ReadFile(filepathext.SmartJoin(dir, "alias-duplicate.txt")) + assert.NoError(t, err) + + var buff bytes.Buffer + e := task.Executor{ + Dir: dir, + Stdout: &buff, + Stderr: &buff, + } + assert.NoError(t, e.Setup()) + assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "x"})) + assert.Equal(t, string(data), buff.String()) +} + +func TestAliasSummary(t *testing.T) { + const dir = "testdata/alias" + + data, err := os.ReadFile(filepathext.SmartJoin(dir, "alias-summary.txt")) + assert.NoError(t, err) + + var buff bytes.Buffer + e := task.Executor{ + Dir: dir, + Summary: true, + Stdout: &buff, + Stderr: &buff, + } + assert.NoError(t, e.Setup()) + assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "f"})) + assert.Equal(t, string(data), buff.String()) +} + func TestLabelUpToDate(t *testing.T) { const dir = "testdata/label_uptodate" @@ -990,7 +1042,7 @@ func TestIncludesInternal(t *testing.T) { err := e.Run(context.Background(), taskfile.Call{Task: test.task}) if test.expectedErr { - assert.Error(t, err, test.expectedErr) + assert.Error(t, err) } else { assert.NoError(t, err) } @@ -1030,7 +1082,7 @@ func TestInternalTask(t *testing.T) { err := e.Run(context.Background(), taskfile.Call{Task: test.task}) if test.expectedErr { - assert.Error(t, err, test.expectedErr) + assert.Error(t, err) } else { assert.NoError(t, err) } diff --git a/taskfile/copy.go b/taskfile/copy.go new file mode 100644 index 0000000000..10e1a01701 --- /dev/null +++ b/taskfile/copy.go @@ -0,0 +1,23 @@ +package taskfile + +import "golang.org/x/exp/constraints" + +func deepCopySlice[T any](orig []T) []T { + if orig == nil { + return nil + } + c := make([]T, len(orig)) + copy(c, orig) + return c +} + +func deepCopyMap[K constraints.Ordered, 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 { + c[k] = v + } + return c +} diff --git a/taskfile/included_taskfile.go b/taskfile/included_taskfile.go index fe83bd7d03..c305d0ea17 100644 --- a/taskfile/included_taskfile.go +++ b/taskfile/included_taskfile.go @@ -8,6 +8,7 @@ import ( "github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/filepathext" + "golang.org/x/exp/slices" "gopkg.in/yaml.v3" ) @@ -17,6 +18,7 @@ type IncludedTaskfile struct { Dir string Optional bool Internal bool + Aliases []string AdvancedImport bool Vars *Vars BaseDir string // The directory from which the including taskfile was loaded; used to resolve relative paths @@ -71,7 +73,7 @@ func (tfs *IncludedTaskfiles) Set(key string, includedTaskfile IncludedTaskfile) if tfs.Mapping == nil { tfs.Mapping = make(map[string]IncludedTaskfile, 1) } - if !stringSliceContains(tfs.Keys, key) { + if !slices.Contains(tfs.Keys, key) { tfs.Keys = append(tfs.Keys, key) } tfs.Mapping[key] = includedTaskfile @@ -103,6 +105,7 @@ func (it *IncludedTaskfile) UnmarshalYAML(unmarshal func(interface{}) error) err Dir string Optional bool Internal bool + Aliases []string Vars *Vars } if err := unmarshal(&includedTaskfile); err != nil { @@ -112,11 +115,29 @@ func (it *IncludedTaskfile) UnmarshalYAML(unmarshal func(interface{}) error) err it.Dir = includedTaskfile.Dir it.Optional = includedTaskfile.Optional it.Internal = includedTaskfile.Internal + it.Aliases = includedTaskfile.Aliases it.AdvancedImport = true it.Vars = includedTaskfile.Vars return nil } +// DeepCopy creates a new instance of IncludedTaskfile and copies +// data by value from the source struct. +func (it *IncludedTaskfile) DeepCopy() *IncludedTaskfile { + if it == nil { + return nil + } + return &IncludedTaskfile{ + Taskfile: it.Taskfile, + Dir: it.Dir, + Optional: it.Optional, + Internal: it.Internal, + AdvancedImport: it.AdvancedImport, + Vars: it.Vars.DeepCopy(), + BaseDir: it.BaseDir, + } +} + // FullTaskfilePath returns the fully qualified path to the included taskfile func (it *IncludedTaskfile) FullTaskfilePath() (string, error) { return it.resolvePath(it.Taskfile) diff --git a/taskfile/merge.go b/taskfile/merge.go index baebad0e46..daef326302 100644 --- a/taskfile/merge.go +++ b/taskfile/merge.go @@ -9,7 +9,7 @@ import ( const NamespaceSeparator = ":" // Merge merges the second Taskfile into the first -func Merge(t1, t2 *Taskfile, internal bool, namespaces ...string) error { +func Merge(t1, t2 *Taskfile, includedTaskfile *IncludedTaskfile, namespaces ...string) error { if t1.Version != t2.Version { return fmt.Errorf(`task: Taskfiles versions should match. First is "%s" but second is "%s"`, t1.Version, t2.Version) } @@ -39,22 +39,38 @@ func Merge(t1, t2 *Taskfile, internal bool, namespaces ...string) error { t1.Tasks = make(Tasks) } for k, v := range t2.Tasks { - // FIXME(@andreynering): Refactor this block, otherwise we can - // have serious side-effects in the future, since we're editing - // the original references instead of deep copying them. + // 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() - v.Internal = v.Internal || internal + // Set the task to internal if EITHER the included task or the included + // taskfile are marked as internal + task.Internal = task.Internal || includedTaskfile.Internal - t1.Tasks[taskNameWithNamespace(k, namespaces...)] = v - - for _, dep := range v.Deps { + // Add namespaces to dependencies, commands and aliases + for _, dep := range task.Deps { dep.Task = taskNameWithNamespace(dep.Task, namespaces...) } - for _, cmd := range v.Cmds { + for _, cmd := range task.Cmds { if cmd != nil && cmd.Task != "" { cmd.Task = taskNameWithNamespace(cmd.Task, namespaces...) } } + for i, alias := range task.Aliases { + task.Aliases[i] = taskNameWithNamespace(alias, namespaces...) + } + // Add namespace aliases + if includedTaskfile != nil { + for _, namespaceAlias := range includedTaskfile.Aliases { + task.Aliases = append(task.Aliases, taskNameWithNamespace(task.Task, namespaceAlias)) + for _, alias := range v.Aliases { + task.Aliases = append(task.Aliases, taskNameWithNamespace(alias, namespaceAlias)) + } + } + } + + // Add the task to the merged taskfile + t1.Tasks[taskNameWithNamespace(k, namespaces...)] = task } return nil diff --git a/taskfile/read/taskfile.go b/taskfile/read/taskfile.go index 220b66c4e6..7c6f8685f6 100644 --- a/taskfile/read/taskfile.go +++ b/taskfile/read/taskfile.go @@ -79,6 +79,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) { Dir: tr.Replace(includedTask.Dir), Optional: includedTask.Optional, Internal: includedTask.Internal, + Aliases: includedTask.Aliases, AdvancedImport: includedTask.AdvancedImport, Vars: includedTask.Vars, BaseDir: includedTask.BaseDir, @@ -149,7 +150,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) { } } - if err = taskfile.Merge(t, includedTaskfile, includedTask.Internal, namespace); err != nil { + if err = taskfile.Merge(t, includedTaskfile, &includedTask, namespace); err != nil { return err } return nil @@ -165,7 +166,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) { if err != nil { return nil, err } - if err = taskfile.Merge(t, osTaskfile, false); err != nil { + if err = taskfile.Merge(t, osTaskfile, nil); err != nil { return nil, err } } diff --git a/taskfile/slice.go b/taskfile/slice.go deleted file mode 100644 index 9cc50105e5..0000000000 --- a/taskfile/slice.go +++ /dev/null @@ -1,10 +0,0 @@ -package taskfile - -func stringSliceContains(s []string, str string) bool { - for _, v := range s { - if v == str { - return true - } - } - return false -} diff --git a/taskfile/task.go b/taskfile/task.go index 46548bbf3f..fd5c393741 100644 --- a/taskfile/task.go +++ b/taskfile/task.go @@ -11,6 +11,7 @@ type Task struct { Label string Desc string Summary string + Aliases []string Sources []string Generates []string Status []string @@ -56,6 +57,7 @@ func (t *Task) UnmarshalYAML(unmarshal func(interface{}) error) error { Label string Desc string Summary string + Aliases []string Sources []string Generates []string Status []string @@ -78,6 +80,7 @@ func (t *Task) UnmarshalYAML(unmarshal func(interface{}) error) error { t.Deps = task.Deps t.Label = task.Label t.Desc = task.Desc + t.Aliases = task.Aliases t.Summary = task.Summary t.Sources = task.Sources t.Generates = task.Generates @@ -95,3 +98,35 @@ func (t *Task) UnmarshalYAML(unmarshal func(interface{}) error) error { t.Run = task.Run return nil } + +// DeepCopy creates a new instance of Task and copies +// data by value from the source struct. +func (t *Task) DeepCopy() *Task { + c := &Task{ + Task: t.Task, + Cmds: deepCopySlice(t.Cmds), + Deps: deepCopySlice(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), + Dir: t.Dir, + Vars: t.Vars.DeepCopy(), + Env: t.Env.DeepCopy(), + Silent: t.Silent, + Interactive: t.Interactive, + Internal: t.Internal, + Method: t.Method, + Prefix: t.Prefix, + IgnoreError: t.IgnoreError, + Run: t.Run, + IncludeVars: t.IncludeVars.DeepCopy(), + IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(), + IncludedTaskfile: t.IncludedTaskfile.DeepCopy(), + } + return c +} diff --git a/taskfile/var.go b/taskfile/var.go index 2693444f71..0f807d8dc4 100644 --- a/taskfile/var.go +++ b/taskfile/var.go @@ -3,6 +3,7 @@ package taskfile import ( "errors" + "golang.org/x/exp/slices" "gopkg.in/yaml.v3" ) @@ -34,6 +35,18 @@ func (vs *Vars) UnmarshalYAML(node *yaml.Node) error { return nil } +// 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 { @@ -47,7 +60,7 @@ func (vs *Vars) Set(key string, value Var) { if vs.Mapping == nil { vs.Mapping = make(map[string]Var, 1) } - if !stringSliceContains(vs.Keys, key) { + if !slices.Contains(vs.Keys, key) { vs.Keys = append(vs.Keys, key) } vs.Mapping[key] = value diff --git a/testdata/alias/Taskfile.yml b/testdata/alias/Taskfile.yml new file mode 100644 index 0000000000..98bd9ad3b3 --- /dev/null +++ b/testdata/alias/Taskfile.yml @@ -0,0 +1,19 @@ +version: '3' + +includes: + included: + taskfile: Taskfile2.yml + aliases: [inc, i] + +tasks: + foo: + aliases: [f, x] + cmds: + - echo "foo" + - task: b + + bar: + aliases: [b, x] + cmds: + - echo "bar" + - task: inc:q diff --git a/testdata/alias/Taskfile2.yml b/testdata/alias/Taskfile2.yml new file mode 100644 index 0000000000..72de3b32e7 --- /dev/null +++ b/testdata/alias/Taskfile2.yml @@ -0,0 +1,7 @@ +version: '3' + +tasks: + qux: + aliases: [q, x] + cmds: + - echo "qux" diff --git a/testdata/alias/alias-duplicate.txt b/testdata/alias/alias-duplicate.txt new file mode 100644 index 0000000000..56e8128e82 --- /dev/null +++ b/testdata/alias/alias-duplicate.txt @@ -0,0 +1 @@ +task: No tasks with description available. Try --list-all to list all tasks diff --git a/testdata/alias/alias-summary.txt b/testdata/alias/alias-summary.txt new file mode 100644 index 0000000000..335e562e72 --- /dev/null +++ b/testdata/alias/alias-summary.txt @@ -0,0 +1,11 @@ +task: foo + +(task does not have description or summary) + +aliases: + - f + - x + +commands: + - echo "foo" + - Task: b diff --git a/testdata/alias/alias.txt b/testdata/alias/alias.txt new file mode 100644 index 0000000000..afef2a620f --- /dev/null +++ b/testdata/alias/alias.txt @@ -0,0 +1,6 @@ +task: [foo] echo "foo" +foo +task: [bar] echo "bar" +bar +task: [included:qux] echo "qux" +qux diff --git a/variables.go b/variables.go index fd2124f096..872b81edce 100644 --- a/variables.go +++ b/variables.go @@ -22,13 +22,12 @@ func (e *Executor) FastCompiledTask(call taskfile.Call) (*taskfile.Task, error) } func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskfile.Task, error) { - origTask, ok := e.Taskfile.Tasks[call.Task] - if !ok { - return nil, &taskNotFoundError{call.Task} + origTask, err := e.GetTask(call) + if err != nil { + return nil, err } var vars *taskfile.Vars - var err error if evaluateShVars { vars, err = e.Compiler.GetVariables(origTask, call) } else { @@ -50,6 +49,7 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf Label: r.Replace(origTask.Label), Desc: r.Replace(origTask.Desc), Summary: r.Replace(origTask.Summary), + Aliases: origTask.Aliases, Sources: r.ReplaceSlice(origTask.Sources), Generates: r.ReplaceSlice(origTask.Generates), Dir: r.Replace(origTask.Dir),