Compare commits

...

78 Commits

Author SHA1 Message Date
Andrey Nering
f2abc13ce2 v3.12.0 2022-03-31 21:44:59 -03:00
Andrey Nering
0f4621fb02 CHANGELOG: Add entry for #691 2022-03-31 21:40:16 -03:00
Andrey Nering
c6ff641f6d Merge branch 'list-task-names' of https://github.com/ardnew/task into ardnew-list-task-names 2022-03-31 21:31:56 -03:00
Andrey Nering
350f74a53d CHANGELOG: Add entry for #656 2022-03-31 21:19:16 -03:00
Andrey Nering
41cd7acc87 Merge pull request #656 from tylermmorton/master
Add support for multi-level includes
2022-03-31 21:12:15 -03:00
Andrey Nering
c6eea26660 go mod tidy 2022-03-21 15:26:41 -03:00
Andrey Nering
61c5718663 Upgrade to Go 1.18 is out. Set 1.17 as the minimal version 2022-03-21 15:23:06 -03:00
ardnew
9897f4b527 refactor with support for --list and --list-all 2022-03-21 12:59:25 -05:00
ardnew
978a6e5ecb quickly print task names only with flags --silent and --list 2022-03-21 12:02:56 -05:00
Andrey Nering
a018997ddc CHANGELOG: Fix typo 2022-03-21 10:31:48 -03:00
Andrey Nering
de09843467 Improvements + CHANGELOG for #677 2022-03-19 18:41:03 -03:00
Andrey Nering
78a57fdb4b Merge branch 'dballweg/vars_in_includedtaskfile' of https://github.com/dballweg/task into dballweg-dballweg/vars_in_includedtaskfile 2022-03-19 17:31:11 -03:00
Andrey Nering
0bc2fd72f0 Merge pull request #689 from go-task/dependabot/go_modules/github.com/stretchr/testify-1.7.1
Bump github.com/stretchr/testify from 1.7.0 to 1.7.1
2022-03-19 11:01:09 -03:00
dependabot[bot]
dda5164efd Bump github.com/stretchr/testify from 1.7.0 to 1.7.1
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.0 to 1.7.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.7.0...v1.7.1)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-19 11:07:08 +00:00
Andrey Nering
3df2396b63 Merge pull request #688 from vicmattos/vicmattosissue-683-docs-install-choco
Add chocolatey in installation documentation
2022-03-18 20:46:50 -03:00
Victor Mattos
d3da84e724 docs: add ownership of choco installation method 2022-03-18 17:41:34 -03:00
Victor Mattos
eb61015477 fix: reference chocolatey link anchor
Co-authored-by: Andrey Nering <andrey@nering.com.br>
2022-03-18 17:38:29 -03:00
Victor Mattos
40c644f006 docs: add chocolatey installation method
Resolves #683
2022-03-09 15:39:54 -03:00
Andrey Nering
c9aa0180a8 Merge pull request #679 from philpennock/zsh-completion
completion: zsh: overhaul and sync to current flags
2022-02-27 16:01:11 -03:00
Phil Pennock
a06e46885d completion: zsh: overhaul and sync to current flags
* List all current option flags
* Provide descriptions for every flag
* Pass the `task -l` descriptions as descriptions for the task completions
  + The prior logic had 4 invocations of sed and 1 of awk, and only kept the
    task name
  + Do all filtering in zsh without forking (except for `task` itself)
* When `--taskfile` is used, complete tasks from _that_ file
  + And otherwise, enable completions if only the `.dist` variant files are
    present
* Ensure mutually exclusive options preclude each other
  + the `+ '(groupname)'` clause defines this
