Compare commits

...

183 Commits

Author SHA1 Message Date
Andrey Nering
5738436d55 v2.4.0 2019-02-21 21:28:10 -03:00
Andrey Nering
5e49b38c33 Mitigate execext.Expand problems on Windows
Closes #170

Co-authored-by: mikynov <micnov@gmail.com>
2019-02-21 21:22:40 -03:00
Andrey Nering
0c94adaff9 Update CHANGELOG.md 2019-02-21 21:06:46 -03:00
Andrey Nering
f8a6c5d06c Fix execext.Expand for file names with spaces
Fixes #176
2019-02-21 20:59:17 -03:00
Andrey Nering
21e66c7c02 Docs: Update theme color 2019-02-09 10:48:48 -02:00
Andrey Nering
902f0d3ac4 Don't persist new checksum on the disk if dry mode is enabled
Fixes #166
2019-02-09 10:44:35 -02:00
Andrey Nering
713ecd35f6 Pass context as an argument 2019-02-09 10:16:13 -02:00
Andrey Nering
27b35157cd Indentation fix 2019-02-09 10:15:38 -02:00
Andrey Nering
f8fb639870 Update documentation and changelog to mention the new --output flag
Ref #173
2019-02-09 10:01:41 -02:00
Andrey Nering
14f41ae619 Merge pull request #173 from kjdev/master
Add execute output style options
2019-02-07 19:51:49 -02:00
kj
a026d72924 Add execute output style options 2019-02-05 15:42:57 +09:00
Andrey Nering
2cb070f5b3 Merge pull request #172 from go-task/allow-calling-root-task-from-included
Allow calling a task of the root Taskfile from within an included Taskfile
2019-02-02 21:26:22 -02:00
Andrey Nering
1dec956e99 Allow calling a task of the root Taskfile from within an included Taskfile
Fixes #161
2019-02-02 21:22:08 -02:00
Tim Foerster
310394aa60 task: Fix merge behavior 2019-02-02 17:19:20 -02:00
Andrey Nering
468ff18243 Merge pull request #164 from saromanov/fix-error-message
taskfile: return defined error when taskfile.yml is not found
2019-01-22 22:16:45 -02:00
Sergey
44a63580f0 taskfile: missing task: prefix to the error message 2019-01-23 02:01:53 +05:00
Andrey Nering
4ac1fa43aa Merge pull request #165 from 0xflotus/patch-1
fixed docs
2019-01-21 23:00:01 -02:00
0xflotus
6f992a3cf7 fixed suppressed 2019-01-21 13:36:04 +01:00
0xflotus
fd4ce656d5 fixed Snapcraft 2019-01-21 13:33:19 +01:00
Sergey
9ed2dca427 taskfile: return defined error when taskfile.yml is not found 2019-01-21 14:56:14 +05:00
Andrey Nering
dfb804fe3f Update vendor/ 2019-01-19 19:25:49 -02:00
Andrey Nering
4f2a84b426 Upgrade some libs 2019-01-19 19:24:49 -02:00
Andrey Nering
14a127b6b3 Pin mattn/go-zglob version 2019-01-19 19:08:30 -02:00
Andrey Nering
06000533fb Pin mvdan/sh version 2019-01-19 19:07:11 -02:00
Andrey Nering
7722aba403 v2.3.0 2019-01-02 13:44:27 -02:00
Andrey Nering
4817d8c67f Move documentation tasks to its own Taskfile 2019-01-02 13:42:06 -02:00
Andrey Nering
9a062d90d1 Merge pull request #159 from go-task/global-environment-variables-#138
Add ability to globally set environment variables
2019-01-02 13:29:14 -02:00
Andrey Nering
959eb45373 Docs: Fix some typos 2019-01-02 13:25:58 -02:00
Andrey Nering
a42f2af9eb Documentation and changelog for global environment variables 2019-01-02 13:21:21 -02:00
Andrey Nering
4ddad68212 Merge global environment variables when merging tasks 2019-01-02 13:20:12 -02:00
Andrey Nering
aac6c5a1c7 Add hability to globally set environment variables
Closes #138
2019-01-02 12:06:12 -02:00
Andrey Nering
5572e31fd4 Merge pull request #157 from frederikhors/patch-2
Misprint
2018-12-27 10:49:24 -02:00
frederikhors
233b8bf81a Misprint 2018-12-27 01:11:36 +01:00
Andrey Nering
2ae3810f80 Merge pull request #156 from frederikhors/patch-1
Misprint
2018-12-26 10:38:20 -02:00
frederikhors
736165876c Misprint 2018-12-26 12:04:00 +01:00
Andrey Nering
61b3fca9a3 Merge pull request #154 from go-task/upgrade-mvdan-sh
Upgrade mvdan/sh
2018-12-24 15:30:45 -02:00
Andrey Nering
469863b7b3 go mod vendor 2018-12-24 15:26:33 -02:00
Andrey Nering
5238bc55fd Upgrade mvdan.cc/sh
Fixes #153
2018-12-24 15:25:19 -02:00
Andrey Nering
57a01aa6ff Fix failing test
There was some breaking changes described at
https://github.com/mvdan/sh/issues/335#issuecomment-447605295
2018-12-24 15:19:53 -02:00
Andrey Nering
9361dbc39e Merge branch 'master' into upgrade-mvdan-sh 2018-12-24 15:08:29 -02:00
Andrey Nering
11d257cb26 Changelog: Mention Scoop 2018-12-24 15:03:30 -02:00
Andrey Nering
a928ab75e3 Docs: Give more visibility to sponsors and contributors 2018-12-24 15:00:14 -02:00
Andrey Nering
55a240c82e Improve documentation on Scoop
Updates #152
2018-12-24 14:42:49 -02:00
Andrey Nering
f8aedf438b Merge pull request #152 from lambdalisue/docs-scoop
Add Scoop
2018-12-24 14:32:53 -02:00
Andrey Nering
9f1bb9a42e Add CHANGELOG.md
This was generated in a semi-automated way using the existing GitHub releases,
fetch thought the GitHub API.
2018-12-15 16:14:39 -02:00
Andrey Nering
0ed7274610 go mod vendor 2018-12-15 15:44:17 -02:00
Andrey Nering
df032b09a7 Upgrade mvdan/sh 2018-12-15 15:43:40 -02:00
Andrey Nering
7dba742e4c v2.2.1 2018-12-09 15:58:59 -02:00
Andrey Nering
83dbe78965 Documentation: Mention that including Taskfiles is still experimental 2018-12-09 15:57:13 -02:00
Andrey Nering
95b75c5330 Fix issue on running dependencies or tasks on included Taskfiles
Fixes #151
2018-12-09 15:54:58 -02:00
lambdalisue
780bd08490 Add Scoop
Task is now available on Scoop for Windows users.
https://github.com/lukesampson/scoop-extras/pull/1485
2018-12-06 22:21:03 +09:00
Andrey Nering
a9b1f38a7c Fix nil errors when merging Taskfiles
Closes #150
2018-12-02 14:17:32 -02:00
Andrey Nering
4ed4ad9852 .gitignore: Ignore the .task directory 2018-12-02 14:04:35 -02:00
Andrey Nering
81f172315c Merge pull request #143 from go-task/switch-to-go-modules
Switch to Go modules
2018-11-10 20:15:16 -02:00
Andrey Nering
54666221a4 Update install documentation after switch to Go Modules 2018-11-10 20:10:44 -02:00
Andrey Nering
5327d702f8 go mod vendor 2018-11-04 21:37:53 -02:00
Andrey Nering
a701ea3007 go mod tidy 2018-11-04 21:36:55 -02:00
Andrey Nering
f0fb7d9661 go.mod: Pin some versions 2018-11-04 21:31:29 -02:00
Andrey Nering
3cbc89769d Re-write import paths to v2 2018-11-04 21:23:35 -02:00
Andrey Nering
cb95200be3 go mod vendor 2018-10-27 17:02:24 -03:00
Andrey Nering
a52f6c0acf Switch to Go modules
Let's keep the vendor directory for a while to not break Task for
users that install it using `go get`.

Closes #119
2018-10-27 17:01:14 -03:00
Andrey Nering
77f9e3dd41 Update link on the Taskfile generated by 2018-10-27 15:30:48 -03:00
Andrey Nering
259d3e2df1 Fix more internal documentation links when viewing on GitHub
Applies fixes done on #142 on more links
2018-10-27 15:25:39 -03:00
Andrey Nering
6eee5421b0 Merge pull request #142 from aseembajaj/patch-1
Fix broken URL to installation doc
2018-10-27 15:19:56 -03:00
Aseem Bajaj
8004e9c943 Fix broken URL to installation doc
The links to installation doc were broken, perhaps it was a typo or maybe installation document was relocated from "installation" directory to "installation.md". In either case, making this change fixes the problem.
2018-10-26 16:06:33 -07:00
Andrey Nering
2ab8511f45 v2.2.0 2018-10-25 20:19:28 -03:00
Andrey Nering
7514ff53c9 Documentation: SeedAndDew script tag 2018-10-21 09:55:14 -03:00
Andrey Nering
309cfb1499 Docs: Google Search Console Verification Tag 2018-10-13 18:42:07 -03:00
Andrey Nering
a567f7ed20 Merge pull request #134 from go-task/feature/include
Support including Taskfiles
2018-10-13 18:31:19 -03:00
Andrey Nering
5720936247 Fix CI 2018-10-13 18:29:23 -03:00
Andrey Nering
5d9de14ca3 Increment the current Taskfile version to 2.2 2018-10-13 18:25:40 -03:00
Andrey Nering
f519f56078 Add documentation for including other Taskfiles 2018-10-13 18:14:42 -03:00
Andrey Nering
5eb1a1f7f5 Fixes to Taskfile including:
- Disallow recursive Taskfile including (i.e. included Taskfile including other Taskfiles)
- Write test for included a file instead of a directory
2018-10-13 17:52:09 -03:00
Andrey Nering
5a28560177 Write first test for including a Taskfile 2018-10-13 16:56:51 -03:00
Andrey Nering
db280adf55 Merge branch 'master' into feature/include 2018-10-13 16:40:32 -03:00
Andrey Nering
b77fcd6c8a Documentation improvements 2018-10-07 18:34:03 -03:00
Andrey Nering
b5b2649283 Fix broken --status flag
Fixes #139
2018-10-06 17:55:23 -03:00
Andrey Nering
318f9b216d Fix typo 2018-09-24 21:52:14 -03:00
Andrey Nering
61247a0b2a Cosmetic changes 2018-09-24 21:42:33 -03:00
Andrey Nering
849a418273 Create CNAME 2018-09-24 21:21:23 -03:00
Andrey Nering
2e63a62e08 Delete CNAME 2018-09-24 21:20:58 -03:00
Andrey Nering
6ccf1f2a3c Documentation: GA 2018-09-23 15:31:00 -03:00
Andrey Nering
e298256b82 Use HTTP for now
GitHub Pages can take up to 1 hour to enable HTTPS on the site
2018-09-23 15:16:53 -03:00
Andrey Nering
787e5b2e29 Create CNAME 2018-09-23 15:08:03 -03:00
Andrey Nering
4aa1e8b093 Documentation improvements 2018-09-23 15:06:43 -03:00
Andrey Nering
9ee224c36b Documentation site using docsify 2018-09-22 18:44:24 -03:00
Andrey Nering
08263c0597 Fix wrong error message beingg print when the file has a syntax error
Fixes #137
2018-09-22 17:29:18 -03:00
Andrey Nering
347fe87229 v2.1.1 2018-09-16 22:17:36 -03:00
Andrey Nering
b65a0a3a8d Fix signal handling when --watch flag is given
Closes #132
2018-09-16 21:59:00 -03:00
Andrey Nering
9a5a1e2253 Start support to including Taskfiles 2018-09-09 22:29:29 -03:00
Andrey Nering
687b4ec837 Fix error when using checksum method and no file exists for a source glob
Closes #131
2018-09-01 11:23:38 -03:00
Andrey Nering
8bdf5c554d Fixes after updating mvdan.cc/sh 2018-09-01 11:02:23 -03:00
Andrey Nering
f4a18e531f Update dependencies 2018-09-01 11:00:49 -03:00
Andrey Nering
df951a0c7c Fix suggestion to use task --init not being shown anymore 2018-08-19 16:23:32 -03:00
Andrey Nering
a6cac2691b Delete unused errors 2018-08-19 16:15:19 -03:00
Andrey Nering
a9f5179066 README: Fix title case 2018-08-19 16:12:53 -03:00
Andrey Nering
687e2699cf Improvements on documentation 2018-08-19 15:47:21 -03:00
Andrey Nering
491da0ceb9 "ignore_error" is only available on version >= 2.1 2018-08-19 15:46:07 -03:00
Andrey Nering
1bac40bc58 README: Update alternatives 2018-08-18 17:50:40 -03:00
Andrey Nering
fb9061480d Merge branch 'Eun-master' 2018-08-05 13:02:22 -03:00
Andrey Nering
a04cf100b4 Improve README a bit 2018-08-05 13:01:30 -03:00
Andrey Nering
76253bf516 Reduce code duplication on test 2018-08-05 12:56:55 -03:00
Andrey Nering
feaf70922d Allow ignore_error at task level 2018-08-05 12:53:42 -03:00
Andrey Nering
c70343a5bc Handle ignore_error one level up on the code 2018-08-05 12:40:11 -03:00
Andrey Nering
550c116aea Merge branch 'master' into master 2018-08-05 11:32:07 -03:00
Andrey Nering
a5f31a4280 Dry run small code style changes 2018-08-05 11:28:02 -03:00
Andrey Nering
27fc4c4ca8 Merge pull request #126 from bebbs/125-dry-run
Implemented dry run mode
2018-08-05 11:21:52 -03:00
Tobias Salzmann
90a5f17f58 Merge branch 'master' into master 2018-08-01 10:47:25 +02:00
Tobias Salzmann
108cb91d95 IgnoreError
* Document ignore_error
* ignore_error only for commands
2018-08-01 10:44:53 +02:00
Josh Bebbington
00a0755ff3 Added test for executor.DryRun 2018-07-31 23:17:21 +01:00
Josh Bebbington
3f7e8c88eb Changed --dry-run flag to --dry 2018-07-29 22:03:22 +01:00
Josh Bebbington
1c7ca94d49 Implemented dry run mode
Added a --dry-run flag that compiles and steps through each task, but does not execute them. The commands that would have been run are printed. See #125.
2018-07-29 00:39:25 +01:00
Andrey Nering
31273cd6ff Update README 2018-07-22 18:15:11 -03:00
Andrey Nering
14e39dd745 Update dependencies 2018-07-22 18:05:13 -03:00
Andrey Nering
cc6f7b6088 Manually implement merging of Taskfiles and remove dependency on github.com/imdario/mergo
I was carreful enough to check the behavior keeps the same
2018-07-22 17:54:44 -03:00
Andrey Nering
da1b0c9558 Error if Taskfile_{{GOOS}}.yml version doesn't match Taskfile.yml version
This change is not backward incompatible because merging was already not
working if versions 1 and 2 coexist in Taskfile.yml and Taskfile_{{GOOS}}.yml
2018-07-22 17:36:30 -03:00
Andrey Nering
9f294b4d10 Refactor and move logic of reading Taskfiles to its own package
Idea to making things easier to start implementing #98
2018-07-22 16:10:20 -03:00
Andrey Nering
13f60bae41 Merge pull request #124 from go-task/shell-expand
Move path expanding logic to shell.Expand
2018-07-22 15:16:21 -03:00
Andrey Nering
67105b332f Move path expanding logic to shell.Expand 2018-07-15 15:49:00 -03:00
Andrey Nering
18961e3d07 README improvements 2018-07-15 15:47:38 -03:00
Andrey Nering
fe31f5050d Taskfile.yml improvements 2018-07-15 15:42:46 -03:00
Andrey Nering
ab8549adea Update dependencies 2018-07-15 14:38:39 -03:00
Tobias Salzmann
05600601ff add ignoreError option 2018-07-10 10:44:58 +02:00
Andrey Nering
c541356289 Fix typo 2018-06-24 11:05:54 -03:00
Andrey Nering
467c4360ca v2.0.3 2018-06-24 10:52:19 -03:00
Andrey Nering
3b152a38b0 .goreleaser.yml: Use CGO_ENABLED=0 2018-06-24 10:51:04 -03:00
Andrey Nering
f4d3855528 Update deps 2018-06-24 10:40:44 -03:00
Andrey Nering
09eab770a7 Skip empty commands
Fixes #120
2018-06-24 10:29:46 -03:00
Andrey Nering
102f8ab74e Expand environment variables on "dir", "sources" and "generates"
So a path like this works: $GOPATH/src/github.com/go-task/task

Allowing of "~" was also implemented. See #74 and baac067a1a

Fixes #116
2018-06-16 14:30:40 -03:00
Andrey Nering
a830dba5da Taskfile: Disable CGO 2018-06-16 14:24:50 -03:00
Andrey Nering
3f13a50ca3 Merge pull request #115 from AlekSi/patch-1
Use latest patch releases of Go
2018-05-31 18:24:06 -03:00
Alexey Palazhchenko
7c456f2ab9 Use latest patch releases of Go 2018-05-31 15:43:52 +00:00
Andrey Nering
bae95cd6f6 Travis: Remove Go 1.8 2018-04-28 15:44:35 -03:00
Andrey Nering
bbd6e443b0 YAML: Don't use strict unmarshaling anymore
Fixes #112
2018-04-28 15:39:54 -03:00
Andrey Nering
db0d847e03 Fix compilation after updating mvdan/sh 2018-04-28 15:36:01 -03:00
Andrey Nering
0afb453fed Update dependencies 2018-04-28 15:35:33 -03:00
Andrey Nering
dbc79b4311 Merge pull request #109 from go-task/feature/parallel-task-output-#104
Add "output" option to Taskfile to control how stuff are printed to stdout/stderr
2018-04-28 15:18:05 -03:00
Andrey Nering
ac6008c33c Merge pull request #111 from sawadashota/add-zsh-completion
Add zsh completion
2018-04-28 15:07:06 -03:00
Shota Sawada
e540e752f2 Add zsh completion 2018-04-27 17:50:56 +09:00
Andrey Nering
cdbe821eb8 Write documentation for output types 2018-04-22 22:13:31 -03:00
Andrey Nering
6be994f1ca Write test for ouput types 2018-04-22 21:40:03 -03:00
Andrey Nering
a407b0a8eb First step implementing "prefixed" output option
Ref #104
2018-04-22 15:41:53 -03:00
Andrey Nering
051ff35878 Add "output" option to Taskfile to control how stuff are printed to stdout/stderr
First step for #104
2018-04-15 14:37:44 -03:00
Andrey Nering
8b3c34c308 Add "output" options to the Taskfile
Also, fix handling of Taskfile by making the version an instance of
`semver.Constraints` instead of `semver.Version`. This makes the version
works as described on TASKFILE_VERSIONS.md document, i.e. version "2" will
include "2.x" features but version "2.0" not.
2018-04-15 11:11:07 -03:00
Andrey Nering
2cb2668803 Fix Travis CI 2018-04-07 15:36:37 -03:00
Andrey Nering
4dccdb95b9 README: Add image links 2018-04-07 15:15:35 -03:00
Andrey Nering
96db9a9410 Using godownloader to generate a install script
Closes #78
2018-04-07 15:07:43 -03:00
Andrey Nering
0cd34bbebc Move variables from Taskvars to Taskfile 2018-04-07 14:42:24 -03:00
Andrey Nering
15f50c0e58 Update README.md and CONTRIBUTING.md 2018-04-07 14:33:10 -03:00
Andrey Nering
7fca9732e7 Merge pull request #105 from michael-k/patch-1
Fix some typos in readme
2018-03-18 21:35:35 -03:00
Michael
0ea8c3ed28 Fix some typos in readme 2018-03-16 06:32:14 +00:00
Andrey Nering
0af9600e92 Update the release process once again:
- Release both Homebrew and Snapcraft packages manually for now;
- Fix some artifacts file names to keep them consistent.
2018-03-11 15:58:32 -03:00
Andrey Nering
328e3725e5 Merge pull request #102 from go-task/develop
v2.0.1
2018-03-11 15:37:18 -03:00
Andrey Nering
2183e1e9f5 Improve release process and testing automatic release on new tag using Travis 2018-03-11 15:31:27 -03:00
Andrey Nering
120d0be84c Fixes panic on task --list
Fixes #99
2018-03-11 14:39:40 -03:00
Andrey Nering
5649f75a8d Merge pull request #97 from seblw/patch-1
Editorial changes in TASKFILE_VERSIONS.md
2018-03-08 08:07:31 -03:00
Sebastian Lawniczak
a209f7d6be Editorial changes in TASKFILE_VERSIONS.md 2018-03-08 10:18:27 +01:00
Andrey Nering
d48a2f3ccf Documentation changes for v2.0.0 release 2018-03-07 22:54:37 -03:00
Andrey Nering
c1ae36866e Remove warning for version 2 2018-03-04 16:27:16 -03:00
Andrey Nering
4f368923a5 Upgrade own Taskfile to version 2 2018-03-04 16:20:26 -03:00
Andrey Nering
7c02097d93 Improve --init flag
- Update generated Taskfile to version 2
- Don't loop anymore on extensions since we only support YAML
- Use 644 for file permission
2018-03-04 16:19:52 -03:00
Andrey Nering
51998f706f Allow customizable number of expansions
Updates #66
2018-03-04 15:50:03 -03:00
Andrey Nering
1a3df08aca Allow global variables in the Taskfile
Closes #66
2018-03-04 15:39:14 -03:00
Andrey Nering
975f262ac0 Fix "test-release" task
Goreleaser changed some flags
2018-03-04 11:06:19 -03:00
Andrey Nering
1cb4a3b8d5 Fix CI
Goreleaser now requires Go 1.10, but we still support Go 1.8
2018-03-04 10:57:46 -03:00
Andrey Nering
35f4b2f686 Travis: Test Go 1.10 2018-03-03 19:28:13 -03:00
Andrey Nering
407ec91ca7 Update dependencies 2018-03-03 19:28:08 -03:00
Andrey Nering
12c0d18932 Move setting of default version to "taskfile" package 2018-03-03 18:56:15 -03:00
Andrey Nering
2d4ca37226 Use semver for Taskfile versions 2018-03-03 18:54:42 -03:00
Andrey Nering
afe6744e97 Merge pull request #93 from go-task/v2_refactor
Re-organization and refactoring targeting v2
2018-02-18 10:14:00 -03:00
Andrey Nering
19d4b8b7f7 Add warning about Taskfile version 2 until v2.0.0 release 2018-02-18 10:04:17 -03:00
Andrey Nering
3556942516 Improve nested variables support
Closes #76 #89 #77 #83
2018-02-18 09:50:39 -03:00
Andrey Nering
87a200e42c Extract some functionality to its own packages
Like variable and template handling, and logging
2018-02-17 16:12:41 -02:00
Andrey Nering
152fc0ad38 Move all structs related to Taskfile to its own package 2018-02-17 14:22:18 -02:00
Andrey Nering
3212ae4713 Update dependencies 2018-02-11 22:02:22 -02:00
Andrey Nering
040cef1479 Fix usage of "dep"
Now, "dep prune" should not be run, and instead pruning options should be set on Gopkg.toml
2018-02-11 22:00:40 -02:00
Andrey Nering
f5f70d7a75 README: Document Homebrew as an installation method 2018-01-27 14:44:20 -02:00
Andrey Nering
42509cf2f5 Update dependencies
Fixes #86
2018-01-21 09:39:15 -02:00
atar-axis
6f74c2d823 Added "passing variables to dependencies" (#85) 2018-01-11 10:10:12 -02:00
Andrey Nering
e23a6dc9f1 Update dependencies 2018-01-03 15:12:40 -02:00
Andrey Nering
134c6b79c4 Add Taskfile struct and start implementing new format
Updates #54, #66 and #77
2017-12-29 18:46:15 -02:00
Andrey Nering
00ff1447ee Update README.md
Includes removing unmaintained alternatives
2017-12-26 21:53:57 -02:00
Andrey Nering
78f6cb08d8 Add --status flag to check is a task is up-to-date
Closes #81
2017-12-26 21:43:52 -02:00
491 changed files with 54285 additions and 24671 deletions

View File

@@ -9,6 +9,6 @@ trim_trailing_whitespace = true
indent_style = tab
indent_size = 8
[*.{md,yml,yaml,json,toml}]
[*.{md,yml,yaml,json,toml,htm,html}]
indent_style = space
indent_size = 2

View File

@@ -1,12 +1,6 @@
* Bug reports and feature requests are welcome in [the issues][issues]
* For questions and discussion there's the [Slack room][slack] ([invititation here][slackinvite])
* Pull Requests are welcome. For more complex changes and features it's
recommended to open an issue first
* About 3 or 4 pull requests accepted one gets write access to the repo.
Even then, possible backward incompatible changes should be discussed first
in an issue or pull request
recommended to open an issue with the feature request first
* Documentation contributions are as important as code contributions
[issues]: https://github.com/go-task/task/issues
[slack]: https://gophers.slack.com/messages/task
[slackinvite]: https://invite.slack.golangbridge.org/

View File

@@ -1,7 +1,4 @@
<!--
For questions and general talk there's the Slack room: https://gophers.slack.com/messages/task
Invite to the Slack is available in this link: https://invite.slack.golangbridge.org/
If relevant, include the following information:
- Task version
- OS

3
.gitignore vendored
View File

@@ -14,6 +14,7 @@
.glide/
./task
.task
dist/
vendor/**/*_test.go
.DS_Store

View File

@@ -11,6 +11,8 @@ build:
ignore:
- goos: darwin
goarch: 386
env:
- CGO_ENABLED=0
archive:
name_template: "{{.Binary}}_{{.Os}}_{{.Arch}}"
@@ -22,12 +24,21 @@ archive:
release:
draft: true
fpm:
snapshot:
name_template: "{{.Tag}}"
checksum:
name_template: "task_checksums.txt"
nfpm:
vendor: Task
homepage: https://github.com/go-task/task
maintainer: Andrey Nering <andrey.nering@gmail.com>
description: Simple task runner written in Go
license: MIT
conflicts:
- taskwarrior
formats:
- deb
- rpm
name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}"

