Compare commits

...

178 Commits

Author SHA1 Message Date
Andrey Nering
5c420f3a34 v2.7.0 2019-09-22 18:21:31 -03:00
Andrey Nering
84da80356d Use stdlib instead of go-homedir 2019-09-08 22:07:48 -03:00
Andrey Nering
bcbb85eac3 Merge pull request #245 from go-task/upgrade-sh
Upgrade mvdan.cc/sh to use edge
2019-09-08 21:59:59 -03:00
Andrey Nering
0e1d8a72e6 Revert "Ensure the $HOME env is being set on Windows"
This reverts commit 52028fc3bc.
2019-09-08 21:56:23 -03:00
Andrey Nering
bbdd698869 Upgrade mvdan.cc/sh to use edge 2019-09-08 21:55:02 -03:00
Andrey Nering
8f684ffa6d Merge pull request #239 from go-task/ci-using-github-actions
Add CI for Linux/Windows/MacOS powered by GitHub Actions
2019-09-07 14:43:00 -03:00
Andrey Nering
9be3666fe7 Trying to fix Travis 2019-09-07 14:40:39 -03:00
Andrey Nering
b7785678f4 CI: Upgrade to Go 1.13 2019-09-07 14:35:48 -03:00
Andrey Nering
d8005b4cf6 Fix typo 2019-09-07 14:32:02 -03:00
Andrey Nering
52028fc3bc Ensure the $HOME env is being set on Windows 2019-09-07 14:29:13 -03:00
Andrey Nering
5285ec23ae Fix summary test on Windows 2019-09-01 22:26:53 -03:00
Andrey Nering
3c882e5c57 Merge branch 'master' into ci-using-github-actions 2019-09-01 22:18:58 -03:00
Andrey Nering
ad569a8a36 Update CHANGELOG 2019-08-25 16:41:54 -03:00
Andrey Nering
0d9fdbaac1 Small improvements to #228 2019-08-25 16:32:25 -03:00
Andrey Nering
f5cd3eab9e Merge pull request #228 from topecongiro/checksum-look-for-generated
Re-run the task if generated files do not exist
2019-08-25 16:32:03 -03:00
Seiichi Uchida
8987cd64a0 Fix a typo 2019-08-21 13:42:19 +09:00
Seiichi Uchida
fac51dcf03 Fix Checksum.IsUpToDate
- Check whether generates exist after the creation of checksum file
- Check whether generates exist if only the user specified generates fields
- Check for each generates field instead of taking it as a whole
2019-08-21 13:35:16 +09:00
Seiichi Uchida
01101a4c9b Remove directory check 2019-08-21 13:34:58 +09:00
Seiichi Uchida
d561e40817 Update glob.go
- Rename glob() to globs()
- Add glob() which handles a single glob pattern
- Change glob() and globs() so that they do not return directoreis
2019-08-21 13:33:12 +09:00
Seiichi Uchida
0cb298ebdf Separate error handlings for readability 2019-08-19 13:00:10 +09:00
Andrey Nering
a149368725 Calling Go directly on Windows 2019-08-15 22:50:28 -03:00
Andrey Nering
afeefe8259 Build on the root directory 2019-08-15 22:45:46 -03:00
Andrey Nering
690d3c27a2 Do we need to use backslash here? 2019-08-15 22:42:06 -03:00
Andrey Nering
3d56ea5ce5 Fix binary calling on Windows 2019-08-15 22:38:36 -03:00
Andrey Nering
fdff7f80a3 Skip linting for now 2019-08-15 22:35:29 -03:00
Andrey Nering
fe6978b107 Fix CI 2019-08-15 22:32:45 -03:00
Andrey Nering
57db6865d2 Build binary first and test all packages 2019-08-15 22:28:55 -03:00
Andrey Nering
d235d5ab28 Add CI for Linux/Windows/MacOS powered by GitHub Actions 2019-08-15 22:24:31 -03:00
Andrey Nering
613dfe06d3 Fix two Goreleaser deprecation messages
https://goreleaser.com/deprecations/#archive

https://goreleaser.com/deprecations/#nfpm
2019-08-11 23:15:08 -03:00
Andrey Nering
a312d61d68 Update CHANGELOG.md 2019-08-11 23:01:25 -03:00
Andrey Nering
e414c1f7b0 Documentation: Improve wording on the Variables section
Thanks @crewjam for the suggestion

Closes #234
2019-08-10 19:50:47 -03:00
Andrey Nering
955359b073 Fix nil panic bug when assigning global var and no var is declared on the Taskfile
Closes #229
Ref #234
2019-08-10 19:38:57 -03:00
Seiichi Uchida
26e0c0887a Re-run the task if generated files do not exist 2019-08-01 13:08:53 +09:00
Andrey Nering
4c295b564a Update CHANGELOG.md 2019-07-21 11:23:49 -03:00
Andrey Nering
2eb52da0db v2.6.0 2019-07-21 11:11:30 -03:00
Andrey Nering
d8bfb3ab13 Add CHANGELOG and documentation for Linux support on Homebrew
Ref: https://github.com/go-task/homebrew-tap/pull/1