* Fix `--dir` to take directories, not files
2022-02-25 01:07:43 -05:00
Dan Ballweg
60fa6e6c0a update 2022-02-24 13:18:35 -06:00
Dan Ballweg
2f18f7927d test include variables 2022-02-24 13:17:20 -06:00
Dan Ballweg
292cf75836 add vars to included taskfiles 2022-02-23 16:53:46 -06:00
tylermmorton
fc95061f4c Add missing newlines 2022-02-21 15:33:54 -05:00
tylermmorton
1f1275255c Fix bug in includes where default taskfiles were not being checked. 2022-02-21 15:31:55 -05:00
Andrey Nering
d8555e5a5d v3.11.0 2022-02-19 19:40:29 -03:00
Andrey Nering
b323531dd5 Improvements and CHANGELOG for #651 2022-02-19 19:31:27 -03:00
Andrey Nering
cfb665310e Merge branch 'group-begin-message' of https://github.com/janslow/task into janslow-group-begin-message 2022-02-19 18:42:34 -03:00
Andrey Nering
51c6ebcd4d Add tests, documentation and changelog for #666 2022-02-19 18:24:43 -03:00
Andrey Nering
e94d1b6b9f Merge branch 'task-498-taskfile-dist' of https://github.com/tylermmorton/task into tylermmorton-task-498-taskfile-dist 2022-02-19 18:01:00 -03:00
Andrey Nering
ca7b32105d Merge pull request #674 from go-task/dependabot/go_modules/mvdan.cc/sh/v3-3.4.3
Bump mvdan.cc/sh/v3 from 3.4.2 to 3.4.3
2022-02-19 17:55:30 -03:00
dependabot[bot]
264db2737b Bump mvdan.cc/sh/v3 from 3.4.2 to 3.4.3
Bumps [mvdan.cc/sh/v3](https://github.com/mvdan/sh) from 3.4.2 to 3.4.3.
- [Release notes](https://github.com/mvdan/sh/releases)
- [Changelog](https://github.com/mvdan/sh/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mvdan/sh/compare/v3.4.2...v3.4.3)

---
updated-dependencies:
- dependency-name: mvdan.cc/sh/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-19 20:51:22 +00:00
Andrey Nering
5f2c9a6e45 Merge pull request #671 from JessThrysoee/bashcompletion
bash-completion refactoring
2022-02-13 19:51:40 -03:00
Jess Thrysoee
19be1f1bf0 bash-completion refactoring
1. 'compgen -c' lists _all_ command names on the system, which is not
   appropriate for this script, furthermore echo does not read from stdin
   so the output is lost.

2. use _get_comp_words_by_ref and __ltrim_colon_completions to handle task
   names with colons.

   "...modifying COMP_WORDBREAKS in your completion script is not safe
   (as it is a global variable and it has the side effect of affecting
   the behavior of other completion scripts"
   Ref.: https://stackoverflow.com/a/12495727/7044304

3. Add options completion

4. Use task --list-all
2022-02-13 23:44:04 +01:00
tylermmorton
7cdf0000d9 Fix error message assertion in task_test 2022-02-03 22:23:01 -05:00
tylermmorton
13606e5e00 Remove note about multi level includes from usage documentation 2022-02-03 22:20:49 -05:00
tylermmorton
35af240faa Add newlines to multi-level test Taskfiles 2022-02-03 22:19:07 -05:00
tylermmorton
0ac56f8973 Add newlines to test Taskfiles 2022-02-03 22:13:43 -05:00
tylermmorton
6e5f8b1fb0 Append task prefix to log messages 2022-02-03 22:12:58 -05:00
tylermmorton
15e831c0b0 Revert "Update docs to account for new feature"
This reverts commit 66748ab5e5.
2022-02-03 22:09:57 -05:00
tylermmorton
248952bc8f Add dist fallbacks to defaultTaskfiles 2022-01-29 11:53:36 -05:00
Andrey Nering
2373743eac Merge pull request #652 from jcwillox/master
Add completions to nfpms packages
2022-01-17 10:18:22 -03:00
Andrey Nering
f119596be6 Docs: Small change to title 2022-01-17 09:38:32 -03:00
Andrey Nering
b7cb41b388 Merge pull request #657 from lechuckroh/intellij-plugin
Update 'community.md' to add IntelliJ plugin link
2022-01-17 09:37:06 -03:00
Lechuck Roh
a65ee26446 Update 'community.md' to add IntelliJ plugin link 2022-01-16 22:24:12 +09:00
tylermmorton
d3e2fbf1e2 Merge branch 'master' of https://github.com/tylermmorton/task 2022-01-15 23:37:40 -05:00
tylermmorton
66748ab5e5 Update docs to account for new feature 2022-01-15 23:37:39 -05:00
tylermmorton
c73a2c8f84 Move circular include logic to a separate function 2022-01-15 23:34:59 -05:00
Tyler Morton
4bbcd99b8b Merge branch 'go-task:master' into master 2022-01-14 22:39:27 -05:00
tylermmorton
02e7ff27c7 Add support for multi-level includes and cyclic include detection 2022-01-14 22:38:37 -05:00
Josh Willox
7ed3cea40b Add completions to nfpms packages 2022-01-14 20:48:28 +11:00
Jay Anslow
74f5cf8f29 Add support for begin/end messages with grouped output
Fixes #647

This allows CI systems that support grouping (such as with [GitHub Actions's `::group::` command](https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions#grouping-log-lines) and [Azure Devops](https://docs.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#formatting-commands)) to collapse all of the logs for a single task, to improve readability of logs

## Example

The following Taskfile

```
# Taskfile.yml
version: 3
output:
  group:
    begin: "::group::{{ .TASK }}"
    end: "::endgroup::"
tasks:
  default:
    cmds:
      - "echo 'Hello, World!'"
```

Results in the following output
```bash
$ task
task: [default] echo 'Hello, World!'
::group::default
Hello, World!
::endgroup::
```

See [this GitHub Actions job](https://github.com/janslow/task/runs/4811059609?check_suite_focus=true) for a full example

<img width="771" alt="image" src="https://user-images.githubusercontent.com/1253367/149429832-6cb0c1b5-0758-442e-9375-c4daa65771bc.png">
<img width="394" alt="image" src="https://user-images.githubusercontent.com/1253367/149429851-1d5d2ab5-9095-4795-9b57-f91750720d40.png">
2022-01-14 00:22:14 +00:00
Andrey Nering
086d13ca2f Docs: Remove line reference from file link 2022-01-13 10:39:38 -03:00
Andrey Nering
2780e96179 Merge pull request #648 from sagikazarmark/docs-nix-install
docs: add nix as an installation method
2022-01-13 10:38:49 -03:00
Mark Sagi-Kazar
191678f9d6 docs: add nix as an installation method 2022-01-13 00:15:04 +01:00
Andrey Nering
79f595d8d1 v3.10.0 2022-01-04 18:19:48 -03:00
Andrey Nering
db2865fb17 Add CHANGELOG entry for #579 2022-01-04 18:10:24 -03:00
Andrey Nering
f945fa60d9 Merge branch 'bugfix/issue-481-dynamic-vars-broken' of https://github.com/masaushi/task into masaushi-bugfix/issue-481-dynamic-vars-broken 2022-01-04 17:39:14 -03:00
Andrey Nering
454988f657 Fix typo 🤦 2022-01-04 17:19:38 -03:00
Andrey Nering
7e0346d6eb Add CHANGELOG, documentation and small improvements to #401 2022-01-04 17:16:21 -03:00
Andrey Nering
00a90d1fe6 Merge branch 'f/list-all' of https://github.com/therealkevinard/task into therealkevinard-f/list-all 2022-01-04 17:03:12 -03:00
Andrey Nering
d6c185580a Add CHANGELOG, documentation and small improvements to #626 2022-01-04 16:56:13 -03:00
Jacob McCollum
fd9132c15d remove extra file 2022-01-03 13:22:06 -05:00
Kevin Ard
42702e81b3 refactor: wrap PrintTasksHelp with arg-less signatures
provide exported methods for accessing PrintTasksHelp variants.
2022-01-03 12:12:18 -05:00
Jacob McCollum
09c9d55695 Changes from PR Review:
- Remove ^task syntax from `defer`
- Support task call syntax in defer
2022-01-02 16:38:06 -05:00
Jacob McCollum
69e9effc88 initial pass at deferred commands 2022-01-02 15:55:43 -05:00
Andrey Nering
1c782c599f Remove deprecated "$" and "^" prefixes
`$` was a variable prefix that make it being evaluated as shell. It was
replaced with `sh:`.

`^` is a command prefix that make it run another task. It was replaced
with `task:`.

These were added long ago when we were experimenting with stuff and kept for
some time for backward compatibility reasons, but sometimes causes confusion
and I think the time to remove the code came.

Closes #644
Closes #645
Ref #642

Co-authored-by: Trite <60318513+Trite8Q1@users.noreply.github.com>
2022-01-02 15:26:42 -03:00
Andrey Nering
ed37071fd6 Merge pull request #621 from kerma/feat/support-yaml
Add support for yaml extension
2022-01-02 15:08:50 -03:00
Margus Kerma
d73cf106b1 Merge branch 'master' into feat/support-yaml 2022-01-02 15:30:23 +02:00
Margus Kerma
1d7982e80a fix(#584): Add support to yaml extension
- init creates Taskfile.yaml
- add changelog entry
- add zsh completion support for Taskfile.yaml
2022-01-02 15:23:10 +02:00
Andrey Nering
d5d1984116 Pin released vetsion of mvdan/sh v3.4.2 2021-12-30 16:45:08 -03:00
Andrey Nering
9eda1629bb Merge pull request #636 from nichady/replace-ioutil
replace usages of ioutil with io and os
2021-12-20 23:58:23 -03:00
nichady
9a5d49774e replace usages of ioutil with io and os 2021-12-20 20:00:34 -06:00
Andrey Nering
b2efebce96 Merge pull request #634 from go-task/upgrade-github-actions
Upgrade GitHub actions
2021-12-19 22:14:23 -03:00
Andrey Nering
85232bd704 Upgrade GitHub actions
Closes #633
2021-12-19 22:06:51 -03:00
masaushi
93dcb20e12 fix error in evaluating dynamic variables with newly created directory 2021-09-26 22:30:32 +09:00
Kevin Ard
347c796662 add tests to previous 2020-11-13 16:24:34 -05:00
Kevin Ard
9bed7f7a9b feat (help): allow cli option to list tasks with no desc
added an add'l cli option that lists all tasks, with or without description.
orig. behavior: task -l lists tasks with desc field
new behaviour: task -la or task -a will list all tasks. if task has desc, it will be included.

BREAKING CHANGES: none, that I know of.
NOTES/Concerns:
- This is wip.
- Haven't checked how it interacts with bash completion.
- The new Executor.TaskNames func does not use e.CompiledTask(taskfile.Call{Task: task.Task})
2020-11-13 15:27:03 -05:00
62 changed files with 1276 additions and 227 deletions

View File

@@ -10,12 +10,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v1 uses: actions/checkout@v2
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v1 uses: actions/setup-go@v2
with: with:
go-version: 1.17.x go-version: 1.18.x
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2 uses: goreleaser/goreleaser-action@v2

View File

@@ -5,18 +5,18 @@ jobs:
name: Test name: Test
strategy: strategy:
matrix: matrix:
go-version: [1.16.x, 1.17.x] go-version: [1.17.x, 1.18.x]
platform: [ubuntu-latest, macos-latest, windows-latest] platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{matrix.platform}} runs-on: ${{matrix.platform}}
steps: steps:
- name: Set up Go ${{matrix.go-version}} - name: Set up Go ${{matrix.go-version}}
uses: actions/setup-go@v1 uses: actions/setup-go@v2
with: with:
go-version: ${{matrix.go-version}} go-version: ${{matrix.go-version}}
id: go id: go
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v1 uses: actions/checkout@v2
- name: Download Go modules - name: Download Go modules
run: go mod download run: go mod download
@@ -27,4 +27,4 @@ jobs:
run: go build -o ./bin/task -v ./cmd/task run: go build -o ./bin/task -v ./cmd/task
- name: Test - name: Test
run: ./bin/task test run: ./bin/task test --output=group --output-group-begin='::group::{{.TASK}}' --output-group-end='::endgroup::'

View File

@@ -54,6 +54,13 @@ nfpms:
- deb - deb
- rpm - rpm
file_name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}" file_name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}"
contents:
- src: completion/bash/task.bash
dst: /etc/bash_completion.d/task
- src: completion/fish/task.fish
dst: /usr/share/fish/completions/task.fish
- src: completion/zsh/_task
dst: /usr/local/share/zsh/site-functions/_task
brews: brews:
- name: go-task - name: go-task

View File

@@ -1,5 +1,48 @@
# Changelog # Changelog
## v3.12.0 - 2022-03-31
- The `--list` and `--list-all` flags can now be combined with the `--silent`
flag to print the task names only, without their description
([#691](https://github.com/go-task/task/pull/691)).
- Added support for multi-level inclusion of Taskfiles. This means that
included Taskfiles can also include other Taskfiles. Before this was limited
to one level
([#390](https://github.com/go-task/task/issues/390), [#623](https://github.com/go-task/task/discussions/623), [#656](https://github.com/go-task/task/pull/656)).
- Add ability to specify vars when including a Taskfile.
[Check out the documentation](https://taskfile.dev/#/usage?id=vars-of-included-taskfiles)
for more information.
([#677](https://github.com/go-task/task/pull/677)).
## v3.11.0 - 2022-02-19
- Task now supports printing begin and end messages when using the `group`
output mode, useful for grouping tasks in CI systems.
[Check out the documentation](http://taskfile.dev/#/usage?id=output-syntax) for more information
([#647](https://github.com/go-task/task/issues/647), [#651](https://github.com/go-task/task/pull/651)).
- Add `Taskfile.dist.yml` and `Taskfile.dist.yaml` to the supported file
name list. [Check out the documentation](https://taskfile.dev/#/usage?id=supported-file-names) for more information
([#498](https://github.com/go-task/task/issues/498), [#666](https://github.com/go-task/task/pull/666)).
## v3.10.0 - 2022-01-04
- A new `--list-all` (alias `-a`) flag is now available. It's similar to the
exiting `--list` (`-l`) but prints all tasks, even those without a
description
([#383](https://github.com/go-task/task/issues/383), [#401](https://github.com/go-task/task/pull/401)).
- It's now possible to schedule cleanup commands to run once a task finishes
with the `defer:` keyword
([Documentation](https://taskfile.dev/#/usage?id=doing-task-cleanup-with-defer), [#475](https://github.com/go-task/task/issues/475), [#626](https://github.com/go-task/task/pull/626)).
- Remove long deprecated and undocumented `$` variable prefix and `^` command
prefix
([#642](https://github.com/go-task/task/issues/642), [#644](https://github.com/go-task/task/issues/644), [#645](https://github.com/go-task/task/pull/645)).
- Add support for `.yaml` extension (as an alternative to `.yml`).
This was requested multiple times throughout the years. Enjoy!
([#183](https://github.com/go-task/task/issues/183), [#184](https://github.com/go-task/task/pull/184), [#369](https://github.com/go-task/task/issues/369), [#584](https://github.com/go-task/task/issues/584), [#621](https://github.com/go-task/task/pull/621)).
- Fixed error when computing a variable when the task directory do not exist
yet
([#481](https://github.com/go-task/task/issues/481), [#579](https://github.com/go-task/task/pull/579)).
## v3.9.2 - 2021-12-02 ## v3.9.2 - 2021-12-02
- Upgrade [mvdan/sh](https://github.com/mvdan/sh) which contains a fix a for - Upgrade [mvdan/sh](https://github.com/mvdan/sh) which contains a fix a for

View File

@@ -60,6 +60,7 @@ func main() {
helpFlag bool helpFlag bool
init bool init bool
list bool list bool
listAll bool
status bool status bool
force bool force bool
watch bool watch bool
@@ -71,14 +72,15 @@ func main() {
concurrency int concurrency int
dir string dir string
entrypoint string entrypoint string
output string output taskfile.Output
color bool color bool
) )
pflag.BoolVar(&versionFlag, "version", false, "show Task version") pflag.BoolVar(&versionFlag, "version", false, "show Task version")
pflag.BoolVarP(&helpFlag, "help", "h", false, "shows Task usage") pflag.BoolVarP(&helpFlag, "help", "h", false, "shows Task usage")
pflag.BoolVarP(&init, "init", "i", false, "creates a new Taskfile.yml in the current folder") 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(&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.BoolVar(&status, "status", false, "exits with non-zero exit code if any of the given tasks is not up-to-date") 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(&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") pflag.BoolVarP(&watch, "watch", "w", false, "enables watch of the given task")
@@ -89,7 +91,9 @@ func main() {
pflag.BoolVar(&summary, "summary", false, "show summary about a task") pflag.BoolVar(&summary, "summary", false, "show summary about a task")
pflag.StringVarP(&dir, "dir", "d", "", "sets directory of execution") pflag.StringVarP(&dir, "dir", "d", "", "sets directory of execution")
pflag.StringVarP(&entrypoint, "taskfile", "t", "", `choose which Taskfile to run. Defaults to "Taskfile.yml"`) pflag.StringVarP(&entrypoint, "taskfile", "t", "", `choose which Taskfile to run. Defaults to "Taskfile.yml"`)
pflag.StringVarP(&output, "output", "o", "", "sets output style: [interleaved|group|prefixed]") pflag.StringVarP(&output.Name, "output", "o", "", "sets output style: [interleaved|group|prefixed]")
pflag.StringVar(&output.Group.Begin, "output-group-begin", "", "message template to print before a task's grouped output")
pflag.StringVar(&output.Group.End, "output-group-end", "", "message template to print after a task's grouped output")
pflag.BoolVarP(&color, "color", "c", true, "colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable") pflag.BoolVarP(&color, "color", "c", true, "colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable")
pflag.IntVarP(&concurrency, "concurrency", "C", 0, "limit number tasks to run concurrently") pflag.IntVarP(&concurrency, "concurrency", "C", 0, "limit number tasks to run concurrently")
pflag.Parse() pflag.Parse()
@@ -122,8 +126,17 @@ func main() {
if entrypoint != "" { if entrypoint != "" {
dir = filepath.Dir(entrypoint) dir = filepath.Dir(entrypoint)
entrypoint = filepath.Base(entrypoint) entrypoint = filepath.Base(entrypoint)
} else { }
entrypoint = "Taskfile.yml"
if output.Name != "group" {
if output.Group.Begin != "" {
log.Fatal("task: You can't set --output-group-begin without --output=group")
return
}
if output.Group.End != "" {
log.Fatal("task: You can't set --output-group-end without --output=group")
return
}
} }
e := task.Executor{ e := task.Executor{
@@ -145,6 +158,12 @@ func main() {
OutputStyle: output, OutputStyle: output,
} }
if (list || listAll) && silent {
e.ListTaskNames(listAll)
return
}
if err := e.Setup(); err != nil { if err := e.Setup(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -155,7 +174,12 @@ func main() {
} }
if list { if list {
e.PrintTasksHelp() e.ListTasksWithDesc()
return
}
if listAll {
e.ListAllTasks()
return return
} }

View File

@@ -1,21 +1,22 @@
# /bin/bash
_task_completion() _task_completion()
{ {
local scripts; local cur
local curr_arg; _get_comp_words_by_ref -n : cur
# Remove colon from word breaks case "$cur" in
COMP_WORDBREAKS=${COMP_WORDBREAKS//:} --*)
local options="$(_parse_help task)"
COMPREPLY=( $(compgen -W "$options" -- "$cur") )
;;
*)
local tasks="$(task --list-all | awk 'NR>1 { sub(/:$/,"",$2); print $2 }')"
COMPREPLY=( $(compgen -W "$tasks" -- "$cur") )
;;
esac
scripts=$(task -l | sed '1d' | awk '{ print $2 }' | sed 's/:$//'); __ltrim_colon_completions "$cur"
curr_arg="${COMP_WORDS[COMP_CWORD]:-"."}"
# Do not accept more than 1 argument
if [ "${#COMP_WORDS[@]}" != "2" ]; then
return
fi
COMPREPLY=($(compgen -c | echo "$scripts" | grep -- $curr_arg));
} }
complete -F _task_completion task complete -F _task_completion task

View File

@@ -1,25 +1,60 @@
#compdef task #compdef task
# Listing commands from Taskfile.yml local context state state_descr line
function __list() { typeset -A opt_args
local -a scripts
if [ -f Taskfile.yml ]; then # Listing commands from Taskfile.yml
scripts=($(task -l | sed '1d' | sed 's/^\* //' | awk '{ print $1 }' | sed 's/:$//' | sed 's/:/\\:/g')) function __task_list() {
_describe 'script' scripts local -a scripts cmd
local -i enabled=0
local taskfile item task desc
cmd=(task)
taskfile="${(v)opt_args[(i)-t|--taskfile]}"
if [[ -n "$taskfile" && -f "$taskfile" ]]; then
enabled=1
cmd+=(--taskfile "$taskfile")
else
for taskfile in Taskfile{,.dist}.{yaml,yml}; do
if [[ -f "$taskfile" ]]; then
enabled=1
break
fi
done
fi fi
(( enabled )) || return 0
scripts=()
for item in "${(@)${(f)$("${cmd[@]}" --list)}[2,-1]#\* }"; do
task="${item%%:[[:space:]]*}"
desc="${item##[^[:space:]]##[[:space:]]##}"
scripts+=( "${task//:/\\:}:$desc" )
done
_describe 'Task to run' scripts
} }
_arguments \ _arguments \
'(-d --dir)'{-d,--dir}': :_files' \ '(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: ' \
'(--dry)'--dry \ '(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]' \
'(-f --force)'{-f,--force} \ '(-f --force)'{-f,--force}'[run even if task is up-to-date]' \
'(-i --init)'{-i,--init} \ '(-c --color)'{-c,--color}'[colored output]' \
'(-l --list)'{-l,--list} \ '(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs' \
'(-s --silent)'{-s,--silent} \ '(--dry)--dry[dry-run mode, compile and print tasks only]' \
'(--status)'--status \ '(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)' \
'(-v --verbose)'{-v,--verbose} \ '(--output-group-begin)--output-group-begin[message template before grouped output]:template text: ' \
'(--version)'--version \ '(--output-group-end)--output-group-end[message template after grouped output]:template text: ' \
'(-w --watch)'{-w,--watch} \ '(-s --silent)'{-s,--silent}'[disable echoing]' \
'(- *)'{-h,--help} \ '(--status)--status[exit non-zero if supplied tasks not up-to-date]' \
'*: :__list' \ '(--summary)--summary[show summary\: field from tasks instead of running them]' \
'(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files' \
'(-v --verbose)'{-v,--verbose}'[verbose mode]' \
'(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]' \
+ '(operation)' \
{-l,--list}'[list describable tasks]' \
{-a,--list-all}'[list all tasks]' \
{-i,--init}'[create new Taskfile.yaml]' \
'(-*)'{-h,--help}'[show help]' \
'(-*)--version[show version and exit]' \
'*: :__task_list'

View File

@@ -29,6 +29,12 @@ There is a convenience wrapper for initializing and running tasks from Sublime T
developed by [@biozz](https://github.com/biozz), the source code is available [here](https://github.com/biozz/sublime-taskfile) developed by [@biozz](https://github.com/biozz), the source code is available [here](https://github.com/biozz/sublime-taskfile)
and it is published on Package Control [here](https://packagecontrol.io/packages/Taskfile). and it is published on Package Control [here](https://packagecontrol.io/packages/Taskfile).
### IntelliJ plugin
There's a JetBrains IntelliJ plugin done by
[@lechuckroh](https://github.com/lechuckroh), which has its code [here](https://github.com/lechuckroh/task-intellij-plugin)
and is published [here](https://plugins.jetbrains.com/plugin/17058-taskfile).
## Installation methods ## Installation methods
Some installation methods are maintained by third party: Some installation methods are maintained by third party:

View File

@@ -25,6 +25,18 @@ right:
sudo snap install task --classic sudo snap install task --classic
``` ```
#### **Chocolatey**
If you're on Windows and have [Chocolatey][choco] installed, getting
Task is as simple as running:
```bash
choco install go-task
```
This installation method is community owned.
#### **Scoop** #### **Scoop**
If you're on Windows and have [Scoop][scoop] installed, use `extras` bucket If you're on Windows and have [Scoop][scoop] installed, use `extras` bucket
@@ -51,6 +63,19 @@ yay -S taskfile-git
This installation method is community owned, but since it's `-git` version of This installation method is community owned, but since it's `-git` version of
the package, it's always latest available version based on the Git repository. the package, it's always latest available version based on the Git repository.
#### **Nix**
If you're on NixOS or have Nix installed
you can install Task from [nixpkgs](https://github.com/NixOS/nixpkgs):
```cmd
nix-env -iA nixpkgs.go-task
```
This installation method is community owned. After a new release of Task, it
may take some time until it's available in [nixpkgs](https://github.com/NixOS/nixpkgs).
<!-- tabs:end --> <!-- tabs:end -->
## Get The Binary ## Get The Binary
@@ -137,4 +162,5 @@ env GO111MODULE=on go get -u github.com/go-task/task/v3/cmd/task@latest
[installscript]: https://github.com/go-task/task/blob/master/install-task.sh [installscript]: https://github.com/go-task/task/blob/master/install-task.sh
[releases]: https://github.com/go-task/task/releases [releases]: https://github.com/go-task/task/releases
[godownloader]: https://github.com/goreleaser/godownloader [godownloader]: https://github.com/goreleaser/godownloader
[choco]: https://chocolatey.org/
[scoop]: https://scoop.sh/ [scoop]: https://scoop.sh/

View File

@@ -31,6 +31,13 @@ of updating versions there by editing
[this file](https://github.com/lukesampson/scoop-extras/blob/master/bucket/task.json). [this file](https://github.com/lukesampson/scoop-extras/blob/master/bucket/task.json).
If you think its Task version is outdated, open an issue to let us know. If you think its Task version is outdated, open an issue to let us know.
# Nix
Nix is a community owned installation method. Nix package maintainers usually take care
of updating versions there by editing
[this file](https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/development/tools/go-task/default.nix).
If you think its Task version is outdated, open an issue to let us know.
[goreleaser]: https://goreleaser.com/#continuous_integration [goreleaser]: https://goreleaser.com/#continuous_integration
[homebrewtap]: https://github.com/go-task/homebrew-tap [homebrewtap]: https://github.com/go-task/homebrew-tap
[gotaskrb]: https://github.com/go-task/homebrew-tap/blob/master/Formula/go-task.rb [gotaskrb]: https://github.com/go-task/homebrew-tap/blob/master/Formula/go-task.rb

View File

@@ -33,6 +33,20 @@ executable called must be available by the OS or in PATH.
If you omit a task name, "default" will be assumed. If you omit a task name, "default" will be assumed.
## Supported file names
Task will look for the following file names, in order of priority:
- Taskfile.yml
- Taskfile.yaml
- Taskfile.dist.yml
- Taskfile.dist.yaml
The intention of having the `.dist` variants is to allow projects to have one
commited version (`.dist`) while still allowing individual users to override
the Taskfile by adding an additional `Taskfile.yml` (which would be on
`.gitignore`).
## Environment variables ## Environment variables
### Task ### Task
@@ -150,10 +164,6 @@ includes:
> The included Taskfiles must be using the same schema version the main > The included Taskfiles must be using the same schema version the main
> Taskfile uses. > Taskfile uses.
> Also, for now included Taskfiles can't include other Taskfiles.
> This was a deliberate decision to keep use and implementation simple.
> If you disagree, open an GitHub issue and explain your use case. =)
### Optional includes ### Optional includes
Includes marked as optional will allow Task to continue execution as normal if Includes marked as optional will allow Task to continue execution as normal if
@@ -173,6 +183,31 @@ tasks:
- echo "This command can still be successfully executed if ./tests/Taskfile.yml does not exist" - echo "This command can still be successfully executed if ./tests/Taskfile.yml does not exist"
``` ```
### Vars of included Taskfiles
You can also specify variables when including a Taskfile. This may be useful
for having reusable Taskfile that can be tweaked or even included more than once:
```yaml
version: '3'
includes:
backend:
taskfile: ./taskfiles/Docker.yml
vars:
DOCKER_IMAGE: backend_image
frontend:
taskfile: ./taskfiles/Docker.yml
vars:
DOCKER_IMAGE: frontend_image
```
> NOTE: Vars declared in the included Taskfile have preference over the
included ones! If you want a variable in an included Taskfile to be overridable
use the [default function](https://go-task.github.io/slim-sprig/defaults.html):
`MY_VAR: '{{.MY_VAR | default "my-default-value"}}'`.
## Task directory ## Task directory
By default, tasks will be executed in the directory where the Taskfile is By default, tasks will be executed in the directory where the Taskfile is
@@ -521,6 +556,8 @@ They are listed below in order of importance (e.g. most important first):
- Variables declared in the task definition - Variables declared in the task definition
- Variables given while calling a task from another - Variables given while calling a task from another
(See [Calling another task](#calling-another-task) above) (See [Calling another task](#calling-another-task) above)
- Variables of the [included Taskfile](#including-other-taskfiles) (when the task is included)
- Variables of the [inclusion of the Taskfile](#vars-of-included-taskfiles) (when the task is included)
- Global variables (those declared in the `vars:` option in the Taskfile) - Global variables (those declared in the `vars:` option in the Taskfile)
- Environment variables - Environment variables
@@ -608,6 +645,45 @@ tasks:
- yarn {{.CLI_ARGS}} - yarn {{.CLI_ARGS}}
``` ```
## Doing task cleanup with `defer`
With the `defer` keyword, it's possible to schedule cleanup to be run once
the task finishes. The difference with just putting it as the last command is
that this command will run even when the task fails.
In the example below `rm -rf tmpdir/` will run even if the third command fails:
```yaml
version: '3'
tasks:
default:
cmds:
- mkdir -p tmpdir/
- defer: rm -rf tmpdir/
- echo 'Do work on tmpdir/'
```
If you want to move the cleanup command into another task, that's possible as
well:
```yaml
version: '3'
tasks:
default:
cmds:
- mkdir -p tmpdir/
- defer: { task: cleanup }
- echo 'Do work on tmpdir/'
cleanup: rm -rf tmpdir/
```
> NOTE: Due to the nature of how the
[Go's own `defer` work](https://go.dev/tour/flowcontrol/13), the deferred
commands are executed in the reverse order if you schedule multiple of them.
## Go's template engine ## Go's template engine
Task parse commands as [Go's template engine][gotemplate] before executing Task parse commands as [Go's template engine][gotemplate] before executing
@@ -703,6 +779,8 @@ would print the following output:
* test: Run all the go tests. * test: Run all the go tests.
``` ```
If you want to see all tasks, there's a `--list-all` (alias `-a`) flag as well.
## Display summary of task ## Display summary of task
Running `task --summary task-name` will show a summary of a task. Running `task --summary task-name` will show a summary of a task.
@@ -918,6 +996,34 @@ tasks:
finishes, so you won't have live feedback for commands that take a long time finishes, so you won't have live feedback for commands that take a long time
to run. to run.
When using the `group` output, you can optionally provide a templated message
to print at the start and end of the group. This can be useful for instructing
CI systems to group all of the output for a given task, such as with
[GitHub Actions' `::group::` command](https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions#grouping-log-lines)
or [Azure Pipelines](https://docs.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?expand=1&view=azure-devops&tabs=bash#formatting-commands).
```yaml
version: '3'
output:
group:
begin: '::begin::{{.TASK}}'
end: '::endgroup::'
tasks:
default:
cmds:
- echo 'Hello, World!'
silent: true
```
```bash
$ task default
::begin::default
Hello, World!
::endgroup::
```
The `prefix` output will prefix every line printed by a command with The `prefix` output will prefix every line printed by a command with
`[task-name] ` as the prefix, but you can customize the prefix for a command `[task-name] ` as the prefix, but you can customize the prefix for a command
with the `prefix:` attribute: with the `prefix:` attribute:

15
go.mod
View File

@@ -8,10 +8,19 @@ require (
github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/radovskyb/watcher v1.0.7 github.com/radovskyb/watcher v1.0.7
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.1
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
mvdan.cc/sh/v3 v3.4.2-0.20211202103622-5ae9d64e1402 mvdan.cc/sh/v3 v3.4.3
) )
go 1.16 require (
github.com/davecgh/go-spew v1.1.1 // indirect
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-20210925032602-92d5a993a665 // indirect
golang.org/x/term v0.0.0-20210916214954-140adaaadfaf // indirect
)
go 1.17

8
go.sum
View File

@@ -46,8 +46,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 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/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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -68,5 +68,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/editorconfig v0.2.0/go.mod h1:lvnnD3BNdBYkhq+B4uBuFFKatfp02eB6HixDvEz91C0= mvdan.cc/editorconfig v0.2.0/go.mod h1:lvnnD3BNdBYkhq+B4uBuFFKatfp02eB6HixDvEz91C0=
mvdan.cc/sh/v3 v3.4.2-0.20211202103622-5ae9d64e1402 h1:IhYcaLRZjSJqGoLrn+sXXFq3Xhd40I0wBkoIyg6cSZs= mvdan.cc/sh/v3 v3.4.3 h1:zbuKH7YH9cqU6PGajhFFXZY7dhPXcDr55iN/cUAqpuw=
mvdan.cc/sh/v3 v3.4.2-0.20211202103622-5ae9d64e1402/go.mod h1:p/tqPPI4Epfk2rICAe2RoaNd8HBSJ8t9Y2DA9yQlbzY= mvdan.cc/sh/v3 v3.4.3/go.mod h1:p/tqPPI4Epfk2rICAe2RoaNd8HBSJ8t9Y2DA9yQlbzY=

72
help.go
View File

@@ -2,18 +2,43 @@ package task
import ( import (
"fmt" "fmt"
"io"
"log"
"os"
"sort" "sort"
"strings"
"text/tabwriter" "text/tabwriter"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/taskfile" "github.com/go-task/task/v3/taskfile"
) )
// PrintTasksHelp prints help os tasks that have a description // ListTasksWithDesc reports tasks that have a description spec.
func (e *Executor) PrintTasksHelp() { func (e *Executor) ListTasksWithDesc() {
tasks := e.tasksWithDesc() e.printTasks(false)
return
}
// ListAllTasks reports all tasks, with or without a description spec.
func (e *Executor) ListAllTasks() {
e.printTasks(true)
return
}
func (e *Executor) printTasks(listAll bool) {
var tasks []*taskfile.Task
if listAll {
tasks = e.allTaskNames()
} else {
tasks = e.tasksWithDesc()
}
if len(tasks) == 0 { if len(tasks) == 0 {
e.Logger.Outf(logger.Yellow, "task: No tasks with description available") if 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")
}
return return
} }
e.Logger.Outf(logger.Default, "task: Available tasks for this project:") e.Logger.Outf(logger.Default, "task: Available tasks for this project:")
@@ -26,6 +51,15 @@ func (e *Executor) PrintTasksHelp() {
w.Flush() w.Flush()
} }
func (e *Executor) allTaskNames() (tasks []*taskfile.Task) {
tasks = make([]*taskfile.Task, 0, len(e.Taskfile.Tasks))
for _, task := range e.Taskfile.Tasks {
tasks = append(tasks, task)
}
sort.Slice(tasks, func(i, j int) bool { return tasks[i].Task < tasks[j].Task })
return
}
func (e *Executor) tasksWithDesc() (tasks []*taskfile.Task) { func (e *Executor) tasksWithDesc() (tasks []*taskfile.Task) {
tasks = make([]*taskfile.Task, 0, len(e.Taskfile.Tasks)) tasks = make([]*taskfile.Task, 0, len(e.Taskfile.Tasks))
for _, task := range e.Taskfile.Tasks { for _, task := range e.Taskfile.Tasks {
@@ -40,3 +74,33 @@ func (e *Executor) tasksWithDesc() (tasks []*taskfile.Task) {
sort.Slice(tasks, func(i, j int) bool { return tasks[i].Task < tasks[j].Task }) sort.Slice(tasks, func(i, j int) bool { return tasks[i].Task < tasks[j].Task })
return return
} }
// PrintTaskNames 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) {
// if called from cmd/task.go, e.Taskfile has not yet been parsed
if e.Taskfile == nil {
if err := e.readTaskfile(); err != nil {
log.Fatal(err)
return
}
}
// use stdout if no output defined
var w io.Writer = os.Stdout
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 != "" {
s = append(s, strings.TrimRight(t.Task, ":"))
}
}
// sort and print all task names
sort.Strings(s)
for _, t := range s {
fmt.Fprintln(w, t)
}
}

View File

@@ -3,7 +3,6 @@ package task
import ( import (
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
) )
@@ -24,15 +23,15 @@ tasks:
// InitTaskfile Taskfile creates a new Taskfile // InitTaskfile Taskfile creates a new Taskfile
func InitTaskfile(w io.Writer, dir string) error { func InitTaskfile(w io.Writer, dir string) error {
f := filepath.Join(dir, "Taskfile.yml") f := filepath.Join(dir, "Taskfile.yaml")
if _, err := os.Stat(f); err == nil { if _, err := os.Stat(f); err == nil {
return ErrTaskfileAlreadyExists return ErrTaskfileAlreadyExists
} }
if err := ioutil.WriteFile(f, []byte(defaultTaskfile), 0644); err != nil { if err := os.WriteFile(f, []byte(defaultTaskfile), 0644); err != nil {
return err return err
} }
fmt.Fprintf(w, "Taskfile.yml created in the current directory\n") fmt.Fprintf(w, "Taskfile.yaml created in the current directory\n")
return nil return nil
} }

View File

@@ -74,12 +74,35 @@ func (c *CompilerV3) getVariables(t *taskfile.Task, call *taskfile.Call, evaluat
} }
rangeFunc := getRangeFunc(c.Dir) rangeFunc := getRangeFunc(c.Dir)
var taskRangeFunc func(k string, v taskfile.Var) error
if t != nil {
// NOTE(@andreynering): We're manually joining these paths here because
// this is the raw task, not the compiled one.
tr := templater.Templater{Vars: result, RemoveNoValue: true}
dir := tr.Replace(t.Dir)
if err := tr.Err(); err != nil {
return nil, err
}
if !filepath.IsAbs(dir) {
dir = filepath.Join(c.Dir, dir)
}
taskRangeFunc = getRangeFunc(dir)
}
if err := c.TaskfileEnv.Range(rangeFunc); err != nil { if err := c.TaskfileEnv.Range(rangeFunc); err != nil {
return nil, err return nil, err
} }
if err := c.TaskfileVars.Range(rangeFunc); err != nil { if err := c.TaskfileVars.Range(rangeFunc); err != nil {
return nil, err return nil, err
} }
if t != nil {
if err := t.IncludedTaskfileVars.Range(taskRangeFunc); err != nil {
return nil, err
}
if err := t.IncludeVars.Range(rangeFunc); err != nil {
return nil, err
}
}
if t == nil || call == nil { if t == nil || call == nil {
return result, nil return result, nil
@@ -88,19 +111,6 @@ func (c *CompilerV3) getVariables(t *taskfile.Task, call *taskfile.Call, evaluat
if err := call.Vars.Range(rangeFunc); err != nil { if err := call.Vars.Range(rangeFunc); err != nil {
return nil, err return nil, err
} }
// NOTE(@andreynering): We're manually joining these paths here because
// this is the raw task, not the compiled one.
tr := templater.Templater{Vars: result, RemoveNoValue: true}
dir := tr.Replace(t.Dir)
if err := tr.Err(); err != nil {
return nil, err
}
if !filepath.IsAbs(dir) {
dir = filepath.Join(c.Dir, dir)
}
taskRangeFunc := getRangeFunc(dir)
if err := t.Vars.Range(taskRangeFunc); err != nil { if err := t.Vars.Range(taskRangeFunc); err != nil {
return nil, err return nil, err
} }

View File

@@ -47,10 +47,10 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
r, err := interp.New( r, err := interp.New(
interp.Params("-e"), interp.Params("-e"),
interp.Dir(opts.Dir),
interp.Env(expand.ListEnviron(environ...)), interp.Env(expand.ListEnviron(environ...)),
interp.OpenHandler(openHandler), interp.OpenHandler(openHandler),
interp.StdIO(opts.Stdin, opts.Stdout, opts.Stderr), interp.StdIO(opts.Stdin, opts.Stdout, opts.Stderr),
dirOption(opts.Dir),
) )
if err != nil { if err != nil {
return err return err
@@ -87,3 +87,24 @@ func openHandler(ctx context.Context, path string, flag int, perm os.FileMode) (
} }
return interp.DefaultOpenHandler()(ctx, path, flag, perm) return interp.DefaultOpenHandler()(ctx, path, flag, perm)
} }
func dirOption(path string) interp.RunnerOption {
return func(r *interp.Runner) error {
err := interp.Dir(path)(r)
if err == nil {
return nil
}
// If the specified directory doesn't exist, it will be created later.
// Therefore, even if `interp.Dir` method returns an error, the
// directory path should be set only when the directory cannot be found.
if absPath, _ := filepath.Abs(path); absPath != "" {
if _, err := os.Stat(absPath); os.IsNotExist(err) {
r.Dir = absPath
return nil
}
}
return err
}
}

View File

@@ -5,15 +5,25 @@ import (
"io" "io"
) )
type Group struct{} type Group struct{
Begin, End string
}
func (Group) WrapWriter(w io.Writer, _ string) io.Writer { func (g Group) WrapWriter(w io.Writer, _ string, tmpl Templater) io.Writer {
return &groupWriter{writer: w} gw := &groupWriter{writer: w}
if g.Begin != "" {
gw.begin = tmpl.Replace(g.Begin) + "\n"
}
if g.End != "" {
gw.end = tmpl.Replace(g.End) + "\n"
}
return gw
} }
type groupWriter struct { type groupWriter struct {
writer io.Writer writer io.Writer
buff bytes.Buffer buff bytes.Buffer
begin, end string
} }
func (gw *groupWriter) Write(p []byte) (int, error) { func (gw *groupWriter) Write(p []byte) (int, error) {
@@ -21,6 +31,14 @@ func (gw *groupWriter) Write(p []byte) (int, error) {
} }
func (gw *groupWriter) Close() error { func (gw *groupWriter) Close() error {
if gw.buff.Len() == 0 {
// don't print begin/end messages if there's no buffered entries
return nil
}
if _, err := io.WriteString(gw.writer, gw.begin); err != nil {
return err
}
gw.buff.WriteString(gw.end)
_, err := io.Copy(gw.writer, &gw.buff) _, err := io.Copy(gw.writer, &gw.buff)
return err return err
} }

View File

@@ -6,6 +6,6 @@ import (
type Interleaved struct{} type Interleaved struct{}
func (Interleaved) WrapWriter(w io.Writer, _ string) io.Writer { func (Interleaved) WrapWriter(w io.Writer, _ string, _ Templater) io.Writer {
return w return w
} }

View File

@@ -1,9 +1,49 @@
package output package output
import ( import (
"fmt"
"io" "io"
"github.com/go-task/task/v3/taskfile"
) )
type Output interface { // Templater executes a template engine.
WrapWriter(w io.Writer, prefix string) io.Writer // It is provided by the templater.Templater package.
type Templater interface {
// Replace replaces the provided template string with a rendered string.
Replace(tmpl string) string
}
type Output interface {
WrapWriter(w io.Writer, prefix string, tmpl Templater) io.Writer
}
// Build the Output for the requested taskfile.Output.
func BuildFor(o *taskfile.Output) (Output, error) {
switch o.Name {
case "interleaved", "":
if err := checkOutputGroupUnset(o); err != nil {
return nil, err
}
return Interleaved{}, nil
case "group":
return Group{
Begin: o.Group.Begin,
End: o.Group.End,
}, nil
case "prefixed":
if err := checkOutputGroupUnset(o); err != nil {
return nil, err
}
return Prefixed{}, nil
default:
return nil, fmt.Errorf(`task: output style %q not recognized`, o.Name)
}
}
func checkOutputGroupUnset(o *taskfile.Output) error {
if o.Group.IsSet() {
return fmt.Errorf("task: output style %q does not support the group begin/end parameter", o.Name)
}
return nil
} }

View File

@@ -6,6 +6,8 @@ import (
"io" "io"
"testing" "testing"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/internal/output"
@@ -14,7 +16,7 @@ import (
func TestInterleaved(t *testing.T) { func TestInterleaved(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer
var o output.Output = output.Interleaved{} var o output.Output = output.Interleaved{}
var w = o.WrapWriter(&b, "") var w = o.WrapWriter(&b, "", nil)
fmt.Fprintln(w, "foo\nbar") fmt.Fprintln(w, "foo\nbar")
assert.Equal(t, "foo\nbar\n", b.String()) assert.Equal(t, "foo\nbar\n", b.String())
@@ -25,7 +27,7 @@ func TestInterleaved(t *testing.T) {
func TestGroup(t *testing.T) { func TestGroup(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer
var o output.Output = output.Group{} var o output.Output = output.Group{}
var w = o.WrapWriter(&b, "").(io.WriteCloser) var w = o.WrapWriter(&b, "", nil).(io.WriteCloser)
fmt.Fprintln(w, "foo\nbar") fmt.Fprintln(w, "foo\nbar")
assert.Equal(t, "", b.String()) assert.Equal(t, "", b.String())
@@ -35,10 +37,43 @@ func TestGroup(t *testing.T) {
assert.Equal(t, "foo\nbar\nbaz\n", b.String()) assert.Equal(t, "foo\nbar\nbaz\n", b.String())
} }
func TestGroupWithBeginEnd(t *testing.T) {
tmpl := templater.Templater{
Vars: &taskfile.Vars{
Keys: []string{"VAR1"},
Mapping: map[string]taskfile.Var{
"VAR1": {Static: "example-value"},
},
},
}
var o output.Output = output.Group{
Begin: "::group::{{ .VAR1 }}",
End: "::endgroup::",
}
t.Run("simple", func(t *testing.T) {
var b bytes.Buffer
var w = o.WrapWriter(&b, "", &tmpl).(io.WriteCloser)
fmt.Fprintln(w, "foo\nbar")
assert.Equal(t, "", b.String())
fmt.Fprintln(w, "baz")
assert.Equal(t, "", b.String())
assert.NoError(t, w.Close())
assert.Equal(t, "::group::example-value\nfoo\nbar\nbaz\n::endgroup::\n", b.String())
})
t.Run("no output", func(t *testing.T) {
var b bytes.Buffer
var w = o.WrapWriter(&b, "", &tmpl).(io.WriteCloser)
assert.NoError(t, w.Close())
assert.Equal(t, "", b.String())
})
}
func TestPrefixed(t *testing.T) { func TestPrefixed(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer
var o output.Output = output.Prefixed{} var o output.Output = output.Prefixed{}
var w = o.WrapWriter(&b, "prefix").(io.WriteCloser) var w = o.WrapWriter(&b, "prefix", nil).(io.WriteCloser)
t.Run("simple use cases", func(t *testing.T) { t.Run("simple use cases", func(t *testing.T) {
b.Reset() b.Reset()

View File

@@ -9,7 +9,7 @@ import (
type Prefixed struct{} type Prefixed struct{}
func (Prefixed) WrapWriter(w io.Writer, prefix string) io.Writer { func (Prefixed) WrapWriter(w io.Writer, prefix string, _ Templater) io.Writer {
return &prefixWriter{writer: w, prefix: prefix} return &prefixWriter{writer: w, prefix: prefix}
} }

View File

@@ -4,7 +4,6 @@ import (
"crypto/md5" "crypto/md5"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@@ -30,7 +29,7 @@ func (c *Checksum) IsUpToDate() (bool, error) {
checksumFile := c.checksumFilePath() checksumFile := c.checksumFilePath()
data, _ := ioutil.ReadFile(checksumFile) data, _ := os.ReadFile(checksumFile)
oldMd5 := strings.TrimSpace(string(data)) oldMd5 := strings.TrimSpace(string(data))
sources, err := globs(c.TaskDir, c.Sources) sources, err := globs(c.TaskDir, c.Sources)
@@ -45,7 +44,7 @@ func (c *Checksum) IsUpToDate() (bool, error) {
if !c.Dry { if !c.Dry {
_ = os.MkdirAll(filepath.Join(c.BaseDir, ".task", "checksum"), 0755) _ = os.MkdirAll(filepath.Join(c.BaseDir, ".task", "checksum"), 0755)
if err = ioutil.WriteFile(checksumFile, []byte(newMd5+"\n"), 0644); err != nil { if err = os.WriteFile(checksumFile, []byte(newMd5+"\n"), 0644); err != nil {
return false, err return false, err
} }
} }

78
task.go
View File

@@ -16,6 +16,7 @@ import (
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/internal/output"
"github.com/go-task/task/v3/internal/summary" "github.com/go-task/task/v3/internal/summary"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile" "github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/read" "github.com/go-task/task/v3/taskfile/read"
@@ -51,7 +52,7 @@ type Executor struct {
Logger *logger.Logger Logger *logger.Logger
Compiler compiler.Compiler Compiler compiler.Compiler
Output output.Output Output output.Output
OutputStyle string OutputStyle taskfile.Output
taskvars *taskfile.Vars taskvars *taskfile.Vars
@@ -68,7 +69,7 @@ func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error {
for _, c := range calls { for _, c := range calls {
if _, ok := e.Taskfile.Tasks[c.Task]; !ok { if _, ok := e.Taskfile.Tasks[c.Task]; !ok {
// FIXME: move to the main package // FIXME: move to the main package
e.PrintTasksHelp() e.ListTasksWithDesc()
return &taskNotFoundError{taskName: c.Task} return &taskNotFoundError{taskName: c.Task}
} }
} }
@@ -103,14 +104,21 @@ func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error {
return g.Wait() return g.Wait()
} }
// readTaskfile selects and parses the entrypoint.
func (e *Executor) readTaskfile() error {
var err error
e.Taskfile, err = read.Taskfile(&read.ReaderNode{
Dir: e.Dir,
Entrypoint: e.Entrypoint,
Parent: nil,
Optional: false,
})
return err
}
// Setup setups Executor's internal state // Setup setups Executor's internal state
func (e *Executor) Setup() error { func (e *Executor) Setup() error {
if e.Entrypoint == "" { err := e.readTaskfile()
e.Entrypoint = "Taskfile.yml"
}
var err error
e.Taskfile, err = read.Taskfile(e.Dir, e.Entrypoint)
if err != nil { if err != nil {
return err return err
} }
@@ -152,11 +160,11 @@ func (e *Executor) Setup() error {
v = 2.6 v = 2.6
} }
if v == 3.0 { if v == 3.0 {
v = 3.7 v = 3.8
} }
if v > 3.7 { if v > 3.8 {
return fmt.Errorf(`task: Taskfile versions greater than v3.7 not implemented in the version of Task`) return fmt.Errorf(`task: Taskfile versions greater than v3.8 not implemented in the version of Task`)
} }
// Color available only on v3 // Color available only on v3
@@ -198,7 +206,7 @@ func (e *Executor) Setup() error {
} }
} }
if v < 2.1 && e.Taskfile.Output != "" { if v < 2.1 && !e.Taskfile.Output.IsSet() {
return fmt.Errorf(`task: Taskfile option "output" is only available starting on Taskfile version v2.1`) return fmt.Errorf(`task: Taskfile option "output" is only available starting on Taskfile version v2.1`)
} }
if v < 2.2 && e.Taskfile.Includes.Len() > 0 { if v < 2.2 && e.Taskfile.Includes.Len() > 0 {
@@ -207,19 +215,16 @@ func (e *Executor) Setup() error {
if v >= 3.0 && e.Taskfile.Expansions > 2 { if v >= 3.0 && e.Taskfile.Expansions > 2 {
return fmt.Errorf(`task: The "expansions" setting is not available anymore on v3.0`) return fmt.Errorf(`task: The "expansions" setting is not available anymore on v3.0`)
} }
if v < 3.8 && e.Taskfile.Output.Group.IsSet() {
if e.OutputStyle != "" { return fmt.Errorf(`task: Taskfile option "output.group" is only available starting on Taskfile version v3.8`)
e.Taskfile.Output = e.OutputStyle
} }
switch e.Taskfile.Output {
case "", "interleaved": if !e.OutputStyle.IsSet() {
e.Output = output.Interleaved{} e.OutputStyle = e.Taskfile.Output
case "group": }
e.Output = output.Group{} e.Output, err = output.BuildFor(&e.OutputStyle)
case "prefixed": if err != nil {
e.Output = output.Prefixed{} return err
default:
return fmt.Errorf(`task: output option "%s" not recognized`, e.Taskfile.Output)
} }
if e.Taskfile.Method == "" { if e.Taskfile.Method == "" {
@@ -343,6 +348,11 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
} }
for i := range t.Cmds { for i := range t.Cmds {
if t.Cmds[i].Defer {
defer e.runDeferred(t, call, i)
continue
}
if err := e.runCommand(ctx, t, call, i); err != nil { if err := e.runCommand(ctx, t, call, i); err != nil {
if err2 := e.statusOnError(t); err2 != nil { if err2 := e.statusOnError(t); err2 != nil {
e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v", err2) e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v", err2)
@@ -399,6 +409,15 @@ func (e *Executor) runDeps(ctx context.Context, t *taskfile.Task) error {
return g.Wait() return g.Wait()
} }
func (e *Executor) runDeferred(t *taskfile.Task, call taskfile.Call, i int) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if err := e.runCommand(ctx, t, call, i); err != nil {
e.Logger.VerboseErrf(logger.Yellow, `task: ignored error in deferred cmd: %s`, err.Error())
}
}
func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfile.Call, i int) error { func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfile.Call, i int) error {
cmd := t.Cmds[i] cmd := t.Cmds[i]
@@ -425,8 +444,13 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
if t.Interactive { if t.Interactive {
outputWrapper = output.Interleaved{} outputWrapper = output.Interleaved{}
} }
stdOut := outputWrapper.WrapWriter(e.Stdout, t.Prefix) vars, err := e.Compiler.FastGetVariables(t, call)
stdErr := outputWrapper.WrapWriter(e.Stderr, t.Prefix) outputTemplater := &templater.Templater{Vars: vars, RemoveNoValue: true}
if err != nil {
return fmt.Errorf("task: failed to get variables: %w", err)
}
stdOut := outputWrapper.WrapWriter(e.Stdout, t.Prefix, outputTemplater)
stdErr := outputWrapper.WrapWriter(e.Stderr, t.Prefix, outputTemplater)
defer func() { defer func() {
if _, ok := stdOut.(*os.File); !ok { if _, ok := stdOut.(*os.File); !ok {
@@ -441,7 +465,7 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
} }
}() }()
err := execext.RunCommand(ctx, &execext.RunCommandOptions{ err = execext.RunCommand(ctx, &execext.RunCommandOptions{
Command: cmd.Cmd, Command: cmd.Cmd,
Dir: t.Dir, Dir: t.Dir,
Env: getEnviron(t), Env: getEnviron(t),

View File

@@ -4,7 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"io/ioutil" "io"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@@ -24,10 +24,11 @@ func init() {
// fileContentTest provides a basic reusable test-case for running a Taskfile // fileContentTest provides a basic reusable test-case for running a Taskfile
// and inspect generated files. // and inspect generated files.
type fileContentTest struct { type fileContentTest struct {
Dir string Dir string
Target string Entrypoint string
TrimSpace bool Target string
Files map[string]string TrimSpace bool
Files map[string]string
} }
func (fct fileContentTest) name(file string) string { func (fct fileContentTest) name(file string) string {
@@ -40,16 +41,17 @@ func (fct fileContentTest) Run(t *testing.T) {
} }
e := &task.Executor{ e := &task.Executor{
Dir: fct.Dir, Dir: fct.Dir,
Stdout: ioutil.Discard, Entrypoint: fct.Entrypoint,
Stderr: ioutil.Discard, Stdout: io.Discard,
Stderr: io.Discard,
} }
assert.NoError(t, e.Setup(), "e.Setup()") assert.NoError(t, e.Setup(), "e.Setup()")
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: fct.Target}), "e.Run(target)") assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: fct.Target}), "e.Run(target)")
for name, expectContent := range fct.Files { for name, expectContent := range fct.Files {
t.Run(fct.name(name), func(t *testing.T) { t.Run(fct.name(name), func(t *testing.T) {
b, err := ioutil.ReadFile(filepath.Join(fct.Dir, name)) b, err := os.ReadFile(filepath.Join(fct.Dir, name))
assert.NoError(t, err, "Error reading file") assert.NoError(t, err, "Error reading file")
s := string(b) s := string(b)
if fct.TrimSpace { if fct.TrimSpace {
@@ -63,8 +65,8 @@ func (fct fileContentTest) Run(t *testing.T) {
func TestEmptyTask(t *testing.T) { func TestEmptyTask(t *testing.T) {
e := &task.Executor{ e := &task.Executor{
Dir: "testdata/empty_task", Dir: "testdata/empty_task",
Stdout: ioutil.Discard, Stdout: io.Discard,
Stderr: ioutil.Discard, Stderr: io.Discard,
} }
assert.NoError(t, e.Setup(), "e.Setup()") assert.NoError(t, e.Setup(), "e.Setup()")
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"})) assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"}))
@@ -168,8 +170,8 @@ func TestVarsInvalidTmpl(t *testing.T) {
e := &task.Executor{ e := &task.Executor{
Dir: dir, Dir: dir,
Stdout: ioutil.Discard, Stdout: io.Discard,
Stderr: ioutil.Discard, Stderr: io.Discard,
} }
assert.NoError(t, e.Setup(), "e.Setup()") assert.NoError(t, e.Setup(), "e.Setup()")
assert.EqualError(t, e.Run(context.Background(), taskfile.Call{Task: target}), expectError, "e.Run(target)") assert.EqualError(t, e.Run(context.Background(), taskfile.Call{Task: target}), expectError, "e.Run(target)")
@@ -183,8 +185,8 @@ func TestConcurrency(t *testing.T) {
e := &task.Executor{ e := &task.Executor{
Dir: dir, Dir: dir,
Stdout: ioutil.Discard, Stdout: io.Discard,
Stderr: ioutil.Discard, Stderr: io.Discard,
Concurrency: 1, Concurrency: 1,
} }
assert.NoError(t, e.Setup(), "e.Setup()") assert.NoError(t, e.Setup(), "e.Setup()")
@@ -236,8 +238,8 @@ func TestDeps(t *testing.T) {
e := &task.Executor{ e := &task.Executor{
Dir: dir, Dir: dir,
Stdout: ioutil.Discard, Stdout: io.Discard,
Stderr: ioutil.Discard, Stderr: io.Discard,
} }
assert.NoError(t, e.Setup()) assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"})) assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"}))
@@ -516,10 +518,58 @@ func TestLabelInList(t *testing.T) {
Stderr: &buff, Stderr: &buff,
} }
assert.NoError(t, e.Setup()) assert.NoError(t, e.Setup())
e.PrintTasksHelp() e.ListTasksWithDesc()
assert.Contains(t, buff.String(), "foobar") assert.Contains(t, buff.String(), "foobar")
} }
// task -al case 1: listAll list all tasks
func TestListAllShowsNoDesc(t *testing.T) {
const dir = "testdata/list_mixed_desc"
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
var title string
e.ListAllTasks()
for _, title = range []string{
"foo",
"voo",
"doo",
} {
assert.Contains(t, buff.String(), title)
}
}
// task -al case 2: !listAll list some tasks (only those with desc)
func TestListCanListDescOnly(t *testing.T) {
const dir = "testdata/list_mixed_desc"
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
e.ListTasksWithDesc()
var title string
assert.Contains(t, buff.String(), "foo")
for _, title = range []string{
"voo",
"doo",
} {
assert.NotContains(t, buff.String(), title)
}
}
func TestStatusVariables(t *testing.T) { func TestStatusVariables(t *testing.T) {
const dir = "testdata/status_vars" const dir = "testdata/status_vars"
@@ -550,20 +600,21 @@ func TestStatusVariables(t *testing.T) {
func TestInit(t *testing.T) { func TestInit(t *testing.T) {
const dir = "testdata/init" const dir = "testdata/init"
var file = filepath.Join(dir, "Taskfile.yml") var file = filepath.Join(dir, "Taskfile.yaml")
_ = os.Remove(file) _ = os.Remove(file)
if _, err := os.Stat(file); err == nil { if _, err := os.Stat(file); err == nil {
t.Errorf("Taskfile.yml should not exist") t.Errorf("Taskfile.yaml should not exist")
} }
if err := task.InitTaskfile(ioutil.Discard, dir); err != nil { if err := task.InitTaskfile(io.Discard, dir); err != nil {
t.Error(err) t.Error(err)
} }
if _, err := os.Stat(file); err != nil { if _, err := os.Stat(file); err != nil {
t.Errorf("Taskfile.yml should exist") t.Errorf("Taskfile.yaml should exist")
} }
_ = os.Remove(file)
} }
func TestCyclicDep(t *testing.T) { func TestCyclicDep(t *testing.T) {
@@ -571,8 +622,8 @@ func TestCyclicDep(t *testing.T) {
e := task.Executor{ e := task.Executor{
Dir: dir, Dir: dir,
Stdout: ioutil.Discard, Stdout: io.Discard,
Stderr: ioutil.Discard, Stderr: io.Discard,
} }
assert.NoError(t, e.Setup()) assert.NoError(t, e.Setup())
assert.IsType(t, &task.MaximumTaskCallExceededError{}, e.Run(context.Background(), taskfile.Call{Task: "task-1"})) assert.IsType(t, &task.MaximumTaskCallExceededError{}, e.Run(context.Background(), taskfile.Call{Task: "task-1"}))
@@ -590,8 +641,8 @@ func TestTaskVersion(t *testing.T) {
t.Run(test.Dir, func(t *testing.T) { t.Run(test.Dir, func(t *testing.T) {
e := task.Executor{ e := task.Executor{
Dir: test.Dir, Dir: test.Dir,
Stdout: ioutil.Discard, Stdout: io.Discard,
Stderr: ioutil.Discard, Stderr: io.Discard,
} }
assert.NoError(t, e.Setup()) assert.NoError(t, e.Setup())
assert.Equal(t, test.Version, e.Taskfile.Version) assert.Equal(t, test.Version, e.Taskfile.Version)
@@ -605,8 +656,8 @@ func TestTaskIgnoreErrors(t *testing.T) {
e := task.Executor{ e := task.Executor{
Dir: dir, Dir: dir,
Stdout: ioutil.Discard, Stdout: io.Discard,
Stderr: ioutil.Discard, Stderr: io.Discard,
} }
assert.NoError(t, e.Setup()) assert.NoError(t, e.Setup())
@@ -668,8 +719,8 @@ func TestDryChecksum(t *testing.T) {
e := task.Executor{ e := task.Executor{
Dir: dir, Dir: dir,
Stdout: ioutil.Discard, Stdout: io.Discard,
Stderr: ioutil.Discard, Stderr: io.Discard,
Dry: true, Dry: true,
} }
assert.NoError(t, e.Setup()) assert.NoError(t, e.Setup())
@@ -702,6 +753,35 @@ func TestIncludes(t *testing.T) {
tt.Run(t) tt.Run(t)
} }
func TestIncludesMultiLevel(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/includes_multi_level",
Target: "default",
TrimSpace: true,
Files: map[string]string{
"called_one.txt": "one",
"called_two.txt": "two",
"called_three.txt": "three",
},
}
tt.Run(t)
}
func TestIncludeCycle(t *testing.T) {
const dir = "testdata/includes_cycle"
expectedError := "task: include cycle detected between testdata/includes_cycle/Taskfile.yml <--> testdata/includes_cycle/one/two/Taskfile.yml"
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
Silent: true,
}
assert.EqualError(t, e.Setup(), expectedError)
}
func TestIncorrectVersionIncludes(t *testing.T) { func TestIncorrectVersionIncludes(t *testing.T) {
const dir = "testdata/incorrect_includes" const dir = "testdata/incorrect_includes"
expectedError := "task: Import with additional parameters is only available starting on Taskfile version v3" expectedError := "task: Import with additional parameters is only available starting on Taskfile version v3"
@@ -769,8 +849,8 @@ func TestIncludesOptional(t *testing.T) {
func TestIncludesOptionalImplicitFalse(t *testing.T) { func TestIncludesOptionalImplicitFalse(t *testing.T) {
e := task.Executor{ e := task.Executor{
Dir: "testdata/includes_optional_implicit_false", Dir: "testdata/includes_optional_implicit_false",
Stdout: ioutil.Discard, Stdout: io.Discard,
Stderr: ioutil.Discard, Stderr: io.Discard,
} }
err := e.Setup() err := e.Setup()
@@ -781,8 +861,8 @@ func TestIncludesOptionalImplicitFalse(t *testing.T) {
func TestIncludesOptionalExplicitFalse(t *testing.T) { func TestIncludesOptionalExplicitFalse(t *testing.T) {
e := task.Executor{ e := task.Executor{
Dir: "testdata/includes_optional_explicit_false", Dir: "testdata/includes_optional_explicit_false",
Stdout: ioutil.Discard, Stdout: io.Discard,
Stderr: ioutil.Discard, Stderr: io.Discard,
} }
err := e.Setup() err := e.Setup()
@@ -790,6 +870,43 @@ func TestIncludesOptionalExplicitFalse(t *testing.T) {
assert.Equal(t, "stat testdata/includes_optional_explicit_false/TaskfileOptional.yml: no such file or directory", err.Error()) assert.Equal(t, "stat testdata/includes_optional_explicit_false/TaskfileOptional.yml: no such file or directory", err.Error())
} }
func TestIncludesFromCustomTaskfile(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/includes_yaml",
Entrypoint: "Custom.ext",
Target: "default",
TrimSpace: true,
Files: map[string]string{
"main.txt": "main",
"included_with_yaml_extension.txt": "included_with_yaml_extension",
"included_with_custom_file.txt": "included_with_custom_file",
},
}
tt.Run(t)
}
func TestSupportedFileNames(t *testing.T) {
fileNames := []string{
"Taskfile.yml",
"Taskfile.yaml",
"Taskfile.dist.yml",
"Taskfile.dist.yaml",
}
for _, fileName := range fileNames {
t.Run(fileName, func(t *testing.T) {
tt := fileContentTest{
Dir: fmt.Sprintf("testdata/file_names/%s", fileName),
Target: "default",
TrimSpace: true,
Files: map[string]string{
"output.txt": "hello",
},
}
tt.Run(t)
})
}
}
func TestSummary(t *testing.T) { func TestSummary(t *testing.T) {
const dir = "testdata/summary" const dir = "testdata/summary"
@@ -804,7 +921,7 @@ func TestSummary(t *testing.T) {
assert.NoError(t, e.Setup()) assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "task-with-summary"}, taskfile.Call{Task: "other-task-with-summary"})) assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "task-with-summary"}, taskfile.Call{Task: "other-task-with-summary"}))
data, err := ioutil.ReadFile(filepath.Join(dir, "task-with-summary.txt")) data, err := os.ReadFile(filepath.Join(dir, "task-with-summary.txt"))
assert.NoError(t, err) assert.NoError(t, err)
expectedOutput := string(data) expectedOutput := string(data)
@@ -877,6 +994,33 @@ func TestWhenDirAttributeItCreatesMissingAndRunsInThatDir(t *testing.T) {
_ = os.RemoveAll(toBeCreated) _ = os.RemoveAll(toBeCreated)
} }
func TestDynamicVariablesRunOnTheNewCreatedDir(t *testing.T) {
const expected = "created"
const dir = "testdata/dir/dynamic_var_on_created_dir/"
const toBeCreated = dir + expected
const target = "default"
var out bytes.Buffer
e := &task.Executor{
Dir: dir,
Stdout: &out,
Stderr: &out,
}
// Ensure that the directory to be created doesn't actually exist.
_ = os.RemoveAll(toBeCreated)
if _, err := os.Stat(toBeCreated); err == nil {
t.Errorf("Directory should not exist: %v", err)
}
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: target}))
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
assert.Equal(t, expected, got, "Mismatch in the working directory")
// Clean-up after ourselves only if no error.
_ = os.RemoveAll(toBeCreated)
}
func TestDynamicVariablesShouldRunOnTheTaskDir(t *testing.T) { func TestDynamicVariablesShouldRunOnTheTaskDir(t *testing.T) {
tt := fileContentTest{ tt := fileContentTest{
Dir: "testdata/dir/dynamic_var", Dir: "testdata/dir/dynamic_var",
@@ -895,8 +1039,8 @@ func TestDynamicVariablesShouldRunOnTheTaskDir(t *testing.T) {
func TestDisplaysErrorOnUnsupportedVersion(t *testing.T) { func TestDisplaysErrorOnUnsupportedVersion(t *testing.T) {
e := task.Executor{ e := task.Executor{
Dir: "testdata/version/v1", Dir: "testdata/version/v1",
Stdout: ioutil.Discard, Stdout: io.Discard,
Stderr: ioutil.Discard, Stderr: io.Discard,
} }
err := e.Setup() err := e.Setup()
assert.Error(t, err) assert.Error(t, err)
@@ -1026,6 +1170,32 @@ func TestRunOnlyRunsJobsHashOnce(t *testing.T) {
tt.Run(t) tt.Run(t)
} }
func TestDeferredCmds(t *testing.T) {
const dir = "testdata/deferred"
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
expectedOutputOrder := strings.TrimSpace(`
task: [task-2] echo 'cmd ran'
cmd ran
task: [task-2] exit 1
task: [task-2] echo 'failing' && exit 2
failing
task: [task-2] echo 'echo ran'
echo ran
task: [task-1] echo 'task-1 ran successfully'
task-1 ran successfully
`)
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "task-2"}))
fmt.Println(buff.String())
assert.Contains(t, buff.String(), expectedOutputOrder)
}
func TestIgnoreNilElements(t *testing.T) { func TestIgnoreNilElements(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -1052,3 +1222,57 @@ func TestIgnoreNilElements(t *testing.T) {
}) })
} }
} }
func TestOutputGroup(t *testing.T) {
const dir = "testdata/output_group"
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
expectedOutputOrder := strings.TrimSpace(`
task: [hello] echo 'Hello!'
::group::hello
Hello!
::endgroup::
task: [bye] echo 'Bye!'
::group::bye
Bye!
::endgroup::
`)
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "bye"}))
t.Log(buff.String())
assert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder)
}
func TestIncludedVars(t *testing.T) {
const dir = "testdata/include_with_vars"
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
expectedOutputOrder := strings.TrimSpace(`
task: [included1:task1] echo "VAR_1 is included1-var1"
VAR_1 is included1-var1
task: [included1:task1] echo "VAR_2 is included-default-var2"
VAR_2 is included-default-var2
task: [included2:task1] echo "VAR_1 is included2-var1"
VAR_1 is included2-var1
task: [included2:task1] echo "VAR_2 is included-default-var2"
VAR_2 is included-default-var2
task: [included3:task1] echo "VAR_1 is included-default-var1"
VAR_1 is included-default-var1
task: [included3:task1] echo "VAR_2 is included-default-var2"
VAR_2 is included-default-var2
`)
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "task1"}))
t.Log(buff.String())
assert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder)
}

View File

@@ -1,9 +1,5 @@
package taskfile package taskfile
import (
"strings"
)
// Cmd is a task command // Cmd is a task command
type Cmd struct { type Cmd struct {
Cmd string Cmd string
@@ -11,6 +7,7 @@ type Cmd struct {
Task string Task string
Vars *Vars Vars *Vars
IgnoreError bool IgnoreError bool
Defer bool
} }
// Dep is a task dependency // Dep is a task dependency
@@ -23,11 +20,7 @@ type Dep struct {
func (c *Cmd) UnmarshalYAML(unmarshal func(interface{}) error) error { func (c *Cmd) UnmarshalYAML(unmarshal func(interface{}) error) error {
var cmd string var cmd string
if err := unmarshal(&cmd); err == nil { if err := unmarshal(&cmd); err == nil {
if strings.HasPrefix(cmd, "^") { c.Cmd = cmd
c.Task = strings.TrimPrefix(cmd, "^")
} else {
c.Cmd = cmd
}
return nil return nil
} }
var cmdStruct struct { var cmdStruct struct {
@@ -41,6 +34,23 @@ func (c *Cmd) UnmarshalYAML(unmarshal func(interface{}) error) error {
c.IgnoreError = cmdStruct.IgnoreError c.IgnoreError = cmdStruct.IgnoreError
return nil return nil
} }
var deferredCmd struct {
Defer string
}
if err := unmarshal(&deferredCmd); err == nil && deferredCmd.Defer != "" {
c.Defer = true
c.Cmd = deferredCmd.Defer
return nil
}
var deferredCall struct {
Defer Call
}
if err := unmarshal(&deferredCall); err == nil && deferredCall.Defer.Task != "" {
c.Defer = true
c.Task = deferredCall.Defer.Task
c.Vars = deferredCall.Defer.Vars
return nil
}
var taskCall struct { var taskCall struct {
Task string Task string
Vars *Vars Vars *Vars

View File

@@ -12,6 +12,7 @@ type IncludedTaskfile struct {
Dir string Dir string
Optional bool Optional bool
AdvancedImport bool AdvancedImport bool
Vars *Vars
} }
// IncludedTaskfiles represents information about included tasksfiles // IncludedTaskfiles represents information about included tasksfiles
@@ -94,6 +95,7 @@ func (it *IncludedTaskfile) UnmarshalYAML(unmarshal func(interface{}) error) err
Taskfile string Taskfile string
Dir string Dir string
Optional bool Optional bool
Vars *Vars
} }
if err := unmarshal(&includedTaskfile); err != nil { if err := unmarshal(&includedTaskfile); err != nil {
return err return err
@@ -102,5 +104,6 @@ func (it *IncludedTaskfile) UnmarshalYAML(unmarshal func(interface{}) error) err
it.Dir = includedTaskfile.Dir it.Dir = includedTaskfile.Dir
it.Optional = includedTaskfile.Optional it.Optional = includedTaskfile.Optional
it.AdvancedImport = true it.AdvancedImport = true
it.Vars = includedTaskfile.Vars
return nil return nil
} }

View File

@@ -17,7 +17,7 @@ func Merge(t1, t2 *Taskfile, namespaces ...string) error {
if t2.Expansions != 0 && t2.Expansions != 2 { if t2.Expansions != 0 && t2.Expansions != 2 {
t1.Expansions = t2.Expansions t1.Expansions = t2.Expansions
} }
if t2.Output != "" { if t2.Output.IsSet() {
t1.Output = t2.Output t1.Output = t2.Output
} }

55
taskfile/output.go Normal file
View File

@@ -0,0 +1,55 @@
package taskfile
import (
"fmt"
)
// Output of the Task output
type Output struct {
// Name of the Output.
Name string `yaml:"-"`
// Group specific style
Group OutputGroup
}
// IsSet returns true if and only if a custom output style is set.
func (s *Output) IsSet() bool {
return s.Name != ""
}
// UnmarshalYAML implements yaml.Unmarshaler
// It accepts a scalar node representing the Output.Name or a mapping node representing the OutputGroup.
func (s *Output) UnmarshalYAML(unmarshal func(interface{}) error) error {
var name string
if err := unmarshal(&name); err == nil {
s.Name = name
return nil
}
var tmp struct {
Group *OutputGroup
}
if err := unmarshal(&tmp); err != nil {
return fmt.Errorf("task: output style must be a string or mapping with a \"group\" key: %w", err)
}
if tmp.Group == nil {
return fmt.Errorf("task: output style must have the \"group\" key when in mapping form")
}
*s = Output{
Name: "group",
Group: *tmp.Group,
}
return nil
}
// OutputGroup is the style options specific to the Group style.
type OutputGroup struct {
Begin, End string
}
// IsSet returns true if and only if a custom output style is set.
func (g *OutputGroup) IsSet() bool {
if g == nil {
return false
}
return g.Begin != "" || g.End != ""
}

View File

@@ -15,18 +15,41 @@ import (
) )
var ( var (
// ErrIncludedTaskfilesCantHaveIncludes is returned when a included Taskfile contains includes
ErrIncludedTaskfilesCantHaveIncludes = errors.New("task: Included Taskfiles can't have includes. Please, move the include to the main Taskfile")
// ErrIncludedTaskfilesCantHaveDotenvs is returned when a included Taskfile contains dotenvs // ErrIncludedTaskfilesCantHaveDotenvs is returned when a included Taskfile contains dotenvs
ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles can't have dotenv declarations. Please, move the dotenv declaration to the main Taskfile") ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles can't have dotenv declarations. Please, move the dotenv declaration to the main Taskfile")
defaultTaskfiles = []string{
"Taskfile.yml",
"Taskfile.yaml",
"Taskfile.dist.yml",
"Taskfile.dist.yaml",
}
) )
type ReaderNode struct {
Dir string
Entrypoint string
Optional bool
Parent *ReaderNode
}
// Taskfile reads a Taskfile for a given directory // Taskfile reads a Taskfile for a given directory
func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) { // Uses current dir when dir is left empty. Uses Taskfile.yml
path := filepath.Join(dir, entrypoint) // or Taskfile.yaml when entrypoint is left empty
if _, err := os.Stat(path); err != nil { func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
return nil, fmt.Errorf(`task: No Taskfile found on "%s". Use "task --init" to create a new one`, path) if readerNode.Dir == "" {
d, err := os.Getwd()
if err != nil {
return nil, err
}
readerNode.Dir = d
} }
path, err := exists(filepath.Join(readerNode.Dir, readerNode.Entrypoint))
if err != nil {
return nil, err
}
readerNode.Entrypoint = filepath.Base(path)
t, err := readTaskfile(path) t, err := readTaskfile(path)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -45,6 +68,7 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
Dir: tr.Replace(includedTask.Dir), Dir: tr.Replace(includedTask.Dir),
Optional: includedTask.Optional, Optional: includedTask.Optional,
AdvancedImport: includedTask.AdvancedImport, AdvancedImport: includedTask.AdvancedImport,
Vars: includedTask.Vars,
} }
if err := tr.Err(); err != nil { if err := tr.Err(); err != nil {
return err return err
@@ -56,25 +80,33 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
return err return err
} }
if !filepath.IsAbs(path) { if !filepath.IsAbs(path) {
path = filepath.Join(dir, path) path = filepath.Join(readerNode.Dir, path)
} }
path, err = exists(path)
info, err := os.Stat(path)
if err != nil { if err != nil {
if includedTask.Optional { if includedTask.Optional {
return nil return nil
} }
return err return err
} }
if info.IsDir() {
path = filepath.Join(path, "Taskfile.yml") includeReaderNode := &ReaderNode{
Dir: filepath.Dir(path),
Entrypoint: filepath.Base(path),
Parent: readerNode,
Optional: includedTask.Optional,
} }
includedTaskfile, err := readTaskfile(path)
if err != nil { if err := checkCircularIncludes(includeReaderNode); err != nil {
return err return err
} }
if includedTaskfile.Includes.Len() > 0 {
return ErrIncludedTaskfilesCantHaveIncludes includedTaskfile, err := Taskfile(includeReaderNode)
if err != nil {
if includedTask.Optional {
return nil
}
return err
} }
if v >= 3.0 && len(includedTaskfile.Dotenv) > 0 { if v >= 3.0 && len(includedTaskfile.Dotenv) > 0 {
@@ -84,12 +116,12 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
if includedTask.AdvancedImport { if includedTask.AdvancedImport {
for k, v := range includedTaskfile.Vars.Mapping { for k, v := range includedTaskfile.Vars.Mapping {
o := v o := v
o.Dir = filepath.Join(dir, includedTask.Dir) o.Dir = filepath.Join(readerNode.Dir, includedTask.Dir)
includedTaskfile.Vars.Mapping[k] = o includedTaskfile.Vars.Mapping[k] = o
} }
for k, v := range includedTaskfile.Env.Mapping { for k, v := range includedTaskfile.Env.Mapping {
o := v o := v
o.Dir = filepath.Join(dir, includedTask.Dir) o.Dir = filepath.Join(readerNode.Dir, includedTask.Dir)
includedTaskfile.Env.Mapping[k] = o includedTaskfile.Env.Mapping[k] = o
} }
@@ -97,6 +129,8 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
if !filepath.IsAbs(task.Dir) { if !filepath.IsAbs(task.Dir) {
task.Dir = filepath.Join(includedTask.Dir, task.Dir) task.Dir = filepath.Join(includedTask.Dir, task.Dir)
} }
task.IncludeVars = includedTask.Vars
task.IncludedTaskfileVars = includedTaskfile.Vars
} }
} }
@@ -110,7 +144,7 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
} }
if v < 3.0 { if v < 3.0 {
path = filepath.Join(dir, fmt.Sprintf("Taskfile_%s.yml", runtime.GOOS)) path = filepath.Join(readerNode.Dir, fmt.Sprintf("Taskfile_%s.yml", runtime.GOOS))
if _, err = os.Stat(path); err == nil { if _, err = os.Stat(path); err == nil {
osTaskfile, err := readTaskfile(path) osTaskfile, err := readTaskfile(path)
if err != nil { if err != nil {
@@ -141,3 +175,44 @@ func readTaskfile(file string) (*taskfile.Taskfile, error) {
var t taskfile.Taskfile var t taskfile.Taskfile
return &t, yaml.NewDecoder(f).Decode(&t) return &t, yaml.NewDecoder(f).Decode(&t)
} }
func exists(path string) (string, error) {
fi, err := os.Stat(path)
if err != nil {
return "", err
}
if fi.Mode().IsRegular() {
return path, nil
}
for _, n := range defaultTaskfiles {
fpath := filepath.Join(path, n)
if _, err := os.Stat(fpath); err == nil {
return fpath, nil
}
}
return "", fmt.Errorf(`task: No Taskfile found in "%s". Use "task --init" to create a new one`, path)
}
func checkCircularIncludes(node *ReaderNode) error {
if node == nil {
return errors.New("task: failed to check for include cycle: node was nil")
}
if node.Parent == nil {
return errors.New("task: failed to check for include cycle: node.Parent was nil")
}
var curNode = node
var basePath = filepath.Join(node.Dir, node.Entrypoint)
for curNode.Parent != nil {
curNode = curNode.Parent
curPath := filepath.Join(curNode.Dir, curNode.Entrypoint)
if curPath == basePath {
return fmt.Errorf("task: include cycle detected between %s <--> %s",
curPath,
filepath.Join(node.Parent.Dir, node.Parent.Entrypoint),
)
}
}
return nil
}

View File

@@ -5,25 +5,27 @@ type Tasks map[string]*Task
// Task represents a task // Task represents a task
type Task struct { type Task struct {
Task string Task string
Cmds []*Cmd Cmds []*Cmd
Deps []*Dep Deps []*Dep
Label string Label string
Desc string Desc string
Summary string Summary string
Sources []string Sources []string
Generates []string Generates []string
Status []string Status []string
Preconditions []*Precondition Preconditions []*Precondition
Dir string Dir string
Vars *Vars Vars *Vars
Env *Vars Env *Vars
Silent bool Silent bool
Interactive bool Interactive bool
Method string Method string
Prefix string Prefix string
IgnoreError bool IgnoreError bool
Run string Run string
IncludeVars *Vars
IncludedTaskfileVars *Vars
} }
func (t *Task) Name() string { func (t *Task) Name() string {

View File

@@ -9,7 +9,7 @@ import (
type Taskfile struct { type Taskfile struct {
Version string Version string
Expansions int Expansions int
Output string Output Output
Method string Method string
Includes *IncludedTaskfiles Includes *IncludedTaskfiles
Vars *Vars Vars *Vars
@@ -25,7 +25,7 @@ func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error {
var taskfile struct { var taskfile struct {
Version string Version string
Expansions int Expansions int
Output string Output Output
Method string Method string
Includes *IncludedTaskfiles Includes *IncludedTaskfiles
Vars *Vars Vars *Vars

View File

@@ -19,6 +19,8 @@ vars:
PARAM1: VALUE1 PARAM1: VALUE1
PARAM2: VALUE2 PARAM2: VALUE2
` `
yamlDeferredCall = `defer: { task: some_task, vars: { PARAM1: "var" } }`
yamlDeferredCmd = `defer: echo 'test'`
) )
tests := []struct { tests := []struct {
content string content string
@@ -41,6 +43,21 @@ vars:
}, },
}}, }},
}, },
{
yamlDeferredCmd,
&taskfile.Cmd{},
&taskfile.Cmd{Cmd: "echo 'test'", Defer: true},
},
{
yamlDeferredCall,
&taskfile.Cmd{},
&taskfile.Cmd{Task: "some_task", Vars: &taskfile.Vars{
Keys: []string{"PARAM1"},
Mapping: map[string]taskfile.Var{
"PARAM1": taskfile.Var{Static: "var"},
},
}, Defer: true},
},
{ {
yamlDep, yamlDep,
&taskfile.Dep{}, &taskfile.Dep{},

View File

@@ -2,7 +2,6 @@ package taskfile
import ( import (
"errors" "errors"
"strings"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -108,11 +107,7 @@ type Var struct {
func (v *Var) UnmarshalYAML(unmarshal func(interface{}) error) error { func (v *Var) UnmarshalYAML(unmarshal func(interface{}) error) error {
var str string var str string
if err := unmarshal(&str); err == nil { if err := unmarshal(&str); err == nil {
if strings.HasPrefix(str, "$") { v.Static = str
v.Sh = strings.TrimPrefix(str, "$")
} else {
v.Static = str
}
return nil return nil
} }

12
testdata/deferred/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
version: "3"
tasks:
task-1:
- echo 'task-1 ran {{.PARAM}}'
task-2:
- defer: { task: "task-1", vars: { PARAM: "successfully" } }
- defer: echo 'echo ran'
- defer: echo 'failing' && exit 2
- echo 'cmd ran'
- exit 1

View File

@@ -0,0 +1,10 @@
version: '3'
tasks:
default:
dir: created
cmds:
- echo {{.TASK_DIR}}
vars:
TASK_DIR:
sh: echo $(pwd)

1
testdata/file_names/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.txt

View File

@@ -0,0 +1,4 @@
version: '3'
tasks:
default: echo "hello" > output.txt

View File

@@ -0,0 +1,4 @@
version: '3'
tasks:
default: echo "hello" > output.txt

View File

@@ -0,0 +1,4 @@
version: '3'
tasks:
default: echo "hello" > output.txt

View File

@@ -0,0 +1,4 @@
version: '3'
tasks:
default: echo "hello" > output.txt

20
testdata/include_with_vars/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
version: "3"
includes:
included1:
taskfile: include/Taskfile.include.yml
vars:
VAR_1: included1-var1
included2:
taskfile: include/Taskfile.include.yml
vars:
VAR_1: included2-var1
included3:
taskfile: include/Taskfile.include.yml
tasks:
task1:
cmds:
- task: included1:task1
- task: included2:task1
- task: included3:task1

View File

@@ -0,0 +1,11 @@
version: "3"
vars:
VAR_1: '{{.VAR_1 | default "included-default-var1"}}'
VAR_2: '{{.VAR_2 | default "included-default-var2"}}'
tasks:
task1:
cmds:
- echo "VAR_1 is {{.VAR_1}}"
- echo "VAR_2 is {{.VAR_2}}"

12
testdata/includes_cycle/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
version: '3'
includes:
'one': ./one/Taskfile.yml
tasks:
default:
cmds:
- echo "called_dep" > called_dep.txt
level1:
cmds:
- echo "hello level 1"

View File

@@ -0,0 +1,9 @@
version: '3'
includes:
'two': ./two/Taskfile.yml
tasks:
level2:
cmds:
- echo "hello level 2"

View File

@@ -0,0 +1,9 @@
version: '3'
includes:
bad: "../../Taskfile.yml"
tasks:
level3:
cmds:
- echo "hello level 3"

View File

@@ -0,0 +1,11 @@
version: '3'
includes:
'one': ./one/
tasks:
default:
cmds:
- task: one:default
- task: one:two:default
- task: one:two:three:default

View File

@@ -0,0 +1 @@
one

View File

@@ -0,0 +1 @@
three

View File

@@ -0,0 +1 @@
two

View File

@@ -0,0 +1,7 @@
version: '3'
includes:
'two': ./two/
tasks:
default: echo one > called_one.txt

View File

@@ -0,0 +1,7 @@
version: '3'
includes:
'three': ./three/Taskfile.yml
tasks:
default: echo two > called_two.txt

View File

@@ -0,0 +1,4 @@
version: '3'
tasks:
default: echo three > called_three.txt

1
testdata/includes_yaml/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.txt

16
testdata/includes_yaml/Custom.ext vendored Normal file
View File

@@ -0,0 +1,16 @@
version: '3'
includes:
included: ./included
custom: ./included/custom.yaml
tasks:
default:
cmds:
- task: gen
- task: included:gen
- task: custom:gen
gen:
cmds:
- echo main > main.txt

View File

@@ -0,0 +1,6 @@
version: '3'
tasks:
gen:
cmds:
- echo included_with_yaml_extension > included_with_yaml_extension.txt

View File

@@ -0,0 +1,6 @@
version: '3'
tasks:
gen:
cmds:
- echo included_with_custom_file > included_with_custom_file.txt

12
testdata/list_mixed_desc/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
version: '3'
tasks:
foo:
label: "foobar"
desc: "foo has desc and label"
voo:
label: "voo has no desc"
doo:
label: "doo has desc, no label"

16
testdata/output_group/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
version: '3'
output:
group:
begin: '::group::{{ .TASK }}'
end: '::endgroup::'
tasks:
hello:
cmds:
- echo 'Hello!'
bye:
deps:
- hello
cmds:
- echo 'Bye!'

View File

@@ -35,7 +35,8 @@ tasks:
- echo '{{.TASK}}' > task_name.txt - echo '{{.TASK}}' > task_name.txt
vars: vars:
FOO: foo FOO: foo
BAR: $echo bar BAR:
sh: echo bar
BAZ: BAZ:
sh: echo baz sh: echo baz
TMPL_FOO: "{{.FOO}}" TMPL_FOO: "{{.FOO}}"

View File

@@ -1,5 +1,6 @@
FOO2: foo2 FOO2: foo2
BAR2: $echo bar2 BAR2:
sh: echo bar2
BAZ2: BAZ2:
sh: echo baz2 sh: echo baz2
TMPL2_FOO: "{{.FOO}}" TMPL2_FOO: "{{.FOO}}"

View File

@@ -46,21 +46,23 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
r := templater.Templater{Vars: vars, RemoveNoValue: v >= 3.0} r := templater.Templater{Vars: vars, RemoveNoValue: v >= 3.0}
new := taskfile.Task{ new := taskfile.Task{
Task: origTask.Task, Task: origTask.Task,
Label: r.Replace(origTask.Label), Label: r.Replace(origTask.Label),
Desc: r.Replace(origTask.Desc), Desc: r.Replace(origTask.Desc),
Summary: r.Replace(origTask.Summary), Summary: r.Replace(origTask.Summary),
Sources: r.ReplaceSlice(origTask.Sources), Sources: r.ReplaceSlice(origTask.Sources),
Generates: r.ReplaceSlice(origTask.Generates), Generates: r.ReplaceSlice(origTask.Generates),
Dir: r.Replace(origTask.Dir), Dir: r.Replace(origTask.Dir),
Vars: nil, Vars: nil,
Env: nil, Env: nil,
Silent: origTask.Silent, Silent: origTask.Silent,
Interactive: origTask.Interactive, Interactive: origTask.Interactive,
Method: r.Replace(origTask.Method), Method: r.Replace(origTask.Method),
Prefix: r.Replace(origTask.Prefix), Prefix: r.Replace(origTask.Prefix),
IgnoreError: origTask.IgnoreError, IgnoreError: origTask.IgnoreError,
Run: r.Replace(origTask.Run), Run: r.Replace(origTask.Run),
IncludeVars: origTask.IncludeVars,
IncludedTaskfileVars: origTask.IncludedTaskfileVars,
} }
new.Dir, err = execext.Expand(new.Dir) new.Dir, err = execext.Expand(new.Dir)
if err != nil { if err != nil {
@@ -102,6 +104,7 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
Cmd: r.Replace(cmd.Cmd), Cmd: r.Replace(cmd.Cmd),
Vars: r.ReplaceVars(cmd.Vars), Vars: r.ReplaceVars(cmd.Vars),
IgnoreError: cmd.IgnoreError, IgnoreError: cmd.IgnoreError,
Defer: cmd.Defer,
}) })
} }
} }