View File

@@ -1,9 +1,22 @@
language: go
go:
- 1.8
- 1.9
- 1.10.x
- 1.11.x
addons:
apt:
packages:
- rpm
script:
- go install github.com/go-task/task/cmd/task
- task dl-deps
- task lint
- task test
- task ci
deploy:
- provider: script
skip_cleanup: true
script: curl -sL http://git.io/goreleaser | bash
on:
tags: true
condition: $TRAVIS_OS_NAME = linux

166
CHANGELOG.md Normal file
View File

@@ -0,0 +1,166 @@
# Changelog
## v2.4.0 - 2019-02-21
- Allow calling a task of the root Taskfile from an included Taskfile
by prefixing it with `:`
([#161](https://github.com/go-task/task/issues/161), [#172](https://github.com/go-task/task/issues/172)),
- Add flag to override the `output` option
([#173](https://github.com/go-task/task/pull/173));
- Fix bug where Task was persisting the new checksum on the disk when the Dry
Mode is enabled
([#166](https://github.com/go-task/task/issues/166));
- Fix file timestamp issue when the file name has spaces
([#176](https://github.com/go-task/task/issues/176));
- Mitigating path expanding issues on Windows
([#170](https://github.com/go-task/task/pull/170)).
## v2.3.0 - 2019-01-02
- On Windows, Task can now be installed using [Scoop](https://scoop.sh/)
([#152](https://github.com/go-task/task/pull/152));
- Fixed issue with file/directory globing
([#153](https://github.com/go-task/task/issues/153));
- Added ability to globally set environment variables
(
[#138](https://github.com/go-task/task/pull/138),
[#159](https://github.com/go-task/task/pull/159)
).
## v2.2.1 - 2018-12-09
- This repository now uses Go Modules (#143). We'll still keep the `vendor` directory in sync for some time, though;
- Fixing a bug when the Taskfile has no tasks but includes another Taskfile (#150);
- Fix a bug when calling another task or a dependency in an included Taskfile (#151).
## v2.2.0 - 2018-10-25
- Added support for [including other Taskfiles](https://taskfile.org/#/usage?id=including-other-taskfiles) (#98)
- This should be considered experimental. For now, only including local files is supported, but support for including remote Taskfiles is being discussed. If you have any feedback, please comment on #98.
- Task now have a dedicated documentation site: https://taskfile.org
- Thanks to [Docsify](https://docsify.js.org/) for making this pretty easy. To check the source code, just take a look at the [docs](https://github.com/go-task/task/tree/master/docs) directory of this repository. Contributions to the documentation is really appreciated.
## v2.1.1 - 2018-09-17
- Fix suggestion to use `task --init` not being shown anymore (when a `Taskfile.yml` is not found)
- Fix error when using checksum method and no file exists for a source glob (#131)
- Fix signal handling when the `--watch` flag is given (#132)
## v2.1.0 - 2018-08-19
- Add a `ignore_error` option to task and command (#123)
- Add a dry run mode (`--dry` flag) (#126)
## v2.0.3 - 2018-06-24
- Expand environment variables on "dir", "sources" and "generates" (#116)
- Fix YAML merging syntax (#112)
- Add ZSH completion (#111)
- Implement new `output` option. Please check out the [documentation](https://github.com/go-task/task#output-syntax)
## v2.0.2 - 2018-05-01
- Fix merging of YAML anchors (#112)
## v2.0.1 - 2018-03-11
- Fixes panic on `task --list`
## v2.0.0 - 2018-03-08
Version 2.0.0 is here, with a new Taskfile format.
Please, make sure to read the [Taskfile versions](https://github.com/go-task/task/blob/master/TASKFILE_VERSIONS.md) document, since it describes in depth what changed for this version.
* New Taskfile version 2 (https://github.com/go-task/task/issues/77)
* Possibility to have global variables in the `Taskfile.yml` instead of `Taskvars.yml` (https://github.com/go-task/task/issues/66)
* Small improvements and fixes
## v1.4.4 - 2017-11-19
- Handle SIGINT and SIGTERM (#75);
- List: print message with there's no task with description;
- Expand home dir ("~" symbol) on paths (#74);
- Add Snap as an installation method;
- Move examples to its own repo;
- Watch: also walk on tasks called on on "cmds", and not only on "deps";
- Print logs to stderr instead of stdout (#68);
- Remove deprecated `set` keyword;
- Add checksum based status check, alternative to timestamp based.
## v1.4.3 - 2017-09-07
- Allow assigning variables to tasks at run time via CLI (#33)
- Added suport for multiline variables from sh (#64)
- Fixes env: remove square braces and evaluate shell (#62)
- Watch: change watch library and few fixes and improvements
- When use watching, cancel and restart long running process on file change (#59 and #60)
## v1.4.2 - 2017-07-30
- Flag to set directory of execution
- Always echo command if is verbose mode
- Add silent mode to disable echoing of commands
- Fixes and improvements of variables (#56)
## v1.4.1 - 2017-07-15
- Allow use of YAML for dynamic variables instead of $ prefix
- `VAR: {sh: echo Hello}` instead of `VAR: $echo Hello`
- Add `--list` (or `-l`) flag to print existing tasks
- OS specific Taskvars file (e.g. `Taskvars_windows.yml`, `Taskvars_linux.yml`, etc)
- Consider task up-to-date on equal timestamps (#49)
- Allow absolute path in generates section (#48)
- Bugfix: allow templating when calling deps (#42)
- Fix panic for invalid task in cyclic dep detection
- Better error output for dynamic variables in Taskvars.yml (#41)
- Allow template evaluation in parameters
## v1.4.0 - 2017-07-06
- Cache dynamic variables
- Add verbose mode (`-v` flag)
- Support to task parameters (overriding vars) (#31) (#32)
- Print command, also when "set:" is specified (#35)
- Improve task command help text (#35)
## v1.3.1 - 2017-06-14
- Fix glob not working on commands (#28)
- Add ExeExt template function
- Add `--init` flag to create a new Taskfile
- Add status option to prevent task from running (#27)
- Allow interpolation on `generates` and `sources` attributes (#26)
## v1.3.0 - 2017-04-24
- Migrate from os/exec.Cmd to a native Go sh/bash interpreter
- This is a potentially breaking change if you use Windows.
- Now, `cmd` is not used anymore on Windows. Always use Bash-like syntax for your commands, even on Windows.
- Add "ToSlash" and "FromSlash" to template functions
- Use functions defined on github.com/Masterminds/sprig
- Do not redirect stdin while running variables commands
- Using `context` and `errgroup` packages (this will make other tasks to be cancelled, if one returned an error)
## v1.2.0 - 2017-04-02
- More tests and Travis integration
- Watch a task (experimental)
- Possibility to call another task
- Fix "=" not being reconized in variables/environment variables
- Tasks can now have a description, and help will print them (#10)
- Task dependencies now run concurrently
- Support for a default task (#16)
## v1.1.0 - 2017-03-08
- Support for YAML, TOML and JSON (#1)
- Support running command in another directory (#4)
- `--force` or `-f` flag to force execution of task even when it's up-to-date
- Detection of cyclic dependencies (#5)
- Support for variables (#6, #9, #14)
- Operation System specific commands and variables (#13)
## v1.0.0 - 2017-02-28
- Add LICENSE file

123
Gopkg.lock generated
View File

@@ -1,123 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/Masterminds/semver"
packages = ["."]
revision = "15d8430ab86497c5c0da827b748823945e1cf1e1"
version = "v1.4.0"
[[projects]]
branch = "master"
name = "github.com/Masterminds/sprig"
packages = ["."]
revision = "82f6f19d47b416d27ae039939b44afaa0575860e"
[[projects]]
name = "github.com/aokoli/goutils"
packages = ["."]
revision = "3391d3790d23d03408670993e957e8f408993c34"
version = "v1.0.1"
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/huandu/xstrings"
packages = ["."]
revision = "d6590c0c31d16526217fa60fbd2067f7afcd78c5"
[[projects]]
name = "github.com/imdario/mergo"
packages = ["."]
revision = "7fe0c75c13abdee74b09fcacef5ea1c6bba6a874"
version = "0.2.4"
[[projects]]
branch = "master"
name = "github.com/mattn/go-zglob"
packages = [".","fastwalk"]
revision = "4b74c24375b3b1ee226867156e01996f4e19a8d6"
[[projects]]
branch = "master"
name = "github.com/mitchellh/go-homedir"
packages = ["."]
revision = "b8bc1bf767474819792c23f32d8286a45736f1c6"
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
name = "github.com/radovskyb/watcher"
packages = ["."]
revision = "6145e1439b9de93806925353403f91d2abbad8a5"
version = "v1.0.2"
[[projects]]
name = "github.com/satori/go.uuid"
packages = ["."]
revision = "879c5887cd475cd7864858769793b2ceb0d44feb"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/spf13/pflag"
packages = ["."]
revision = "4c012f6dcd9546820e378d0bdda4d8fc772cdfea"
[[projects]]
name = "github.com/stretchr/testify"
packages = ["assert"]
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
version = "v1.1.4"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["pbkdf2","scrypt","ssh/terminal"]
revision = "9f005a07e0d31d45e6656d241bb5c0f2efd4bc94"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = ["context"]
revision = "9dfe39835686865bff950a07b394c12a98ddc811"
[[projects]]
branch = "master"
name = "golang.org/x/sync"
packages = ["errgroup"]
revision = "fd80eb99c8f653c847d294a001bdf2a3a6f768f5"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = ["unix","windows"]
revision = "0ac51a24ef1c37380f98ba8b98f56e3bffd59850"
[[projects]]
branch = "v2"
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "287cf08546ab5e7e37d55a84f7ed3fd1db036de5"
[[projects]]
branch = "master"
name = "mvdan.cc/sh"
packages = ["interp","syntax"]
revision = "5758e57655f2f2242603195aaaad08d1cb9f8f85"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "da52cb2c602c1362c303cf241aa18dfd6199f30484bb12684adb0b6927391cbf"
solver-name = "gps-cdcl"
solver-version = 1

View File

@@ -1,103 +0,0 @@
## Gopkg.toml example (these lines may be deleted)
## "metadata" defines metadata about the project that could be used by other independent
## systems. The metadata defined here will be ignored by dep.
# [metadata]
# key1 = "value that convey data to other systems"
# system1-data = "value that is used by a system"
# system2-data = "value that is used by another system"
## "required" lists a set of packages (not projects) that must be included in
## Gopkg.lock. This list is merged with the set of packages imported by the current
## project. Use it when your project needs a package it doesn't explicitly import -
## including "main" packages.
# required = ["github.com/user/thing/cmd/thing"]
## "ignored" lists a set of packages (not projects) that are ignored when
## dep statically analyzes source code. Ignored packages can be in this project,
## or in a dependency.
# ignored = ["github.com/user/project/badpkg"]
## Constraints are rules for how directly imported projects
## may be incorporated into the depgraph. They are respected by
## dep whether coming from the Gopkg.toml of the current project or a dependency.
# [[constraint]]
## Required: the root import path of the project being constrained.
# name = "github.com/user/project"
#
## Recommended: the version constraint to enforce for the project.
## Only one of "branch", "version" or "revision" can be specified.
# version = "1.0.0"
# branch = "master"
# revision = "abc123"
#
## Optional: an alternate location (URL or import path) for the project's source.
# source = "https://github.com/myfork/package.git"
#
## "metadata" defines metadata about the dependency or override that could be used
## by other independent systems. The metadata defined here will be ignored by dep.
# [metadata]
# key1 = "value that convey data to other systems"
# system1-data = "value that is used by a system"
# system2-data = "value that is used by another system"
## Overrides have the same structure as [[constraint]], but supersede all
## [[constraint]] declarations from all projects. Only [[override]] from
## the current project's are applied.
##
## Overrides are a sledgehammer. Use them only as a last resort.
# [[override]]
## Required: the root import path of the project being constrained.
# name = "github.com/user/project"
#
## Optional: specifying a version constraint override will cause all other
## constraints on this project to be ignored; only the overridden constraint
## need be satisfied.
## Again, only one of "branch", "version" or "revision" can be specified.
# version = "1.0.0"
# branch = "master"
# revision = "abc123"
#
## Optional: specifying an alternate source location as an override will
## enforce that the alternate location is used for that project, regardless of
## what source location any dependent projects specify.
# source = "https://github.com/myfork/package.git"
[[constraint]]
branch = "master"
name = "github.com/Masterminds/sprig"
[[constraint]]
branch = "master"
name = "github.com/imdario/mergo"
[[constraint]]
branch = "master"
name = "github.com/mattn/go-zglob"
[[constraint]]
branch = "master"
name = "mvdan.cc/sh"
[[constraint]]
branch = "master"
name = "github.com/spf13/pflag"
[[constraint]]
branch = "master"
name = "golang.org/x/sync"
[[constraint]]
branch = "v2"
name = "gopkg.in/yaml.v2"
[[constraint]]
branch = "master"
name = "github.com/radovskyb/watcher"
[[constraint]]
branch = "master"
name = "github.com/mitchellh/go-homedir"

568
README.md
View File

@@ -1,568 +1,22 @@
[![Join Slack room](https://img.shields.io/badge/%23task-chat%20room-blue.svg)](https://gophers.slack.com/messages/task)
[![Build Status](https://travis-ci.org/go-task/task.svg?branch=master)](https://travis-ci.org/go-task/task)
# Task - A task runner / simpler Make alternative written in Go
# Task
Task is a simple tool that allows you to easily run development and build
tasks. Task is written in Golang, but can be used to develop any language.
It aims to be simpler and easier to use then [GNU Make][make].
Task is a task runner / build tool that aims to be simpler and easier to use
than, for example, [GNU Make](https://www.gnu.org/software/make/).
- [Installation](#installation)
- [Go](#go)
- [Snap](#snap)
- [Binary](#binary)
- [Usage](#usage)
- [Environment](#environment)
- [OS specific task](#os-specific-task)
- [Task directory](#task-directory)
- [Task dependencies](#task-dependencies)
- [Calling another task](#calling-another-task)
- [Prevent unnecessary work](#prevent-unnecessary-work)
- [Variables](#variables)
- [Dynamic variables](#dynamic-variables)
- [Go's template engine](#gos-template-engine)
- [Help](#help)
- [Silent mode](#silent-mode)
- [Watch tasks](#watch-tasks-experimental)
- [Examples](#examples)
- [Task in the wild](#task-in-the-wild)
- [Alternative task runners](#alternative-task-runners)
See [taskfile.org](https://taskfile.org) for documentation.
## Installation
---
### Go
## Sponsors
If you have a [Golang][golang] environment setup, you can simply run:
[![Sponsors](https://opencollective.com/task/sponsors.svg?width=890)](https://opencollective.com/task)
```bash
go get -u -v github.com/go-task/task/cmd/task
```
## Backers
### Snap
[![Backers](https://opencollective.com/task/backers.svg?width=890)](https://opencollective.com/task)
Task is available for [Snapcraft][snapcraft], but keep in mind that your
Linux distribution should allow classic confinement for Snaps to Task work
right:
## Contributors
```bash
sudo snap install task
```
### Binary
Or you can download the binary from the [releases][releases] page and add to
your `PATH`. DEB and RPM packages are also available.
The `task_checksums.txt` file contains the sha256 checksum for each file.
## Usage
Create a file called `Taskfile.yml` in the root of the project.
The `cmds` attribute should contains the commands of a task.
The example below allows compile a Go app and uses [Minify][minify] to concat
and minify multiple CSS files into a single one.
```yml
build:
cmds:
- go build -v -i main.go
assets:
cmds:
- minify -o public/style.css src/css
```
Running the tasks is as simple as running:
```bash
task assets build
```
Task uses [github.com/mvdan/sh](https://github.com/mvdan/sh), a native Go sh
interpreter. So you can write sh/bash commands and it will work even on
Windows, where `sh` or `bash` is usually not available. Just remember any
executable called must be available by the OS or in PATH.
If you ommit a task name, "default" will be assumed.
### Environment
You can specify environment variables that are added when running a command:
```yml
build:
cmds:
- echo $hallo
env:
hallo: welt
```
### OS specific task
If you add a `Taskfile_{{GOOS}}.yml` you can override or amend your taskfile
based on the operating system.
Example:
Taskfile.yml:
```yml
build:
cmds:
- echo "default"
```
Taskfile_linux.yml:
```yml
build:
cmds:
- echo "linux"
```
Will print out `linux` and not default.
It's also possible to have OS specific `Taskvars.yml` file, like
`Taskvars_windows.yml`, `Taskfile_linux.yml` or `Taskvars_darwin.yml`. See the
[variables section](#variables) below.
### Task directory
By default, tasks will be executed in the directory where the Taskfile is
located. But you can easily make the task run in another folder informing
`dir`:
```yml
serve:
dir: public/www
cmds:
# run http server
- caddy
```
### Task dependencies
You may have tasks that depends on others. Just pointing them on `deps` will
make them run automatically before running the parent task:
```yml
build:
deps: [assets]
cmds:
- go build -v -i main.go
assets:
cmds:
- minify -o public/style.css src/css
```
In the above example, `assets` will always run right before `build` if you run
`task build`.
A task can have only dependencies and no commands to group tasks together:
```yml
assets:
deps: [js, css]
js:
cmds:
- minify -o public/script.js src/js
css:
cmds:
- minify -o public/style.css src/css
```
If there are more than one dependency, they always run in parallel for better
performance.
### Calling another task
When a task has many dependencies, they are executed concurrently. This will
often result in a faster build pipeline. But in some situations you may need
to call other tasks serially. In this case, just use the following syntax:
```yml
main-task:
cmds:
- task: task-to-be-called
- task: another-task
- echo "Both done"
task-to-be-called:
cmds:
- echo "Task to be called"
another-task:
cmds:
- echo "Another task"
```
Overriding variables in the called task is as simple as informing `vars`
attribute:
```yml
main-task:
cmds:
- task: write-file
vars: {FILE: "hello.txt", CONTENT: "Hello!"}
- task: write-file
vars: {FILE: "world.txt", CONTENT: "World!"}
write-file:
cmds:
- echo "{{.CONTENT}}" > {{.FILE}}
```
The above syntax is also supported in `deps`.
> NOTE: It's also possible to call a task without any param prefixing it
with `^`, but this syntax is deprecated:
```yml
a-task:
cmds:
- ^another-task
another-task:
cmds:
- echo "Another task"
```
### Prevent unnecessary work
If a task generates something, you can inform Task the source and generated
files, so Task will prevent to run them if not necessary.
```yml
build:
deps: [js, css]
cmds:
- go build -v -i main.go
js:
cmds:
- minify -o public/script.js src/js
sources:
- src/js/**/*.js
generates:
- public/script.js
css:
cmds:
- minify -o public/style.css src/css
sources:
- src/css/**/*.css
generates:
- public/style.css
```
`sources` and `generates` can be files or file patterns. When both are given,
Task will compare the modification date/time of the files to determine if it's
necessary to run the task. If not, it will just print a message like
`Task "js" is up to date`.
If you prefer this check to be made by the content of the files, instead of
its timestamp, just set the `method` property to `checksum`.
You will probably want to ignore the `.task` folder in your `.gitignore` file
(It's there that Task stores the last checksum).
This feature is still experimental and can change until it's stable.
```yml
build:
cmds:
- go build .
sources:
- ./*.go
generates:
- app{{exeExt}}
method: checksum
```
> TIP: method `none` skips any validation and always run the task.
Alternatively, you can inform a sequence of tests as `status`. If no error
is returned (exit status 0), the task is considered up-to-date:
```yml
generate-files:
cmds:
- mkdir directory
- touch directory/file1.txt
- touch directory/file2.txt
# test existence of files
status:
- test -d directory
- test -f directory/file1.txt
- test -f directory/file2.txt
```
You can use `--force` or `-f` if you want to force a task to run even when
up-to-date.
### Variables
When doing interpolation of variables, Task will look for the below.
They are listed below in order of importance (e.g. most important first):
- Variables given while calling a task from another.
(See [Calling another task](#calling-another-task) above)
- Environment variables
- Variables declared locally in the task
- Variables available in the `Taskvars.yml` file
Example of overriding with environment variables:
```bash
$ TASK_VARIABLE=a-value task do-something
```
Since some shells don't support above syntax to set environment variables
(Windows) tasks also accepts a similar style when not in the beginning of
the command. Variables given in this form are only visible to the task called
right before.
```bash
$ task write-file FILE=file.txt "CONTENT=Hello, World!" print "MESSAGE=All done!"
```
Example of locally declared vars:
```yml
print-var:
cmds:
echo "{{.VAR}}"
vars:
VAR: Hello!
```
Example of `Taskvars.yml` file:
```yml
PROJECT_NAME: My Project
DEV_MODE: production
GIT_COMMIT: {sh: git log -n 1 --format=%h}
```
#### Dynamic variables
The below syntax (`sh:` prop in a variable) is considered a dynamic variable.
The value will be treated as a command and the output assigned. If there is one
or more trailing newlines, the last newline will be trimmed.
```yml
build:
cmds:
- go build -ldflags="-X main.Version={{.GIT_COMMIT}}" main.go
vars:
GIT_COMMIT:
sh: git log -n 1 --format=%h
```
This works for all types of variables.
> It's also possible to prefix the variable with `$` to have a dynamic
variable, but this is now considered deprecated:
```yml
# Taskvars.yml
# recommended
GIT_COMMIT:
sh: git log -n 1 --format=%h
# deprecated
GIT_COMMIT: $git log -n 1 --format=%h
```
### Go's template engine
Task parse commands as [Go's template engine][gotemplate] before executing
them. Variables are accessible through dot syntax (`.VARNAME`).
All functions by the Go's [sprig lib](http://masterminds.github.io/sprig/)
are available. The following example gets the current date in a given format:
```yml
print-date:
cmds:
- echo {{now | date "2006-01-02"}}
```
Task also adds the following functions:
- `OS`: Returns operating system. Possible values are "windows", "linux",
"darwin" (macOS) and "freebsd".
- `ARCH`: return the architecture Task was compiled to: "386", "amd64", "arm"
or "s390x".
- `splitLines`: Splits Unix (\n) and Windows (\r\n) styled newlines.
- `catLines`: Replaces Unix (\n) and Windows (\r\n) styled newlines with a space.
- `toSlash`: Does nothing on Unix, but on Windows converts a string from `\`
path format to `/`.
- `fromSlash`: Oposite of `toSlash`. Does nothing on Unix, but on Windows
converts a string from `\` path format to `/`.
- `exeExt`: Returns the right executable extension for the current OS
(`".exe"` for Windows, `""` for others).
Example:
```yml
print-os:
cmds:
- echo '{{OS}} {{ARCH}}'
- echo '{{if eq OS "windows"}}windows-command{{else}}unix-command{{end}}'
# This will be path/to/file on Unix but path\to\file on Windows
- echo '{{fromSlash "path/to/file"}}'
enumerated-file:
vars:
CONTENT: |
foo
bar
cmds:
- |
cat << EOF > output.txt
{{range $i, $line := .CONTENT | splitLines -}}
{{printf "%3d" $i}}: {{$line}}
{{end}}EOF
```
> NOTE: There are some deprecated function names still available: `ToSlash`,
`FromSlash` and `ExeExt`. These where changed for consistency with sprig lib.
### Help
Running `task --list` (or `task -l`) lists all tasks with a description.
The following taskfile:
```yml
build:
desc: Build the go binary.
cmds:
- go build -v -i main.go
test:
desc: Run all the go tests.
cmds:
- go test -race ./...
js:
cmds:
- minify -o public/script.js src/js
css:
cmds:
- minify -o public/style.css src/css
```
would print the following output:
```bash
* build: Build the go binary.
* test: Run all the go tests.
```
## Silent mode
Silent mode disables echoing of commands before Task runs it.
For the following Taskfile:
```yml
echo:
cmds:
- echo "Print something"
```
Normally this will be print:
```sh
echo "Print something"
Print something
```
With silent mode on, the below will be print instead:
```sh
Print something
```
There's three ways to enable silent mode:
* At command level:
```yml
echo:
cmds:
- cmd: echo "Print something"
silent: true
```
* At task level:
```yml
echo:
cmds:
- echo "Print something"
silent: true
```
* Or globally with `--silent` or `-s` flag
If you want to suppress stdout instead, just redirect a command to `/dev/null`:
```yml
echo:
cmds:
- echo "This will print nothing" > /dev/null
```
## Watch tasks (experimental)
If you give a `--watch` or `-w` argument, task will watch for files changes
and run the task again. This requires the `sources` attribute to be given,
so task know which files to watch.
## Examples
The [go-task/examples][examples] intends to be a collection of Taskfiles for
various use cases.
(It still lacks many examples, though. Contributions are welcome).
## Task in the wild
- [How I Build My Static Assets for Hugo][post-hugo]
## Alternative task runners
- YAML based:
- [tj/robo][robo]
- [dogtools/dog][dog]
- [goeuro/myke][myke]
- [dreadl0ck/zeus][zeus]
- [rliebz/tusk][tusk]
- Go based:
- [go-godo/godo][godo]
- [markbates/grift][grift]
- [nstratos/make.go][make.go]
- [magefile/mage][mage]
- Make based:
- [tj/mmake][mmake]
[make]: https://www.gnu.org/software/make/
[releases]: https://github.com/go-task/task/releases
[golang]: https://golang.org/
[gotemplate]: https://golang.org/pkg/text/template/
[robo]: https://github.com/tj/robo
[dog]: https://github.com/dogtools/dog
[myke]: https://github.com/goeuro/myke
[zeus]: https://github.com/dreadl0ck/zeus
[tusk]: https://github.com/rliebz/tusk
[godo]: https://github.com/go-godo/godo
[grift]: https://github.com/markbates/grift
[make.go]: https://github.com/nstratos/make.go
[mage]: https://github.com/magefile/mage
[mmake]: https://github.com/tj/mmake
[sh]: https://github.com/mvdan/sh
[post-hugo]: https://blog.carlmjohnson.net/post/2017/hugo-asset-pipeline/
[minify]: https://github.com/tdewolff/minify/tree/master/cmd/minify
[examples]: https://github.com/go-task/examples
[snapcraft]: https://snapcraft.io/
[![Contributors](https://opencollective.com/task/contributors.svg?width=890)](https://github.com/go-task/task/graphs/contributors)

View File

@@ -1,55 +1,88 @@
# compiles current source code and make "task" executable available on
# $GOPATH/bin/task{.exe}
install:
desc: Installs Task
cmds:
- go install -v -ldflags="-w -s -X main.version={{.GIT_COMMIT}}" ./cmd/task
version: '2'
dl-deps:
desc: Downloads cli dependencies
cmds:
- go get -u github.com/golang/lint/golint
- go get -u github.com/goreleaser/goreleaser
- go get -u github.com/asticode/go-astitodo/astitodo
- go get -u github.com/golang/dep/cmd/dep
includes:
docs: ./docs
update-deps:
desc: Updates dependencies
cmds:
- dep ensure
- dep ensure -update
- dep prune
vars:
GIT_COMMIT:
sh: git log -n 1 --format=%h
clean:
desc: Cleans temp files and folders
cmds:
- rm -rf dist/
GO_PACKAGES:
sh: go list ./...
lint:
desc: Runs golint
cmds:
- golint {{.GO_PACKAGES}}
silent: true
tasks:
default:
cmds:
- task: test
test:
desc: Runs test suite
deps: [install]
cmds:
- go test {{.GO_PACKAGES}}
install:
desc: Installs Task
cmds:
- go install -v -ldflags="-w -s -X main.version={{.GIT_COMMIT}}" ./cmd/task
env:
CGO_ENABLED: '0'
# https://github.com/goreleaser/goreleaser
release:
desc: Release Task
cmds:
- goreleaser
dl-deps:
desc: Downloads cli dependencies
cmds:
- task: go-get
vars: {REPO: golang.org/x/lint/golint}
- task: go-get
vars: {REPO: github.com/goreleaser/goreleaser}
- task: go-get
vars: {REPO: github.com/goreleaser/godownloader}
test-release:
desc: Tests release process without publishing
cmds:
- goreleaser --skip-validate --skip-publish
vendor:
desc: Sync vendor/ directory according to go.mod file
cmds:
- go mod vendor
todo:
desc: Prints TODO comments present in the code
cmds:
- astitodo {{.GO_PACKAGES}}
silent: true
update-deps:
desc: Updates dependencies
cmds:
- dep ensure
- dep ensure -update
clean:
desc: Cleans temp files and folders
cmds:
- rm -rf dist/
lint:
desc: Runs golint
cmds:
- golint {{catLines .GO_PACKAGES}}
silent: true
test:
desc: Runs test suite
deps: [install]
cmds:
- go test {{catLines .GO_PACKAGES}}
test-release:
desc: Tests release process without publishing
cmds:
- goreleaser --snapshot --rm-dist
generate-install-script:
desc: Generate install script using https://github.com/goreleaser/godownloader
cmds:
- godownloader --repo go-task/task -o install-task.sh
- cp ./install-task.sh ./docs/install.sh
ci:
cmds:
- task: go-get
vars: {REPO: golang.org/x/lint/golint}
- task: lint
- task: test
go-get:
cmds:
- go get -u {{.REPO}}
packages:
cmds:
- echo '{{.GO_PACKAGES}}'
silent: true

View File

@@ -1,9 +0,0 @@
GIT_COMMIT:
sh: git log -n 1 --format=%h
GO_PACKAGES:
.
./cmd/task
./internal/args
./internal/execext
./internal/status

View File

@@ -7,8 +7,8 @@ import (
"os/signal"
"syscall"
"github.com/go-task/task"
"github.com/go-task/task/internal/args"
"github.com/go-task/task/v2"
"github.com/go-task/task/v2/internal/args"
"github.com/spf13/pflag"
)
@@ -17,7 +17,7 @@ var (
version = "master"
)
const usage = `Usage: task [-ilfwvsd] [--init] [--list] [--force] [--watch] [--verbose] [--silent] [--dir] [task...]
const usage = `Usage: task [-ilfwvsd] [--init] [--list] [--force] [--watch] [--verbose] [--silent] [--dir] [--dry] [task...]
Runs the specified task(s). Falls back to the "default" task if no task name
was specified, or lists all tasks if an unknown task name was specified.
@@ -50,21 +50,27 @@ func main() {
versionFlag bool
init bool
list bool
status bool
force bool
watch bool
verbose bool
silent bool
dry bool
dir string
output string
)
pflag.BoolVar(&versionFlag, "version", false, "show Task version")
pflag.BoolVarP(&init, "init", "i", false, "creates a new Taskfile.yml in the current folder")
pflag.BoolVarP(&list, "list", "l", false, "lists tasks with description of current Taskfile")
pflag.BoolVar(&status, "status", false, "exits with non-zero exit code if any of the given tasks is not up-to-date")
pflag.BoolVarP(&force, "force", "f", false, "forces execution even when the task is up-to-date")
pflag.BoolVarP(&watch, "watch", "w", false, "enables watch of the given task")
pflag.BoolVarP(&verbose, "verbose", "v", false, "enables verbose mode")
pflag.BoolVarP(&silent, "silent", "s", false, "disables echoing")
pflag.BoolVar(&dry, "dry", false, "compiles and prints tasks in the order that they would be run, without executing them")
pflag.StringVarP(&dir, "dir", "d", "", "sets directory of execution")
pflag.StringVarP(&output, "output", "o", "", "sets output style: [interleaved|group|prefixed]")
pflag.Parse()
if versionFlag {
@@ -89,14 +95,15 @@ func main() {
Verbose: verbose,
Silent: silent,
Dir: dir,
Context: getSignalContext(),
Dry: dry,
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
OutputStyle: output,
}
if err := e.ReadTaskfile(); err != nil {
if err := e.Setup(); err != nil {
log.Fatal(err)
}
@@ -116,7 +123,19 @@ func main() {
log.Fatal(err)
}
if err := e.Run(calls...); err != nil {
ctx := context.Background()
if !watch {
ctx = getSignalContext()
}
if status {
if err = e.Status(ctx, calls...); err != nil {
log.Fatal(err)
}
return
}
if err := e.Run(ctx, calls...); err != nil {
log.Fatal(err)
}
}

25
completion/zsh/_task Normal file
View File

@@ -0,0 +1,25 @@
#compdef task
# Listing commands from Taskfile.yml
function __list() {
local -a scripts
if [ -f Taskfile.yml ]; then
scripts=($(task -l | sed '1d' | sed 's/://' | awk '{ print $2 }'))
_describe 'script' scripts
fi
}
_arguments \
'(-d --dir)'{-d,--dir}': :_files' \
'(--dry)'--dry \
'(-f --force)'{-f,--force} \
'(-i --init)'{-i,--init} \
'(-l --list)'{-l,--list} \
'(-s --silent)'{-s,--silent} \
'(--status)'--status \
'(-v --verbose)'{-v,--verbose} \
'(--version)'--version \
'(-w --watch)'{-w,--watch} \
'(- *)'{-h,--help} \
'*: :__list' \

0
docs/.nojekyll Normal file
View File

1
docs/CNAME Normal file
View File

@@ -0,0 +1 @@
taskfile.org

59
docs/README.md Normal file
View File

@@ -0,0 +1,59 @@
# Task
Task is a task runner / build tool that aims to be simpler and easier to use
than, for example, [GNU Make][make].
Since it's written in [Go][go], Task is just a single binary and has no other
dependencies, which means you don't need to mess with any complicated install
setups just to use a build tool.
Once [installed](installation.md), you just need to describe your build tasks
using a simple [YAML][yaml] schema in a file called `Taskfile.yml`:
```yaml
version: '2'
tasks:
hello:
cmds:
- echo 'Hello World from Task!'
silent: true
```
And call it by running `task hello` from you terminal.
The above example is just the start, you can take a look at the [usage](usage.md)
guide to check the full schema documentation and Task features.
## Features
- [Easy installation](installation.md): just download a single binary, add to
$PATH and you're done! Or you can also install using [Homebrew][homebrew],
[Snapcraft][snapcraft], or [Scoop][scoop] if you want;
- Available on CIs: by adding [this simple command](installation.md#install-script)
to install on your CI script and you're done to use Task as part of your CI pipeline;
- Truly cross-platform: while most build tools only work well on Linux or macOS,
Task also supports Windows thanks to [this awesome shell interpreter for Go][sh];
- Great for code generation: you can easily [prevent a task from running](usage.md#prevent-unnecessary-work)
if a given set of files haven't changed since last run (based either on its
timestamp or content).
## Sponsors
[![Sponsors](https://opencollective.com/task/sponsors.svg?width=890)](https://opencollective.com/task)
## Backers
[![Backers](https://opencollective.com/task/backers.svg?width=890)](https://opencollective.com/task)
## Contributors
[![Contributors](https://opencollective.com/task/contributors.svg?width=890)](https://github.com/go-task/task/graphs/contributors)
[make]: https://www.gnu.org/software/make/
[go]: https://golang.org/
[yaml]: http://yaml.org/
[homebrew]: https://brew.sh/
[snapcraft]: https://snapcraft.io/
[scoop]: https://scoop.sh/
[sh]: https://mvdan.cc/sh

12
docs/Taskfile.yml Normal file
View File

@@ -0,0 +1,12 @@
version: '2'
tasks:
install:
desc: Installs docsify to work the on the documentation site
cmds:
- npm install docsify-cli -g
serve:
desc: Serves the documentation site locally
cmds:
- docsify serve docs

7
docs/_sidebar.md Normal file
View File

@@ -0,0 +1,7 @@
- [Installation](installation.md)
- [Usage](usage.md)
- [Taskfile Versions](taskfile_versions.md)
- [Examples](examples.md)
- [Releasing Task](releasing_task.md)
- [Alternative Task Runners](alternative_task_runners.md)
- [![Github](https://icongram.jgog.in/simple/github.svg?color=808080&size=16)Github](https://github.com/go-task/task)

View File

@@ -0,0 +1,17 @@
# Alternative task runners
## YAML based
- [rliebz/tusk][tusk]
## Go based
- [magefile/mage][mage]
## Make similar
- [casey/just][just]
[tusk]: https://github.com/rliebz/tusk
[mage]: https://github.com/magefile/mage
[just]: https://github.com/casey/just

7
docs/examples.md Normal file
View File

@@ -0,0 +1,7 @@
# Examples
The [go-task/examples][examples] intends to be a collection of Taskfiles for
various use cases.
(It still lacks many examples, though. Contributions are welcome).
[examples]: https://github.com/go-task/examples

BIN
docs/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

49
docs/index.html Normal file
View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Task</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="A task runner / simpler Make alternative written in Go">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="//unpkg.com/docsify-themeable/dist/css/theme-simple.css">
<meta name="google-site-verification" content="VGAYkbdmuaciIDGkBe-eAg9yfZg0C6ostgonbGxxOa0" />
<script>
var SeedAndDewConfig = {};
(function() {
SeedAndDewConfig['adClass'] = "snd-ad";
/* * * DON'T EDIT BELOW THIS LINE * * */
SeedAndDewConfig['projectId'] = '16e0aed0-b265-48c9-9eae-0aad56147553';
SeedAndDewConfig['loadStartTime'] = performance.now();
SeedAndDewConfig['apiVersion'] = '2018-05-28'
SeedAndDewConfig['sessionId'] = Math.random().toString(36).substring(2, 15);
var snd = document.createElement('script');
snd.type = 'text/javascript';
snd.async = true;
snd.src = 'https://www.seedanddew.com/static/embed.min.js';
(document.getElementsByTagName('head')[0] ||
document.getElementsByTagName('body')[0]).appendChild(snd);
})();
</script>
</head>
<body>
<div id="app"></div>
<script>
window.$docsify = {
name: 'Task',
repo: 'go-task/task',
ga: 'UA-126286662-1',
themeColor: '#00add8',
loadSidebar: true,
auto2top: true,
maxLevel: 3,
subMaxLevel: 3
}
</script>
<script src="//unpkg.com/docsify/lib/docsify.min.js"></script>
<script src="//unpkg.com/docsify/lib/plugins/ga.min.js"></script>
<script src="//unpkg.com/docsify-themeable"></script>
<script src="//unpkg.com/prismjs/components/prism-bash.min.js"></script>
<script src="//unpkg.com/prismjs/components/prism-yaml.min.js"></script>
</body>
</html>

390
docs/install.sh Executable file
View File

@@ -0,0 +1,390 @@
#!/bin/sh
set -e
# Code generated by godownloader on 2018-04-07T17:47:38Z. DO NOT EDIT.
#
usage() {
this=$1
cat <<EOF
$this: download go binaries for go-task/task
Usage: $this [-b] bindir [-d] [tag]
-b sets bindir or installation directory, Defaults to ./bin
-d turns on debug logging
[tag] is a tag from
https://github.com/go-task/task/releases
If tag is missing, then the latest will be used.
Generated by godownloader
https://github.com/goreleaser/godownloader
EOF
exit 2
}
parse_args() {
#BINDIR is ./bin unless set be ENV
# over-ridden by flag below
BINDIR=${BINDIR:-./bin}
while getopts "b:dh?" arg; do
case "$arg" in
b) BINDIR="$OPTARG" ;;
d) log_set_priority 10 ;;
h | \?) usage "$0" ;;
esac
done
shift $((OPTIND - 1))
TAG=$1
}
# this function wraps all the destructive operations
# if a curl|bash cuts off the end of the script due to
# network, either nothing will happen or will syntax error
# out preventing half-done work
execute() {
tmpdir=$(mktmpdir)
log_debug "downloading files into ${tmpdir}"
http_download "${tmpdir}/${TARBALL}" "${TARBALL_URL}"
http_download "${tmpdir}/${CHECKSUM}" "${CHECKSUM_URL}"
hash_sha256_verify "${tmpdir}/${TARBALL}" "${tmpdir}/${CHECKSUM}"
srcdir="${tmpdir}"
(cd "${tmpdir}" && untar "${TARBALL}")
install -d "${BINDIR}"
for binexe in "task" ; do
if [ "$OS" = "windows" ]; then
binexe="${binexe}.exe"
fi
install "${srcdir}/${binexe}" "${BINDIR}/"
log_info "installed ${BINDIR}/${binexe}"
done
}
is_supported_platform() {
platform=$1
found=1
case "$platform" in
windows/386) found=0 ;;
windows/amd64) found=0 ;;
darwin/386) found=0 ;;
darwin/amd64) found=0 ;;
linux/386) found=0 ;;
linux/amd64) found=0 ;;
esac
case "$platform" in
darwin/386) found=1 ;;
esac
return $found
}
check_platform() {
if is_supported_platform "$PLATFORM"; then
# optional logging goes here
true
else
log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
exit 1
fi
}
tag_to_version() {
if [ -z "${TAG}" ]; then
log_info "checking GitHub for latest tag"
else
log_info "checking GitHub for tag '${TAG}'"
fi
REALTAG=$(github_release "$OWNER/$REPO" "${TAG}") && true
if test -z "$REALTAG"; then
log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${PREFIX}/releases for details"
exit 1
fi
# if version starts with 'v', remove it
TAG="$REALTAG"
VERSION=${TAG#v}
}
adjust_format() {
# change format (tar.gz or zip) based on ARCH
case ${ARCH} in
windows) FORMAT=zip ;;
esac
true
}
adjust_os() {
# adjust archive name based on OS
true
}
adjust_arch() {
# adjust archive name based on ARCH
true
}
cat /dev/null <<EOF
------------------------------------------------------------------------
https://github.com/client9/shlib - portable posix shell functions
Public domain - http://unlicense.org
https://github.com/client9/shlib/blob/master/LICENSE.md
but credit (and pull requests) appreciated.
------------------------------------------------------------------------
EOF
is_command() {
command -v "$1" >/dev/null
}
echoerr() {
echo "$@" 1>&2
}
log_prefix() {
echo "$0"
}
_logp=6
log_set_priority() {
_logp="$1"
}
log_priority() {
if test -z "$1"; then
echo "$_logp"
return
fi
[ "$1" -le "$_logp" ]
}
log_tag() {
case $1 in
0) echo "emerg" ;;
1) echo "alert" ;;
2) echo "crit" ;;
3) echo "err" ;;
4) echo "warning" ;;
5) echo "notice" ;;
6) echo "info" ;;
7) echo "debug" ;;
*) echo "$1" ;;
esac
}
log_debug() {
log_priority 7 || return 0
echoerr "$(log_prefix)" "$(log_tag 7)" "$@"
}
log_info() {
log_priority 6 || return 0
echoerr "$(log_prefix)" "$(log_tag 6)" "$@"
}
log_err() {
log_priority 3 || return 0
echoerr "$(log_prefix)" "$(log_tag 3)" "$@"
}
log_crit() {
log_priority 2 || return 0
echoerr "$(log_prefix)" "$(log_tag 2)" "$@"
}
uname_os() {
os=$(uname -s | tr '[:upper:]' '[:lower:]')
case "$os" in
msys_nt) os="windows" ;;
esac
echo "$os"
}
uname_arch() {
arch=$(uname -m)
case $arch in
x86_64) arch="amd64" ;;
x86) arch="386" ;;
i686) arch="386" ;;
i386) arch="386" ;;
aarch64) arch="arm64" ;;
armv5*) arch="arm5" ;;
armv6*) arch="arm6" ;;
armv7*) arch="arm7" ;;
esac
echo ${arch}
}
uname_os_check() {
os=$(uname_os)
case "$os" in
darwin) return 0 ;;
dragonfly) return 0 ;;
freebsd) return 0 ;;
linux) return 0 ;;
android) return 0 ;;
nacl) return 0 ;;
netbsd) return 0 ;;
openbsd) return 0 ;;
plan9) return 0 ;;
solaris) return 0 ;;
windows) return 0 ;;
esac
log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib"
return 1
}
uname_arch_check() {
arch=$(uname_arch)
case "$arch" in
386) return 0 ;;
amd64) return 0 ;;
arm64) return 0 ;;
armv5) return 0 ;;
armv6) return 0 ;;
armv7) return 0 ;;
ppc64) return 0 ;;
ppc64le) return 0 ;;
mips) return 0 ;;
mipsle) return 0 ;;
mips64) return 0 ;;
mips64le) return 0 ;;
s390x) return 0 ;;
amd64p32) return 0 ;;
esac
log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib"
return 1
}
untar() {
tarball=$1
case "${tarball}" in
*.tar.gz | *.tgz) tar -xzf "${tarball}" ;;
*.tar) tar -xf "${tarball}" ;;
*.zip) unzip "${tarball}" ;;
*)
log_err "untar unknown archive format for ${tarball}"
return 1
;;
esac
}
mktmpdir() {
test -z "$TMPDIR" && TMPDIR="$(mktemp -d)"
mkdir -p "${TMPDIR}"
echo "${TMPDIR}"
}
http_download_curl() {
local_file=$1
source_url=$2
header=$3
if [ -z "$header" ]; then
code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url")
else
code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url")
fi
if [ "$code" != "200" ]; then
log_debug "http_download_curl received HTTP status $code"
return 1
fi
return 0
}
http_download_wget() {
local_file=$1
source_url=$2
header=$3
if [ -z "$header" ]; then
wget -q -O "$local_file" "$source_url"
else
wget -q --header "$header" -O "$local_file" "$source_url"
fi
}
http_download() {
log_debug "http_download $2"
if is_command curl; then
http_download_curl "$@"
return
elif is_command wget; then
http_download_wget "$@"
return
fi
log_crit "http_download unable to find wget or curl"
return 1
}
http_copy() {
tmp=$(mktemp)
http_download "${tmp}" "$1" "$2" || return 1
body=$(cat "$tmp")
rm -f "${tmp}"
echo "$body"
}
github_release() {
owner_repo=$1
version=$2
test -z "$version" && version="latest"
giturl="https://github.com/${owner_repo}/releases/${version}"
json=$(http_copy "$giturl" "Accept:application/json")
test -z "$json" && return 1
version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//')
test -z "$version" && return 1
echo "$version"
}
hash_sha256() {
TARGET=${1:-/dev/stdin}
if is_command gsha256sum; then
hash=$(gsha256sum "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command sha256sum; then
hash=$(sha256sum "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command shasum; then
hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command openssl; then
hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f a
else
log_crit "hash_sha256 unable to find command to compute sha-256 hash"
return 1
fi
}
hash_sha256_verify() {
TARGET=$1
checksums=$2
if [ -z "$checksums" ]; then
log_err "hash_sha256_verify checksum file not specified in arg2"
return 1
fi
BASENAME=${TARGET##*/}
want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
if [ -z "$want" ]; then
log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'"
return 1
fi
got=$(hash_sha256 "$TARGET")
if [ "$want" != "$got" ]; then
log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got"
return 1
fi
}
cat /dev/null <<EOF
------------------------------------------------------------------------
End of functions from https://github.com/client9/shlib
------------------------------------------------------------------------
EOF
PROJECT_NAME="task"
OWNER=go-task
REPO="task"
BINARY=task
FORMAT=tar.gz
OS=$(uname_os)
ARCH=$(uname_arch)
PREFIX="$OWNER/$REPO"
# use in logging routines
log_prefix() {
echo "$PREFIX"
}
PLATFORM="${OS}/${ARCH}"
GITHUB_DOWNLOAD=https://github.com/${OWNER}/${REPO}/releases/download
uname_os_check "$OS"
uname_arch_check "$ARCH"
parse_args "$@"
check_platform
tag_to_version
adjust_format
adjust_os
adjust_arch
log_info "found version: ${VERSION} for ${TAG}/${OS}/${ARCH}"
NAME=${BINARY}_${OS}_${ARCH}
TARBALL=${NAME}.${FORMAT}
TARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL}
CHECKSUM=task_checksums.txt
CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM}
execute

90
docs/installation.md Normal file
View File

@@ -0,0 +1,90 @@
# Installation
## Binary
Or you can download the binary from the [releases][releases] page and add to
your $PATH. DEB and RPM packages are also available.
The `task_checksums.txt` file contains the sha256 checksum for each file.
## Homebrew
If you're on macOS and have [Homebrew][homebrew] installed, getting Task is
as simple as running:
```bash
brew install go-task/tap/go-task
```
## Snap
Task is available for [Snapcraft][snapcraft], but keep in mind that your
Linux distribution should allow classic confinement for Snaps to Task work
right:
```bash
sudo snap install task
```
## Scoop
If you're on Windows and have [Scoop][scoop] installed, use `extras` bucket
to install Task like:
```cmd
scoop bucket add extras
scoop install task
```
This installation method is community owned. After a new release of Task, it
may take some time until it's available on Scoop.
## Go
Task now uses [Go Modules](https://github.com/golang/go/wiki/Modules), which
means you may have trouble compiling it on older Go versions.
For CI environments we recommend using the [Install Script](#install-script)
instead, which is faster and more stable, since it'll just download the latest
released binary, instead of compiling the edge (master branch) version.
Installing in your `$GOPATH`:
```bash
go get -u -v github.com/go-task/task/cmd/task
```
Installing in another directory:
```bash
git clone https://github.com/go-task/task
cd task
# compiling binary to $GOPATH/bin
go install -v
# compiling it to another location
# use -o ./task.exe on Windows
go build -v -o ./task ./cmd/task
```
Both methods requires having the [Go][go] environment properly setup locally.
## Install script
We also have a [install script][installscript], which is very useful on
scenarios like CIs. Many thanks to [godownloader][godownloader] for allowing
easily generating this script.
```bash
curl -s https://taskfile.org/install.sh | sh
```
> This method will download the binary on the local `./bin` directory by default.
[go]: https://golang.org/
[snapcraft]: https://snapcraft.io/
[homebrew]: https://brew.sh/
[installscript]: https://github.com/go-task/task/blob/master/install-task.sh
[releases]: https://github.com/go-task/task/releases
[godownloader]: https://github.com/goreleaser/godownloader
[scoop]: https://scoop.sh/

39
docs/releasing_task.md Normal file
View File

@@ -0,0 +1,39 @@
# Releasing Task
The release process of Task is done with the help of
[GoReleaser][goreleaser]. You can test the release process locally by calling
the `test-release` task of the Taskfile.
The Travis CI should release automatically when a new
Git tag is pushed to master, either for the artifact uploading (raw executables
and DEB and RPM packages)
# Homebrew
To release a new version on the [Homebrew tap][homebrewtap] edit the
[Formula/go-task.rb][gotaskrb] file, updating with the new version, download
URL and sha256.
# Snapcraft
The exception is the publishing of a new version of the
[snap package][snappackage]. This current require two steps after publishing
the binaries:
* Updating the current version on [snapcraft.yaml][snapcraftyaml];
* Moving both `i386` and `amd64` new artifacts to the stable channel on
the [Snapcraft dashboard][snapcraftdashboard]
# Scoop
Scoop is a community owned installation method. Scoop owners usually take care
of updating versions there by editing
[this file](https://github.com/lukesampson/scoop-extras/blob/master/task.json).
If you think its Task version is outdated, open an issue to let us know.
[goreleaser]: https://goreleaser.com/#continuous_integration
[homebrewtap]: https://github.com/go-task/homebrew-tap
[gotaskrb]: https://github.com/go-task/homebrew-tap/blob/master/Formula/go-task.rb
[snappackage]: https://github.com/go-task/snap
[snapcraftyaml]: https://github.com/go-task/snap/blob/master/snap/snapcraft.yaml#L2
[snapcraftdashboard]: https://dashboard.snapcraft.io/

148
docs/taskfile_versions.md Normal file
View File

@@ -0,0 +1,148 @@
# Taskfile Versions
The Taskfile syntax and features changed with time. This document explains what
changed on each version and how to upgrade your Taskfile.
## What the Taskfile version mean
The Taskfile version follows the Task version. E.g. the change to Taskfile
version `2` means that Task `v2.0.0` should be release to support it.
The `version:` key on Taskfile accepts a semver string, so either `2`, `2.0` or
`2.0.0` is accepted. You you choose to use `2.0` Task will not enable future
`2.1` features, but if you choose to use `2`, than any `2.x.x` features will be
available, but not `3.0.0+`.
## Version 1
In the first version of the `Taskfile`, the `version:` key was not available,
because the tasks was in the root of the YAML document. Like this:
```yaml
echo:
cmds:
- echo "Hello, World!"
```
The variable priority order was also different:
1. Call variables
2. Environment
3. Task variables
4. `Taskvars.yml` variables
## Version 2.0
At version 2, we introduced the `version:` key, to allow us to envolve Task
with new features without breaking existing Taskfiles. The new syntax is as
follows:
```yaml
version: '2'
tasks:
echo:
cmds:
- echo "Hello, World!"
```
Version 2 allows you to write global variables directly in the Taskfile,
if you don't want to create a `Taskvars.yml`:
```yaml
version: '2'
vars:
GREETING: Hello, World!
tasks:
greet:
cmds:
- echo "{{.GREETING}}"
```
The variable priority order changed to the following:
1. Task variables
2. Call variables
3. Taskfile variables
4. Taskvars file variables
5. Environment variables
A new global option was added to configure the number of variables expansions
(which default to 2):
```yaml
version: '2'
expansions: 3
vars:
FOO: foo
BAR: bar
BAZ: baz
FOOBAR: "{{.FOO}}{{.BAR}}"
FOOBARBAZ: "{{.FOOBAR}}{{.BAZ}}"
tasks:
default:
cmds:
- echo "{{.FOOBARBAZ}}"
```
## Version 2.1
Version 2.1 includes a global `output` option, to allow having more control
over how commands output are printed to the console
(see [documentation][output] for more info):
```yaml
version: '2'
output: prefixed
tasks:
server:
cmds:
- go run main.go
prefix: server
```
From this version it's not also possible to ignore errors of a command or task
(check documentation [here][ignore_errors]):
```yaml
version: '2'
tasks:
example-1:
cmds:
- cmd: exit 1
ignore_error: true
- echo "This will be print"
example-2:
cmds:
- exit 1
- echo "This will be print"
ignore_error: true
```
## Version 2.2
Version 2.2 comes with a global `includes` options to include other
Taskfiles:
```yaml
version: '2'
includes:
docs: ./documentation # will look for ./documentation/Taskfile.yml
docker: ./DockerTasks.yml
```
Please check the [documentation][includes]
[output]: usage.md#output-syntax
[ignore_errors]: usage.md#ignore-errors
[includes]: usage.md#including-other-taskfiles

720
docs/usage.md Normal file
View File

@@ -0,0 +1,720 @@
# Usage
## Getting started
Create a file called `Taskfile.yml` in the root of your project.
The `cmds` attribute should contain the commands of a task.
The example below allows compiling a Go app and uses [Minify][minify] to concat
and minify multiple CSS files into a single one.
```yaml
version: '2'
tasks:
build:
cmds:
- go build -v -i main.go
assets:
cmds:
- minify -o public/style.css src/css
```
Running the tasks is as simple as running:
```bash
task assets build
```
Task uses [github.com/mvdan/sh](https://github.com/mvdan/sh), a native Go sh
interpreter. So you can write sh/bash commands and it will work even on
Windows, where `sh` or `bash` are usually not available. Just remember any
executable called must be available by the OS or in PATH.
If you omit a task name, "default" will be assumed.
## Environment
You can use `env` to set custom environment variables for a specific task:
```yaml
version: '2'
tasks:
greet:
cmds:
- echo $GREETING
env:
GREETING: Hey, there!
```
Additionally, you can set globally environment variables, that'll be available
to all tasks:
```yaml
version: '2'
env:
GREETING: Hey, there!
tasks:
greet:
cmds:
- echo $GREETING
```
> NOTE: `env` supports expansion and and retrieving output from a shell command
> just like variables, as you can see on the [Variables](#variables) section.
## Operating System specific tasks
If you add a `Taskfile_{{GOOS}}.yml` you can override or amend your Taskfile
based on the operating system.
Example:
Taskfile.yml:
```yaml
version: '2'
tasks:
build:
cmds:
- echo "default"
```
Taskfile_linux.yml:
```yaml
version: '2'
tasks:
build:
cmds:
- echo "linux"
```
Will print out `linux` and not `default`.
Keep in mind that the version of the files should match. Also, when redefining
a task the whole task is replaced, properties of the task are not merged.
It's also possible to have an OS specific `Taskvars.yml` file, like
`Taskvars_windows.yml`, `Taskvars_linux.yml`, or `Taskvars_darwin.yml`. See the
[variables section](#variables) below.
## Including other Taskfiles
> This feature is still experimental and may have bugs.
If you want to share tasks between different projects (Taskfiles), you can use
the importing mechanism to include other Taskfiles using the `includes` keyword:
```yaml
version: '2'
includes:
docs: ./documentation # will look for ./documentation/Taskfile.yml
docker: ./DockerTasks.yml
```
The tasks described in the given Taskfiles will be available with the informed
namespace. So, you'd call `task docs:serve` to run the `serve` task from
`documentation/Taskfile.yml` or `task docker:build` to run the `build` task
from the `DockerTasks.yml` file.
> The included Taskfiles must be using the same schema version the main
> 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. =)
## Task directory
By default, tasks will be executed in the directory where the Taskfile is
located. But you can easily make the task run in another folder informing
`dir`:
```yaml
version: '2'
tasks:
serve:
dir: public/www
cmds:
# run http server
- caddy
```
## Task dependencies
You may have tasks that depend on others. Just pointing them on `deps` will
make them run automatically before running the parent task:
```yaml
version: '2'
tasks:
build:
deps: [assets]
cmds:
- go build -v -i main.go
assets:
cmds:
- minify -o public/style.css src/css
```
In the above example, `assets` will always run right before `build` if you run
`task build`.
A task can have only dependencies and no commands to group tasks together:
```yaml
version: '2'
tasks:
assets:
deps: [js, css]
js:
cmds:
- minify -o public/script.js src/js
css:
cmds:
- minify -o public/style.css src/css
```
If there is more than one dependency, they always run in parallel for better
performance.
If you want to pass information to dependencies, you can do that the same
manner as you would to [call another task](#calling-another-task):
```yaml
version: '2'
tasks:
default:
deps:
- task: echo_sth
vars: {TEXT: "before 1"}
- task: echo_sth
vars: {TEXT: "before 2"}
cmds:
- echo "after"
echo_sth:
cmds:
- echo {{.TEXT}}
```
## Calling another task
When a task has many dependencies, they are executed concurrently. This will
often result in a faster build pipeline. But in some situations you may need
to call other tasks serially. In this case, just use the following syntax:
```yaml
version: '2'
tasks:
main-task:
cmds:
- task: task-to-be-called
- task: another-task
- echo "Both done"
task-to-be-called:
cmds:
- echo "Task to be called"
another-task:
cmds:
- echo "Another task"
```
Overriding variables in the called task is as simple as informing `vars`
attribute:
```yaml
version: '2'
tasks:
main-task:
cmds:
- task: write-file
vars: {FILE: "hello.txt", CONTENT: "Hello!"}
- task: write-file
vars: {FILE: "world.txt", CONTENT: "World!"}
write-file:
cmds:
- echo "{{.CONTENT}}" > {{.FILE}}
```
The above syntax is also supported in `deps`.
> NOTE: If you want to call a task declared in the root Taskfile from within an
> [included Taskfile](#including-other-taskfiles), add a leading `:` like this:
> `task: :task-name`.
## Prevent unnecessary work
If a task generates something, you can inform Task the source and generated
files, so Task will prevent to run them if not necessary.
```yaml
version: '2'
tasks:
build:
deps: [js, css]
cmds:
- go build -v -i main.go
js:
cmds:
- minify -o public/script.js src/js
sources:
- src/js/**/*.js
generates:
- public/script.js
css:
cmds:
- minify -o public/style.css src/css
sources:
- src/css/**/*.css
generates:
- public/style.css
```
`sources` and `generates` can be files or file patterns. When both are given,
Task will compare the modification date/time of the files to determine if it's
necessary to run the task. If not, it will just print a message like
`Task "js" is up to date`.
If you prefer this check to be made by the content of the files, instead of
its timestamp, just set the `method` property to `checksum`.
You will probably want to ignore the `.task` folder in your `.gitignore` file
(It's there that Task stores the last checksum).
```yaml
version: '2'
tasks:
build:
cmds:
- go build .
sources:
- ./*.go
generates:
- app{{exeExt}}
method: checksum
```
> TIP: method `none` skips any validation and always run the task.
Alternatively, you can inform a sequence of tests as `status`. If no error
is returned (exit status 0), the task is considered up-to-date:
```yaml
version: '2'
tasks:
generate-files:
cmds:
- mkdir directory
- touch directory/file1.txt
- touch directory/file2.txt
# test existence of files
status:
- test -d directory
- test -f directory/file1.txt
- test -f directory/file2.txt
```
You can use `--force` or `-f` if you want to force a task to run even when
up-to-date.
Also, `task --status [tasks]...` will exit with a non-zero exit code if any of
the tasks are not up-to-date.
## Variables
When doing interpolation of variables, Task will look for the below.
They are listed below in order of importance (e.g. most important first):
- Variables declared locally in the task
- Variables given while calling a task from another.
(See [Calling another task](#calling-another-task) above)
- Variables declared in the `vars:` option in the `Taskfile`
- Variables available in the `Taskvars.yml` file
- Environment variables
Example of sending parameters with environment variables:
```bash
$ TASK_VARIABLE=a-value task do-something
```
Since some shells don't support above syntax to set environment variables
(Windows) tasks also accepts a similar style when not in the beginning of
the command. Variables given in this form are only visible to the task called
right before.
```bash
$ task write-file FILE=file.txt "CONTENT=Hello, World!" print "MESSAGE=All done!"
```
Example of locally declared vars:
```yaml
version: '2'
tasks:
print-var:
cmds:
echo "{{.VAR}}"
vars:
VAR: Hello!
```
Example of global vars in a `Taskfile.yml`:
```yaml
version: '2'
vars:
GREETING: Hello from Taskfile!
tasks:
greet:
cmds:
- echo "{{.GREETING}}"
```
Example of `Taskvars.yml` file:
```yaml
PROJECT_NAME: My Project
DEV_MODE: production
GIT_COMMIT: {sh: git log -n 1 --format=%h}
```
### Variables expansion
Variables are expanded 2 times by default. You can change that by setting the
`expansions:` option. Change that will be necessary if you compose many
variables together:
```yaml
version: '2'
expansions: 3
vars:
FOO: foo
BAR: bar
BAZ: baz
FOOBAR: "{{.FOO}}{{.BAR}}"
FOOBARBAZ: "{{.FOOBAR}}{{.BAZ}}"
tasks:
default:
cmds:
- echo "{{.FOOBARBAZ}}"
```
### Dynamic variables
The below syntax (`sh:` prop in a variable) is considered a dynamic variable.
The value will be treated as a command and the output assigned. If there is one
or more trailing newlines, the last newline will be trimmed.
```yaml
version: '2'
tasks:
build:
cmds:
- go build -ldflags="-X main.Version={{.GIT_COMMIT}}" main.go
vars:
GIT_COMMIT:
sh: git log -n 1 --format=%h
```
This works for all types of variables.
## Go's template engine
Task parse commands as [Go's template engine][gotemplate] before executing
them. Variables are accessible through dot syntax (`.VARNAME`).
All functions by the Go's [sprig lib](http://masterminds.github.io/sprig/)
are available. The following example gets the current date in a given format:
```yaml
version: '2'
tasks:
print-date:
cmds:
- echo {{now | date "2006-01-02"}}
```
Task also adds the following functions:
- `OS`: Returns operating system. Possible values are "windows", "linux",
"darwin" (macOS) and "freebsd".
- `ARCH`: return the architecture Task was compiled to: "386", "amd64", "arm"
or "s390x".
- `splitLines`: Splits Unix (\n) and Windows (\r\n) styled newlines.
- `catLines`: Replaces Unix (\n) and Windows (\r\n) styled newlines with a space.
- `toSlash`: Does nothing on Unix, but on Windows converts a string from `\`
path format to `/`.
- `fromSlash`: Opposite of `toSlash`. Does nothing on Unix, but on Windows
converts a string from `\` path format to `/`.
- `exeExt`: Returns the right executable extension for the current OS
(`".exe"` for Windows, `""` for others).
Example:
```yaml
version: '2'
tasks:
print-os:
cmds:
- echo '{{OS}} {{ARCH}}'
- echo '{{if eq OS "windows"}}windows-command{{else}}unix-command{{end}}'
# This will be path/to/file on Unix but path\to\file on Windows
- echo '{{fromSlash "path/to/file"}}'
enumerated-file:
vars:
CONTENT: |
foo
bar
cmds:
- |
cat << EOF > output.txt
{{range $i, $line := .CONTENT | splitLines -}}
{{printf "%3d" $i}}: {{$line}}
{{end}}EOF
```
## Help
Running `task --list` (or `task -l`) lists all tasks with a description.
The following Taskfile:
```yaml
version: '2'
tasks:
build:
desc: Build the go binary.
cmds:
- go build -v -i main.go
test:
desc: Run all the go tests.
cmds:
- go test -race ./...
js:
cmds:
- minify -o public/script.js src/js
css:
cmds:
- minify -o public/style.css src/css
```
would print the following output:
```bash
* build: Build the go binary.
* test: Run all the go tests.
```
## Silent mode
Silent mode disables echoing of commands before Task runs it.
For the following Taskfile:
```yaml
version: '2'
tasks:
echo:
cmds:
- echo "Print something"
```
Normally this will be print:
```sh
echo "Print something"
Print something
```
With silent mode on, the below will be print instead:
```sh
Print something
```
There's three ways to enable silent mode:
* At command level:
```yaml
version: '2'
tasks:
echo:
cmds:
- cmd: echo "Print something"
silent: true
```
* At task level:
```yaml
version: '2'
tasks:
echo:
cmds:
- echo "Print something"
silent: true
```
* Or globally with `--silent` or `-s` flag
If you want to suppress STDOUT instead, just redirect a command to `/dev/null`:
```yaml
version: '2'
tasks:
echo:
cmds:
- echo "This will print nothing" > /dev/null
```
## Dry run mode
Dry run mode (`--dry`) compiles and steps through each task, printing the commands
that would be run without executing them. This is useful for debugging your Taskfiles.
## Ignore errors
You have the option to ignore errors during command execution.
Given the following Taskfile:
```yaml
version: '2'
tasks:
echo:
cmds:
- exit 1
- echo "Hello World"
```
Task will abort the execution after running `exit 1` because the status code `1` stands for `EXIT_FAILURE`.
However it is possible to continue with execution using `ignore_error`:
```yaml
version: '2'
tasks:
echo:
cmds:
- cmd: exit 1
ignore_error: true
- echo "Hello World"
```
`ignore_error` can also be set for a task, which mean errors will be suppressed
for all commands. But keep in mind this option won't propagate to other tasks
called either by `deps` or `cmds`!
## Output syntax
By default, Task just redirect the STDOUT and STDERR of the running commands
to the shell in real time. This is good for having live feedback for log
printed by commands, but the output can become messy if you have multiple
commands running at the same time and printing lots of stuff.
To make this more customizable, there are currently three different output
options you can choose:
- `interleaved` (default)
- `group`
- `prefixed`
To choose another one, just set it to root in the Taskfile:
```yaml
version: '2'
output: 'group'
tasks:
# ...
```
The `group` output will print the entire output of a command once, after it
finishes, so you won't have live feedback for commands that take a long time
to run.
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
with the `prefix:` attribute:
```yaml
version: '2'
output: prefixed
tasks:
default:
deps:
- task: print
vars: {TEXT: foo}
- task: print
vars: {TEXT: bar}
- task: print
vars: {TEXT: baz}
print:
cmds:
- echo "{{.TEXT}}"
prefix: "print-{{.TEXT}}"
silent: true
```
```bash
$ task default
[print-foo] foo
[print-bar] bar
[print-baz] baz
```
> The `output` option can also be specified by the `--output` or `-o` flags.
## Watch tasks
If you give a `--watch` or `-w` argument, task will watch for file changes
and run the task again. This requires the `sources` attribute to be given,
so task know which files to watch.
[gotemplate]: https://golang.org/pkg/text/template/
[minify]: https://github.com/tdewolff/minify/tree/master/cmd/minify

View File

@@ -10,14 +10,6 @@ var (
ErrTaskfileAlreadyExists = errors.New("task: A Taskfile already exists")
)
type taskFileNotFound struct {
taskFile string
}
func (err taskFileNotFound) Error() string {
return fmt.Sprintf(`task: No task file found (is it named "%s"?). Use "task --init" to create a new one`, err.taskFile)
}
type taskNotFoundError struct {
taskName string
}
@@ -35,31 +27,6 @@ func (err *taskRunError) Error() string {
return fmt.Sprintf(`task: Failed to run task "%s": %v`, err.taskName, err.err)
}
type cyclicDepError struct {
taskName string
}
func (err *cyclicDepError) Error() string {
return fmt.Sprintf(`task: Cyclic dependency of task "%s" detected`, err.taskName)
}
type cantWatchNoSourcesError struct {
taskName string
}
func (err *cantWatchNoSourcesError) Error() string {
return fmt.Sprintf(`task: Can't watch task "%s" because it has no specified sources`, err.taskName)
}
type dynamicVarError struct {
cause error
cmd string
}
func (err *dynamicVarError) Error() string {
return fmt.Sprintf(`task: Command "%s" in taskvars file failed: %s`, err.cmd, err.cause)
}
// MaximumTaskCallExceededError is returned when a task is called too
// many times. In this case you probably have a cyclic dependendy or
// infinite loop

23
go.mod Normal file
View File

@@ -0,0 +1,23 @@
module github.com/go-task/task/v2
require (
github.com/Masterminds/semver v1.4.2
github.com/Masterminds/sprig v2.16.0+incompatible
github.com/aokoli/goutils v1.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.0.0 // indirect
github.com/huandu/xstrings v1.1.0 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/mattn/go-zglob v0.0.1
github.com/mitchellh/go-homedir v1.0.0
github.com/radovskyb/watcher v1.0.5
github.com/spf13/pflag v1.0.3
github.com/stretchr/testify v1.3.0
golang.org/x/crypto v0.0.0-20180830192347-182538f80094 // indirect
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d // indirect
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f
golang.org/x/sys v0.0.0-20180831094639-fa5fdf94c789 // indirect
gopkg.in/yaml.v2 v2.2.1
mvdan.cc/sh v2.6.3+incompatible
)

47
go.sum Normal file
View File

@@ -0,0 +1,47 @@
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/sprig v2.16.0+incompatible h1:QZbMUPxRQ50EKAq3LFMnxddMu88/EUUG3qmxwtDmPsY=
github.com/Masterminds/sprig v2.16.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg=
github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/huandu/xstrings v1.1.0 h1:9oZY6Z/H3A1gytJxzuicbmV5QoR8M1TAPcn9WTg7vqg=
github.com/huandu/xstrings v1.1.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY=
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/radovskyb/watcher v1.0.5 h1:wqt7gb+HjGacvFoLTKeT44C+XVPxu7bvHvKT1IvZ7rw=
github.com/radovskyb/watcher v1.0.5/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.0.0-20180830192347-182538f80094 h1:rVTAlhYa4+lCfNxmAIEOGQRoD23UqP72M3+rSWVGDTg=
golang.org/x/crypto v0.0.0-20180830192347-182538f80094/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180831094639-fa5fdf94c789 h1:T8D7l6WB3tLu+VpKvw06ieD/OhBi1XpJmG1U/FtttZg=
golang.org/x/sys v0.0.0-20180831094639-fa5fdf94c789/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
mvdan.cc/sh v2.6.3+incompatible h1:uXnnFNSBQbKUwwh2iBSkVjG+GbwoMuI+UmBVPnNiWhA=
mvdan.cc/sh v2.6.3+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=

17
help.go
View File

@@ -4,31 +4,34 @@ import (
"fmt"
"sort"
"text/tabwriter"
"github.com/go-task/task/v2/internal/taskfile"
)
// PrintTasksHelp prints help os tasks that have a description
func (e *Executor) PrintTasksHelp() {
tasks := e.tasksWithDesc()
if len(tasks) == 0 {
e.outf("task: No tasks with description available")
e.Logger.Outf("task: No tasks with description available")
return
}
e.outf("task: Available tasks for this project:")
e.Logger.Outf("task: Available tasks for this project:")
// Format in tab-separated columns with a tab stop of 8.
w := tabwriter.NewWriter(e.Stdout, 0, 8, 0, '\t', 0)
for _, task := range tasks {
fmt.Fprintln(w, fmt.Sprintf("* %s: \t%s", task, e.Tasks[task].Desc))
fmt.Fprintf(w, "* %s: \t%s\n", task.Task, task.Desc)
}
w.Flush()
}
func (e *Executor) tasksWithDesc() (tasks []string) {
for name, task := range e.Tasks {
func (e *Executor) tasksWithDesc() (tasks []*taskfile.Task) {
tasks = make([]*taskfile.Task, 0, len(e.Taskfile.Tasks))
for _, task := range e.Taskfile.Tasks {
if task.Desc != "" {
tasks = append(tasks, name)
tasks = append(tasks, task)
}
}
sort.Strings(tasks)
sort.Slice(tasks, func(i, j int) bool { return tasks[i].Task < tasks[j].Task })
return
}

29
init.go
View File

@@ -8,24 +8,29 @@ import (
"path/filepath"
)
const defaultTaskfile = `# github.com/go-task/task
const defaultTaskfile = `# https://taskfile.org
default:
cmds:
- echo "Hello, World!"
version: '2'
vars:
GREETING: Hello, World!
tasks:
default:
cmds:
- echo "{{.GREETING}}"
silent: true
`
// InitTaskfile Taskfile creates a new Taskfile
func InitTaskfile(w io.Writer, path string) error {
for _, f := range []string{"Taskfile.yml", "Taskfile.toml", "Taskfile.json"} {
f = filepath.Join(path, f)
if _, err := os.Stat(f); err == nil {
return ErrTaskfileAlreadyExists
}
func InitTaskfile(w io.Writer, dir string) error {
f := filepath.Join(dir, "Taskfile.yml")
if _, err := os.Stat(f); err == nil {
return ErrTaskfileAlreadyExists
}
f := filepath.Join(path, "Taskfile.yml")
if err := ioutil.WriteFile(f, []byte(defaultTaskfile), 0666); err != nil {
if err := ioutil.WriteFile(f, []byte(defaultTaskfile), 0644); err != nil {
return err
}
fmt.Fprintf(w, "Taskfile.yml created in the current directory\n")

390
install-task.sh Executable file
View File

@@ -0,0 +1,390 @@
#!/bin/sh
set -e
# Code generated by godownloader on 2018-04-07T17:47:38Z. DO NOT EDIT.
#
usage() {
this=$1
cat <<EOF
$this: download go binaries for go-task/task
Usage: $this [-b] bindir [-d] [tag]
-b sets bindir or installation directory, Defaults to ./bin
-d turns on debug logging
[tag] is a tag from
https://github.com/go-task/task/releases
If tag is missing, then the latest will be used.
Generated by godownloader
https://github.com/goreleaser/godownloader
EOF
exit 2
}
parse_args() {
#BINDIR is ./bin unless set be ENV
# over-ridden by flag below
BINDIR=${BINDIR:-./bin}
while getopts "b:dh?" arg; do
case "$arg" in
b) BINDIR="$OPTARG" ;;
d) log_set_priority 10 ;;
h | \?) usage "$0" ;;
esac
done
shift $((OPTIND - 1))
TAG=$1
}
# this function wraps all the destructive operations
# if a curl|bash cuts off the end of the script due to
# network, either nothing will happen or will syntax error
# out preventing half-done work
execute() {
tmpdir=$(mktmpdir)
log_debug "downloading files into ${tmpdir}"
http_download "${tmpdir}/${TARBALL}" "${TARBALL_URL}"
http_download "${tmpdir}/${CHECKSUM}" "${CHECKSUM_URL}"
hash_sha256_verify "${tmpdir}/${TARBALL}" "${tmpdir}/${CHECKSUM}"
srcdir="${tmpdir}"
(cd "${tmpdir}" && untar "${TARBALL}")
install -d "${BINDIR}"
for binexe in "task" ; do
if [ "$OS" = "windows" ]; then
binexe="${binexe}.exe"
fi
install "${srcdir}/${binexe}" "${BINDIR}/"
log_info "installed ${BINDIR}/${binexe}"
done
}
is_supported_platform() {
platform=$1
found=1
case "$platform" in
windows/386) found=0 ;;
windows/amd64) found=0 ;;
darwin/386) found=0 ;;
darwin/amd64) found=0 ;;
linux/386) found=0 ;;
linux/amd64) found=0 ;;
esac
case "$platform" in
darwin/386) found=1 ;;
esac
return $found
}
check_platform() {
if is_supported_platform "$PLATFORM"; then
# optional logging goes here
true
else
log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
exit 1
fi
}
tag_to_version() {
if [ -z "${TAG}" ]; then
log_info "checking GitHub for latest tag"
else
log_info "checking GitHub for tag '${TAG}'"
fi
REALTAG=$(github_release "$OWNER/$REPO" "${TAG}") && true
if test -z "$REALTAG"; then
log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${PREFIX}/releases for details"
exit 1
fi
# if version starts with 'v', remove it
TAG="$REALTAG"
VERSION=${TAG#v}
}
adjust_format() {
# change format (tar.gz or zip) based on ARCH
case ${ARCH} in
windows) FORMAT=zip ;;
esac
true
}
adjust_os() {
# adjust archive name based on OS
true
}
adjust_arch() {
# adjust archive name based on ARCH
true
}
cat /dev/null <<EOF
------------------------------------------------------------------------
https://github.com/client9/shlib - portable posix shell functions
Public domain - http://unlicense.org
https://github.com/client9/shlib/blob/master/LICENSE.md
but credit (and pull requests) appreciated.
------------------------------------------------------------------------
EOF
is_command() {
command -v "$1" >/dev/null
}
echoerr() {
echo "$@" 1>&2
}
log_prefix() {
echo "$0"
}
_logp=6
log_set_priority() {
_logp="$1"
}
log_priority() {
if test -z "$1"; then
echo "$_logp"
return
fi
[ "$1" -le "$_logp" ]
}
log_tag() {
case $1 in
0) echo "emerg" ;;
1) echo "alert" ;;
2) echo "crit" ;;
3) echo "err" ;;
4) echo "warning" ;;
5) echo "notice" ;;
6) echo "info" ;;
7) echo "debug" ;;
*) echo "$1" ;;
esac
}
log_debug() {
log_priority 7 || return 0
echoerr "$(log_prefix)" "$(log_tag 7)" "$@"
}
log_info() {
log_priority 6 || return 0
echoerr "$(log_prefix)" "$(log_tag 6)" "$@"
}
log_err() {
log_priority 3 || return 0
echoerr "$(log_prefix)" "$(log_tag 3)" "$@"
}
log_crit() {
log_priority 2 || return 0
echoerr "$(log_prefix)" "$(log_tag 2)" "$@"
}
uname_os() {
os=$(uname -s | tr '[:upper:]' '[:lower:]')
case "$os" in
msys_nt) os="windows" ;;
esac
echo "$os"
}
uname_arch() {
arch=$(uname -m)
case $arch in
x86_64) arch="amd64" ;;
x86) arch="386" ;;
i686) arch="386" ;;
i386) arch="386" ;;
aarch64) arch="arm64" ;;
armv5*) arch="arm5" ;;
armv6*) arch="arm6" ;;
armv7*) arch="arm7" ;;
esac
echo ${arch}
}
uname_os_check() {
os=$(uname_os)
case "$os" in
darwin) return 0 ;;
dragonfly) return 0 ;;
freebsd) return 0 ;;
linux) return 0 ;;
android) return 0 ;;
nacl) return 0 ;;
netbsd) return 0 ;;
openbsd) return 0 ;;
plan9) return 0 ;;
solaris) return 0 ;;
windows) return 0 ;;
esac
log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib"
return 1
}
uname_arch_check() {
arch=$(uname_arch)
case "$arch" in
386) return 0 ;;
amd64) return 0 ;;
arm64) return 0 ;;
armv5) return 0 ;;
armv6) return 0 ;;
armv7) return 0 ;;
ppc64) return 0 ;;
ppc64le) return 0 ;;
mips) return 0 ;;
mipsle) return 0 ;;
mips64) return 0 ;;
mips64le) return 0 ;;
s390x) return 0 ;;
amd64p32) return 0 ;;
esac
log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib"
return 1
}
untar() {
tarball=$1
case "${tarball}" in
*.tar.gz | *.tgz) tar -xzf "${tarball}" ;;
*.tar) tar -xf "${tarball}" ;;
*.zip) unzip "${tarball}" ;;
*)
log_err "untar unknown archive format for ${tarball}"
return 1
;;
esac
}
mktmpdir() {
test -z "$TMPDIR" && TMPDIR="$(mktemp -d)"
mkdir -p "${TMPDIR}"
echo "${TMPDIR}"
}
http_download_curl() {
local_file=$1
source_url=$2
header=$3
if [ -z "$header" ]; then
code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url")
else
code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url")
fi
if [ "$code" != "200" ]; then
log_debug "http_download_curl received HTTP status $code"
return 1
fi
return 0
}
http_download_wget() {
local_file=$1
source_url=$2
header=$3
if [ -z "$header" ]; then
wget -q -O "$local_file" "$source_url"
else
wget -q --header "$header" -O "$local_file" "$source_url"
fi
}
http_download() {
log_debug "http_download $2"
if is_command curl; then
http_download_curl "$@"
return
elif is_command wget; then
http_download_wget "$@"
return
fi
log_crit "http_download unable to find wget or curl"
return 1
}
http_copy() {
tmp=$(mktemp)
http_download "${tmp}" "$1" "$2" || return 1
body=$(cat "$tmp")
rm -f "${tmp}"
echo "$body"
}
github_release() {
owner_repo=$1
version=$2
test -z "$version" && version="latest"
giturl="https://github.com/${owner_repo}/releases/${version}"
json=$(http_copy "$giturl" "Accept:application/json")
test -z "$json" && return 1
version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//')
test -z "$version" && return 1
echo "$version"
}
hash_sha256() {
TARGET=${1:-/dev/stdin}
if is_command gsha256sum; then
hash=$(gsha256sum "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command sha256sum; then
hash=$(sha256sum "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command shasum; then
hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command openssl; then
hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f a
else
log_crit "hash_sha256 unable to find command to compute sha-256 hash"
return 1
fi
}
hash_sha256_verify() {
TARGET=$1
checksums=$2
if [ -z "$checksums" ]; then
log_err "hash_sha256_verify checksum file not specified in arg2"
return 1
fi
BASENAME=${TARGET##*/}
want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
if [ -z "$want" ]; then
log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'"
return 1
fi
got=$(hash_sha256 "$TARGET")
if [ "$want" != "$got" ]; then
log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got"
return 1
fi
}
cat /dev/null <<EOF
------------------------------------------------------------------------
End of functions from https://github.com/client9/shlib
------------------------------------------------------------------------
EOF
PROJECT_NAME="task"
OWNER=go-task
REPO="task"
BINARY=task
FORMAT=tar.gz
OS=$(uname_os)
ARCH=$(uname_arch)
PREFIX="$OWNER/$REPO"
# use in logging routines
log_prefix() {
echo "$PREFIX"
}
PLATFORM="${OS}/${ARCH}"
GITHUB_DOWNLOAD=https://github.com/${OWNER}/${REPO}/releases/download
uname_os_check "$OS"
uname_arch_check "$ARCH"
parse_args "$@"
check_platform
tag_to_version
adjust_format
adjust_os
adjust_arch
log_info "found version: ${VERSION} for ${TAG}/${OS}/${ARCH}"
NAME=${BINARY}_${OS}_${ARCH}
TARBALL=${NAME}.${FORMAT}
TARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL}
CHECKSUM=task_checksums.txt
CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM}
execute

View File

@@ -4,7 +4,7 @@ import (
"errors"
"strings"
"github.com/go-task/task"
"github.com/go-task/task/v2/internal/taskfile"
)
var (
@@ -13,12 +13,12 @@ var (
)
// Parse parses command line argument: tasks and vars of each task
func Parse(args ...string) ([]task.Call, error) {
var calls []task.Call
func Parse(args ...string) ([]taskfile.Call, error) {
var calls []taskfile.Call
for _, arg := range args {
if !strings.Contains(arg, "=") {
calls = append(calls, task.Call{Task: arg})
calls = append(calls, taskfile.Call{Task: arg})
continue
}
if len(calls) < 1 {
@@ -26,11 +26,11 @@ func Parse(args ...string) ([]task.Call, error) {
}
if calls[len(calls)-1].Vars == nil {
calls[len(calls)-1].Vars = make(task.Vars)
calls[len(calls)-1].Vars = make(taskfile.Vars)
}
pair := strings.SplitN(arg, "=", 2)
calls[len(calls)-1].Vars[pair[0]] = task.Var{Static: pair[1]}
calls[len(calls)-1].Vars[pair[0]] = taskfile.Var{Static: pair[1]}
}
return calls, nil
}

View File

@@ -4,8 +4,8 @@ import (
"fmt"
"testing"
"github.com/go-task/task"
"github.com/go-task/task/internal/args"
"github.com/go-task/task/v2/internal/args"
"github.com/go-task/task/v2/internal/taskfile"
"github.com/stretchr/testify/assert"
)
@@ -13,12 +13,12 @@ import (
func TestArgs(t *testing.T) {
tests := []struct {
Args []string
Expected []task.Call
Expected []taskfile.Call
Err error
}{
{
Args: []string{"task-a", "task-b", "task-c"},
Expected: []task.Call{
Expected: []taskfile.Call{
{Task: "task-a"},
{Task: "task-b"},
{Task: "task-c"},
@@ -26,30 +26,30 @@ func TestArgs(t *testing.T) {
},
{
Args: []string{"task-a", "FOO=bar", "task-b", "task-c", "BAR=baz", "BAZ=foo"},
Expected: []task.Call{
Expected: []taskfile.Call{
{
Task: "task-a",
Vars: task.Vars{
"FOO": task.Var{Static: "bar"},
Vars: taskfile.Vars{
"FOO": taskfile.Var{Static: "bar"},
},
},
{Task: "task-b"},
{
Task: "task-c",
Vars: task.Vars{
"BAR": task.Var{Static: "baz"},
"BAZ": task.Var{Static: "foo"},
Vars: taskfile.Vars{
"BAR": taskfile.Var{Static: "baz"},
"BAZ": taskfile.Var{Static: "foo"},
},
},
},
},
{
Args: []string{"task-a", "CONTENT=with some spaces"},
Expected: []task.Call{
Expected: []taskfile.Call{
{
Task: "task-a",
Vars: task.Vars{
"CONTENT": task.Var{Static: "with some spaces"},
Vars: taskfile.Vars{
"CONTENT": taskfile.Var{Static: "with some spaces"},
},
},
},

View File

@@ -0,0 +1,12 @@
package compiler
import (
"github.com/go-task/task/v2/internal/taskfile"
)
// Compiler handles compilation of a task before its execution.
// E.g. variable merger, template processing, etc.
type Compiler interface {
GetVariables(t *taskfile.Task, call taskfile.Call) (taskfile.Vars, error)
HandleDynamicVar(v taskfile.Var) (string, error)
}

24
internal/compiler/env.go Normal file
View File

@@ -0,0 +1,24 @@
package compiler
import (
"os"
"strings"
"github.com/go-task/task/v2/internal/taskfile"
)
// GetEnviron the all return all environment variables encapsulated on a
// taskfile.Vars
func GetEnviron() taskfile.Vars {
var (
env = os.Environ()
m = make(taskfile.Vars, len(env))
)
for _, e := range env {
keyVal := strings.SplitN(e, "=", 2)
key, val := keyVal[0], keyVal[1]
m[key] = taskfile.Var{Static: val}
}
return m
}

View File

@@ -0,0 +1,137 @@
package v1
import (
"bytes"
"context"
"fmt"
"strings"
"sync"
"github.com/go-task/task/v2/internal/compiler"
"github.com/go-task/task/v2/internal/execext"
"github.com/go-task/task/v2/internal/logger"
"github.com/go-task/task/v2/internal/taskfile"
"github.com/go-task/task/v2/internal/templater"
)
var _ compiler.Compiler = &CompilerV1{}
type CompilerV1 struct {
Dir string
Vars taskfile.Vars
Logger *logger.Logger
dynamicCache map[string]string
muDynamicCache sync.Mutex
}
// GetVariables returns fully resolved variables following the priority order:
// 1. Call variables (should already have been resolved)
// 2. Environment (should not need to be resolved)
// 3. Task variables, resolved with access to:
// - call, taskvars and environment variables
// 4. Taskvars variables, resolved with access to:
// - environment variables
func (c *CompilerV1) GetVariables(t *taskfile.Task, call taskfile.Call) (taskfile.Vars, error) {
merge := func(dest taskfile.Vars, srcs ...taskfile.Vars) {
for _, src := range srcs {
for k, v := range src {
dest[k] = v
}
}
}
varsKeys := func(srcs ...taskfile.Vars) []string {
m := make(map[string]struct{})
for _, src := range srcs {
for k := range src {
m[k] = struct{}{}
}
}
lst := make([]string, 0, len(m))
for k := range m {
lst = append(lst, k)
}
return lst
}
replaceVars := func(dest taskfile.Vars, keys []string) error {
r := templater.Templater{Vars: dest}
for _, k := range keys {
v := dest[k]
dest[k] = taskfile.Var{
Static: r.Replace(v.Static),
Sh: r.Replace(v.Sh),
}
}
return r.Err()
}
resolveShell := func(dest taskfile.Vars, keys []string) error {
for _, k := range keys {
v := dest[k]
static, err := c.HandleDynamicVar(v)
if err != nil {
return err
}
dest[k] = taskfile.Var{Static: static}
}
return nil
}
update := func(dest taskfile.Vars, srcs ...taskfile.Vars) error {
merge(dest, srcs...)
// updatedKeys ensures template evaluation is run only once.
updatedKeys := varsKeys(srcs...)
if err := replaceVars(dest, updatedKeys); err != nil {
return err
}
return resolveShell(dest, updatedKeys)
}
// Resolve taskvars variables to "result" with environment override variables.
override := compiler.GetEnviron()
result := make(taskfile.Vars, len(c.Vars)+len(t.Vars)+len(override))
if err := update(result, c.Vars, override); err != nil {
return nil, err
}
// Resolve task variables to "result" with environment and call override variables.
merge(override, call.Vars)
if err := update(result, t.Vars, override); err != nil {
return nil, err
}
return result, nil
}
func (c *CompilerV1) HandleDynamicVar(v taskfile.Var) (string, error) {
if v.Static != "" || v.Sh == "" {
return v.Static, nil
}
c.muDynamicCache.Lock()
defer c.muDynamicCache.Unlock()
if c.dynamicCache == nil {
c.dynamicCache = make(map[string]string, 30)
}
if result, ok := c.dynamicCache[v.Sh]; ok {
return result, nil
}
var stdout bytes.Buffer
opts := &execext.RunCommandOptions{
Command: v.Sh,
Dir: c.Dir,
Stdout: &stdout,
Stderr: c.Logger.Stderr,
}
if err := execext.RunCommand(context.Background(), opts); err != nil {
return "", fmt.Errorf(`task: Command "%s" in taskvars file failed: %s`, opts.Command, err)
}
// Trim a single trailing newline from the result to make most command
// output easier to use in shell commands.
result := strings.TrimSuffix(stdout.String(), "\n")
c.dynamicCache[v.Sh] = result
c.Logger.VerboseErrf(`task: dynamic variable: '%s' result: '%s'`, v.Sh, result)
return result, nil
}

View File

@@ -0,0 +1,109 @@
package v2
import (
"bytes"
"context"
"fmt"
"strings"
"sync"
"github.com/go-task/task/v2/internal/compiler"
"github.com/go-task/task/v2/internal/execext"
"github.com/go-task/task/v2/internal/logger"
"github.com/go-task/task/v2/internal/taskfile"
"github.com/go-task/task/v2/internal/templater"
)
var _ compiler.Compiler = &CompilerV2{}
type CompilerV2 struct {
Dir string
Taskvars taskfile.Vars
TaskfileVars taskfile.Vars
Expansions int
Logger *logger.Logger
dynamicCache map[string]string
muDynamicCache sync.Mutex
}
// GetVariables returns fully resolved variables following the priority order:
// 1. Task variables
// 2. Call variables
// 3. Taskfile variables
// 4. Taskvars file variables
// 5. Environment variables
func (c *CompilerV2) GetVariables(t *taskfile.Task, call taskfile.Call) (taskfile.Vars, error) {
vr := varResolver{c: c, vars: compiler.GetEnviron()}
for _, vars := range []taskfile.Vars{c.Taskvars, c.TaskfileVars, call.Vars, t.Vars} {
for i := 0; i < c.Expansions; i++ {
vr.merge(vars)
}
}
return vr.vars, vr.err
}
type varResolver struct {
c *CompilerV2
vars taskfile.Vars
err error
}
func (vr *varResolver) merge(vars taskfile.Vars) {
if vr.err != nil {
return
}
tr := templater.Templater{Vars: vr.vars}
for k, v := range vars {
v = taskfile.Var{
Static: tr.Replace(v.Static),
Sh: tr.Replace(v.Sh),
}
static, err := vr.c.HandleDynamicVar(v)
if err != nil {
vr.err = err
return
}
vr.vars[k] = taskfile.Var{Static: static}
}
vr.err = tr.Err()
}
func (c *CompilerV2) HandleDynamicVar(v taskfile.Var) (string, error) {
if v.Static != "" || v.Sh == "" {
return v.Static, nil
}
c.muDynamicCache.Lock()
defer c.muDynamicCache.Unlock()
if c.dynamicCache == nil {
c.dynamicCache = make(map[string]string, 30)
}
if result, ok := c.dynamicCache[v.Sh]; ok {
return result, nil
}
var stdout bytes.Buffer
opts := &execext.RunCommandOptions{
Command: v.Sh,
Dir: c.Dir,
Stdout: &stdout,
Stderr: c.Logger.Stderr,
}
if err := execext.RunCommand(context.Background(), opts); err != nil {
return "", fmt.Errorf(`task: Command "%s" in taskvars file failed: %s`, opts.Command, err)
}
// Trim a single trailing newline from the result to make most command
// output easier to use in shell commands.
result := strings.TrimSuffix(stdout.String(), "\n")
c.dynamicCache[v.Sh] = result
c.Logger.VerboseErrf(`task: dynamic variable: '%s' result: '%s'`, v.Sh, result)
return result, nil
}

View File

@@ -4,15 +4,18 @@ import (
"context"
"errors"
"io"
"os"
"path/filepath"
"strings"
"mvdan.cc/sh/expand"
"mvdan.cc/sh/interp"
"mvdan.cc/sh/shell"
"mvdan.cc/sh/syntax"
)
// RunCommandOptions is the options for the RunCommand func
type RunCommandOptions struct {
Context context.Context
Command string
Dir string
Env []string
@@ -27,7 +30,7 @@ var (
)
// RunCommand runs a shell command
func RunCommand(opts *RunCommandOptions) error {
func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
if opts == nil {
return ErrNilOptions
}
@@ -37,20 +40,47 @@ func RunCommand(opts *RunCommandOptions) error {
return err
}
r := interp.Runner{
Context: opts.Context,
Dir: opts.Dir,
Env: opts.Env,
Exec: interp.DefaultExec,
Open: interp.OpenDevImpls(interp.DefaultOpen),
Stdin: opts.Stdin,
Stdout: opts.Stdout,
Stderr: opts.Stderr,
environ := opts.Env
if len(environ) == 0 {
environ = os.Environ()
}
if err = r.Reset(); err != nil {
r, err := interp.New(
interp.Dir(opts.Dir),
interp.Env(expand.ListEnviron(environ...)),
interp.Module(interp.DefaultExec),
interp.Module(interp.OpenDevImpls(interp.DefaultOpen)),
interp.StdIO(opts.Stdin, opts.Stdout, opts.Stderr),
)
if err != nil {
return err
}
return r.Run(p)
return r.Run(ctx, p)
}
// IsExitError returns true the given error is an exis status error
func IsExitError(err error) bool {
switch err.(type) {
case interp.ExitStatus, interp.ShellExitStatus:
return true
default:
return false
}
}
// Expand is a helper to mvdan.cc/shell.Fields that returns the first field
// if available.
func Expand(s string) (string, error) {
s = filepath.ToSlash(s)
s = strings.Replace(s, " ", `\ `, -1)
fields, err := shell.Fields(s, nil)
if err != nil {
return "", err
}
if len(fields) > 0 {
return fields[0], nil
}
return "", nil
}

38
internal/logger/logger.go Normal file
View File

@@ -0,0 +1,38 @@
package logger
import (
"fmt"
"io"
)
type Logger struct {
Stdout io.Writer
Stderr io.Writer
Verbose bool
}
func (l *Logger) Outf(s string, args ...interface{}) {
if len(args) == 0 {
s, args = "%s", []interface{}{s}
}
fmt.Fprintf(l.Stdout, s+"\n", args...)
}
func (l *Logger) VerboseOutf(s string, args ...interface{}) {
if l.Verbose {
l.Outf(s, args...)
}
}
func (l *Logger) Errf(s string, args ...interface{}) {
if len(args) == 0 {
s, args = "%s", []interface{}{s}
}
fmt.Fprintf(l.Stderr, s+"\n", args...)
}
func (l *Logger) VerboseErrf(s string, args ...interface{}) {
if l.Verbose {
l.Errf(s, args...)
}
}

26
internal/output/group.go Normal file
View File

@@ -0,0 +1,26 @@
package output
import (
"bytes"
"io"
)
type Group struct{}
func (Group) WrapWriter(w io.Writer, _ string) io.WriteCloser {
return &groupWriter{writer: w}
}
type groupWriter struct {
writer io.Writer
buff bytes.Buffer
}
func (gw *groupWriter) Write(p []byte) (int, error) {
return gw.buff.Write(p)
}
func (gw *groupWriter) Close() error {
_, err := io.Copy(gw.writer, &gw.buff)
return err
}

View File

@@ -0,0 +1,23 @@
package output
import (
"io"
)
type Interleaved struct{}
func (Interleaved) WrapWriter(w io.Writer, _ string) io.WriteCloser {
return nopWriterCloser{w: w}
}
type nopWriterCloser struct {
w io.Writer
}
func (wc nopWriterCloser) Write(p []byte) (int, error) {
return wc.w.Write(p)
}
func (wc nopWriterCloser) Close() error {
return nil
}

View File

@@ -0,0 +1,9 @@
package output
import (
"io"
)
type Output interface {
WrapWriter(w io.Writer, prefix string) io.WriteCloser
}

View File

@@ -0,0 +1,62 @@
package output_test
import (
"bytes"
"fmt"
"testing"
"github.com/go-task/task/v2/internal/output"
"github.com/stretchr/testify/assert"
)
func TestInterleaved(t *testing.T) {
var b bytes.Buffer
var o output.Output = output.Interleaved{}
var w = o.WrapWriter(&b, "")
fmt.Fprintln(w, "foo\nbar")
assert.Equal(t, "foo\nbar\n", b.String())
fmt.Fprintln(w, "baz")
assert.Equal(t, "foo\nbar\nbaz\n", b.String())
}
func TestGroup(t *testing.T) {
var b bytes.Buffer
var o output.Output = output.Group{}
var w = o.WrapWriter(&b, "")
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, "foo\nbar\nbaz\n", b.String())
}
func TestPrefixed(t *testing.T) {
var b bytes.Buffer
var o output.Output = output.Prefixed{}
var w = o.WrapWriter(&b, "prefix")
t.Run("simple use cases", func(t *testing.T) {
b.Reset()
fmt.Fprintln(w, "foo\nbar")
assert.Equal(t, "[prefix] foo\n[prefix] bar\n", b.String())
fmt.Fprintln(w, "baz")
assert.Equal(t, "[prefix] foo\n[prefix] bar\n[prefix] baz\n", b.String())
})
t.Run("multiple writes for a single line", func(t *testing.T) {
b.Reset()
for _, char := range []string{"T", "e", "s", "t", "!"} {
fmt.Fprint(w, char)
assert.Equal(t, "", b.String())
}
assert.NoError(t, w.Close())
assert.Equal(t, "[prefix] Test!\n", b.String())
})
}

View File

@@ -0,0 +1,65 @@
package output
import (
"bytes"
"fmt"
"io"
"strings"
)
type Prefixed struct{}
func (Prefixed) WrapWriter(w io.Writer, prefix string) io.WriteCloser {
return &prefixWriter{writer: w, prefix: prefix}
}
type prefixWriter struct {
writer io.Writer
prefix string
buff bytes.Buffer
}
func (pw *prefixWriter) Write(p []byte) (int, error) {
n, err := pw.buff.Write(p)
if err != nil {
return n, err
}
return n, pw.writeOutputLines(false)
}
func (pw *prefixWriter) Close() error {
return pw.writeOutputLines(true)
}
func (pw *prefixWriter) writeOutputLines(force bool) error {
for {
line, err := pw.buff.ReadString('\n')
if err == nil {
if err = pw.writeLine(line); err != nil {
return err
}
} else if err == io.EOF {
// if this line was not a complete line, re-add to the buffer
if !force && !strings.HasSuffix(line, "\n") {
_, err = pw.buff.WriteString(line)
return err
}
return pw.writeLine(line)
} else {
return err
}
}
}
func (pw *prefixWriter) writeLine(line string) error {
if line == "" {
return nil
}
if !strings.HasSuffix(line, "\n") {
line += "\n"
}
_, err := fmt.Fprintf(pw.writer, "[%s] %s", pw.prefix, line)
return err
}

View File

@@ -17,6 +17,7 @@ type Checksum struct {
Dir string
Task string
Sources []string
Dry bool
}
// IsUpToDate implements the Checker interface
@@ -36,9 +37,11 @@ func (c *Checksum) IsUpToDate() (bool, error) {
return false, nil
}
_ = os.MkdirAll(filepath.Join(c.Dir, ".task", "checksum"), 0755)
if err = ioutil.WriteFile(checksumFile, []byte(newMd5+"\n"), 0644); err != nil {
return false, err
if !c.Dry {
_ = os.MkdirAll(filepath.Join(c.Dir, ".task", "checksum"), 0755)
if err = ioutil.WriteFile(checksumFile, []byte(newMd5+"\n"), 0644); err != nil {
return false, err
}
}
return oldMd5 == newMd5, nil
}

View File

@@ -4,8 +4,9 @@ import (
"path/filepath"
"sort"
"github.com/go-task/task/v2/internal/execext"
"github.com/mattn/go-zglob"
"github.com/mitchellh/go-homedir"
)
func glob(dir string, globs []string) (files []string, err error) {
@@ -13,13 +14,13 @@ func glob(dir string, globs []string) (files []string, err error) {
if !filepath.IsAbs(g) {
g = filepath.Join(dir, g)
}
g, err = homedir.Expand(g)
g, err = execext.Expand(g)
if err != nil {
return nil, err
}
f, err := zglob.Glob(g)
if err != nil {
return nil, err
continue
}
files = append(files, f...)
}

View File

@@ -0,0 +1,7 @@
package taskfile
// Call is the parameters to a task call
type Call struct {
Task string
Vars Vars
}

View File

@@ -1,4 +1,4 @@
package task
package taskfile
import (
"errors"
@@ -7,10 +7,11 @@ import (
// Cmd is a task command
type Cmd struct {
Cmd string
Silent bool
Task string
Vars Vars
Cmd string
Silent bool
Task string
Vars Vars
IgnoreError bool
}
// Dep is a task dependency
@@ -38,12 +39,14 @@ func (c *Cmd) UnmarshalYAML(unmarshal func(interface{}) error) error {
return nil
}
var cmdStruct struct {
Cmd string
Silent bool
Cmd string
Silent bool
IgnoreError bool `yaml:"ignore_error"`
}
if err := unmarshal(&cmdStruct); err == nil && cmdStruct.Cmd != "" {
c.Cmd = cmdStruct.Cmd
c.Silent = cmdStruct.Silent
c.IgnoreError = cmdStruct.IgnoreError
return nil
}
var taskCall struct {
@@ -76,9 +79,3 @@ func (d *Dep) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
return ErrCantUnmarshalDep
}
// Call is the parameters to a task call
type Call struct {
Task string
Vars Vars
}

View File

@@ -0,0 +1,73 @@
package taskfile
import (
"fmt"
"strings"
)
// NamespaceSeparator contains the character that separates namescapes
const NamespaceSeparator = ":"
// Merge merges the second Taskfile into the first
func Merge(t1, t2 *Taskfile, namespaces ...string) error {
if t1.Version != t2.Version {
return fmt.Errorf(`Taskfiles versions should match. First is "%s" but second is "%s"`, t1.Version, t2.Version)
}
if t2.Expansions != 0 && t2.Expansions != 2 {
t1.Expansions = t2.Expansions
}
if t2.Output != "" {
t1.Output = t2.Output
}
if t1.Includes == nil {
t1.Includes = make(map[string]string)
}
for k, v := range t2.Includes {
t1.Includes[k] = v
}
if t1.Vars == nil {
t1.Vars = make(Vars)
}
for k, v := range t2.Vars {
t1.Vars[k] = v
}
if t1.Env == nil {
t1.Env = make(Vars)
}
for k, v := range t2.Env {
t1.Env[k] = v
}
if t1.Tasks == nil {
t1.Tasks = make(Tasks)
}
for k, v := range t2.Tasks {
// FIXME(@andreynering): Refactor this block, otherwise we can
// have serious side-effects in the future, since we're editing
// the original references instead of deep copying them.
t1.Tasks[taskNameWithNamespace(k, namespaces...)] = v
for _, dep := range v.Deps {
dep.Task = taskNameWithNamespace(dep.Task, namespaces...)
}
for _, cmd := range v.Cmds {
if cmd.Task != "" {
cmd.Task = taskNameWithNamespace(cmd.Task, namespaces...)
}
}
}
return nil
}
func taskNameWithNamespace(taskName string, namespaces ...string) string {
if strings.HasPrefix(taskName, ":") {
return strings.TrimPrefix(taskName, ":")
}
return strings.Join(append(namespaces, taskName), NamespaceSeparator)
}

View File

@@ -0,0 +1,80 @@
package read
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"github.com/go-task/task/v2/internal/taskfile"
"gopkg.in/yaml.v2"
)
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")
// ErrNoTaskfileFound is returned when Taskfile.yml is not found
ErrNoTaskfileFound = errors.New(`task: No Taskfile.yml found. Use "task --init" to create a new one`)
)
// Taskfile reads a Taskfile for a given directory
func Taskfile(dir string) (*taskfile.Taskfile, error) {
path := filepath.Join(dir, "Taskfile.yml")
if _, err := os.Stat(path); err != nil {
return nil, ErrNoTaskfileFound
}
t, err := readTaskfile(path)
if err != nil {
return nil, err
}
for namespace, path := range t.Includes {
path = filepath.Join(dir, path)
info, err := os.Stat(path)
if err != nil {
return nil, err
}
if info.IsDir() {
path = filepath.Join(path, "Taskfile.yml")
}
includedTaskfile, err := readTaskfile(path)
if err != nil {
return nil, err
}
if len(includedTaskfile.Includes) > 0 {
return nil, ErrIncludedTaskfilesCantHaveIncludes
}
if err = taskfile.Merge(t, includedTaskfile, namespace); err != nil {
return nil, err
}
}
path = filepath.Join(dir, fmt.Sprintf("Taskfile_%s.yml", runtime.GOOS))
if _, err = os.Stat(path); err == nil {
osTaskfile, err := readTaskfile(path)
if err != nil {
return nil, err
}
if err = taskfile.Merge(t, osTaskfile); err != nil {
return nil, err
}
}
for name, task := range t.Tasks {
task.Task = name
}
return t, nil
}
func readTaskfile(file string) (*taskfile.Taskfile, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
}
var t taskfile.Taskfile
return &t, yaml.NewDecoder(f).Decode(&t)
}

View File

@@ -0,0 +1,52 @@
package read
import (
"fmt"
"os"
"path/filepath"
"runtime"
"github.com/go-task/task/v2/internal/taskfile"
"gopkg.in/yaml.v2"
)
// Taskvars reads a Taskvars for a given directory
func Taskvars(dir string) (taskfile.Vars, error) {
vars := make(taskfile.Vars)
path := filepath.Join(dir, "Taskvars.yml")
if _, err := os.Stat(path); err == nil {
vars, err = readTaskvars(path)
if err != nil {
return nil, err
}
}
path = filepath.Join(dir, fmt.Sprintf("Taskvars_%s.yml", runtime.GOOS))
if _, err := os.Stat(path); err == nil {
osVars, err := readTaskvars(path)
if err != nil {
return nil, err
}
if vars == nil {
vars = osVars
} else {
for k, v := range osVars {
vars[k] = v
}
}
}
return vars, nil
}
func readTaskvars(file string) (taskfile.Vars, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
}
var vars taskfile.Vars
return vars, yaml.NewDecoder(f).Decode(&vars)
}

22
internal/taskfile/task.go Normal file
View File

@@ -0,0 +1,22 @@
package taskfile
// Tasks representas a group of tasks
type Tasks map[string]*Task
// Task represents a task
type Task struct {
Task string
Cmds []*Cmd
Deps []*Dep
Desc string
Sources []string
Generates []string
Status []string
Dir string
Vars Vars
Env Vars
Silent bool
Method string
Prefix string
IgnoreError bool `yaml:"ignore_error"`
}

View File

@@ -0,0 +1,44 @@
package taskfile
// Taskfile represents a Taskfile.yml
type Taskfile struct {
Version string
Expansions int
Output string
Includes map[string]string
Vars Vars
Env Vars
Tasks Tasks
}
// UnmarshalYAML implements yaml.Unmarshaler interface
func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error {
if err := unmarshal(&tf.Tasks); err == nil {
tf.Version = "1"
return nil
}
var taskfile struct {
Version string
Expansions int
Output string
Includes map[string]string
Vars Vars
Env Vars
Tasks Tasks
}
if err := unmarshal(&taskfile); err != nil {
return err
}
tf.Version = taskfile.Version
tf.Expansions = taskfile.Expansions
tf.Output = taskfile.Output
tf.Includes = taskfile.Includes
tf.Vars = taskfile.Vars
tf.Env = taskfile.Env
tf.Tasks = taskfile.Tasks
if tf.Expansions <= 0 {
tf.Expansions = 2
}
return nil
}

View File

@@ -1,9 +1,9 @@
package task_test
package taskfile_test
import (
"testing"
"github.com/go-task/task"
"github.com/go-task/task/v2/internal/taskfile"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v2"
@@ -27,28 +27,28 @@ vars:
}{
{
yamlCmd,
&task.Cmd{},
&task.Cmd{Cmd: `echo "a string command"`},
&taskfile.Cmd{},
&taskfile.Cmd{Cmd: `echo "a string command"`},
},
{
yamlTaskCall,
&task.Cmd{},
&task.Cmd{Task: "another-task", Vars: task.Vars{
"PARAM1": task.Var{Static: "VALUE1"},
"PARAM2": task.Var{Static: "VALUE2"},
&taskfile.Cmd{},
&taskfile.Cmd{Task: "another-task", Vars: taskfile.Vars{
"PARAM1": taskfile.Var{Static: "VALUE1"},
"PARAM2": taskfile.Var{Static: "VALUE2"},
}},
},
{
yamlDep,
&task.Dep{},
&task.Dep{Task: "task-name"},
&taskfile.Dep{},
&taskfile.Dep{Task: "task-name"},
},
{
yamlTaskCall,
&task.Dep{},
&task.Dep{Task: "another-task", Vars: task.Vars{
"PARAM1": task.Var{Static: "VALUE1"},
"PARAM2": task.Var{Static: "VALUE2"},
&taskfile.Dep{},
&taskfile.Dep{Task: "another-task", Vars: taskfile.Vars{
"PARAM1": taskfile.Var{Static: "VALUE1"},
"PARAM2": taskfile.Var{Static: "VALUE2"},
}},
},
}

58
internal/taskfile/var.go Normal file
View File

@@ -0,0 +1,58 @@
package taskfile
import (
"errors"
"strings"
)
var (
// ErrCantUnmarshalVar is returned for invalid var YAML.
ErrCantUnmarshalVar = errors.New("task: can't unmarshal var value")
)
// Vars is a string[string] variables map.
type Vars map[string]Var
// ToStringMap converts Vars to a string map containing only the static
// variables
func (vs Vars) ToStringMap() (m map[string]string) {
m = make(map[string]string, len(vs))
for k, v := range vs {
if v.Sh != "" {
// Dynamic variable is not yet resolved; trigger
// <no value> to be used in templates.
continue
}
m[k] = v.Static
}
return
}
// Var represents either a static or dynamic variable.
type Var struct {
Static string
Sh string
}
// UnmarshalYAML implements yaml.Unmarshaler interface.
func (v *Var) UnmarshalYAML(unmarshal func(interface{}) error) error {
var str string
if err := unmarshal(&str); err == nil {
if strings.HasPrefix(str, "$") {
v.Sh = strings.TrimPrefix(str, "$")
} else {
v.Static = str
}
return nil
}
var sh struct {
Sh string
}
if err := unmarshal(&sh); err == nil {
v.Sh = sh.Sh
return nil
}
return ErrCantUnmarshalVar
}

View File

@@ -0,0 +1,46 @@
package version
import (
"github.com/Masterminds/semver"
)
var (
v1 = mustVersion("1")
v2 = mustVersion("2")
v21 = mustVersion("2.1")
v22 = mustVersion("2.2")
v23 = mustVersion("2.3")
)
// IsV1 returns if is a given Taskfile version is version 1
func IsV1(v *semver.Constraints) bool {
return v.Check(v1)
}
// IsV2 returns if is a given Taskfile version is at least version 2
func IsV2(v *semver.Constraints) bool {
return v.Check(v2)
}
// IsV21 returns if is a given Taskfile version is at least version 2.1
func IsV21(v *semver.Constraints) bool {
return v.Check(v21)
}
// IsV22 returns if is a given Taskfile version is at least version 2.2
func IsV22(v *semver.Constraints) bool {
return v.Check(v22)
}
// IsV23 returns if is a given Taskfile version is at least version 2.3
func IsV23(v *semver.Constraints) bool {
return v.Check(v23)
}
func mustVersion(s string) *semver.Version {
v, err := semver.NewVersion(s)
if err != nil {
panic(err)
}
return v
}

View File

@@ -0,0 +1,52 @@
package templater
import (
"path/filepath"
"runtime"
"strings"
"text/template"
"github.com/Masterminds/sprig"
)
var (
templateFuncs template.FuncMap
)
func init() {
taskFuncs := template.FuncMap{
"OS": func() string { return runtime.GOOS },
"ARCH": func() string { return runtime.GOARCH },
"catLines": func(s string) string {
s = strings.Replace(s, "\r\n", " ", -1)
return strings.Replace(s, "\n", " ", -1)
},
"splitLines": func(s string) []string {
s = strings.Replace(s, "\r\n", "\n", -1)
return strings.Split(s, "\n")
},
"fromSlash": func(path string) string {
return filepath.FromSlash(path)
},
"toSlash": func(path string) string {
return filepath.ToSlash(path)
},
"exeExt": func() string {
if runtime.GOOS == "windows" {
return ".exe"
}
return ""
},
// IsSH is deprecated.
"IsSH": func() bool { return true },
}
// Deprecated aliases for renamed functions.
taskFuncs["FromSlash"] = taskFuncs["fromSlash"]
taskFuncs["ToSlash"] = taskFuncs["toSlash"]
taskFuncs["ExeExt"] = taskFuncs["exeExt"]
templateFuncs = sprig.TxtFuncMap()
for k, v := range taskFuncs {
templateFuncs[k] = v
}
}

View File

@@ -0,0 +1,73 @@
package templater
import (
"bytes"
"text/template"
"github.com/go-task/task/v2/internal/taskfile"
)
// Templater is a help struct that allow us to call "replaceX" funcs multiple
// times, without having to check for error each time. The first error that
// happen will be assigned to r.err, and consecutive calls to funcs will just
// return the zero value.
type Templater struct {
Vars taskfile.Vars
strMap map[string]string
err error
}
func (r *Templater) Replace(str string) string {
if r.err != nil || str == "" {
return ""
}
templ, err := template.New("").Funcs(templateFuncs).Parse(str)
if err != nil {
r.err = err
return ""
}
if r.strMap == nil {
r.strMap = r.Vars.ToStringMap()
}
var b bytes.Buffer
if err = templ.Execute(&b, r.strMap); err != nil {
r.err = err
return ""
}
return b.String()
}
func (r *Templater) ReplaceSlice(strs []string) []string {
if r.err != nil || len(strs) == 0 {
return nil
}
new := make([]string, len(strs))
for i, str := range strs {
new[i] = r.Replace(str)
}
return new
}
func (r *Templater) ReplaceVars(vars taskfile.Vars) taskfile.Vars {
if r.err != nil || len(vars) == 0 {
return nil
}
new := make(taskfile.Vars, len(vars))
for k, v := range vars {
new[k] = taskfile.Var{
Static: r.Replace(v.Static),
Sh: r.Replace(v.Sh),
}
}
return new
}
func (r *Templater) Err() error {
return r.err
}

31
log.go
View File

@@ -1,31 +0,0 @@
package task
import (
"fmt"
)
func (e *Executor) outf(s string, args ...interface{}) {
if len(args) == 0 {
s, args = "%s", []interface{}{s}
}
fmt.Fprintf(e.Stdout, s+"\n", args...)
}
func (e *Executor) verboseOutf(s string, args ...interface{}) {
if e.Verbose {
e.outf(s, args...)
}
}
func (e *Executor) errf(s string, args ...interface{}) {
if len(args) == 0 {
s, args = "%s", []interface{}{s}
}
fmt.Fprintf(e.Stderr, s+"\n", args...)
}
func (e *Executor) verboseErrf(s string, args ...interface{}) {
if e.Verbose {
e.errf(s, args...)
}
}

View File

@@ -4,16 +4,35 @@ import (
"context"
"fmt"
"github.com/go-task/task/internal/execext"
"github.com/go-task/task/internal/status"
"github.com/go-task/task/v2/internal/execext"
"github.com/go-task/task/v2/internal/status"
"github.com/go-task/task/v2/internal/taskfile"
)
func (t *Task) isUpToDate(ctx context.Context) (bool, error) {
// Status returns an error if any the of given tasks is not up-to-date
func (e *Executor) Status(ctx context.Context, calls ...taskfile.Call) error {
for _, call := range calls {
t, err := e.CompiledTask(call)
if err != nil {
return err
}
isUpToDate, err := e.isTaskUpToDate(ctx, t)
if err != nil {
return err
}
if !isUpToDate {
return fmt.Errorf(`task: Task "%s" is not up-to-date`, t.Task)
}
}
return nil
}
func (e *Executor) isTaskUpToDate(ctx context.Context, t *taskfile.Task) (bool, error) {
if len(t.Status) > 0 {
return t.isUpToDateStatus(ctx)
return e.isTaskUpToDateStatus(ctx, t)
}
checker, err := t.getStatusChecker()
checker, err := e.getStatusChecker(t)
if err != nil {
return false, err
}
@@ -21,15 +40,15 @@ func (t *Task) isUpToDate(ctx context.Context) (bool, error) {
return checker.IsUpToDate()
}
func (t *Task) statusOnError() error {
checker, err := t.getStatusChecker()
func (e *Executor) statusOnError(t *taskfile.Task) error {
checker, err := e.getStatusChecker(t)
if err != nil {
return err
}
return checker.OnError()
}
func (t *Task) getStatusChecker() (status.Checker, error) {
func (e *Executor) getStatusChecker(t *taskfile.Task) (status.Checker, error) {
switch t.Method {
case "", "timestamp":
return &status.Timestamp{
@@ -42,6 +61,7 @@ func (t *Task) getStatusChecker() (status.Checker, error) {
Dir: t.Dir,
Task: t.Task,
Sources: t.Sources,
Dry: e.Dry,
}, nil
case "none":
return status.None{}, nil
@@ -50,13 +70,12 @@ func (t *Task) getStatusChecker() (status.Checker, error) {
}
}
func (t *Task) isUpToDateStatus(ctx context.Context) (bool, error) {
func (e *Executor) isTaskUpToDateStatus(ctx context.Context, t *taskfile.Task) (bool, error) {
for _, s := range t.Status {
err := execext.RunCommand(&execext.RunCommandOptions{
Context: ctx,
err := execext.RunCommand(ctx, &execext.RunCommandOptions{
Command: s,
Dir: t.Dir,
Env: t.getEnviron(),
Env: getEnviron(t),
})
if err != nil {
return false, nil

256
task.go
View File

@@ -5,17 +5,23 @@ import (
"fmt"
"io"
"os"
"sync"
"sync/atomic"
"github.com/go-task/task/internal/execext"
"github.com/go-task/task/v2/internal/compiler"
compilerv1 "github.com/go-task/task/v2/internal/compiler/v1"
compilerv2 "github.com/go-task/task/v2/internal/compiler/v2"
"github.com/go-task/task/v2/internal/execext"
"github.com/go-task/task/v2/internal/logger"
"github.com/go-task/task/v2/internal/output"
"github.com/go-task/task/v2/internal/taskfile"
"github.com/go-task/task/v2/internal/taskfile/read"
"github.com/go-task/task/v2/internal/taskfile/version"
"github.com/Masterminds/semver"
"golang.org/x/sync/errgroup"
)
const (
// TaskFilePath is the default Taskfile
TaskFilePath = "Taskfile"
// MaximumTaskCall is the max number of times a task can be called.
// This exists to prevent infinite loops on cyclic dependencies
MaximumTaskCall = 100
@@ -23,73 +29,33 @@ const (
// Executor executes a Taskfile
type Executor struct {
Tasks Tasks
Dir string
Force bool
Watch bool
Verbose bool
Silent bool
Context context.Context
Taskfile *taskfile.Taskfile
Dir string
Force bool
Watch bool
Verbose bool
Silent bool
Dry bool
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
taskvars Vars
Logger *logger.Logger
Compiler compiler.Compiler
Output output.Output
OutputStyle string
taskvars taskfile.Vars
taskCallCount map[string]*int32
dynamicCache map[string]string
muDynamicCache sync.Mutex
}
// Tasks representas a group of tasks
type Tasks map[string]*Task
// Task represents a task
type Task struct {
Task string
Cmds []*Cmd
Deps []*Dep
Desc string
Sources []string
Generates []string
Status []string
Dir string
Vars Vars
Env Vars
Silent bool
Method string
}
// Run runs Task
func (e *Executor) Run(calls ...Call) error {
if e.Context == nil {
e.Context = context.Background()
}
if e.Stdin == nil {
e.Stdin = os.Stdin
}
if e.Stdout == nil {
e.Stdout = os.Stdout
}
if e.Stderr == nil {
e.Stderr = os.Stderr
}
e.taskCallCount = make(map[string]*int32, len(e.Tasks))
for k := range e.Tasks {
e.taskCallCount[k] = new(int32)
}
if e.dynamicCache == nil {
e.dynamicCache = make(map[string]string, 10)
}
func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error {
// check if given tasks exist
for _, c := range calls {
if _, ok := e.Tasks[c.Task]; !ok {
if _, ok := e.Taskfile.Tasks[c.Task]; !ok {
// FIXME: move to the main package
e.PrintTasksHelp()
return &taskNotFoundError{taskName: c.Task}
@@ -101,15 +67,107 @@ func (e *Executor) Run(calls ...Call) error {
}
for _, c := range calls {
if err := e.RunTask(e.Context, c); err != nil {
if err := e.RunTask(ctx, c); err != nil {
return err
}
}
return nil
}
// Setup setups Executor's internal state
func (e *Executor) Setup() error {
var err error
e.Taskfile, err = read.Taskfile(e.Dir)
if err != nil {
return err
}
e.taskvars, err = read.Taskvars(e.Dir)
if err != nil {
return err
}
v, err := semver.NewConstraint(e.Taskfile.Version)
if err != nil {
return fmt.Errorf(`task: could not parse taskfile version "%s": %v`, e.Taskfile.Version, err)
}
if e.Stdin == nil {
e.Stdin = os.Stdin
}
if e.Stdout == nil {
e.Stdout = os.Stdout
}
if e.Stderr == nil {
e.Stderr = os.Stderr
}
e.Logger = &logger.Logger{
Stdout: e.Stdout,
Stderr: e.Stderr,
Verbose: e.Verbose,
}
switch {
case version.IsV1(v):
e.Compiler = &compilerv1.CompilerV1{
Dir: e.Dir,
Vars: e.taskvars,
Logger: e.Logger,
}
case version.IsV2(v), version.IsV21(v), version.IsV22(v):
e.Compiler = &compilerv2.CompilerV2{
Dir: e.Dir,
Taskvars: e.taskvars,
TaskfileVars: e.Taskfile.Vars,
Expansions: e.Taskfile.Expansions,
Logger: e.Logger,
}
case version.IsV23(v):
return fmt.Errorf(`task: Taskfile versions greater than v2.3 not implemented in the version of Task`)
}
if !version.IsV21(v) && e.Taskfile.Output != "" {
return fmt.Errorf(`task: Taskfile option "output" is only available starting on Taskfile version v2.1`)
}
if !version.IsV22(v) && len(e.Taskfile.Includes) > 0 {
return fmt.Errorf(`task: Including Taskfiles is only available starting on Taskfile version v2.2`)
}
if e.OutputStyle != "" {
e.Taskfile.Output = e.OutputStyle
}
switch e.Taskfile.Output {
case "", "interleaved":
e.Output = output.Interleaved{}
case "group":
e.Output = output.Group{}
case "prefixed":
e.Output = output.Prefixed{}
default:
return fmt.Errorf(`task: output option "%s" not recognized`, e.Taskfile.Output)
}
if !version.IsV21(v) {
err := fmt.Errorf(`task: Taskfile option "ignore_error" is only available starting on Taskfile version v2.1`)
for _, task := range e.Taskfile.Tasks {
if task.IgnoreError {
return err
}
for _, cmd := range task.Cmds {
if cmd.IgnoreError {
return err
}
}
}
}
e.taskCallCount = make(map[string]*int32, len(e.Taskfile.Tasks))
for k := range e.Taskfile.Tasks {
e.taskCallCount[k] = new(int32)
}
return nil
}
// RunTask runs a task by its name
func (e *Executor) RunTask(ctx context.Context, call Call) error {
func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
t, err := e.CompiledTask(call)
if err != nil {
return err
@@ -123,13 +181,13 @@ func (e *Executor) RunTask(ctx context.Context, call Call) error {
}
if !e.Force {
upToDate, err := t.isUpToDate(ctx)
upToDate, err := e.isTaskUpToDate(ctx, t)
if err != nil {
return err
}
if upToDate {
if !e.Silent {
e.errf(`task: Task "%s" is up to date`, t.Task)
e.Logger.Errf(`task: Task "%s" is up to date`, t.Task)
}
return nil
}
@@ -137,59 +195,81 @@ func (e *Executor) RunTask(ctx context.Context, call Call) error {
for i := range t.Cmds {
if err := e.runCommand(ctx, t, call, i); err != nil {
if err2 := t.statusOnError(); err2 != nil {
e.verboseErrf("task: error cleaning status on error: %v", err2)
if err2 := e.statusOnError(t); err2 != nil {
e.Logger.VerboseErrf("task: error cleaning status on error: %v", err2)
}
if execext.IsExitError(err) && t.IgnoreError {
e.Logger.VerboseErrf("task: task error ignored: %v", err)
continue
}
return &taskRunError{t.Task, err}
}
}
return nil
}
func (e *Executor) runDeps(ctx context.Context, t *Task) error {
func (e *Executor) runDeps(ctx context.Context, t *taskfile.Task) error {
g, ctx := errgroup.WithContext(ctx)
for _, d := range t.Deps {
d := d
g.Go(func() error {
return e.RunTask(ctx, Call{Task: d.Task, Vars: d.Vars})
return e.RunTask(ctx, taskfile.Call{Task: d.Task, Vars: d.Vars})
})
}
return g.Wait()
}
func (e *Executor) runCommand(ctx context.Context, t *Task, call Call, i int) error {
func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfile.Call, i int) error {
cmd := t.Cmds[i]
if cmd.Cmd == "" {
return e.RunTask(ctx, Call{Task: cmd.Task, Vars: cmd.Vars})
}
switch {
case cmd.Task != "":
return e.RunTask(ctx, taskfile.Call{Task: cmd.Task, Vars: cmd.Vars})
case cmd.Cmd != "":
if e.Verbose || (!cmd.Silent && !t.Silent && !e.Silent) {
e.Logger.Errf(cmd.Cmd)
}
if e.Verbose || (!cmd.Silent && !t.Silent && !e.Silent) {
e.errf(cmd.Cmd)
}
if e.Dry {
return nil
}
return execext.RunCommand(&execext.RunCommandOptions{
Context: ctx,
Command: cmd.Cmd,
Dir: t.Dir,
Env: t.getEnviron(),
Stdin: e.Stdin,
Stdout: e.Stdout,
Stderr: e.Stderr,
})
stdOut := e.Output.WrapWriter(e.Stdout, t.Prefix)
stdErr := e.Output.WrapWriter(e.Stderr, t.Prefix)
defer stdOut.Close()
defer stdErr.Close()
err := execext.RunCommand(ctx, &execext.RunCommandOptions{
Command: cmd.Cmd,
Dir: t.Dir,
Env: getEnviron(t),
Stdin: e.Stdin,
Stdout: stdOut,
Stderr: stdErr,
})
if execext.IsExitError(err) && cmd.IgnoreError {
e.Logger.VerboseErrf("task: command error ignored: %v", err)
return nil
}
return err
default:
return nil
}
}
func (t *Task) getEnviron() []string {
func getEnviron(t *taskfile.Task) []string {
if t.Env == nil {
return nil
}
envs := os.Environ()
for k, v := range t.Env.toStringMap() {
envs = append(envs, fmt.Sprintf("%s=%s", k, v))
environ := os.Environ()
for k, v := range t.Env.ToStringMap() {
environ = append(environ, fmt.Sprintf("%s=%s", k, v))
}
return envs
return environ
}

View File

@@ -2,6 +2,7 @@ package task_test
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"os"
@@ -9,8 +10,10 @@ import (
"strings"
"testing"
"github.com/go-task/task"
"github.com/go-task/task/v2"
"github.com/go-task/task/v2/internal/taskfile"
"github.com/mitchellh/go-homedir"
"github.com/stretchr/testify/assert"
)
@@ -37,8 +40,8 @@ func (fct fileContentTest) Run(t *testing.T) {
Stdout: ioutil.Discard,
Stderr: ioutil.Discard,
}
assert.NoError(t, e.ReadTaskfile(), "e.ReadTaskfile()")
assert.NoError(t, e.Run(task.Call{Task: fct.Target}), "e.Run(target)")
assert.NoError(t, e.Setup(), "e.Setup()")
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: fct.Target}), "e.Run(target)")
for name, expectContent := range fct.Files {
t.Run(fct.name(name), func(t *testing.T) {
@@ -51,7 +54,6 @@ func (fct fileContentTest) Run(t *testing.T) {
assert.Equal(t, expectContent, s, "unexpected file content")
})
}
}
func TestEnv(t *testing.T) {
@@ -60,15 +62,16 @@ func TestEnv(t *testing.T) {
Target: "default",
TrimSpace: false,
Files: map[string]string{
"env.txt": "GOOS='linux' GOARCH='amd64' CGO_ENABLED='0'\n",
"local.txt": "GOOS='linux' GOARCH='amd64' CGO_ENABLED='0'\n",
"global.txt": "FOO='foo' BAR='overriden' BAZ='baz'\n",
},
}
tt.Run(t)
}
func TestVars(t *testing.T) {
func TestVarsV1(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/vars",
Dir: "testdata/vars/v1",
Target: "default",
TrimSpace: true,
Files: map[string]string{
@@ -102,30 +105,70 @@ func TestVars(t *testing.T) {
tt.Target = "hello"
tt.Run(t)
}
func TestMultilineVars(t *testing.T) {
func TestVarsV2(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/vars/multiline",
Dir: "testdata/vars/v2",
Target: "default",
TrimSpace: false,
TrimSpace: true,
Files: map[string]string{
// Note:
// - task does not strip a trailing newline from var entries
// - task strips one trailing newline from shell output
// - the cat command adds a trailing newline
"echo_foobar.txt": "foo\nbar\n",
"echo_n_foobar.txt": "foo\nbar\n",
"echo_n_multiline.txt": "\n\nfoo\n bar\nfoobar\n\nbaz\n\n",
"var_multiline.txt": "\n\nfoo\n bar\nfoobar\n\nbaz\n\n\n",
"var_catlines.txt": " foo bar foobar baz \n",
"var_enumfile.txt": "0:\n1:\n2:foo\n3: bar\n4:foobar\n5:\n6:baz\n7:\n8:\n",
"foo.txt": "foo",
"bar.txt": "bar",
"baz.txt": "baz",
"tmpl_foo.txt": "foo",
"tmpl_bar.txt": "bar",
"tmpl_foo2.txt": "foo2",
"tmpl_bar2.txt": "bar2",
"shtmpl_foo.txt": "foo",
"shtmpl_foo2.txt": "foo2",
"nestedtmpl_foo.txt": "<no value>",
"nestedtmpl_foo2.txt": "foo2",
"foo2.txt": "foo2",
"bar2.txt": "bar2",
"baz2.txt": "baz2",
"tmpl2_foo.txt": "<no value>",
"tmpl2_foo2.txt": "foo2",
"tmpl2_bar.txt": "<no value>",
"tmpl2_bar2.txt": "bar2",
"shtmpl2_foo.txt": "<no value>",
"shtmpl2_foo2.txt": "foo2",
"nestedtmpl2_foo2.txt": "<no value>",
"override.txt": "bar",
"nested.txt": "Taskvars-TaskfileVars-TaskVars",
},
}
tt.Run(t)
// Ensure identical results when running hello task directly.
tt.Target = "hello"
tt.Run(t)
}
func TestMultilineVars(t *testing.T) {
for _, dir := range []string{"testdata/vars/v1/multiline", "testdata/vars/v2/multiline"} {
tt := fileContentTest{
Dir: dir,
Target: "default",
TrimSpace: false,
Files: map[string]string{
// Note:
// - task does not strip a trailing newline from var entries
// - task strips one trailing newline from shell output
// - the cat command adds a trailing newline
"echo_foobar.txt": "foo\nbar\n",
"echo_n_foobar.txt": "foo\nbar\n",
"echo_n_multiline.txt": "\n\nfoo\n bar\nfoobar\n\nbaz\n\n",
"var_multiline.txt": "\n\nfoo\n bar\nfoobar\n\nbaz\n\n\n",
"var_catlines.txt": " foo bar foobar baz \n",
"var_enumfile.txt": "0:\n1:\n2:foo\n3: bar\n4:foobar\n5:\n6:baz\n7:\n8:\n",
},
}
tt.Run(t)
}
}
func TestVarsInvalidTmpl(t *testing.T) {
const (
dir = "testdata/vars"
dir = "testdata/vars/v1"
target = "invalid-var-tmpl"
expectError = "template: :1: unexpected EOF"
)
@@ -135,8 +178,8 @@ func TestVarsInvalidTmpl(t *testing.T) {
Stdout: ioutil.Discard,
Stderr: ioutil.Discard,
}
assert.NoError(t, e.ReadTaskfile(), "e.ReadTaskfile()")
assert.EqualError(t, e.Run(task.Call{Task: target}), expectError, "e.Run(target)")
assert.NoError(t, e.Setup(), "e.Setup()")
assert.EqualError(t, e.Run(context.Background(), taskfile.Call{Task: target}), expectError, "e.Run(target)")
}
func TestParams(t *testing.T) {
@@ -187,8 +230,8 @@ func TestDeps(t *testing.T) {
Stdout: ioutil.Discard,
Stderr: ioutil.Discard,
}
assert.NoError(t, e.ReadTaskfile())
assert.NoError(t, e.Run(task.Call{Task: "default"}))
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"}))
for _, f := range files {
f = filepath.Join(dir, f)
@@ -208,21 +251,22 @@ func TestStatus(t *testing.T) {
t.Errorf("File should not exists: %v", err)
}
var buff bytes.Buffer
e := &task.Executor{
Dir: dir,
Stdout: ioutil.Discard,
Stderr: ioutil.Discard,
Stdout: &buff,
Stderr: &buff,
Silent: true,
}
assert.NoError(t, e.ReadTaskfile())
assert.NoError(t, e.Run(task.Call{Task: "gen-foo"}))
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "gen-foo"}))
if _, err := os.Stat(file); err != nil {
t.Errorf("File should exists: %v", err)
}
buff := bytes.NewBuffer(nil)
e.Stdout, e.Stderr = buff, buff
assert.NoError(t, e.Run(task.Call{Task: "gen-foo"}))
e.Silent = false
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "gen-foo"}))
if buff.String() != `task: Task "gen-foo" is up to date`+"\n" {
t.Errorf("Wrong output message: %s", buff.String())
@@ -230,16 +274,19 @@ func TestStatus(t *testing.T) {
}
func TestGenerates(t *testing.T) {
var srcTask = "sub/src.txt"
var relTask = "rel.txt"
var absTask = "abs.txt"
const (
srcTask = "sub/src.txt"
relTask = "rel.txt"
absTask = "abs.txt"
fileWithSpaces = "my text file.txt"
)
// This test does not work with a relative dir.
dir, err := filepath.Abs("testdata/generates")
assert.NoError(t, err)
var srcFile = filepath.Join(dir, srcTask)
for _, task := range []string{srcTask, relTask, absTask} {
for _, task := range []string{srcTask, relTask, absTask, fileWithSpaces} {
path := filepath.Join(dir, task)
_ = os.Remove(path)
if _, err := os.Stat(path); err == nil {
@@ -253,15 +300,15 @@ func TestGenerates(t *testing.T) {
Stdout: buff,
Stderr: buff,
}
assert.NoError(t, e.ReadTaskfile())
assert.NoError(t, e.Setup())
for _, theTask := range []string{relTask, absTask} {
for _, theTask := range []string{relTask, absTask, fileWithSpaces} {
var destFile = filepath.Join(dir, theTask)
var upToDate = fmt.Sprintf("task: Task \"%s\" is up to date\n", srcTask) +
fmt.Sprintf("task: Task \"%s\" is up to date\n", theTask)
// Run task for the first time.
assert.NoError(t, e.Run(task.Call{Task: theTask}))
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: theTask}))
if _, err := os.Stat(srcFile); err != nil {
t.Errorf("File should exists: %v", err)
@@ -276,7 +323,7 @@ func TestGenerates(t *testing.T) {
buff.Reset()
// Re-run task to ensure it's now found to be up-to-date.
assert.NoError(t, e.Run(task.Call{Task: theTask}))
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: theTask}))
if buff.String() != upToDate {
t.Errorf("Wrong output message: %s", buff.String())
}
@@ -305,16 +352,16 @@ func TestStatusChecksum(t *testing.T) {
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.ReadTaskfile())
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(task.Call{Task: "build"}))
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build"}))
for _, f := range files {
_, err := os.Stat(filepath.Join(dir, f))
assert.NoError(t, err)
}
buff.Reset()
assert.NoError(t, e.Run(task.Call{Task: "build"}))
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build"}))
assert.Equal(t, `task: Task "build" is up to date`+"\n", buff.String())
}
@@ -344,6 +391,165 @@ func TestCyclicDep(t *testing.T) {
Stdout: ioutil.Discard,
Stderr: ioutil.Discard,
}
assert.NoError(t, e.ReadTaskfile())
assert.IsType(t, &task.MaximumTaskCallExceededError{}, e.Run(task.Call{Task: "task-1"}))
assert.NoError(t, e.Setup())
assert.IsType(t, &task.MaximumTaskCallExceededError{}, e.Run(context.Background(), taskfile.Call{Task: "task-1"}))
}
func TestTaskVersion(t *testing.T) {
tests := []struct {
Dir string
Version string
}{
{"testdata/version/v1", "1"},
{"testdata/version/v2", "2"},
}
for _, test := range tests {
t.Run(test.Dir, func(t *testing.T) {
e := task.Executor{
Dir: test.Dir,
Stdout: ioutil.Discard,
Stderr: ioutil.Discard,
}
assert.NoError(t, e.Setup())
assert.Equal(t, test.Version, e.Taskfile.Version)
assert.Equal(t, 2, len(e.Taskfile.Tasks))
})
}
}
func TestTaskIgnoreErrors(t *testing.T) {
const dir = "testdata/ignore_errors"
e := task.Executor{
Dir: dir,
Stdout: ioutil.Discard,
Stderr: ioutil.Discard,
}
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "task-should-pass"}))
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "task-should-fail"}))
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "cmd-should-pass"}))
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "cmd-should-fail"}))
}
func TestExpand(t *testing.T) {
const dir = "testdata/expand"
home, err := homedir.Dir()
if err != nil {
t.Errorf("Couldn't get $HOME: %v", err)
}
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "pwd"}))
assert.Equal(t, home, strings.TrimSpace(buff.String()))
}
func TestDry(t *testing.T) {
const dir = "testdata/dry"
file := filepath.Join(dir, "file.txt")
_ = os.Remove(file)
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
Dry: true,
}
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build"}))
assert.Equal(t, "touch file.txt", strings.TrimSpace(buff.String()))
if _, err := os.Stat(file); err == nil {
t.Errorf("File should not exist %s", file)
}
}
// TestDryChecksum tests if the checksum file is not being written to disk
// if the dry mode is enabled.
func TestDryChecksum(t *testing.T) {
const dir = "testdata/dry_checksum"
checksumFile := filepath.Join(dir, ".task/checksum/default")
_ = os.Remove(checksumFile)
e := task.Executor{
Dir: dir,
Stdout: ioutil.Discard,
Stderr: ioutil.Discard,
Dry: true,
}
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"}))
_, err := os.Stat(checksumFile)
assert.Error(t, err, "checksum file should not exist")
e.Dry = false
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"}))
_, err = os.Stat(checksumFile)
assert.NoError(t, err, "checksum file should exist")
}
func TestIncludes(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/includes",
Target: "default",
TrimSpace: true,
Files: map[string]string{
"main.txt": "main",
"included_directory.txt": "included_directory",
"included_taskfile.txt": "included_taskfile",
},
}
tt.Run(t)
}
func TestIncludesEmptyMain(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/includes_empty",
Target: "included:default",
TrimSpace: true,
Files: map[string]string{
"file.txt": "default",
},
}
tt.Run(t)
}
func TestIncludesDependencies(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/includes_deps",
Target: "default",
TrimSpace: true,
Files: map[string]string{
"default.txt": "default",
"called_dep.txt": "called_dep",
"called_task.txt": "called_task",
},
}
tt.Run(t)
}
func TestIncludesCallingRoot(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/includes_call_root_task",
Target: "included:call-root",
TrimSpace: true,
Files: map[string]string{
"root_task.txt": "root task",
},
}
tt.Run(t)
}

View File

@@ -1,70 +0,0 @@
package task
import (
"fmt"
"io/ioutil"
"path/filepath"
"runtime"
"github.com/imdario/mergo"
"gopkg.in/yaml.v2"
)
// ReadTaskfile parses Taskfile from the disk
func (e *Executor) ReadTaskfile() error {
path := filepath.Join(e.Dir, TaskFilePath)
var err error
e.Tasks, err = e.readTaskfileData(path)
if err != nil {
return err
}
osTasks, err := e.readTaskfileData(fmt.Sprintf("%s_%s", path, runtime.GOOS))
if err != nil {
switch err.(type) {
case taskFileNotFound:
default:
return err
}
}
if err := mergo.MapWithOverwrite(&e.Tasks, osTasks); err != nil {
return err
}
for name, task := range e.Tasks {
task.Task = name
}
return e.readTaskvars()
}
func (e *Executor) readTaskfileData(path string) (tasks map[string]*Task, err error) {
if b, err := ioutil.ReadFile(path + ".yml"); err == nil {
return tasks, yaml.UnmarshalStrict(b, &tasks)
}
return nil, taskFileNotFound{path}
}
func (e *Executor) readTaskvars() error {
var (
file = filepath.Join(e.Dir, TaskvarsFilePath)
osSpecificFile = fmt.Sprintf("%s_%s", file, runtime.GOOS)
)
if b, err := ioutil.ReadFile(file + ".yml"); err == nil {
if err := yaml.UnmarshalStrict(b, &e.taskvars); err != nil {
return err
}
}
if b, err := ioutil.ReadFile(osSpecificFile + ".yml"); err == nil {
osTaskvars := make(Vars, 10)
if err := yaml.UnmarshalStrict(b, &osTaskvars); err != nil {
return err
}
for k, v := range osTaskvars {
e.taskvars[k] = v
}
}
return nil
}

View File

@@ -2,6 +2,7 @@ build:
cmds:
- cp ./source.txt ./generated.txt
sources:
- ./**/glob-with-inexistent-file.txt
- ./source.txt
generates:
- ./generated.txt

6
testdata/dry/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,6 @@
version: '2'
tasks:
build:
cmds:
- touch file.txt

9
testdata/dry_checksum/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
version: '2'
tasks:
default:
cmds:
- echo "Working..."
sources:
- source.txt
method: checksum

1
testdata/dry_checksum/source.txt vendored Normal file
View File

@@ -0,0 +1 @@
Something...

View File

@@ -1 +1 @@
env.txt
*.txt

View File

@@ -1,10 +1,33 @@
default:
vars:
AMD64: amd64
env:
GOOS: linux
GOARCH: "{{.AMD64}}"
CGO_ENABLED:
sh: echo '0'
cmds:
- echo "GOOS='$GOOS' GOARCH='$GOARCH' CGO_ENABLED='$CGO_ENABLED'" > env.txt
version: '2'
vars:
BAZ:
sh: echo baz
env:
FOO: foo
BAR: bar
BAZ: "{{.BAZ}}"
tasks:
default:
cmds:
- task: local
- task: global
local:
vars:
AMD64: amd64
env:
GOOS: linux
GOARCH: "{{.AMD64}}"
CGO_ENABLED:
sh: echo '0'
cmds:
- echo "GOOS='$GOOS' GOARCH='$GOARCH' CGO_ENABLED='$CGO_ENABLED'" > local.txt
global:
env:
BAR: overriden
cmds:
- echo "FOO='$FOO' BAR='$BAR' BAZ='$BAZ'" > global.txt

8
testdata/expand/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
version: '2'
tasks:
pwd:
cmds:
- pwd
dir: '~'
silent: true

View File

@@ -29,3 +29,13 @@ sub/src.txt:
- echo "hello world" > sub/src.txt
status:
- test -f sub/src.txt
'my text file.txt':
desc: generate file with spaces in the name
deps: [sub/src.txt]
cmds:
- cat sub/src.txt > 'my text file.txt'
sources:
- sub/src.txt
generates:
- 'my text file.txt'

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

@@ -0,0 +1,20 @@
version: '2'
tasks:
task-should-pass:
cmds:
- exit 1
ignore_error: true
task-should-fail:
cmds:
- exit 1
cmd-should-pass:
cmds:
- cmd: exit 1
ignore_error: true
cmd-should-fail:
cmds:
- cmd: exit 1

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

@@ -0,0 +1,16 @@
version: '2'
includes:
included: ./included
included_taskfile: ./Taskfile2.yml
tasks:
default:
cmds:
- task: gen
- task: included:gen
- task: included_taskfile:gen
gen:
cmds:
- echo main > main.txt

6
testdata/includes/Taskfile2.yml vendored Normal file
View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
version: '2'
includes:
included: Taskfile2.yml
tasks:
root-task:
cmds:
- echo "root task" > root_task.txt

View File

@@ -0,0 +1,6 @@
version: '2'
tasks:
call-root:
cmds:
- task: :root-task

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

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

9
testdata/includes_deps/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
version: '2'
includes:
included: Taskfile2.yml
tasks:
default:
cmds:
- task: included:default

16
testdata/includes_deps/Taskfile2.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
version: '2'
tasks:
default:
deps: [called_dep]
cmds:
- echo "default" > default.txt
- task: called_task
called_dep:
cmds:
- echo "called_dep" > called_dep.txt
called_task:
cmds:
- echo "called_task" > called_task.txt

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

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

4
testdata/includes_empty/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
version: '2'
includes:
included: Taskfile2.yml

10
testdata/includes_empty/Taskfile2.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
version: '2'
vars:
FILE: file.txt
CONTENT: default
tasks:
default:
cmds:
- echo "{{.CONTENT}}" > {{.FILE}}

1
testdata/vars/v1/.gitignore vendored Normal file
View File

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

1
testdata/vars/v2/.gitignore vendored Normal file
View File

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

56
testdata/vars/v2/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
version: '2'
vars:
NESTED2: "{{.NESTED1}}-TaskfileVars"
tasks:
default:
deps: [hello]
hello:
cmds:
- echo {{.FOO}} > foo.txt
- echo {{.BAR}} > bar.txt
- echo {{.BAZ}} > baz.txt
- echo '{{.TMPL_FOO}}' > tmpl_foo.txt
- echo '{{.TMPL_BAR}}' > tmpl_bar.txt
- echo '{{.TMPL_FOO2}}' > tmpl_foo2.txt
- echo '{{.TMPL_BAR2}}' > tmpl_bar2.txt
- echo '{{.SHTMPL_FOO}}' > shtmpl_foo.txt
- echo '{{.SHTMPL_FOO2}}' > shtmpl_foo2.txt
- echo '{{.NESTEDTMPL_FOO}}' > nestedtmpl_foo.txt
- echo '{{.NESTEDTMPL_FOO2}}' > nestedtmpl_foo2.txt
- echo {{.FOO2}} > foo2.txt
- echo {{.BAR2}} > bar2.txt
- echo {{.BAZ2}} > baz2.txt
- echo '{{.TMPL2_FOO}}' > tmpl2_foo.txt
- echo '{{.TMPL2_BAR}}' > tmpl2_bar.txt
- echo '{{.TMPL2_FOO2}}' > tmpl2_foo2.txt
- echo '{{.TMPL2_BAR2}}' > tmpl2_bar2.txt
- echo '{{.SHTMPL2_FOO}}' > shtmpl2_foo.txt
- echo '{{.SHTMPL2_FOO2}}' > shtmpl2_foo2.txt
- echo '{{.NESTEDTMPL2_FOO2}}' > nestedtmpl2_foo2.txt
- echo {{.OVERRIDE}} > override.txt
- echo '{{.NESTED3}}' > nested.txt
vars:
FOO: foo
BAR: $echo bar
BAZ:
sh: echo baz
TMPL_FOO: "{{.FOO}}"
TMPL_BAR: "{{.BAR}}"
TMPL_FOO2: "{{.FOO2}}"
TMPL_BAR2: "{{.BAR2}}"
SHTMPL_FOO:
sh: "echo '{{.FOO}}'"
SHTMPL_FOO2:
sh: "echo '{{.FOO2}}'"
NESTEDTMPL_FOO: "{{.TMPL_FOO}}"
NESTEDTMPL_FOO2: "{{.TMPL2_FOO2}}"
OVERRIDE: "bar"
NESTED3: "{{.NESTED2}}-TaskVars"
invalid-var-tmpl:
vars:
CHARS: "abcd"
INVALID: "{{range .CHARS}}no end"

13
testdata/vars/v2/Taskvars.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
FOO2: foo2
BAR2: $echo bar2
BAZ2:
sh: echo baz2
TMPL2_FOO: "{{.FOO}}"
TMPL2_BAR: "{{.BAR}}"
TMPL2_FOO2: "{{.FOO2}}"
TMPL2_BAR2: "{{.BAR2}}"
SHTMPL2_FOO2:
sh: "echo '{{.FOO2}}'"
NESTEDTMPL2_FOO2: "{{.TMPL2_FOO2}}"
OVERRIDE: "foo"
NESTED1: "Taskvars"

45
testdata/vars/v2/multiline/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
version: '2'
tasks:
default:
vars:
MULTILINE: "\n\nfoo\n bar\nfoobar\n\nbaz\n\n"
cmds:
- task: file
vars:
CONTENT:
sh: "echo 'foo\nbar'"
FILE: "echo_foobar.txt"
- task: file
vars:
CONTENT:
sh: "echo -n 'foo\nbar'"
FILE: "echo_n_foobar.txt"
- task: file
vars:
CONTENT:
sh: echo -n "{{.MULTILINE}}"
FILE: "echo_n_multiline.txt"
- task: file
vars:
CONTENT: "{{.MULTILINE}}"
FILE: "var_multiline.txt"
- task: file
vars:
CONTENT: "{{.MULTILINE | catLines}}"
FILE: "var_catlines.txt"
- task: enumfile
vars:
LINES: "{{.MULTILINE}}"
FILE: "var_enumfile.txt"
file:
cmds:
- |
cat << EOF > '{{.FILE}}'
{{.CONTENT}}
EOF
enumfile:
cmds:
- |
cat << EOF > '{{.FILE}}'
{{range $i, $line := .LINES| splitLines}}{{$i}}:{{$line}}
{{end}}EOF

9
testdata/version/v1/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
version: 1
tasks:
foo:
cmds:
- echo "Foo"
bar:
cmds:
- echo "Bar"

9
testdata/version/v2/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
version: 2
tasks:
foo:
cmds:
- echo "Foo"
bar:
cmds:
- echo "Bar"

View File

@@ -1,364 +1,88 @@
package task
import (
"bytes"
"errors"
"os"
"path/filepath"
"runtime"
"strings"
"text/template"
"github.com/go-task/task/internal/execext"
"github.com/Masterminds/sprig"
"github.com/mitchellh/go-homedir"
"github.com/go-task/task/v2/internal/execext"
"github.com/go-task/task/v2/internal/taskfile"
"github.com/go-task/task/v2/internal/templater"
)
var (
// TaskvarsFilePath file containing additional variables.
TaskvarsFilePath = "Taskvars"
)
var (
// ErrCantUnmarshalVar is returned for invalid var YAML.
ErrCantUnmarshalVar = errors.New("task: can't unmarshal var value")
)
// Vars is a string[string] variables map.
type Vars map[string]Var
func getEnvironmentVariables() Vars {
var (
env = os.Environ()
m = make(Vars, len(env))
)
for _, e := range env {
keyVal := strings.SplitN(e, "=", 2)
key, val := keyVal[0], keyVal[1]
m[key] = Var{Static: val}
}
return m
}
func (vs Vars) toStringMap() (m map[string]string) {
m = make(map[string]string, len(vs))
for k, v := range vs {
if v.Sh != "" {
// Dynamic variable is not yet resolved; trigger
// <no value> to be used in templates.
continue
}
m[k] = v.Static
}
return
}
// Var represents either a static or dynamic variable.
type Var struct {
Static string
Sh string
}
// UnmarshalYAML implements yaml.Unmarshaler interface.
func (v *Var) UnmarshalYAML(unmarshal func(interface{}) error) error {
var str string
if err := unmarshal(&str); err == nil {
if strings.HasPrefix(str, "$") {
v.Sh = strings.TrimPrefix(str, "$")
} else {
v.Static = str
}
return nil
}
var sh struct {
Sh string
}
if err := unmarshal(&sh); err == nil {
v.Sh = sh.Sh
return nil
}
return ErrCantUnmarshalVar
}
// getVariables returns fully resolved variables following the priority order:
// 1. Call variables (should already have been resolved)
// 2. Environment (should not need to be resolved)
// 3. Task variables, resolved with access to:
// - call, taskvars and environment variables
// 4. Taskvars variables, resolved with access to:
// - environment variables
func (e *Executor) getVariables(call Call) (Vars, error) {
t, ok := e.Tasks[call.Task]
if !ok {
return nil, &taskNotFoundError{call.Task}
}
merge := func(dest Vars, srcs ...Vars) {
for _, src := range srcs {
for k, v := range src {
dest[k] = v
}
}
}
varsKeys := func(srcs ...Vars) []string {
m := make(map[string]struct{})
for _, src := range srcs {
for k := range src {
m[k] = struct{}{}
}
}
lst := make([]string, 0, len(m))
for k := range m {
lst = append(lst, k)
}
return lst
}
replaceVars := func(dest Vars, keys []string) error {
r := varReplacer{vars: dest}
for _, k := range keys {
v := dest[k]
dest[k] = Var{
Static: r.replace(v.Static),
Sh: r.replace(v.Sh),
}
}
return r.err
}
resolveShell := func(dest Vars, keys []string) error {
for _, k := range keys {
v := dest[k]
static, err := e.handleShVar(v)
if err != nil {
return err
}
dest[k] = Var{Static: static}
}
return nil
}
update := func(dest Vars, srcs ...Vars) error {
merge(dest, srcs...)
// updatedKeys ensures template evaluation is run only once.
updatedKeys := varsKeys(srcs...)
if err := replaceVars(dest, updatedKeys); err != nil {
return err
}
return resolveShell(dest, updatedKeys)
}
// Resolve taskvars variables to "result" with environment override variables.
override := getEnvironmentVariables()
result := make(Vars, len(e.taskvars)+len(t.Vars)+len(override))
if err := update(result, e.taskvars, override); err != nil {
return nil, err
}
// Resolve task variables to "result" with environment and call override variables.
merge(override, call.Vars)
if err := update(result, t.Vars, override); err != nil {
return nil, err
}
return result, nil
}
func (e *Executor) handleShVar(v Var) (string, error) {
if v.Static != "" || v.Sh == "" {
return v.Static, nil
}
e.muDynamicCache.Lock()
defer e.muDynamicCache.Unlock()
if result, ok := e.dynamicCache[v.Sh]; ok {
return result, nil
}
var stdout bytes.Buffer
opts := &execext.RunCommandOptions{
Command: v.Sh,
Dir: e.Dir,
Stdout: &stdout,
Stderr: e.Stderr,
}
if err := execext.RunCommand(opts); err != nil {
return "", &dynamicVarError{cause: err, cmd: opts.Command}
}
// Trim a single trailing newline from the result to make most command
// output easier to use in shell commands.
result := strings.TrimSuffix(stdout.String(), "\n")
e.dynamicCache[v.Sh] = result
e.verboseErrf(`task: dynamic variable: '%s' result: '%s'`, v.Sh, result)
return result, nil
}
// CompiledTask returns a copy of a task, but replacing variables in almost all
// properties using the Go template package.
func (e *Executor) CompiledTask(call Call) (*Task, error) {
origTask, ok := e.Tasks[call.Task]
func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) {
origTask, ok := e.Taskfile.Tasks[call.Task]
if !ok {
return nil, &taskNotFoundError{call.Task}
}
vars, err := e.getVariables(call)
vars, err := e.Compiler.GetVariables(origTask, call)
if err != nil {
return nil, err
}
r := varReplacer{vars: vars}
r := templater.Templater{Vars: vars}
new := Task{
Task: origTask.Task,
Desc: r.replace(origTask.Desc),
Sources: r.replaceSlice(origTask.Sources),
Generates: r.replaceSlice(origTask.Generates),
Status: r.replaceSlice(origTask.Status),
Dir: r.replace(origTask.Dir),
Vars: nil,
Env: r.replaceVars(origTask.Env),
Silent: origTask.Silent,
Method: r.replace(origTask.Method),
new := taskfile.Task{
Task: origTask.Task,
Desc: r.Replace(origTask.Desc),
Sources: r.ReplaceSlice(origTask.Sources),
Generates: r.ReplaceSlice(origTask.Generates),
Status: r.ReplaceSlice(origTask.Status),
Dir: r.Replace(origTask.Dir),
Vars: nil,
Env: nil,
Silent: origTask.Silent,
Method: r.Replace(origTask.Method),
Prefix: r.Replace(origTask.Prefix),
IgnoreError: origTask.IgnoreError,
}
new.Dir, err = homedir.Expand(new.Dir)
new.Dir, err = execext.Expand(new.Dir)
if err != nil {
return nil, err
}
if e.Dir != "" && !filepath.IsAbs(new.Dir) {
new.Dir = filepath.Join(e.Dir, new.Dir)
}
if new.Prefix == "" {
new.Prefix = new.Task
}
new.Env = make(taskfile.Vars, len(e.Taskfile.Env)+len(origTask.Env))
for k, v := range r.ReplaceVars(e.Taskfile.Env) {
new.Env[k] = v
}
for k, v := range r.ReplaceVars(origTask.Env) {
new.Env[k] = v
}
for k, v := range new.Env {
static, err := e.handleShVar(v)
static, err := e.Compiler.HandleDynamicVar(v)
if err != nil {
return nil, err
}
new.Env[k] = Var{Static: static}
new.Env[k] = taskfile.Var{Static: static}
}
if len(origTask.Cmds) > 0 {
new.Cmds = make([]*Cmd, len(origTask.Cmds))
new.Cmds = make([]*taskfile.Cmd, len(origTask.Cmds))
for i, cmd := range origTask.Cmds {
new.Cmds[i] = &Cmd{
Task: r.replace(cmd.Task),
Silent: cmd.Silent,
Cmd: r.replace(cmd.Cmd),
Vars: r.replaceVars(cmd.Vars),
new.Cmds[i] = &taskfile.Cmd{
Task: r.Replace(cmd.Task),
Silent: cmd.Silent,
Cmd: r.Replace(cmd.Cmd),
Vars: r.ReplaceVars(cmd.Vars),
IgnoreError: cmd.IgnoreError,
}
}
}
if len(origTask.Deps) > 0 {
new.Deps = make([]*Dep, len(origTask.Deps))
new.Deps = make([]*taskfile.Dep, len(origTask.Deps))
for i, dep := range origTask.Deps {
new.Deps[i] = &Dep{
Task: r.replace(dep.Task),
Vars: r.replaceVars(dep.Vars),
new.Deps[i] = &taskfile.Dep{
Task: r.Replace(dep.Task),
Vars: r.ReplaceVars(dep.Vars),
}
}
}
return &new, r.err
}
// varReplacer is a help struct that allow us to call "replaceX" funcs multiple
// times, without having to check for error each time. The first error that
// happen will be assigned to r.err, and consecutive calls to funcs will just
// return the zero value.
type varReplacer struct {
vars Vars
strMap map[string]string
err error
}
func (r *varReplacer) replace(str string) string {
if r.err != nil || str == "" {
return ""
}
templ, err := template.New("").Funcs(templateFuncs).Parse(str)
if err != nil {
r.err = err
return ""
}
if r.strMap == nil {
r.strMap = r.vars.toStringMap()
}
var b bytes.Buffer
if err = templ.Execute(&b, r.strMap); err != nil {
r.err = err
return ""
}
return b.String()
}
func (r *varReplacer) replaceSlice(strs []string) []string {
if r.err != nil || len(strs) == 0 {
return nil
}
new := make([]string, len(strs))
for i, str := range strs {
new[i] = r.replace(str)
}
return new
}
func (r *varReplacer) replaceVars(vars Vars) Vars {
if r.err != nil || len(vars) == 0 {
return nil
}
new := make(Vars, len(vars))
for k, v := range vars {
new[k] = Var{
Static: r.replace(v.Static),
Sh: r.replace(v.Sh),
}
}
return new
}
var (
templateFuncs template.FuncMap
)
func init() {
taskFuncs := template.FuncMap{
"OS": func() string { return runtime.GOOS },
"ARCH": func() string { return runtime.GOARCH },
"catLines": func(s string) string {
s = strings.Replace(s, "\r\n", " ", -1)
return strings.Replace(s, "\n", " ", -1)
},
"splitLines": func(s string) []string {
s = strings.Replace(s, "\r\n", "\n", -1)
return strings.Split(s, "\n")
},
"fromSlash": func(path string) string {
return filepath.FromSlash(path)
},
"toSlash": func(path string) string {
return filepath.ToSlash(path)
},
"exeExt": func() string {
if runtime.GOOS == "windows" {
return ".exe"
}
return ""
},
// IsSH is deprecated.
"IsSH": func() bool { return true },
}
// Deprecated aliases for renamed functions.
taskFuncs["FromSlash"] = taskFuncs["fromSlash"]
taskFuncs["ToSlash"] = taskFuncs["toSlash"]
taskFuncs["ExeExt"] = taskFuncs["exeExt"]
templateFuncs = sprig.TxtFuncMap()
for k, v := range taskFuncs {
templateFuncs[k] = v
}
return &new, r.Err()
}

View File

@@ -1,9 +1,11 @@
language: go
go:
- 1.6
- 1.7
- 1.8
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
- tip
# Setting sudo access to false will let Travis CI use containers rather than
@@ -13,8 +15,8 @@ go:
sudo: false
script:
- GO15VENDOREXPERIMENT=1 make setup
- GO15VENDOREXPERIMENT=1 make test
- make setup
- make test
notifications:
webhooks:

Some files were not shown because too many files have changed in this diff Show More