Thanks @dawidd6
2019-07-21 11:03:22 -03:00
Andrey Nering
d970e93507 Add --taskfile flag (alias -t) to allow running another Taskfile
Closes #221
2019-07-21 10:57:04 -03:00
Andrey Nering
762714de68 Merge pull request #213 from marco-m/document-dir-creation
Document dir: creation (see PR #211)
2019-06-16 11:03:08 -03:00
Marco Molteni
82a3651a18 Document dir: creation (see PR #211) 2019-06-16 11:33:00 +02:00
Andrey Nering
abe0352de9 Fixed some bugs regarding minor version checks on version:
1. I have forgot to update it on recent releases. Seems that most people just
   use round versions since nobody complained.
2. It's too hard to understand how the github.com/Masterminds/semver package
   works, so I just got rid of it and we're now using plain float checks.
2019-06-15 22:39:35 -03:00
Andrey Nering
4cee4aa5a8 Fix typo 2019-06-15 21:58:37 -03:00
Andrey Nering
9c68c7c50b Add changelog for #205 2019-06-15 21:56:34 -03:00
Andrey Nering
0608782cfa Merge pull request #205 from CypherpunkArmory/add-precondition-to-task
Add Preconditions to Tasks
2019-06-15 21:55:20 -03:00
Andrey Nering
edeaf3794a Merge pull request #212 from ezhukov/patch-1
Add missing "-" in usage.md
2019-06-15 21:21:41 -03:00
Andrey Nering
fe2b8c8afa Post-fixes to #211 2019-06-15 21:12:54 -03:00
Andrey Nering
b66bf58064 Merge branch 'marco-m-209-create-dir' 2019-06-15 20:53:34 -03:00
Andrey Nering
957dfa9cdf Merge branch '209-create-dir' of https://github.com/marco-m/task into marco-m-209-create-dir 2019-06-15 20:52:08 -03:00
Stephen Prater
cc9264854e Change error output 2019-06-11 12:20:56 -07:00
Stephen Prater
d1463b3e24 Fix typos per review 2019-06-11 11:46:22 -07:00
Eugene Zhukov
f1082520e1 Add missing "-" in usage.md 2019-06-11 11:35:10 +03:00
Marco Molteni
733c563194 Protect creation of "dir:" with a mutex 2019-06-10 17:40:20 +02:00
Andrey Nering
0200d043c3 Add funding button via OpenCollective 2019-06-09 21:53:55 -03:00
Marco Molteni
9c475c36e7 Handle the common case when the task directory is not specified
Closes #209
2019-06-06 20:40:31 +02:00
Marco Molteni
c663c5c507 When "dir:" attribute points to a non-existing dir, create it
Closes #209
2019-06-04 18:58:22 +02:00
Marco Molteni
1e93c38307 Task directory: test when "dir:" attribute points to an existing dir 2019-06-04 18:36:35 +02:00
Marco Molteni
81baf808c9 Task directory: test default case (no "dir:" attribute) 2019-06-04 18:24:01 +02:00
Marco Molteni
74537689dc Fix spelling 2019-06-04 08:08:25 +02:00
Stephen Prater
12ab01d5e6 Clarify difference between status and precondition in docs 2019-05-28 13:18:06 -07:00
Stephen Prater
044d3a0ff9 Remove ignore_errors 2019-05-28 13:02:59 -07:00
Stephen Prater
659cae6a4c Apply suggestions from code review
Co-Authored-By: Andrey Nering <andrey.nering@gmail.com>
2019-05-28 12:28:29 -07:00
Stephen Prater
bd5882f0f0 Add Preconditions to Tasks 2019-05-17 13:51:15 -07:00
Andrey Nering
6ff9ba9df9 v2.5.2 2019-05-11 11:28:21 -03:00
Andrey Nering
b2df398a12 go mod vendor 2019-05-11 11:22:47 -03:00
Andrey Nering
83d618e1eb Revert "Upgrade to yaml/go-yaml v3"
This reverts commit 8001fb3915.
2019-05-11 11:22:13 -03:00
Andrey Nering
f0768b3af1 Allow setting global variables through the CLI
Closes #192
2019-05-11 11:06:47 -03:00
Andrey Nering
0233ce52ed v2.5.1 2019-04-27 17:56:30 -03:00
Andrey Nering
6e6f337509 Updated change log 2019-04-27 17:28:58 -03:00
Andrey Nering
1546415b8f Update CHANGELOG.md 2019-04-21 17:16:35 -03:00
Andrey Nering
20725c69bf Merge pull request #200 from go-task/fix-output-issues
Fixes some bugs relatated to commands output handling
2019-04-21 17:05:21 -03:00
Andrey Nering
90613220c6 Fixes some bugs relatated to commands output handling
This seems to fix some of the bugs reported by issues like #114 and #190.

Seems that the standard library's os/exec package has some black magic to
detect if a writer is an actual *os.File, and some stuff are handled
differently, then.

Fixes #114
Fixes #190
2019-04-21 16:55:47 -03:00
Andrey Nering
659fd2ae93 Update Go version on CI 2019-04-13 17:44:55 -03:00
Andrey Nering
29d899f7da Merge pull request #198 from go-task/yaml-v3
Upgrade to go-yaml/yaml v3
2019-04-13 17:29:36 -03:00
Andrey Nering
902a0a01a9 go vendor mod 2019-04-13 17:26:27 -03:00
Andrey Nering
8001fb3915 Upgrade to yaml/go-yaml v3 2019-04-13 17:25:28 -03:00
Andrey Nering
e81e2802f0 Small fix to redirector 2019-03-23 17:48:18 -03:00
Andrey Nering
1ee066ec42 Merge pull request #188 from sosiska/patch-1
Rewrite if-else chain to switch statement
2019-03-23 17:04:31 -03:00
Kirill Motkov
53d54d1c4a Rewrite if-else chain to switch statement 2019-03-19 14:19:21 +03:00
Andrey Nering
10082b60b8 v2.5.0 2019-03-16 10:46:22 -03:00
Andrey Nering
c5b9773922 go mod vendor 2019-03-16 10:42:54 -03:00
Andrey Nering
de11323d28 mvdan.cc/sh: Use v2.6.4 2019-03-16 10:42:23 -03:00
Andrey Nering
9f269e1a95 Migrating from taskfile.org to taskfile.dev 2019-03-04 23:23:30 -03:00
Andrey Nering
e4204168a0 Remove unnecessary extra empty line 2019-03-04 22:56:23 -03:00
Andrey Nering
9c350f8ef1 Update Change Log 2019-03-04 22:56:23 -03:00
Andrey Nering
db19fdac29 Update CNAME 2019-03-04 22:23:39 -03:00
Andrey Nering
d516b238b1 Merge pull request #180 from jaedle/master
Display task summary
2019-03-04 21:44:44 -03:00
Andrey Nering
f9330f6cd9 Merge pull request #182 from GuillaumeAmat/fix-completion
Fix the zsh completion with sub-tasks
2019-03-04 21:39:43 -03:00
jaedle
360da29e1f refactoring 2019-03-04 13:04:04 +01:00
jaedle
9cfac1642a rename method for summary/summaries 2019-03-04 13:03:13 +01:00
jaedle
db90e87d10 rearrange imports 2019-03-04 12:53:06 +01:00
jaedle
b7564080bc add space between tasks 2019-03-04 12:48:26 +01:00
jaedle
1d783bf6c7 refactoring 2019-03-04 12:47:01 +01:00
jaedle
1025c2e3a1 add unit test for spacing between summaries 2019-03-04 12:46:02 +01:00
jaedle
4fd82ab222 refactoring 2019-03-04 12:28:26 +01:00
jaedle
8eadfc1bf6 refactoring 2019-03-04 12:28:11 +01:00
jaedle
f66edbad50 refactoring 2019-03-04 12:27:10 +01:00
jaedle
c7f17b5319 refactoring 2019-03-04 12:25:42 +01:00
jaedle
23c4adcef6 add spacing for tasks 2019-03-04 12:15:40 +01:00
jaedle
808542bed0 remove unnecassry test for multiple summaries 2019-03-04 12:13:13 +01:00
jaedle
93bfd57856 print summary for multiple tasks 2019-03-04 12:09:58 +01:00
jaedle
7e7e1bccba rearrange imports 2019-03-04 12:04:31 +01:00
jaedle
34f6da86c3 rearrange imports 2019-03-04 12:03:28 +01:00
Guillaume AMAT
15c0381c3c Fix the indentation 2019-03-04 07:03:06 +01:00
Guillaume AMAT
c2f4a57e02 Remove \s for MacOS compatibility, use awk instead 2019-03-03 23:32:35 +01:00
Andrey Nering
f945cf2343 Update internal/summary/summary_test.go
Co-Authored-By: jaedle <32975714+jaedle@users.noreply.github.com>
2019-03-03 19:45:00 +01:00
Andrey Nering
5bca3cfd71 Update testdata/summary/Taskfile.yml
Co-Authored-By: jaedle <32975714+jaedle@users.noreply.github.com>
2019-03-03 19:44:27 +01:00
Andrey Nering
26ce4e6886 Update testdata/summary/Taskfile.yml
Co-Authored-By: jaedle <32975714+jaedle@users.noreply.github.com>
2019-03-03 19:44:14 +01:00
Andrey Nering
f5f0e0c376 Update internal/summary/summary.go
Co-Authored-By: jaedle <32975714+jaedle@users.noreply.github.com>
2019-03-03 19:43:57 +01:00
Andrey Nering
9dea1e7f3e Update docs/usage.md
Co-Authored-By: jaedle <32975714+jaedle@users.noreply.github.com>
2019-03-03 19:43:23 +01:00
Andrey Nering
c2e0f8c81f Update docs/usage.md
Co-Authored-By: jaedle <32975714+jaedle@users.noreply.github.com>
2019-03-03 18:56:42 +01:00
Andrey Nering
d341bc25ce Revert "Try out Windows builds in Travis"
This reverts commit fc34d6b56f.

Unfortunately, something seems wrong with Windows build on Travis.
And the output log is unhelpful to debug the problem.
2019-03-03 14:42:33 -03:00
Andrey Nering
0379e2b51b Merge pull request #175 from emirb/patch-1
Try out Windows builds in Travis
2019-03-02 11:18:03 -03:00
Guillaume AMAT
e79026b840 Fix the zsh completion with sub-tasks 2019-03-02 01:40:31 +01:00
Emir Beganović
fc34d6b56f Try out Windows builds in Travis 2019-02-25 09:59:25 +04:00
jaedle
2a1571a99e refactoring 2019-02-24 19:14:15 +01:00
jaedle
c158608255 fix error in documentation 2019-02-24 19:10:44 +01:00
jaedle
3ca590b185 display summary for tasks without summary/description 2019-02-24 19:02:44 +01:00
jaedle
3f8ee21849 print error messsage if no summary or description present 2019-02-24 18:26:16 +01:00
jaedle
845b88a193 print only task name if summary 2019-02-24 18:20:59 +01:00
jaedle
e252972c7f rename test 2019-02-24 17:29:03 +01:00
jaedle
a9012ebfc5 refactoring 2019-02-24 17:28:06 +01:00
jaedle
5cfd9bbbbd refactoring 2019-02-24 17:25:03 +01:00
jaedle
c82a7240bb print task in command section 2019-02-24 17:23:31 +01:00
jaedle
a4a20d92a4 add unit test for full output 2019-02-24 17:20:29 +01:00
jaedle
890996f595 hides commands keywoard if not present 2019-02-24 17:12:22 +01:00
jaedle
474f27c6d3 add unit test for displaying commands 2019-02-24 17:10:59 +01:00
jaedle
33f3894372 add unit tests for summary 2019-02-24 17:05:37 +01:00
jaedle
24436ac76e refactoring 2019-02-24 16:26:46 +01:00
jaedle
3ee66ef705 remove output to own package 2019-02-24 16:25:27 +01:00
jaedle
a1765e1d33 refactoring 2019-02-24 16:17:47 +01:00
jaedle
765e3dbf72 print only commands if present 2019-02-24 16:15:59 +01:00
jaedle
80f5cee599 refactoring 2019-02-24 16:10:43 +01:00
jaedle
4dcb124693 print commands on summary only if commands are present 2019-02-24 16:08:32 +01:00
jaedle
31ecf167cc rename to summary in test fixtures 2019-02-24 15:54:11 +01:00
jaedle
3999480d64 refactoring 2019-02-24 15:45:39 +01:00
Andrey Nering
9dbb503c23 Update vendor directory 2019-02-24 11:45:32 -03:00
Andrey Nering
a98f803d87 Upgrade mvdan.cc/sh 2019-02-24 11:44:53 -03:00
jaedle
9e9ffeb5d5 refactoring 2019-02-24 15:43:45 +01:00
jaedle
33d4ad4d84 rename to summary 2019-02-24 15:38:18 +01:00
jaedle
d05d418c4c renaming field in taskfile to summary 2019-02-24 15:37:02 +01:00
jaedle
06d0af7a1d rename details in Executor to summary 2019-02-24 15:33:09 +01:00
jaedle
9a3b726068 change help to summary 2019-02-24 15:32:24 +01:00
jaedle
2676ab9a59 renamed program flag to summary 2019-02-24 15:31:46 +01:00
jaedle
a1837d553e refactoring 2019-02-24 14:59:19 +01:00
jaedle
fdbc130d8d do not show empty dependencies 2019-02-24 14:55:04 +01:00
jaedle
4b3cea3812 display dependend tasks 2019-02-24 14:53:39 +01:00
jaedle
1c3082ffa6 rename test fixture 2019-02-24 14:48:48 +01:00
jaedle
0446cfdba0 display commands of task 2019-02-24 14:37:14 +01:00
jaedle
db1d3183b6 refatoring 2019-02-24 14:32:47 +01:00
jaedle
fb666394fc refatoring 2019-02-24 14:31:29 +01:00
jaedle
1054c89a9d add missing test fixture file 2019-02-24 14:24:55 +01:00
jaedle
8dd87dc482 refactoring 2019-02-24 14:23:44 +01:00
jaedle
b2edbf05a1 refactoring 2019-02-24 14:20:39 +01:00
jaedle
6fb53a406b remove unusued expectations 2019-02-24 14:18:51 +01:00
jaedle
b05fa0821d move expectations for output to testdata 2019-02-24 14:18:07 +01:00
jaedle
0a808b1212 fix swapped expected and actual parameter 2019-02-24 14:10:46 +01:00
jaedle
f1d83e92a7 print command stub on details 2019-02-24 14:08:27 +01:00
jaedle
31b60f7f60 display task name on details 2019-02-24 14:01:53 +01:00
jaedle
c0f9af5daa refactoring 2019-02-24 12:15:59 +01:00
jaedle
b25a9e8884 refactoring 2019-02-24 12:13:18 +01:00
jaedle
3c0cf3cd55 fix documentation 2019-02-24 12:00:45 +01:00
jaedle
1ac6f17e6a should not surpress empty lines expect on last line 2019-02-24 11:58:44 +01:00
jaedle
399a2b38f3 add documentation for details 2019-02-24 11:52:31 +01:00
jaedle
b97221cdb2 ignore empty lines on description 2019-02-24 11:31:25 +01:00
jaedle
0164bc21ea be more specific in tests about output 2019-02-24 11:28:15 +01:00
jaedle
5a23250d32 simplified tests 2019-02-24 11:25:26 +01:00
jaedle
80d88d9789 refactoring 2019-02-24 11:22:14 +01:00
jaedle
31ead854c7 fix test expectation 2019-02-24 11:19:08 +01:00
jaedle
4b64fcb8a4 add more tests 2019-02-24 11:09:55 +01:00
jaedle
a951f2403d add more tests for details 2019-02-24 11:01:48 +01:00
jaedle
f9adeba7f1 add basic test for details 2019-02-24 09:53:49 +01:00
jaedle
5c823d51d0 revert changes for taskfile 2019-02-24 09:29:19 +01:00
jaedle
9be7521b83 refactoring 2019-02-24 09:28:25 +01:00
jaedle
c73ddc3552 refactoring 2019-02-24 09:27:26 +01:00
jaedle
4b7f058f41 refacotring 2019-02-24 09:25:39 +01:00
jaedle
07221a1b20 output detailed task description 2019-02-24 09:24:57 +01:00
jaedle
13614fb3c4 add details flag for cli 2019-02-24 08:51:20 +01:00
jaedle
4fa983bde7 ignore ide configuration 2019-02-24 08:24:09 +01:00
Andrey Nering
9cb1db8c0a Docs: Fix wrong URL 2019-02-21 21:57:21 -03:00
60 changed files with 1707 additions and 280 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
open_collective: task

59
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: Test
on: [push]
jobs:
linux:
name: Linux
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v1
with:
go-version: 1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: Build
run: go build -o ./task -v ./cmd/task
- name: Test
run: ./task test
windows:
name: Windows
runs-on: windows-latest
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v1
with:
go-version: 1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: Build
run: go install -v ./cmd/task
- name: Test
run: go test -v ./...
macos:
name: MacOS
runs-on: macOS-latest
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v1
with:
go-version: 1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: Build
run: go build -o ./task -v ./cmd/task
- name: Test
run: ./task test

8
.gitignore vendored
View File

@@ -18,3 +18,11 @@
dist/ dist/
.DS_Store .DS_Store
# intellij idea/goland
.idea/
# exuberant ctags
tags
/bin

View File

@@ -14,12 +14,11 @@ build:
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
archive: archives:
name_template: "{{.Binary}}_{{.Os}}_{{.Arch}}" - name_template: "{{.Binary}}_{{.Os}}_{{.Arch}}"
format_overrides:
format_overrides: - goos: windows
- goos: windows format: zip
format: zip
release: release:
draft: true draft: true
@@ -30,15 +29,15 @@ snapshot:
checksum: checksum:
name_template: "task_checksums.txt" name_template: "task_checksums.txt"
nfpm: nfpms:
vendor: Task - vendor: Task
homepage: https://github.com/go-task/task homepage: https://github.com/go-task/task
maintainer: Andrey Nering <andrey.nering@gmail.com> maintainer: Andrey Nering <andrey.nering@gmail.com>
description: Simple task runner written in Go description: Simple task runner written in Go
license: MIT license: MIT
conflicts: conflicts:
- taskwarrior - taskwarrior
formats: formats:
- deb - deb
- rpm - rpm
name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}" name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}"

View File

@@ -1,8 +1,8 @@
language: go language: go
go: go:
- 1.10.x - 1.12.x
- 1.11.x - 1.13.x
addons: addons:
apt: apt:
@@ -10,7 +10,7 @@ addons:
- rpm - rpm
script: script:
- go install github.com/go-task/task/cmd/task - go install -v ./cmd/task
- task ci - task ci
deploy: deploy:

View File

@@ -1,5 +1,50 @@
# Changelog # Changelog
## v2.7.0 - 2019-09-22
- Fixed panic bug when assigning a global variable
([#229](https://github.com/go-task/task/issues/229), [#243](https://github.com/go-task/task/issues/234)).
- A task with `method: checksum` will now re-run if generated files are deleted
([#228](https://github.com/go-task/task/pull/228), [#238](https://github.com/go-task/task/issues/238)).
## v2.6.0 - 2019-07-21
- Fixed some bugs regarding minor version checks on `version:`.
- Add `preconditions:` to task
([#205](https://github.com/go-task/task/pull/205)).
- Create directory informed on `dir:` if it doesn't exist
([#209](https://github.com/go-task/task/issues/209), [#211](https://github.com/go-task/task/pull/211)).
- We now have a `--taskfile` flag (alias `-t`), which can be used to run
another Taskfile (other than the default `Taskfile.yml`)
([#221](https://github.com/go-task/task/pull/221)).
- It's now possible to install Task using Homebrew on Linux
([go-task/homebrew-tap#1](https://github.com/go-task/homebrew-tap/pull/1)).
## v2.5.2 - 2019-05-11
- Reverted YAML upgrade due issues with CRLF on Windows
([#201](https://github.com/go-task/task/issues/201), [go-yaml/yaml#450](https://github.com/go-yaml/yaml/issues/450)).
- Allow setting global variables through the CLI
([#192](https://github.com/go-task/task/issues/192)).
## 2.5.1 - 2019-04-27
- Fixed some issues with interactive command line tools, where sometimes
the output were not being shown, and similar issues
([#114](https://github.com/go-task/task/issues/114), [#190](https://github.com/go-task/task/issues/190), [#200](https://github.com/go-task/task/pull/200)).
- Upgraded [go-yaml/yaml](https://github.com/go-yaml/yaml) from v2 to v3.
## v2.5.0 - 2019-03-16
- We moved from the taskfile.org domain to the new fancy taskfile.dev domain.
While stuff is being redirected, we strongly recommend to everyone that use
[this install script](https://taskfile.dev/#/installation?id=install-script)
to use the new taskfile.dev domain on scripts from now on.
- Fixed to the ZSH completion
([#182](https://github.com/go-task/task/pull/182)).
- Add [`--summary` flag along with `summary:` task attribute](https://taskfile.org/#/usage?id=display-summary-of-task)
([#180](https://github.com/go-task/task/pull/180)).
## v2.4.0 - 2019-02-21 ## v2.4.0 - 2019-02-21
- Allow calling a task of the root Taskfile from an included Taskfile - Allow calling a task of the root Taskfile from an included Taskfile

View File

@@ -5,7 +5,7 @@
Task is a task runner / build tool that aims to be simpler and easier to use 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/). than, for example, [GNU Make](https://www.gnu.org/software/make/).
See [taskfile.org](https://taskfile.org) for documentation. See [taskfile.dev](https://taskfile.dev) for documentation.
--- ---

View File

@@ -0,0 +1,421 @@
// This small web app is used to redirect from the old taskfile.org domain
// to the new taskfile.dev without breaking CIs that uses cURL to download
// "/install.sh" without the -L flag (which follow redirects).
package main
import (
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/install.sh" {
println("Dumping install.sh")
w.Write(installShContent)
return
}
println("Redirecting to https://taskfile.dev" + r.URL.Path)
w.Header().Set("Location", "https://taskfile.dev"+r.URL.Path)
w.WriteHeader(301)
})
println("Listening :8080")
panic(http.ListenAndServe(":8080", nil))
}
var installShContent = []byte(`#!/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

@@ -5,6 +5,7 @@ import (
"log" "log"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"syscall" "syscall"
"github.com/go-task/task/v2" "github.com/go-task/task/v2"
@@ -17,7 +18,7 @@ var (
version = "master" version = "master"
) )
const usage = `Usage: task [-ilfwvsd] [--init] [--list] [--force] [--watch] [--verbose] [--silent] [--dir] [--dry] [task...] const usage = `Usage: task [-ilfwvsd] [--init] [--list] [--force] [--watch] [--verbose] [--silent] [--dir] [--taskfile] [--dry] [--summary] [task...]
Runs the specified task(s). Falls back to the "default" task if no task name 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. was specified, or lists all tasks if an unknown task name was specified.
@@ -56,7 +57,9 @@ func main() {
verbose bool verbose bool
silent bool silent bool
dry bool dry bool
summary bool
dir string dir string
entrypoint string
output string output string
) )
@@ -69,7 +72,9 @@ func main() {
pflag.BoolVarP(&verbose, "verbose", "v", false, "enables verbose mode") pflag.BoolVarP(&verbose, "verbose", "v", false, "enables verbose mode")
pflag.BoolVarP(&silent, "silent", "s", false, "disables echoing") 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.BoolVar(&dry, "dry", false, "compiles and prints tasks in the order that they would be run, without executing them")
pflag.BoolVar(&summary, "summary", false, "show summary about a task")
pflag.StringVarP(&dir, "dir", "d", "", "sets directory of execution") pflag.StringVarP(&dir, "dir", "d", "", "sets directory of execution")
pflag.StringVarP(&entrypoint, "taskfile", "t", "", `choose which Taskfile to run. Defaults to "Taskfile.yml"`)
pflag.StringVarP(&output, "output", "o", "", "sets output style: [interleaved|group|prefixed]") pflag.StringVarP(&output, "output", "o", "", "sets output style: [interleaved|group|prefixed]")
pflag.Parse() pflag.Parse()
@@ -89,13 +94,26 @@ func main() {
return return
} }
if dir != "" && entrypoint != "" {
log.Fatal("task: You can't set both --dir and --taskfile")
return
}
if entrypoint != "" {
dir = filepath.Dir(entrypoint)
entrypoint = filepath.Base(entrypoint)
} else {
entrypoint = "Taskfile.yml"
}
e := task.Executor{ e := task.Executor{
Force: force, Force: force,
Watch: watch, Watch: watch,
Verbose: verbose, Verbose: verbose,
Silent: silent, Silent: silent,
Dir: dir, Dir: dir,
Dry: dry, Dry: dry,
Entrypoint: entrypoint,
Summary: summary,
Stdin: os.Stdin, Stdin: os.Stdin,
Stdout: os.Stdout, Stdout: os.Stdout,
@@ -118,9 +136,9 @@ func main() {
arguments = []string{"default"} arguments = []string{"default"}
} }
calls, err := args.Parse(arguments...) calls, globals := args.Parse(arguments...)
if err != nil { for name, value := range globals {
log.Fatal(err) e.Taskfile.Vars[name] = value
} }
ctx := context.Background() ctx := context.Background()
@@ -129,7 +147,7 @@ func main() {
} }
if status { if status {
if err = e.Status(ctx, calls...); err != nil { if err := e.Status(ctx, calls...); err != nil {
log.Fatal(err) log.Fatal(err)
} }
return return

2
completion/zsh/_task Normal file → Executable file
View File

@@ -5,7 +5,7 @@ function __list() {
local -a scripts local -a scripts
if [ -f Taskfile.yml ]; then if [ -f Taskfile.yml ]; then
scripts=($(task -l | sed '1d' | sed 's/://' | awk '{ print $2 }')) scripts=($(task -l | sed '1d' | sed 's/^\* //' | awk '{ print $1 }' | sed 's/:$//' | sed 's/:/\\:/'))
_describe 'script' scripts _describe 'script' scripts
fi fi
} }

View File

@@ -1 +1 @@
taskfile.org taskfile.dev

View File

@@ -8,13 +8,15 @@ The `task_checksums.txt` file contains the sha256 checksum for each file.
## Homebrew ## Homebrew
If you're on macOS and have [Homebrew][homebrew] installed, getting Task is If you're on macOS or Linux and have [Homebrew][homebrew] installed, getting
as simple as running: Task is as simple as running:
```bash ```bash
brew install go-task/tap/go-task brew install go-task/tap/go-task
``` ```
> This installation method is only currently supported on amd64 architectures.
## Snap ## Snap
Task is available for [Snapcraft][snapcraft], but keep in mind that your Task is available for [Snapcraft][snapcraft], but keep in mind that your
@@ -76,7 +78,7 @@ scenarios like CIs. Many thanks to [godownloader][godownloader] for allowing
easily generating this script. easily generating this script.
```bash ```bash
curl -s https://taskfile.org/install.sh | sh curl -sL https://taskfile.dev/install.sh | sh
``` ```
> This method will download the binary on the local `./bin` directory by default. > This method will download the binary on the local `./bin` directory by default.

View File

@@ -28,7 +28,7 @@ the [Snapcraft dashboard][snapcraftdashboard]
Scoop is a community owned installation method. Scoop owners usually take care Scoop is a community owned installation method. Scoop owners usually take care
of updating versions there by editing of updating versions there by editing
[this file](https://github.com/lukesampson/scoop-extras/blob/master/task.json). [this file](https://github.com/lukesampson/scoop-extras/blob/master/bucket/task.json).
If you think its Task version is outdated, open an issue to let us know. If you think its Task version is outdated, open an issue to let us know.
[goreleaser]: https://goreleaser.com/#continuous_integration [goreleaser]: https://goreleaser.com/#continuous_integration

View File

@@ -141,6 +141,21 @@ includes:
docker: ./DockerTasks.yml docker: ./DockerTasks.yml
``` ```
## Version 2.6
Version 2.6 comes with `preconditions` stanza in tasks.
```yaml
version: '2'
tasks:
upload_environment:
preconditions:
- test -f .env
cmds:
- aws s3 cp .env s3://myenvironment
```
Please check the [documentation][includes] Please check the [documentation][includes]
[output]: usage.md#output-syntax [output]: usage.md#output-syntax

View File

@@ -148,6 +148,8 @@ tasks:
- caddy - caddy
``` ```
If the directory doesn't exist, `task` creates it.
## Task dependencies ## Task dependencies
You may have tasks that depend on others. Just pointing them on `deps` will You may have tasks that depend on others. Just pointing them on `deps` will
@@ -344,12 +346,61 @@ up-to-date.
Also, `task --status [tasks]...` will exit with a non-zero exit code if any of Also, `task --status [tasks]...` will exit with a non-zero exit code if any of
the tasks are not up-to-date. the tasks are not up-to-date.
If you need a certain set of conditions to be _true_ you can use the
`preconditions` stanza. `preconditions` are very similar to `status`
lines except they support `sh` expansion and they SHOULD all return 0.
```yaml
version: '2'
tasks:
generate-files:
cmds:
- mkdir directory
- touch directory/file1.txt
- touch directory/file2.txt
# test existence of files
preconditions:
- test -f .env
- sh: "[ 1 = 0 ]"
msg: "One doesn't equal Zero, Halting"
```
Preconditions can set specific failure messages that can tell
a user what steps to take using the `msg` field.
If a task has a dependency on a sub-task with a precondition, and that
precondition is not met - the calling task will fail. Note that a task
executed with a failing precondition will not run unless `--force` is
given.
Unlike `status` which will skip a task if it is up to date, and continue
executing tasks that depend on it, a `precondition` will fail a task, along
with any other tasks that depend on it.
```yaml
version: '2'
tasks:
task_will_fail:
preconditions:
- sh: "exit 1"
task_will_also_fail:
deps:
- task_will_fail
task_will_still_fail:
cmds:
- task: task_will_fail
- echo "I will not run"
```
## Variables ## Variables
When doing interpolation of variables, Task will look for the below. When doing interpolation of variables, Task will look for the below.
They are listed below in order of importance (e.g. most important first): They are listed below in order of importance (e.g. most important first):
- Variables declared locally in the task - Variables declared in the task definition
- Variables given while calling a task from another. - Variables given while calling a task from another.
(See [Calling another task](#calling-another-task) above) (See [Calling another task](#calling-another-task) above)
- Variables declared in the `vars:` option in the `Taskfile` - Variables declared in the `vars:` option in the `Taskfile`
@@ -371,6 +422,12 @@ right before.
$ task write-file FILE=file.txt "CONTENT=Hello, World!" print "MESSAGE=All done!" $ task write-file FILE=file.txt "CONTENT=Hello, World!" print "MESSAGE=All done!"
``` ```
If you want to set global variables using this syntax, give it before any task:
```bash
$ task OUTPUT=file.txt generate-file
```
Example of locally declared vars: Example of locally declared vars:
```yaml ```yaml
@@ -379,7 +436,7 @@ version: '2'
tasks: tasks:
print-var: print-var:
cmds: cmds:
echo "{{.VAR}}" - echo "{{.VAR}}"
vars: vars:
VAR: Hello! VAR: Hello!
``` ```
@@ -542,6 +599,51 @@ would print the following output:
* test: Run all the go tests. * test: Run all the go tests.
``` ```
## Display summary of task
Running `task --summary task-name` will show a summary of a task
The following Taskfile:
```yaml
version: '2'
tasks:
release:
deps: [build]
summary: |
Release your project to github
It will build your project before starting the release it.
Please make sure that you have set GITHUB_TOKEN before starting.
cmds:
- your-release-tool
build:
cmds:
- your-build-tool
```
with running ``task --summary release`` would print the following output:
```
task: release
Release your project to github
It will build your project before starting the release it.
Please make sure that you have set GITHUB_TOKEN before starting.
dependencies:
- build
commands:
- your-release-tool
```
If a summary is missing, the description will be printed.
If the task does not have a summary or a description, a warning is printed.
Please note: *showing the summary will not execute the command*.
## Silent mode ## Silent mode
Silent mode disables echoing of commands before Task runs it. Silent mode disables echoing of commands before Task runs it.

13
go.mod
View File

@@ -1,23 +1,20 @@
module github.com/go-task/task/v2 module github.com/go-task/task/v2
require ( require (
github.com/Masterminds/semver v1.4.2 github.com/Masterminds/semver v1.4.2 // indirect
github.com/Masterminds/sprig v2.16.0+incompatible github.com/Masterminds/sprig v2.16.0+incompatible
github.com/aokoli/goutils v1.0.1 // indirect github.com/aokoli/goutils v1.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.0.0 // indirect github.com/google/uuid v1.0.0 // indirect
github.com/huandu/xstrings v1.1.0 // indirect github.com/huandu/xstrings v1.1.0 // indirect
github.com/imdario/mergo v0.3.6 // 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/mattn/go-zglob v0.0.1
github.com/mitchellh/go-homedir v1.0.0
github.com/radovskyb/watcher v1.0.5 github.com/radovskyb/watcher v1.0.5
github.com/spf13/pflag v1.0.3 github.com/spf13/pflag v1.0.3
github.com/stretchr/testify v1.3.0 github.com/stretchr/testify v1.3.0
golang.org/x/crypto v0.0.0-20180830192347-182538f80094 // indirect golang.org/x/sync v0.0.0-20190423024810-112230192c58
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 gopkg.in/yaml.v2 v2.2.1
mvdan.cc/sh v2.6.3+incompatible mvdan.cc/sh/v3 v3.0.0-alpha2.0.20190908210725-4a0ebd2f3c1b
) )
go 1.13

29
go.sum
View File

@@ -20,28 +20,33 @@ 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/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 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY=
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 h1:wqt7gb+HjGacvFoLTKeT44C+XVPxu7bvHvKT1IvZ7rw=
github.com/radovskyb/watcher v1.0.5/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg= github.com/radovskyb/watcher v1.0.5/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 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/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 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20180830192347-182538f80094/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180831094639-fa5fdf94c789/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI=
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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/v3 v3.0.0-alpha2.0.20190908210725-4a0ebd2f3c1b h1:kzTXBacNrjp7n8ncNC894X9NUeL8yeKVS5MS0bigJwE=
mvdan.cc/sh v2.6.3+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8= mvdan.cc/sh/v3 v3.0.0-alpha2.0.20190908210725-4a0ebd2f3c1b/go.mod h1:6Cd5lQRZZIHeKAdYoLvhhse0oDOIrS5gBoGacWnuiUE=

View File

@@ -8,7 +8,7 @@ import (
"path/filepath" "path/filepath"
) )
const defaultTaskfile = `# https://taskfile.org const defaultTaskfile = `# https://taskfile.dev
version: '2' version: '2'

View File

@@ -1,36 +1,43 @@
package args package args
import ( import (
"errors"
"strings" "strings"
"github.com/go-task/task/v2/internal/taskfile" "github.com/go-task/task/v2/internal/taskfile"
) )
var (
// ErrVariableWithoutTask is returned when variables are given before any task
ErrVariableWithoutTask = errors.New("task: variable given before any task")
)
// Parse parses command line argument: tasks and vars of each task // Parse parses command line argument: tasks and vars of each task
func Parse(args ...string) ([]taskfile.Call, error) { func Parse(args ...string) ([]taskfile.Call, taskfile.Vars) {
var calls []taskfile.Call var calls []taskfile.Call
var globals taskfile.Vars
for _, arg := range args { for _, arg := range args {
if !strings.Contains(arg, "=") { if !strings.Contains(arg, "=") {
calls = append(calls, taskfile.Call{Task: arg}) calls = append(calls, taskfile.Call{Task: arg})
continue continue
} }
if len(calls) < 1 { if len(calls) < 1 {
return nil, ErrVariableWithoutTask if globals == nil {
} globals = taskfile.Vars{}
}
if calls[len(calls)-1].Vars == nil { name, value := splitVar(arg)
calls[len(calls)-1].Vars = make(taskfile.Vars) globals[name] = taskfile.Var{Static: value}
} } else {
if calls[len(calls)-1].Vars == nil {
calls[len(calls)-1].Vars = make(taskfile.Vars)
}
pair := strings.SplitN(arg, "=", 2) name, value := splitVar((arg))
calls[len(calls)-1].Vars[pair[0]] = taskfile.Var{Static: pair[1]} calls[len(calls)-1].Vars[name] = taskfile.Var{Static: value}
}
} }
return calls, nil
return calls, globals
}
func splitVar(s string) (string, string) {
pair := strings.SplitN(s, "=", 2)
return pair[0], pair[1]
} }

View File

@@ -12,13 +12,13 @@ import (
func TestArgs(t *testing.T) { func TestArgs(t *testing.T) {
tests := []struct { tests := []struct {
Args []string Args []string
Expected []taskfile.Call ExpectedCalls []taskfile.Call
Err error ExpectedGlobals taskfile.Vars
}{ }{
{ {
Args: []string{"task-a", "task-b", "task-c"}, Args: []string{"task-a", "task-b", "task-c"},
Expected: []taskfile.Call{ ExpectedCalls: []taskfile.Call{
{Task: "task-a"}, {Task: "task-a"},
{Task: "task-b"}, {Task: "task-b"},
{Task: "task-c"}, {Task: "task-c"},
@@ -26,7 +26,7 @@ func TestArgs(t *testing.T) {
}, },
{ {
Args: []string{"task-a", "FOO=bar", "task-b", "task-c", "BAR=baz", "BAZ=foo"}, Args: []string{"task-a", "FOO=bar", "task-b", "task-c", "BAR=baz", "BAZ=foo"},
Expected: []taskfile.Call{ ExpectedCalls: []taskfile.Call{
{ {
Task: "task-a", Task: "task-a",
Vars: taskfile.Vars{ Vars: taskfile.Vars{
@@ -45,7 +45,7 @@ func TestArgs(t *testing.T) {
}, },
{ {
Args: []string{"task-a", "CONTENT=with some spaces"}, Args: []string{"task-a", "CONTENT=with some spaces"},
Expected: []taskfile.Call{ ExpectedCalls: []taskfile.Call{
{ {
Task: "task-a", Task: "task-a",
Vars: taskfile.Vars{ Vars: taskfile.Vars{
@@ -55,16 +55,22 @@ func TestArgs(t *testing.T) {
}, },
}, },
{ {
Args: []string{"FOO=bar", "task-a"}, Args: []string{"FOO=bar", "task-a", "task-b"},
Err: args.ErrVariableWithoutTask, ExpectedCalls: []taskfile.Call{
{Task: "task-a"},
{Task: "task-b"},
},
ExpectedGlobals: taskfile.Vars{
"FOO": {Static: "bar"},
},
}, },
} }
for i, test := range tests { for i, test := range tests {
t.Run(fmt.Sprintf("TestArgs%d", i+1), func(t *testing.T) { t.Run(fmt.Sprintf("TestArgs%d", i+1), func(t *testing.T) {
calls, err := args.Parse(test.Args...) calls, globals := args.Parse(test.Args...)
assert.Equal(t, test.Err, err) assert.Equal(t, test.ExpectedCalls, calls)
assert.Equal(t, test.Expected, calls) assert.Equal(t, test.ExpectedGlobals, globals)
}) })
} }
} }

View File

@@ -8,10 +8,10 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"mvdan.cc/sh/expand" "mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/interp" "mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/shell" "mvdan.cc/sh/v3/shell"
"mvdan.cc/sh/syntax" "mvdan.cc/sh/v3/syntax"
) )
// RunCommandOptions is the options for the RunCommand func // RunCommandOptions is the options for the RunCommand func
@@ -49,8 +49,7 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
interp.Dir(opts.Dir), interp.Dir(opts.Dir),
interp.Env(expand.ListEnviron(environ...)), interp.Env(expand.ListEnviron(environ...)),
interp.Module(interp.DefaultExec), interp.WithOpenModules(interp.OpenDevImpls),
interp.Module(interp.OpenDevImpls(interp.DefaultOpen)),
interp.StdIO(opts.Stdin, opts.Stdout, opts.Stderr), interp.StdIO(opts.Stdin, opts.Stdout, opts.Stderr),
) )

View File

@@ -7,7 +7,7 @@ import (
type Group struct{} type Group struct{}
func (Group) WrapWriter(w io.Writer, _ string) io.WriteCloser { func (Group) WrapWriter(w io.Writer, _ string) io.Writer {
return &groupWriter{writer: w} return &groupWriter{writer: w}
} }

View File

@@ -6,18 +6,6 @@ import (
type Interleaved struct{} type Interleaved struct{}
func (Interleaved) WrapWriter(w io.Writer, _ string) io.WriteCloser { func (Interleaved) WrapWriter(w io.Writer, _ string) io.Writer {
return nopWriterCloser{w: w} return 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

@@ -5,5 +5,5 @@ import (
) )
type Output interface { type Output interface {
WrapWriter(w io.Writer, prefix string) io.WriteCloser WrapWriter(w io.Writer, prefix string) io.Writer
} }

View File

@@ -3,6 +3,7 @@ package output_test
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io"
"testing" "testing"
"github.com/go-task/task/v2/internal/output" "github.com/go-task/task/v2/internal/output"
@@ -24,7 +25,7 @@ func TestInterleaved(t *testing.T) {
func TestGroup(t *testing.T) { func TestGroup(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer
var o output.Output = output.Group{} var o output.Output = output.Group{}
var w = o.WrapWriter(&b, "") var w = o.WrapWriter(&b, "").(io.WriteCloser)
fmt.Fprintln(w, "foo\nbar") fmt.Fprintln(w, "foo\nbar")
assert.Equal(t, "", b.String()) assert.Equal(t, "", b.String())
@@ -37,7 +38,7 @@ func TestGroup(t *testing.T) {
func TestPrefixed(t *testing.T) { func TestPrefixed(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer
var o output.Output = output.Prefixed{} var o output.Output = output.Prefixed{}
var w = o.WrapWriter(&b, "prefix") var w = o.WrapWriter(&b, "prefix").(io.WriteCloser)
t.Run("simple use cases", func(t *testing.T) { t.Run("simple use cases", func(t *testing.T) {
b.Reset() b.Reset()

View File

@@ -9,7 +9,7 @@ import (
type Prefixed struct{} type Prefixed struct{}
func (Prefixed) WrapWriter(w io.Writer, prefix string) io.WriteCloser { func (Prefixed) WrapWriter(w io.Writer, prefix string) io.Writer {
return &prefixWriter{writer: w, prefix: prefix} return &prefixWriter{writer: w, prefix: prefix}
} }
@@ -34,12 +34,12 @@ func (pw *prefixWriter) Close() error {
func (pw *prefixWriter) writeOutputLines(force bool) error { func (pw *prefixWriter) writeOutputLines(force bool) error {
for { for {
line, err := pw.buff.ReadString('\n') switch line, err := pw.buff.ReadString('\n'); err {
if err == nil { case nil:
if err = pw.writeLine(line); err != nil { if err = pw.writeLine(line); err != nil {
return err return err
} }
} else if err == io.EOF { case io.EOF:
// if this line was not a complete line, re-add to the buffer // if this line was not a complete line, re-add to the buffer
if !force && !strings.HasSuffix(line, "\n") { if !force && !strings.HasSuffix(line, "\n") {
_, err = pw.buff.WriteString(line) _, err = pw.buff.WriteString(line)
@@ -47,7 +47,7 @@ func (pw *prefixWriter) writeOutputLines(force bool) error {
} }
return pw.writeLine(line) return pw.writeLine(line)
} else { default:
return err return err
} }
} }

View File

@@ -14,10 +14,11 @@ import (
// Checksum validades if a task is up to date by calculating its source // Checksum validades if a task is up to date by calculating its source
// files checksum // files checksum
type Checksum struct { type Checksum struct {
Dir string Dir string
Task string Task string
Sources []string Sources []string
Dry bool Generates []string
Dry bool
} }
// IsUpToDate implements the Checker interface // IsUpToDate implements the Checker interface
@@ -27,7 +28,7 @@ func (c *Checksum) IsUpToDate() (bool, error) {
data, _ := ioutil.ReadFile(checksumFile) data, _ := ioutil.ReadFile(checksumFile)
oldMd5 := strings.TrimSpace(string(data)) oldMd5 := strings.TrimSpace(string(data))
sources, err := glob(c.Dir, c.Sources) sources, err := globs(c.Dir, c.Sources)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -43,6 +44,23 @@ func (c *Checksum) IsUpToDate() (bool, error) {
return false, err return false, err
} }
} }
if len(c.Generates) > 0 {
// For each specified 'generates' field, check whether the files actually exist
for _, g := range c.Generates {
generates, err := glob(c.Dir, g)
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, err
}
if len(generates) == 0 {
return false, nil
}
}
}
return oldMd5 == newMd5, nil return oldMd5 == newMd5, nil
} }
@@ -50,21 +68,14 @@ func (c *Checksum) checksum(files ...string) (string, error) {
h := md5.New() h := md5.New()
for _, f := range files { for _, f := range files {
// also sum the filename, so checksum changes for renaming a file
if _, err := io.Copy(h, strings.NewReader(filepath.Base(f))); err != nil {
return "", err
}
f, err := os.Open(f) f, err := os.Open(f)
if err != nil { if err != nil {
return "", err return "", err
} }
info, err := f.Stat()
if err != nil {
return "", err
}
if info.IsDir() {
continue
}
// also sum the filename, so checksum changes for renaming a file
if _, err = io.Copy(h, strings.NewReader(info.Name())); err != nil {
return "", err
}
if _, err = io.Copy(h, f); err != nil { if _, err = io.Copy(h, f); err != nil {
return "", err return "", err
} }

View File

@@ -1,6 +1,7 @@
package status package status
import ( import (
"os"
"path/filepath" "path/filepath"
"sort" "sort"
@@ -9,21 +10,41 @@ import (
"github.com/mattn/go-zglob" "github.com/mattn/go-zglob"
) )
func glob(dir string, globs []string) (files []string, err error) { func globs(dir string, globs []string) ([]string, error) {
files := make([]string, 0)
for _, g := range globs { for _, g := range globs {
if !filepath.IsAbs(g) { f, err := glob(dir, g)
g = filepath.Join(dir, g)
}
g, err = execext.Expand(g)
if err != nil {
return nil, err
}
f, err := zglob.Glob(g)
if err != nil { if err != nil {
continue continue
} }
files = append(files, f...) files = append(files, f...)
} }
sort.Strings(files) sort.Strings(files)
return return files, nil
}
func glob(dir string, g string) ([]string, error) {
files := make([]string, 0)
if !filepath.IsAbs(g) {
g = filepath.Join(dir, g)
}
g, err := execext.Expand(g)
if err != nil {
return nil, err
}
fs, err := zglob.Glob(g)
if err != nil {
return nil, err
}
for _, f := range fs {
info, err := os.Stat(f)
if err != nil {
return nil, err
}
if info.IsDir() {
continue
}
files = append(files, f)
}
return files, nil
} }

View File

@@ -19,11 +19,11 @@ func (t *Timestamp) IsUpToDate() (bool, error) {
return false, nil return false, nil
} }
sources, err := glob(t.Dir, t.Sources) sources, err := globs(t.Dir, t.Sources)
if err != nil { if err != nil {
return false, nil return false, nil
} }
generates, err := glob(t.Dir, t.Generates) generates, err := globs(t.Dir, t.Generates)
if err != nil { if err != nil {
return false, nil return false, nil
} }

103
internal/summary/summary.go Normal file
View File

@@ -0,0 +1,103 @@
package summary
import (
"strings"
"github.com/go-task/task/v2/internal/logger"
"github.com/go-task/task/v2/internal/taskfile"
)
func PrintTasks(l *logger.Logger, t *taskfile.Taskfile, c []taskfile.Call) {
for i, call := range c {
printSpaceBetweenSummaries(l, i)
PrintTask(l, t.Tasks[call.Task])
}
}
func printSpaceBetweenSummaries(l *logger.Logger, i int) {
spaceRequired := i > 0
if !spaceRequired {
return
}
l.Outf("")
l.Outf("")
}
func PrintTask(l *logger.Logger, t *taskfile.Task) {
printTaskName(l, t)
printTaskDescribingText(t, l)
printTaskDependencies(l, t)
printTaskCommands(l, t)
}
func printTaskDescribingText(t *taskfile.Task, l *logger.Logger) {
if hasSummary(t) {
printTaskSummary(l, t)
} else if hasDescription(t) {
printTaskDescription(l, t)
} else {
printNoDescriptionOrSummary(l)
}
}
func hasSummary(t *taskfile.Task) bool {
return t.Summary != ""
}
func printTaskSummary(l *logger.Logger, t *taskfile.Task) {
lines := strings.Split(t.Summary, "\n")
for i, line := range lines {
notLastLine := i+1 < len(lines)
if notLastLine || line != "" {
l.Outf(line)
}
}
}
func printTaskName(l *logger.Logger, t *taskfile.Task) {
l.Outf("task: %s", t.Task)
l.Outf("")
}
func hasDescription(t *taskfile.Task) bool {
return t.Desc != ""
}
func printTaskDescription(l *logger.Logger, t *taskfile.Task) {
l.Outf(t.Desc)
}
func printNoDescriptionOrSummary(l *logger.Logger) {
l.Outf("(task does not have description or summary)")
}
func printTaskDependencies(l *logger.Logger, t *taskfile.Task) {
if len(t.Deps) == 0 {
return
}
l.Outf("")
l.Outf("dependencies:")
for _, d := range t.Deps {
l.Outf(" - %s", d.Task)
}
}
func printTaskCommands(l *logger.Logger, t *taskfile.Task) {
if len(t.Cmds) == 0 {
return
}
l.Outf("")
l.Outf("commands:")
for _, c := range t.Cmds {
isCommand := c.Cmd != ""
if isCommand {
l.Outf(" - %s", c.Cmd)
} else {
l.Outf(" - Task: %s", c.Task)
}
}
}

View File

@@ -0,0 +1,173 @@
package summary_test
import (
"bytes"
"strings"
"testing"
"github.com/go-task/task/v2/internal/logger"
"github.com/go-task/task/v2/internal/summary"
"github.com/go-task/task/v2/internal/taskfile"
"github.com/stretchr/testify/assert"
)
func TestPrintsDependenciesIfPresent(t *testing.T) {
buffer, l := createDummyLogger()
task := &taskfile.Task{
Deps: []*taskfile.Dep{
{Task: "dep1"},
{Task: "dep2"},
{Task: "dep3"},
},
}
summary.PrintTask(&l, task)
assert.Contains(t, buffer.String(), "\ndependencies:\n - dep1\n - dep2\n - dep3\n")
}
func createDummyLogger() (*bytes.Buffer, logger.Logger) {
buffer := &bytes.Buffer{}
l := logger.Logger{
Stderr: buffer,
Stdout: buffer,
Verbose: false,
}
return buffer, l
}
func TestDoesNotPrintDependenciesIfMissing(t *testing.T) {
buffer, l := createDummyLogger()
task := &taskfile.Task{
Deps: []*taskfile.Dep{},
}
summary.PrintTask(&l, task)
assert.NotContains(t, buffer.String(), "dependencies:")
}
func TestPrintTaskName(t *testing.T) {
buffer, l := createDummyLogger()
task := &taskfile.Task{
Task: "my-task-name",
}
summary.PrintTask(&l, task)
assert.Contains(t, buffer.String(), "task: my-task-name\n")
}
func TestPrintTaskCommandsIfPresent(t *testing.T) {
buffer, l := createDummyLogger()
task := &taskfile.Task{
Cmds: []*taskfile.Cmd{
{Cmd: "command-1"},
{Cmd: "command-2"},
{Task: "task-1"},
},
}
summary.PrintTask(&l, task)
assert.Contains(t, buffer.String(), "\ncommands:\n")
assert.Contains(t, buffer.String(), "\n - command-1\n")
assert.Contains(t, buffer.String(), "\n - command-2\n")
assert.Contains(t, buffer.String(), "\n - Task: task-1\n")
}
func TestDoesNotPrintCommandIfMissing(t *testing.T) {
buffer, l := createDummyLogger()
task := &taskfile.Task{
Cmds: []*taskfile.Cmd{},
}
summary.PrintTask(&l, task)
assert.NotContains(t, buffer.String(), "commands")
}
func TestLayout(t *testing.T) {
buffer, l := createDummyLogger()
task := &taskfile.Task{
Task: "sample-task",
Summary: "line1\nline2\nline3\n",
Deps: []*taskfile.Dep{
{Task: "dependency"},
},
Cmds: []*taskfile.Cmd{
{Cmd: "command"},
},
}
summary.PrintTask(&l, task)
assert.Equal(t, expectedOutput(), buffer.String())
}
func expectedOutput() string {
expected := `task: sample-task
line1
line2
line3
dependencies:
- dependency
commands:
- command
`
return expected
}
func TestPrintDescriptionAsFallback(t *testing.T) {
buffer, l := createDummyLogger()
taskWithoutSummary := &taskfile.Task{
Desc: "description",
}
taskWithSummary := &taskfile.Task{
Desc: "description",
Summary: "summary",
}
taskWithoutSummaryOrDescription := &taskfile.Task{}
summary.PrintTask(&l, taskWithoutSummary)
assert.Contains(t, buffer.String(), "description")
buffer.Reset()
summary.PrintTask(&l, taskWithSummary)
assert.NotContains(t, buffer.String(), "description")
buffer.Reset()
summary.PrintTask(&l, taskWithoutSummaryOrDescription)
assert.Contains(t, buffer.String(), "\n(task does not have description or summary)\n")
}
func TestPrintAllWithSpaces(t *testing.T) {
buffer, l := createDummyLogger()
t1 := &taskfile.Task{Task: "t1"}
t2 := &taskfile.Task{Task: "t2"}
t3 := &taskfile.Task{Task: "t3"}
tasks := make(taskfile.Tasks, 3)
tasks["t1"] = t1
tasks["t2"] = t2
tasks["t3"] = t3
summary.PrintTasks(&l,
&taskfile.Taskfile{Tasks: tasks},
[]taskfile.Call{{Task: "t1"}, {Task: "t2"}, {Task: "t3"}})
assert.True(t, strings.HasPrefix(buffer.String(), "task: t1"))
assert.Contains(t, buffer.String(), "\n(task does not have description or summary)\n\n\ntask: t2")
assert.Contains(t, buffer.String(), "\n(task does not have description or summary)\n\n\ntask: t3")
}

View File

@@ -0,0 +1,45 @@
package taskfile
import (
"errors"
"fmt"
)
var (
// ErrCantUnmarshalPrecondition is returned for invalid precond YAML.
ErrCantUnmarshalPrecondition = errors.New("task: Can't unmarshal precondition value")
)
// Precondition represents a precondition necessary for a task to run
type Precondition struct {
Sh string
Msg string
}
// UnmarshalYAML implements yaml.Unmarshaler interface.
func (p *Precondition) UnmarshalYAML(unmarshal func(interface{}) error) error {
var cmd string
if err := unmarshal(&cmd); err == nil {
p.Sh = cmd
p.Msg = fmt.Sprintf("`%s` failed", cmd)
return nil
}
var sh struct {
Sh string
Msg string
}
if err := unmarshal(&sh); err != nil {
return err
}
p.Sh = sh.Sh
p.Msg = sh.Msg
if p.Msg == "" {
p.Msg = fmt.Sprintf("%s failed", sh.Sh)
}
return nil
}

View File

@@ -0,0 +1,48 @@
package taskfile_test
import (
"testing"
"github.com/go-task/task/v2/internal/taskfile"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v2"
)
func TestPreconditionParse(t *testing.T) {
tests := []struct {
content string
v interface{}
expected interface{}
}{
{
"test -f foo.txt",
&taskfile.Precondition{},
&taskfile.Precondition{Sh: `test -f foo.txt`, Msg: "`test -f foo.txt` failed"},
},
{
"sh: '[ 1 = 0 ]'",
&taskfile.Precondition{},
&taskfile.Precondition{Sh: "[ 1 = 0 ]", Msg: "[ 1 = 0 ] failed"},
},
{`
sh: "[ 1 = 2 ]"
msg: "1 is not 2"
`,
&taskfile.Precondition{},
&taskfile.Precondition{Sh: "[ 1 = 2 ]", Msg: "1 is not 2"},
},
{`
sh: "[ 1 = 2 ]"
msg: "1 is not 2"
`,
&taskfile.Precondition{},
&taskfile.Precondition{Sh: "[ 1 = 2 ]", Msg: "1 is not 2"},
},
}
for _, test := range tests {
err := yaml.Unmarshal([]byte(test.content), test.v)
assert.NoError(t, err)
assert.Equal(t, test.expected, test.v)
}
}

View File

@@ -15,16 +15,13 @@ import (
var ( var (
// ErrIncludedTaskfilesCantHaveIncludes is returned when a included Taskfile contains includes // 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") 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 // Taskfile reads a Taskfile for a given directory
func Taskfile(dir string) (*taskfile.Taskfile, error) { func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
path := filepath.Join(dir, "Taskfile.yml") path := filepath.Join(dir, entrypoint)
if _, err := os.Stat(path); err != nil { if _, err := os.Stat(path); err != nil {
return nil, ErrNoTaskfileFound return nil, fmt.Errorf(`task: No Taskfile found on "%s". Use "task --init" to create a new one`, path)
} }
t, err := readTaskfile(path) t, err := readTaskfile(path)
if err != nil { if err != nil {

View File

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

View File

@@ -40,5 +40,8 @@ func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error {
if tf.Expansions <= 0 { if tf.Expansions <= 0 {
tf.Expansions = 2 tf.Expansions = 2
} }
if tf.Vars == nil {
tf.Vars = make(Vars)
}
return nil return nil
} }

View File

@@ -1,46 +0,0 @@
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
}

31
precondition.go Normal file
View File

@@ -0,0 +1,31 @@
package task
import (
"context"
"errors"
"github.com/go-task/task/v2/internal/execext"
"github.com/go-task/task/v2/internal/taskfile"
)
var (
// ErrPreconditionFailed is returned when a precondition fails
ErrPreconditionFailed = errors.New("task: precondition not met")
)
func (e *Executor) areTaskPreconditionsMet(ctx context.Context, t *taskfile.Task) (bool, error) {
for _, p := range t.Preconditions {
err := execext.RunCommand(ctx, &execext.RunCommandOptions{
Command: p.Sh,
Dir: t.Dir,
Env: getEnviron(t),
})
if err != nil {
e.Logger.Errf("task: %s", p.Msg)
return false, ErrPreconditionFailed
}
}
return true, nil
}

View File

@@ -58,10 +58,11 @@ func (e *Executor) getStatusChecker(t *taskfile.Task) (status.Checker, error) {
}, nil }, nil
case "checksum": case "checksum":
return &status.Checksum{ return &status.Checksum{
Dir: t.Dir, Dir: t.Dir,
Task: t.Task, Task: t.Task,
Sources: t.Sources, Sources: t.Sources,
Dry: e.Dry, Generates: t.Generates,
Dry: e.Dry,
}, nil }, nil
case "none": case "none":
return status.None{}, nil return status.None{}, nil
@@ -78,8 +79,10 @@ func (e *Executor) isTaskUpToDateStatus(ctx context.Context, t *taskfile.Task) (
Env: getEnviron(t), Env: getEnviron(t),
}) })
if err != nil { if err != nil {
e.Logger.VerboseOutf("task: status command %s exited non-zero: %s", s, err)
return false, nil return false, nil
} }
e.Logger.VerboseOutf("task: status command %s exited zero", s)
} }
return true, nil return true, nil
} }

136
task.go
View File

@@ -2,9 +2,12 @@ package task
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
"strconv"
"sync"
"sync/atomic" "sync/atomic"
"github.com/go-task/task/v2/internal/compiler" "github.com/go-task/task/v2/internal/compiler"
@@ -13,11 +16,10 @@ import (
"github.com/go-task/task/v2/internal/execext" "github.com/go-task/task/v2/internal/execext"
"github.com/go-task/task/v2/internal/logger" "github.com/go-task/task/v2/internal/logger"
"github.com/go-task/task/v2/internal/output" "github.com/go-task/task/v2/internal/output"
"github.com/go-task/task/v2/internal/summary"
"github.com/go-task/task/v2/internal/taskfile" "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/read"
"github.com/go-task/task/v2/internal/taskfile/version"
"github.com/Masterminds/semver"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
@@ -30,12 +32,15 @@ const (
// Executor executes a Taskfile // Executor executes a Taskfile
type Executor struct { type Executor struct {
Taskfile *taskfile.Taskfile Taskfile *taskfile.Taskfile
Dir string
Force bool Dir string
Watch bool Entrypoint string
Verbose bool Force bool
Silent bool Watch bool
Dry bool Verbose bool
Silent bool
Dry bool
Summary bool
Stdin io.Reader Stdin io.Reader
Stdout io.Writer Stdout io.Writer
@@ -49,6 +54,7 @@ type Executor struct {
taskvars taskfile.Vars taskvars taskfile.Vars
taskCallCount map[string]*int32 taskCallCount map[string]*int32
mkdirMutexMap map[string]*sync.Mutex
} }
// Run runs Task // Run runs Task
@@ -62,6 +68,11 @@ func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error {
} }
} }
if e.Summary {
summary.PrintTasks(e.Logger, e.Taskfile, calls)
return nil
}
if e.Watch { if e.Watch {
return e.watchTasks(calls...) return e.watchTasks(calls...)
} }
@@ -76,8 +87,12 @@ func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error {
// Setup setups Executor's internal state // Setup setups Executor's internal state
func (e *Executor) Setup() error { func (e *Executor) Setup() error {
if e.Entrypoint == "" {
e.Entrypoint = "Taskfile.yml"
}
var err error var err error
e.Taskfile, err = read.Taskfile(e.Dir) e.Taskfile, err = read.Taskfile(e.Dir, e.Entrypoint)
if err != nil { if err != nil {
return err return err
} }
@@ -86,11 +101,6 @@ func (e *Executor) Setup() error {
return err 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 { if e.Stdin == nil {
e.Stdin = os.Stdin e.Stdin = os.Stdin
} }
@@ -105,14 +115,30 @@ func (e *Executor) Setup() error {
Stderr: e.Stderr, Stderr: e.Stderr,
Verbose: e.Verbose, Verbose: e.Verbose,
} }
switch {
case version.IsV1(v): v, err := strconv.ParseFloat(e.Taskfile.Version, 64)
if err != nil {
return fmt.Errorf(`task: Could not parse taskfile version "%s": %v`, e.Taskfile.Version, err)
}
// consider as equal to the greater version if round
if v == 2.0 {
v = 2.6
}
if v < 1 {
return fmt.Errorf(`task: Taskfile version should be greater or equal to v1`)
}
if v > 2.6 {
return fmt.Errorf(`task: Taskfile versions greater than v2.6 not implemented in the version of Task`)
}
if v < 2 {
e.Compiler = &compilerv1.CompilerV1{ e.Compiler = &compilerv1.CompilerV1{
Dir: e.Dir, Dir: e.Dir,
Vars: e.taskvars, Vars: e.taskvars,
Logger: e.Logger, Logger: e.Logger,
} }
case version.IsV2(v), version.IsV21(v), version.IsV22(v): } else { // v >= 2
e.Compiler = &compilerv2.CompilerV2{ e.Compiler = &compilerv2.CompilerV2{
Dir: e.Dir, Dir: e.Dir,
Taskvars: e.taskvars, Taskvars: e.taskvars,
@@ -120,16 +146,15 @@ func (e *Executor) Setup() error {
Expansions: e.Taskfile.Expansions, Expansions: e.Taskfile.Expansions,
Logger: e.Logger, 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 != "" { if v < 2.1 && e.Taskfile.Output != "" {
return fmt.Errorf(`task: Taskfile option "output" is only available starting on Taskfile version v2.1`) return fmt.Errorf(`task: Taskfile option "output" is only available starting on Taskfile version v2.1`)
} }
if !version.IsV22(v) && len(e.Taskfile.Includes) > 0 { if v < 2.2 && len(e.Taskfile.Includes) > 0 {
return fmt.Errorf(`task: Including Taskfiles is only available starting on Taskfile version v2.2`) return fmt.Errorf(`task: Including Taskfiles is only available starting on Taskfile version v2.2`)
} }
if e.OutputStyle != "" { if e.OutputStyle != "" {
e.Taskfile.Output = e.OutputStyle e.Taskfile.Output = e.OutputStyle
} }
@@ -144,8 +169,8 @@ func (e *Executor) Setup() error {
return fmt.Errorf(`task: output option "%s" not recognized`, e.Taskfile.Output) return fmt.Errorf(`task: output option "%s" not recognized`, e.Taskfile.Output)
} }
if !version.IsV21(v) { if v <= 2.1 {
err := fmt.Errorf(`task: Taskfile option "ignore_error" is only available starting on Taskfile version v2.1`) err := errors.New(`task: Taskfile option "ignore_error" is only available starting on Taskfile version v2.1`)
for _, task := range e.Taskfile.Tasks { for _, task := range e.Taskfile.Tasks {
if task.IgnoreError { if task.IgnoreError {
@@ -159,9 +184,19 @@ func (e *Executor) Setup() error {
} }
} }
if v < 2.6 {
for _, task := range e.Taskfile.Tasks {
if len(task.Preconditions) > 0 {
return errors.New(`task: Task option "preconditions" is only available starting on Taskfile version v2.6`)
}
}
}
e.taskCallCount = make(map[string]*int32, len(e.Taskfile.Tasks)) e.taskCallCount = make(map[string]*int32, len(e.Taskfile.Tasks))
e.mkdirMutexMap = make(map[string]*sync.Mutex, len(e.Taskfile.Tasks))
for k := range e.Taskfile.Tasks { for k := range e.Taskfile.Tasks {
e.taskCallCount[k] = new(int32) e.taskCallCount[k] = new(int32)
e.mkdirMutexMap[k] = &sync.Mutex{}
} }
return nil return nil
} }
@@ -181,11 +216,17 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
} }
if !e.Force { if !e.Force {
preCondMet, err := e.areTaskPreconditionsMet(ctx, t)
if err != nil {
return err
}
upToDate, err := e.isTaskUpToDate(ctx, t) upToDate, err := e.isTaskUpToDate(ctx, t)
if err != nil { if err != nil {
return err return err
} }
if upToDate {
if upToDate && preCondMet {
if !e.Silent { if !e.Silent {
e.Logger.Errf(`task: Task "%s" is up to date`, t.Task) e.Logger.Errf(`task: Task "%s" is up to date`, t.Task)
} }
@@ -193,6 +234,10 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
} }
} }
if err := e.mkdir(t); err != nil {
e.Logger.Errf("task: cannot make directory %q: %v", t.Dir, err)
}
for i := range t.Cmds { for i := range t.Cmds {
if err := e.runCommand(ctx, t, call, i); err != nil { if err := e.runCommand(ctx, t, call, i); err != nil {
if err2 := e.statusOnError(t); err2 != nil { if err2 := e.statusOnError(t); err2 != nil {
@@ -210,6 +255,23 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
return nil return nil
} }
func (e *Executor) mkdir(t *taskfile.Task) error {
if t.Dir == "" {
return nil
}
mutex := e.mkdirMutexMap[t.Task]
mutex.Lock()
defer mutex.Unlock()
if _, err := os.Stat(t.Dir); os.IsNotExist(err) {
if err := os.MkdirAll(t.Dir, 0755); err != nil {
return err
}
}
return nil
}
func (e *Executor) runDeps(ctx context.Context, t *taskfile.Task) error { func (e *Executor) runDeps(ctx context.Context, t *taskfile.Task) error {
g, ctx := errgroup.WithContext(ctx) g, ctx := errgroup.WithContext(ctx)
@@ -217,7 +279,11 @@ func (e *Executor) runDeps(ctx context.Context, t *taskfile.Task) error {
d := d d := d
g.Go(func() error { g.Go(func() error {
return e.RunTask(ctx, taskfile.Call{Task: d.Task, Vars: d.Vars}) err := e.RunTask(ctx, taskfile.Call{Task: d.Task, Vars: d.Vars})
if err != nil {
return err
}
return nil
}) })
} }
@@ -229,7 +295,11 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
switch { switch {
case cmd.Task != "": case cmd.Task != "":
return e.RunTask(ctx, taskfile.Call{Task: cmd.Task, Vars: cmd.Vars}) err := e.RunTask(ctx, taskfile.Call{Task: cmd.Task, Vars: cmd.Vars})
if err != nil {
return err
}
return nil
case cmd.Cmd != "": case cmd.Cmd != "":
if e.Verbose || (!cmd.Silent && !t.Silent && !e.Silent) { if e.Verbose || (!cmd.Silent && !t.Silent && !e.Silent) {
e.Logger.Errf(cmd.Cmd) e.Logger.Errf(cmd.Cmd)
@@ -241,8 +311,18 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
stdOut := e.Output.WrapWriter(e.Stdout, t.Prefix) stdOut := e.Output.WrapWriter(e.Stdout, t.Prefix)
stdErr := e.Output.WrapWriter(e.Stderr, t.Prefix) stdErr := e.Output.WrapWriter(e.Stderr, t.Prefix)
defer stdOut.Close() defer func() {
defer stdErr.Close() if _, ok := stdOut.(*os.File); !ok {
if closer, ok := stdOut.(io.Closer); ok {
closer.Close()
}
}
if _, ok := stdErr.(*os.File); !ok {
if closer, ok := stdErr.(io.Closer); ok {
closer.Close()
}
}
}()
err := execext.RunCommand(ctx, &execext.RunCommandOptions{ err := execext.RunCommand(ctx, &execext.RunCommandOptions{
Command: cmd.Cmd, Command: cmd.Cmd,

View File

@@ -7,13 +7,13 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"testing" "testing"
"github.com/go-task/task/v2" "github.com/go-task/task/v2"
"github.com/go-task/task/v2/internal/taskfile" "github.com/go-task/task/v2/internal/taskfile"
"github.com/mitchellh/go-homedir"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -236,7 +236,7 @@ func TestDeps(t *testing.T) {
for _, f := range files { for _, f := range files {
f = filepath.Join(dir, f) f = filepath.Join(dir, f)
if _, err := os.Stat(f); err != nil { if _, err := os.Stat(f); err != nil {
t.Errorf("File %s should exists", f) t.Errorf("File %s should exist", f)
} }
} }
} }
@@ -248,7 +248,7 @@ func TestStatus(t *testing.T) {
_ = os.Remove(file) _ = os.Remove(file)
if _, err := os.Stat(file); err == nil { if _, err := os.Stat(file); err == nil {
t.Errorf("File should not exists: %v", err) t.Errorf("File should not exist: %v", err)
} }
var buff bytes.Buffer var buff bytes.Buffer
@@ -262,7 +262,7 @@ func TestStatus(t *testing.T) {
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "gen-foo"})) assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "gen-foo"}))
if _, err := os.Stat(file); err != nil { if _, err := os.Stat(file); err != nil {
t.Errorf("File should exists: %v", err) t.Errorf("File should exist: %v", err)
} }
e.Silent = false e.Silent = false
@@ -273,6 +273,47 @@ func TestStatus(t *testing.T) {
} }
} }
func TestPrecondition(t *testing.T) {
const dir = "testdata/precondition"
var buff bytes.Buffer
e := &task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
}
// A precondition that has been met
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "foo"}))
if buff.String() != "" {
t.Errorf("Got Output when none was expected: %s", buff.String())
}
// A precondition that was not met
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "impossible"}))
if buff.String() != "task: 1 != 0 obviously!\n" {
t.Errorf("Wrong output message: %s", buff.String())
}
buff.Reset()
// Calling a task with a precondition in a dependency fails the task
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "depends_on_impossible"}))
if buff.String() != "task: 1 != 0 obviously!\n" {
t.Errorf("Wrong output message: %s", buff.String())
}
buff.Reset()
// Calling a task with a precondition in a cmd fails the task
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "executes_failing_task_as_cmd"}))
if buff.String() != "task: 1 != 0 obviously!\n" {
t.Errorf("Wrong output message: %s", buff.String())
}
buff.Reset()
}
func TestGenerates(t *testing.T) { func TestGenerates(t *testing.T) {
const ( const (
srcTask = "sub/src.txt" srcTask = "sub/src.txt"
@@ -290,7 +331,7 @@ func TestGenerates(t *testing.T) {
path := filepath.Join(dir, task) path := filepath.Join(dir, task)
_ = os.Remove(path) _ = os.Remove(path)
if _, err := os.Stat(path); err == nil { if _, err := os.Stat(path); err == nil {
t.Errorf("File should not exists: %v", err) t.Errorf("File should not exist: %v", err)
} }
} }
@@ -311,10 +352,10 @@ func TestGenerates(t *testing.T) {
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: theTask})) assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: theTask}))
if _, err := os.Stat(srcFile); err != nil { if _, err := os.Stat(srcFile); err != nil {
t.Errorf("File should exists: %v", err) t.Errorf("File should exist: %v", err)
} }
if _, err := os.Stat(destFile); err != nil { if _, err := os.Stat(destFile); err != nil {
t.Errorf("File should exists: %v", err) t.Errorf("File should exist: %v", err)
} }
// Ensure task was not incorrectly found to be up-to-date on first run. // Ensure task was not incorrectly found to be up-to-date on first run.
if buff.String() == upToDate { if buff.String() == upToDate {
@@ -371,7 +412,7 @@ func TestInit(t *testing.T) {
_ = os.Remove(file) _ = os.Remove(file)
if _, err := os.Stat(file); err == nil { if _, err := os.Stat(file); err == nil {
t.Errorf("Taskfile.yml should not exists") t.Errorf("Taskfile.yml should not exist")
} }
if err := task.InitTaskfile(ioutil.Discard, dir); err != nil { if err := task.InitTaskfile(ioutil.Discard, dir); err != nil {
@@ -379,7 +420,7 @@ func TestInit(t *testing.T) {
} }
if _, err := os.Stat(file); err != nil { if _, err := os.Stat(file); err != nil {
t.Errorf("Taskfile.yml should exists") t.Errorf("Taskfile.yml should exist")
} }
} }
@@ -437,7 +478,7 @@ func TestTaskIgnoreErrors(t *testing.T) {
func TestExpand(t *testing.T) { func TestExpand(t *testing.T) {
const dir = "testdata/expand" const dir = "testdata/expand"
home, err := homedir.Dir() home, err := os.UserHomeDir()
if err != nil { if err != nil {
t.Errorf("Couldn't get $HOME: %v", err) t.Errorf("Couldn't get $HOME: %v", err)
} }
@@ -553,3 +594,90 @@ func TestIncludesCallingRoot(t *testing.T) {
} }
tt.Run(t) tt.Run(t)
} }
func TestSummary(t *testing.T) {
const dir = "testdata/summary"
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
Summary: true,
Silent: true,
}
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "task-with-summary"}, taskfile.Call{Task: "other-task-with-summary"}))
data, err := ioutil.ReadFile(filepath.Join(dir, "task-with-summary.txt"))
assert.NoError(t, err)
expectedOutput := string(data)
if runtime.GOOS == "windows" {
expectedOutput = strings.Replace(expectedOutput, "\r\n", "\n", -1)
}
assert.Equal(t, expectedOutput, buff.String())
}
func TestWhenNoDirAttributeItRunsInSameDirAsTaskfile(t *testing.T) {
const expected = "dir"
const dir = "testdata/" + expected
var out bytes.Buffer
e := &task.Executor{
Dir: dir,
Stdout: &out,
Stderr: &out,
}
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "whereami"}))
// got should be the "dir" part of "testdata/dir"
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
assert.Equal(t, expected, got, "Mismatch in the working directory")
}
func TestWhenDirAttributeAndDirExistsItRunsInThatDir(t *testing.T) {
const expected = "exists"
const dir = "testdata/dir/explicit_exists"
var out bytes.Buffer
e := &task.Executor{
Dir: dir,
Stdout: &out,
Stderr: &out,
}
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "whereami"}))
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
assert.Equal(t, expected, got, "Mismatch in the working directory")
}
func TestWhenDirAttributeItCreatesMissingAndRunsInThatDir(t *testing.T) {
const expected = "createme"
const dir = "testdata/dir/explicit_doesnt_exist/"
const toBeCreated = dir + expected
const target = "whereami"
var out bytes.Buffer
e := &task.Executor{
Dir: dir,
Stdout: &out,
Stderr: &out,
}
// Ensure that the directory to be created doesn't actually exist.
_ = os.Remove(toBeCreated)
if _, err := os.Stat(toBeCreated); err == nil {
t.Errorf("Directory should not exist: %v", err)
}
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: target}))
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
assert.Equal(t, expected, got, "Mismatch in the working directory")
// Clean-up after ourselves only if no error.
_ = os.Remove(toBeCreated)
}

7
testdata/dir/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
version: '2'
tasks:
whereami:
cmds:
- pwd
silent: true

View File

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

View File

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

View File

19
testdata/precondition/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
version: '2'
tasks:
foo:
preconditions:
- test -f foo.txt
impossible:
preconditions:
- sh: "[ 1 = 0 ]"
msg: "1 != 0 obviously!"
depends_on_impossible:
deps:
- impossible
executes_failing_task_as_cmd:
cmds:
- task: impossible

0
testdata/precondition/foo.txt vendored Normal file
View File

26
testdata/summary/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
version: 2
tasks:
task-with-summary:
deps: [dependend-task-1, dependend-task-2]
summary: |
summary of task-with-summary - line 1
line 2
line 3
cmds:
- echo 'task-with-summary was executed'
- echo 'another command'
- exit 0
other-task-with-summary:
summary: summary of other-task-with-summary
cmds:
- echo 'other-task-with-summary was executed'
dependend-task-1:
cmds:
- echo 'dependend-task-1 was executed'
dependend-task-2:
cmds:
- echo 'dependend-task-2 was executed'

22
testdata/summary/task-with-summary.txt vendored Normal file
View File

@@ -0,0 +1,22 @@
task: task-with-summary
summary of task-with-summary - line 1
line 2
line 3
dependencies:
- dependend-task-1
- dependend-task-2
commands:
- echo 'task-with-summary was executed'
- echo 'another command'
- exit 0
task: other-task-with-summary
summary of other-task-with-summary
commands:
- echo 'other-task-with-summary was executed'

View File

@@ -84,5 +84,15 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) {
} }
} }
if len(origTask.Preconditions) > 0 {
new.Preconditions = make([]*taskfile.Precondition, len(origTask.Preconditions))
for i, precond := range origTask.Preconditions {
new.Preconditions[i] = &taskfile.Precondition{
Sh: r.Replace(precond.Sh),
Msg: r.Replace(precond.Msg),
}
}
}
return &new, r.Err() return &new, r.Err()
} }

2
vendor/modules.txt vendored
View File

@@ -38,7 +38,7 @@ golang.org/x/sys/unix
golang.org/x/sys/windows golang.org/x/sys/windows
# gopkg.in/yaml.v2 v2.2.1 # gopkg.in/yaml.v2 v2.2.1
gopkg.in/yaml.v2 gopkg.in/yaml.v2
# mvdan.cc/sh v2.6.3+incompatible # mvdan.cc/sh v2.6.4+incompatible
mvdan.cc/sh/expand mvdan.cc/sh/expand
mvdan.cc/sh/interp mvdan.cc/sh/interp
mvdan.cc/sh/shell mvdan.cc/sh/shell

View File

@@ -373,7 +373,11 @@ func (cfg *Config) wordField(wps []syntax.WordPart, ql quoteLevel) ([]fieldPart,
case *syntax.Lit: case *syntax.Lit:
s := x.Value s := x.Value
if i == 0 && ql == quoteNone { if i == 0 && ql == quoteNone {
s = cfg.expandUser(s) if prefix, rest := cfg.expandUser(s); prefix != "" {
// TODO: return two separate fieldParts,
// like in wordFields?
s = prefix + rest
}
} }
if ql == quoteDouble && strings.Contains(s, "\\") { if ql == quoteDouble && strings.Contains(s, "\\") {
buf := cfg.strBuilder() buf := cfg.strBuilder()
@@ -468,7 +472,12 @@ func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) {
case *syntax.Lit: case *syntax.Lit:
s := x.Value s := x.Value
if i == 0 { if i == 0 {
s = cfg.expandUser(s) prefix, rest := cfg.expandUser(s)
curField = append(curField, fieldPart{
quote: quoteSingle,
val: prefix,
})
s = rest
} }
if strings.Contains(s, "\\") { if strings.Contains(s, "\\") {
buf := cfg.strBuilder() buf := cfg.strBuilder()
@@ -562,28 +571,27 @@ func (cfg *Config) quotedElems(pe *syntax.ParamExp) []string {
return nil return nil
} }
func (cfg *Config) expandUser(field string) string { func (cfg *Config) expandUser(field string) (prefix, rest string) {
if len(field) == 0 || field[0] != '~' { if len(field) == 0 || field[0] != '~' {
return field return "", field
} }
name := field[1:] name := field[1:]
rest := ""
if i := strings.Index(name, "/"); i >= 0 { if i := strings.Index(name, "/"); i >= 0 {
rest = name[i:] rest = name[i:]
name = name[:i] name = name[:i]
} }
if name == "" { if name == "" {
return cfg.Env.Get("HOME").String() + rest return cfg.Env.Get("HOME").String(), rest
} }
if vr := cfg.Env.Get("HOME " + name); vr.IsSet() { if vr := cfg.Env.Get("HOME " + name); vr.IsSet() {
return vr.String() + rest return vr.String(), rest
} }
u, err := user.Lookup(name) u, err := user.Lookup(name)
if err != nil { if err != nil {
return field return "", field
} }
return u.HomeDir + rest return u.HomeDir, rest
} }
func findAllIndex(pattern, name string, n int) [][]int { func findAllIndex(pattern, name string, n int) [][]int {

View File

@@ -70,6 +70,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a
r.errf("set: %v\n", err) r.errf("set: %v\n", err)
return 2 return 2
} }
r.updateExpandOpts()
case "shift": case "shift":
n := 1 n := 1
switch len(args) { switch len(args) {

View File

@@ -260,7 +260,6 @@ func Params(args ...string) func(*Runner) error {
args = args[1:] args = args[1:]
} }
r.Params = args r.Params = args
r.updateExpandOpts()
return nil return nil
} }
} }
@@ -479,6 +478,7 @@ func (r *Runner) Reset() {
Exec: r.Exec, Exec: r.Exec,
Open: r.Open, Open: r.Open,
KillTimeout: r.KillTimeout, KillTimeout: r.KillTimeout,
opts: r.opts,
// emptied below, to reuse the space // emptied below, to reuse the space
Vars: r.Vars, Vars: r.Vars,
@@ -791,7 +791,11 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) {
switch y := x.Loop.(type) { switch y := x.Loop.(type) {
case *syntax.WordIter: case *syntax.WordIter:
name := y.Name.Value name := y.Name.Value
for _, field := range r.fields(y.Items...) { items := r.Params // for i; do ...
if y.InPos.IsValid() {
items = r.fields(y.Items...) // for i in ...; do ...
}
for _, field := range items {
r.setVarString(name, field) r.setVarString(name, field)
if r.loopStmtsBroken(ctx, x.Do) { if r.loopStmtsBroken(ctx, x.Do) {
break break
@@ -1182,6 +1186,10 @@ func (r *Runner) findExecutable(file string, exts []string) string {
return "" return ""
} }
func driveLetter(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
// splitList is like filepath.SplitList, but always using the unix path // splitList is like filepath.SplitList, but always using the unix path
// list separator ':'. On Windows, it also makes sure not to split // list separator ':'. On Windows, it also makes sure not to split
// [A-Z]:[/\]. // [A-Z]:[/\].
@@ -1198,8 +1206,7 @@ func splitList(path string) []string {
for i := 0; i < len(list); i++ { for i := 0; i < len(list); i++ {
s := list[i] s := list[i]
switch { switch {
case len(s) != 1, s[0] < 'A', s[0] > 'Z': case len(s) != 1, !driveLetter(s[0]):
// not a disk name
case i+1 >= len(list): case i+1 >= len(list):
// last element // last element
case strings.IndexAny(list[i+1], `/\`) != 0: case strings.IndexAny(list[i+1], `/\`) != 0:

View File

@@ -19,7 +19,7 @@ import (
func (r *Runner) bashTest(ctx context.Context, expr syntax.TestExpr, classic bool) string { func (r *Runner) bashTest(ctx context.Context, expr syntax.TestExpr, classic bool) string {
switch x := expr.(type) { switch x := expr.(type) {
case *syntax.Word: case *syntax.Word:
return r.literal(x) return r.document(x)
case *syntax.ParenTest: case *syntax.ParenTest:
return r.bashTest(ctx, x.X, classic) return r.bashTest(ctx, x.X, classic)
case *syntax.BinaryTest: case *syntax.BinaryTest:

View File

@@ -3,4 +3,12 @@
// Package shell contains high-level features that use the syntax, expand, and // Package shell contains high-level features that use the syntax, expand, and
// interp packages under the hood. // interp packages under the hood.
//
// Please note that this package uses POSIX Shell syntax. As such, path names on
// Windows need to use double backslashes or be within single quotes when given
// to functions like Fields. For example:
//
// shell.Fields("echo /foo/bar") // on Unix-like
// shell.Fields("echo C:\\foo\\bar") // on Windows
// shell.Fields("echo 'C:\foo\bar'") // on Windows, with quotes
package shell package shell

View File

@@ -376,14 +376,21 @@ func (*WordIter) loopNode() {}
func (*CStyleLoop) loopNode() {} func (*CStyleLoop) loopNode() {}
// WordIter represents the iteration of a variable over a series of words in a // WordIter represents the iteration of a variable over a series of words in a
// for clause. // for clause. If InPos is an invalid position, the "in" token was missing, so
// the iteration is over the shell's positional parameters.
type WordIter struct { type WordIter struct {
Name *Lit Name *Lit
InPos Pos // position of "in"
Items []*Word Items []*Word
} }
func (w *WordIter) Pos() Pos { return w.Name.Pos() } func (w *WordIter) Pos() Pos { return w.Name.Pos() }
func (w *WordIter) End() Pos { return posMax(w.Name.End(), wordLastEnd(w.Items)) } func (w *WordIter) End() Pos {
if len(w.Items) > 0 {
return wordLastEnd(w.Items)
}
return posMax(w.Name.End(), posAddCol(w.InPos, 2))
}
// CStyleLoop represents the behaviour of a for clause similar to the C // CStyleLoop represents the behaviour of a for clause similar to the C
// language. // language.
@@ -781,8 +788,12 @@ func (a *ArrayExpr) Pos() Pos { return a.Lparen }
func (a *ArrayExpr) End() Pos { return posAddCol(a.Rparen, 1) } func (a *ArrayExpr) End() Pos { return posAddCol(a.Rparen, 1) }
// ArrayElem represents a Bash array element. // ArrayElem represents a Bash array element.
//
// Index can be nil; for example, declare -a x=(value).
// Value can be nil; for example, declare -A x=([index]=).
// Finally, neither can be nil; for example, declare -A x=([index]=value)
type ArrayElem struct { type ArrayElem struct {
Index ArithmExpr // [i]=, ["k"]= Index ArithmExpr
Value *Word Value *Word
Comments []Comment Comments []Comment
} }
@@ -793,7 +804,12 @@ func (a *ArrayElem) Pos() Pos {
} }
return a.Value.Pos() return a.Value.Pos()
} }
func (a *ArrayElem) End() Pos { return a.Value.End() } func (a *ArrayElem) End() Pos {
if a.Value != nil {
return a.Value.End()
}
return posAddCol(a.Index.Pos(), 1)
}
// ExtGlob represents a Bash extended globbing expression. Note that these are // ExtGlob represents a Bash extended globbing expression. Note that these are
// parsed independently of whether shopt has been called or not. // parsed independently of whether shopt has been called or not.

View File

@@ -1003,6 +1003,11 @@ func (p *Parser) wordPart() WordPart {
p.next() p.next()
cs.StmtList = p.stmtList() cs.StmtList = p.stmtList()
if p.tok == bckQuote && p.lastBquoteEsc < p.openBquotes-1 {
// e.g. found ` before the nested backquote \` was closed.
p.tok = _EOF
p.quoteErr(cs.Pos(), bckQuote)
}
p.postNested(old) p.postNested(old)
p.openBquotes-- p.openBquotes--
cs.Right = p.pos cs.Right = p.pos
@@ -1587,11 +1592,16 @@ func (p *Parser) getAssign(needEqual bool) *Assign {
p.follow(left, `"[x]"`, assgn) p.follow(left, `"[x]"`, assgn)
} }
if ae.Value = p.getWord(); ae.Value == nil { if ae.Value = p.getWord(); ae.Value == nil {
if p.tok == leftParen { switch p.tok {
case leftParen:
p.curErr("arrays cannot be nested") p.curErr("arrays cannot be nested")
return nil
case _Newl, rightParen, leftBrack:
// TODO: support [index]=[
default:
p.curErr("array element values must be words")
break
} }
p.curErr("array element values must be words")
break
} }
if len(p.accComs) > 0 { if len(p.accComs) > 0 {
c := p.accComs[0] c := p.accComs[0]
@@ -2012,7 +2022,8 @@ func (p *Parser) wordIter(ftok string, fpos Pos) *WordIter {
return wi return wi
} }
p.got(_Newl) p.got(_Newl)
if _, ok := p.gotRsrv("in"); ok { if pos, ok := p.gotRsrv("in"); ok {
wi.InPos = pos
for !stopToken(p.tok) { for !stopToken(p.tok) {
if w := p.getWord(); w == nil { if w := p.getWord(); w == nil {
p.curErr("word list can only contain words") p.curErr("word list can only contain words")

View File

@@ -620,7 +620,7 @@ func (p *Printer) loop(loop Loop) {
switch x := loop.(type) { switch x := loop.(type) {
case *WordIter: case *WordIter:
p.WriteString(x.Name.Value) p.WriteString(x.Name.Value)
if len(x.Items) > 0 { if x.InPos.IsValid() {
p.spacedString(" in", Pos{}) p.spacedString(" in", Pos{})
p.wordJoin(x.Items) p.wordJoin(x.Items)
} }
@@ -788,7 +788,9 @@ func (p *Printer) elemJoin(elems []*ArrayElem, last []Comment) {
if p.wroteIndex(el.Index) { if p.wroteIndex(el.Index) {
p.WriteByte('=') p.WriteByte('=')
} }
p.word(el.Value) if el.Value != nil {
p.word(el.Value)
}
p.comments(left...) p.comments(left...)
} }
if len(last) > 0 { if len(last) > 0 {

View File

@@ -199,7 +199,9 @@ func Walk(node Node, f func(Node) bool) {
if x.Index != nil { if x.Index != nil {
Walk(x.Index, f) Walk(x.Index, f)
} }
Walk(x.Value, f) if x.Value != nil {
Walk(x.Value, f)
}
case *ExtGlob: case *ExtGlob:
Walk(x.Pattern, f) Walk(x.Pattern, f)
case *ProcSubst: case *ProcSubst: