Compare commits

..

159 Commits

Author SHA1 Message Date
Andrey Nering
467c4360ca v2.0.3 2018-06-24 10:52:19 -03:00
Andrey Nering
3b152a38b0 .goreleaser.yml: Use CGO_ENABLED=0 2018-06-24 10:51:04 -03:00
Andrey Nering
f4d3855528 Update deps 2018-06-24 10:40:44 -03:00
Andrey Nering
09eab770a7 Skip empty commands
Fixes #120
2018-06-24 10:29:46 -03:00
Andrey Nering
102f8ab74e Expand environment variables on "dir", "sources" and "generates"
So a path like this works: $GOPATH/src/github.com/go-task/task

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

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

Reintroducing this. Reverted unintentionally.
2017-11-19 18:45:41 -02:00
Andrey Nering
71e7cd5808 listening for SIGINT and SIGTERM
closes #75
2017-11-19 18:33:57 -02:00
Andrey Nering
57e42af238 update deps 2017-11-19 18:26:37 -02:00
Andrey Nering
e065dcb816 Revert "list: print message with there's no task with description"
This reverts commit 2508bed363.

I still want to do this, but since it break some existing Taskfiles,
let's give a little more thought on this first.
2017-11-19 18:12:16 -02:00
Andrey Nering
9619c7f54d simplify getVariables() and improve nested variables support
/cc @smyrman
2017-11-17 00:54:26 -02:00
Andrey Nering
2508bed363 list: print message with there's no task with description 2017-11-16 23:35:53 -02:00
Andrey Nering
c469632ee0 update deps 2017-11-02 10:41:46 -02:00
Andrey Nering
44a52359dc checksum: save them in a subdir .task/checksum
future-proof since the .task directory can be used to save other state
in the future
2017-11-02 10:37:02 -02:00
Andrey Nering
2022551b26 checksum: also sum the name of the files, so it changes after renaming 2017-11-02 10:31:34 -02:00
Andrey Nering
baac067a1a expand home dir ("~" symbol) on paths
fixes #74
2017-11-02 10:25:50 -02:00
Andrey Nering
f4216dd67f list tasks: ensure at least one space after colon 2017-11-02 09:45:00 -02:00
Andrey Nering
60186bdcd5 readme: document Snap as an installation method 2017-10-30 21:12:20 -02:00
Andrey Nering
2c2eb1684b readme: fix markdown 2017-10-15 18:03:04 -02:00
Andrey Nering
33b167215d move some packages to the "internal" directory
- this makes it impossible to import these packages outside Task
- as a side effect, it makes the root directory cleaner
2017-10-15 17:58:21 -02:00
Andrey Nering
c53db134c6 move examples to its own repo 2017-10-15 17:55:26 -02:00
Andrey Nering
0513a21e25 update dependencies
also make sure the module that enables /dev/null path of the sh
interpreter is enabled
2017-10-15 17:41:15 -02:00
Andrey Nering
2fc32414f5 project stuff
- move some files to .github/ folder
- add an issue template
- add two more alternatives
2017-10-01 15:46:47 -03:00
Andrey Nering
309bc4ee4c checksum: add tests for filename 2017-10-01 15:06:12 -03:00
Andrey Nering
7977e6fb16 watch: also walk on tasks called on on "cmds" 2017-09-30 15:19:58 -03:00
Andrey Nering
abb19dfbf8 print logs to stderr instead of stdout
also, don't print up-to-date status when the --silent flag was given

closes #68
2017-09-30 14:56:35 -03:00
Andrey Nering
14676dc3f8 checksum: normalize filename 2017-09-30 14:40:11 -03:00
Andrey Nering
c16f8a4d46 checksum: skip directories 2017-09-19 14:03:24 -03:00
Andrey Nering
48bf09da21 remove deprecated set keyword 2017-09-17 11:24:30 -03:00
Andrey Nering
c295a1998a add checksum based status check, alternative to timestamp based 2017-09-17 11:06:47 -03:00
Andrey Nering
95f7b9443f v1.4.3 2017-09-07 14:40:54 -03:00
Andrey Nering
0160f5dd30 update deps
fixes #67 fixed upstream on mvdan/sh#159
2017-09-07 14:02:52 -03:00
Andrey Nering
f3097845b4 allow assigning variables to tasks at run time via CLI
using a similar syntax than setting env variables to command in bash,
but used right after the task:

```bash
task print MESSAGE=Hello!
```

closes #33
2017-09-07 13:57:06 -03:00
Andrey Nering
5e72de4ba2 Revert "execext: use sync.Pool to instantiate parser and runner"
This reverts commit 451b965fb0.
2017-09-07 10:40:21 -03:00
Sindre Røkenes Myren
7a64530e83 Added suport for multiline variables from sh
Instead of giving an error on multiline results from sh, the results are
now stored as is, except that the last newline is stripped away to make
the output of most commands easy to use in shell commands.

Two helper functions have been added to help deal with multi-line
results. In addition, previous PascalCase template function names have
been renamed to camelCase for consistency with the sprig lib.
2017-09-04 10:14:09 +02:00
Andrey Nering
36f3be9979 travis: test Go 1.8 and 1.9 2017-09-02 11:42:33 -03:00
Andrey Nering
451b965fb0 execext: use sync.Pool to instantiate parser and runner
A benchmark was added. The performance improvement is considerable:

BenchmarkNoPool-4   	   30000	     43405 ns/op
BenchmarkPool-4     	   20000	     71219 ns/op
2017-09-02 11:32:24 -03:00
Andrey Nering
2b2852aad7 update deps and moving from github.com/mvdan/sh to mvdan.cc/sh 2017-09-02 11:19:00 -03:00
Sindre Røkenes Myren
72bfd94329 Fixes env: remove square braces and evaluate shell (#62)
Fixes bug #61, and makes `env` work more similar to `vars` by allowing
dynamic shell values to be evaluated as part of `CompiledTask`.
2017-08-16 08:04:58 -03:00
Andrey Nering
300376b0b1 update README 2017-08-05 14:52:32 -03:00
Andrey Nering
e67792177d execext: fix sh 2017-08-05 14:20:44 -03:00
Andrey Nering
0f24fd593e update dependencies
- add watch
- remove fsnotify
- update others
2017-08-05 14:13:35 -03:00
Andrey Nering
23ec6c721d Merge branch 'watch' 2017-08-05 14:06:46 -03:00
Andrey Nering
26761e5445 watch: isContextError is as a var func 2017-08-05 14:04:34 -03:00
Andrey Nering
e78e4e6a2e watch: ignore usually big dirs
Two items by now:
- ".git"
- "node_modules"
2017-08-05 13:35:10 -03:00
Andrey Nering
e765b7a9c4 do not check for maximum call if is watch 2017-08-05 13:26:30 -03:00
Andrey Nering
c210e34ce3 watch: switch to watcher as waych lib and few fixes
watcher whould be more consistent across differente OSes
2017-08-05 11:50:39 -03:00
Jack Mordaunt
ddd063f29e Cancel and restart long running process on file change by using context.WithCancel(..)
Closes #59
2017-08-04 12:48:15 -03:00
Andrey Nering
a2c96e9cdd re-generate vendor/ folder
maybe a bug on dep? these two files were not copied
2017-07-30 21:09:05 -03:00
Andrey Nering
f2416d68b8 fix travis again
something was wrong with .gitignore rules
2017-07-30 21:00:50 -03:00
Andrey Nering
1eccb61d44 fix travis
somehow the dev.go file was ignored by Git
2017-07-30 20:53:54 -03:00
Andrey Nering
cf2b189fbd v1.4.2 2017-07-30 20:42:15 -03:00
Andrey Nering
394b69676a rename func to handleShVar 2017-07-30 20:36:35 -03:00
Andrey Nering
9704dc5734 reuse of minTime and maxTime funcs 2017-07-30 20:00:35 -03:00
Andrey Nering
f54dde4f78 func isUpToDate should be bound to Task struct 2017-07-30 19:45:59 -03:00
Andrey Nering
70ef9fbcfe rename func: ReplaceVariables -> CompiledTask 2017-07-30 19:34:28 -03:00
Andrey Nering
bb1aff84cf custom stdout for InitTaskfile func 2017-07-30 19:29:49 -03:00
Andrey Nering
dc1ec77da5 own Taskfile: non-verbose test 2017-07-30 19:26:18 -03:00
Andrey Nering
7077b20a54 flag to set directory of execution 2017-07-30 19:24:53 -03:00
Andrey Nering
09e84c2583 always echo command if is verbose mode 2017-07-30 19:21:06 -03:00
Andrey Nering
e3ac6f9e01 fix sh field name 2017-07-30 19:12:26 -03:00
Andrey Nering
f91bbe9397 update deps 2017-07-30 19:11:34 -03:00
Sindre Røkenes Myren
31faf05c3a Reintroduce template evaluation in variables
With a recent commit, template evaluation for variables in tasks got
broken. This reindroudces temmplate evaluation in taks, and resolves
a series of issues that where previouisly present on master, such as:

- Taskvars did not get evaluated as templates.
- Taskvars would, in contrast to the documentation, _override_ task
  variables for the taks called directly via `Executor.Run(args
  ...string)`. This caused different behaviour in the "default" task
  v.s. other tasks.

This commit ensures:
 - Priority order for variables is now according to the documentation,
   also for the "default" task.
 - Variables gets resolved in a particular order to ensure logical
   access to varaibles on template compile time, and that template
   compilation finds place _before_ resolution of dynamic variables.

This change also allows the following to work:

    task:
      vars:
        A: "52"
        B: "{{.A}}"

However, the following will always replace C with the uncompiled
`{{.A}}`:

    task:
      vars:
        A: "52"
        C: "{{.B}}"
        B: "{{.A}}"

Several tests have also been added to prevent this feature from breaking
again. This should hopefully finally resolve issue #40.
2017-07-22 00:50:23 +02:00
Andrey Nering
55672410cd own Taskfile: small improvement of lint task 2017-07-19 20:24:40 -03:00
Andrey Nering
8a66857fb9 Merge branch 'silent' 2017-07-19 20:20:49 -03:00
Andrey Nering
d0b37df615 add silent mode to disable echoing of commands 2017-07-19 20:20:24 -03:00
Andrey Nering
dc6cb68327 rename file.go to status.go, and move related code there 2017-07-16 16:15:29 -03:00
Andrey Nering
72250b32d3 replace variables in a task once, instead of mixing these calls with unrelated code
this is the first big step of #45

now t.ReplaceVariable will return a copy of the task, but with variables
replaced

now we don't need to replace variables everywhere in the code, and will
make other refactorings easier
2017-07-16 16:09:55 -03:00
Andrey Nering
726130be09 v1.4.1 2017-07-15 15:52:33 -03:00
Andrey Nering
968a29d869 deduplicate conversion from dep and cmd to call 2017-07-15 15:46:53 -03:00
Andrey Nering
ce27e973be hotpath for a blank variable template 2017-07-15 15:38:57 -03:00
Andrey Nering
2607866c49 readme: document new syntax for synamic variables 2017-07-15 15:37:24 -03:00
Andrey Nering
e8e914b11c use YAML for dynamix variable instead of $ prefix
$ prefix still works but is now deprecated

before:

    VAR: $echo var

after:

    VAR:
      sh: echo bar

closes #46
2017-07-15 15:28:59 -03:00
Andrey Nering
d22b3b0d88 readme: document the task_checksum.txt file on releases
closes #44
2017-07-15 14:52:31 -03:00
Andrey Nering
5ece1d74f6 fix Taskvars.yml vars not available while interpolating vars prop
closes #40
2017-07-15 14:40:58 -03:00
Andrey Nering
ac7ab42d94 Create CODE_OF_CONDUCT.md 2017-07-15 14:31:53 -03:00
Andrey Nering
38a3f538f5 add CONTRIBUTING.md 2017-07-15 14:27:45 -03:00
Andrey Nering
998935ea55 add --list (or -l) flag to print existing tasks
If an inexixtent task is given, the help also prints as before

Also fixing README documentation

Closes #51
2017-07-15 14:10:46 -03:00
Andrey Nering
e781ac2512 own Taskfile: add task "todo" to print TODOs in code 2017-07-15 13:58:03 -03:00
Andrey Nering
ef2695974d os specific Taskvars file
Now it's possible to have Taskfile_windows.yml or Taskvars_linux.yml
(and others).

Asked in a comment in #46
2017-07-15 13:52:02 -03:00
Andrey Nering
b552cc2b12 yaml: use UnmarshalStrict instead on Unmarshal
https://github.com/go-yaml/yaml/pull/262
2017-07-15 13:36:22 -03:00
Andrey Nering
4ae9c2445d update deps 2017-07-15 13:34:42 -03:00
Sindre Røkenes Myren
769e25f080 Consider task up-to-date on equal timestamps
Fixes issue #48 by considering a task up-to-date if the newest
file from sources and the oldest file from generates has
exactly the same time-stamp.
2017-07-12 18:18:26 +02:00
Sindre Røkenes Myren
86a23849e0 Allow absolute path in generates section
Fixes issue #47 by allowing absolute paths in a task's generates and
sources sections. Tests are added for the generates section only at this
time.
2017-07-12 18:14:34 +02:00
Andrey Nering
a586fef414 Merge pull request #43 from smyrman/issue-42
Bugfix: allow templating when calling deps
2017-07-09 20:02:39 -03:00
Sindre Myren
a548c63a92 Bugfix: allow templating when calling deps
Fixes issue #42 by allowing for template evaluatation on task override
variables when the task is launched as dependency.
2017-07-09 23:48:36 +02:00
Andrey Nering
5268df6bfd only get variable map twice and implement vars template
ref #40
2017-07-08 16:00:17 -03:00
Andrey Nering
82e1c0f810 refactor: functions reorder (higher level first) 2017-07-08 15:13:27 -03:00
Andrey Nering
81b0ffb7f4 rename: readTaskvarsFile() -> readTaskvars() 2017-07-08 15:10:01 -03:00
Andrey Nering
0da130ee2c refactor: simplify some controls 2017-07-08 15:08:44 -03:00
Andrey Nering
6e880c9027 refactor: don't need to run template on Executor.Dir 2017-07-08 15:01:45 -03:00
Andrey Nering
082fa321cb rename 2 files
- read_taskfile.go -> taskfile.go
- variable_handling.go -> variables.go
2017-07-08 14:58:43 -03:00
Andrey Nering
ff1c49f111 refactor: better usage of bytes.Buffer type 2017-07-08 14:57:12 -03:00
Andrey Nering
50f592c540 refactor getVariables() 2017-07-08 14:48:37 -03:00
Andrey Nering
7a7f66dfdc refactor: join task and vars parameters in a single Call struct 2017-07-08 14:34:17 -03:00
Andrey Nering
a1140aa62f Merge branch 'cyclic-dep' 2017-07-08 13:36:45 -03:00
Andrey Nering
2dd3564da1 changed cyclic dep detection
since interpolation can be used, detection should be a execution time,
and not before

now, to prevent infinite execution, there's a miximum of 100 calls per
task

closes #37
2017-07-08 13:33:55 -03:00
Andrey Nering
fb4b0a187e Merge branch 'issue-37-cyclic-dep' 2017-07-08 10:26:18 -03:00
Andrey Nering
ac48ee066e fix panic for invalid task in cyclic dep detection
resolves partly #37
2017-07-08 10:25:15 -03:00
Andrey Nering
06031efc09 cyclic: refactor to return error instead 2017-07-08 10:13:56 -03:00
Andrey Nering
9bea80b862 Merge pull request #41 from zbindenren/master
better error output for dynamic variables in Taskvars.yml
2017-07-07 07:01:21 -03:00
Rene Zbinden
92ecb1c7ec better error output for dynamic variables in Taskvars.yml 2017-07-07 08:33:56 +02:00
Andrey Nering
645f77b849 Merge pull request #39 from smyrman/issue-38
Allow template evaluation when calling a task with vars
2017-07-06 21:30:47 -03:00
Sindre Røkenes Myren
2f9381065d Allow template evaluation in parameters
When passing variables to a sub-task, allow template evaluation
within the passed-on variables.
2017-07-07 00:47:56 +02:00
Sindre Røkenes Myren
774ef61c2f Update usage text slightly 2017-07-06 18:40:01 +02:00
473 changed files with 72833 additions and 24839 deletions

46
.github/CODE_OF_CONDUCT.md vendored Normal file
View File

@@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at andrey.nering@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

9
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,9 @@
* Bug reports and feature requests are welcome in [the issues][issues]
* For questions and discussion there's the [Slack room][slack] ([invititation here][slackinvite])
* Pull Requests are welcome. For more complex changes and features it's
recommended to open an issue with the feature request first
* Documentation contributions are as important as code contributions
[issues]: https://github.com/go-task/task/issues
[slack]: https://gophers.slack.com/messages/task
[slackinvite]: https://invite.slack.golangbridge.org/

9
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

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

8
.gitignore vendored
View File

@@ -16,10 +16,4 @@
./task
dist/
vendor/**
!vendor/**/*.go
!vendor/**/LICENSE
!vendor/**/COPYING
!vendor/**/README
!vendor/**/README.md
vendor/**/*_test.go
.DS_Store

View File

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

View File

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

82
Gopkg.lock generated
View File

@@ -2,16 +2,16 @@
[[projects]]
branch = "master"
name = "github.com/Masterminds/semver"
packages = ["."]
revision = "abff1900528dbdaf6f3f5aa92c398be1eaf2a9f7"
version = "v1.3.0"
revision = "3c560837130448941620d7694991d3ec440aefc0"
[[projects]]
branch = "master"
name = "github.com/Masterminds/sprig"
packages = ["."]
revision = "e039e20e500c2c025d9145be375e27cf42a94174"
revision = "6b2a58267f6a8b1dc8e2eb5519b984008fa85e8c"
version = "v2.15.0"
[[projects]]
name = "github.com/aokoli/goutils"
@@ -26,34 +26,31 @@
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/fsnotify/fsnotify"
name = "github.com/google/uuid"
packages = ["."]
revision = "4da3e2cfbabc9f751898f250b49f2439785783a1"
revision = "064e2069ce9c359c118179501254f67d7d37ba24"
version = "0.2"
[[projects]]
branch = "master"
name = "github.com/huandu/xstrings"
packages = ["."]
revision = "3959339b333561bf62a38b424fd41517c2c90f40"
revision = "2bf18b218c51864a87384c06996e40ff9dcff8e1"
version = "v1.0.0"
[[projects]]
branch = "master"
name = "github.com/imdario/mergo"
packages = ["."]
revision = "e3000cb3d28c72b837601cac94debd91032d19fe"
revision = "9316a62528ac99aaecb4e47eadd6dc8aa6533d58"
version = "v0.3.5"
[[projects]]
branch = "master"
name = "github.com/mattn/go-zglob"
packages = [".","fastwalk"]
revision = "95345c4e1c0ebc9d16a3284177f09360f4d20fab"
[[projects]]
branch = "master"
name = "github.com/mvdan/sh"
packages = ["interp","syntax"]
revision = "6773c283e820450b8ab307ac68e502960d473470"
packages = [
".",
"fastwalk"
]
revision = "49693fbb3fe3c3a75fc4e4d6fb1d7cedcbdeb385"
[[projects]]
name = "github.com/pmezard/go-difflib"
@@ -62,56 +59,73 @@
version = "v1.0.0"
[[projects]]
name = "github.com/satori/go.uuid"
branch = "master"
name = "github.com/radovskyb/watcher"
packages = ["."]
revision = "879c5887cd475cd7864858769793b2ceb0d44feb"
version = "v1.1.0"
revision = "0d9d32686dbf6395752c9b209398a59e302a7f1e"
[[projects]]
branch = "master"
name = "github.com/spf13/pflag"
packages = ["."]
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
revision = "3ebe029320b2676d667ae88da602a5f854788a8a"
[[projects]]
name = "github.com/stretchr/testify"
packages = ["assert"]
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
version = "v1.1.4"
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
version = "v1.2.2"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["pbkdf2","scrypt"]
revision = "b286ef4198388fdb0e4ae62be12820df5da9b4c2"
packages = [
"pbkdf2",
"scrypt",
"ssh/terminal"
]
revision = "a49355c7e3f8fe157a85be2f77e6e269a0f89602"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = ["context"]
revision = "570fa1c91359c1869590e9cedf3b53162a51a167"
revision = "afe8f62b1d6bbd81f31868121a50b06d8188e1f9"
[[projects]]
branch = "master"
name = "golang.org/x/sync"
packages = ["errgroup"]
revision = "f52d1811a62927559de87708c8913c1650ce4f26"
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = ["unix"]
revision = "6faef541c73732f438fb660a212750a9ba9f9362"
packages = [
"unix",
"windows"
]
revision = "63fc586f45fe72d95d5240a5d5eb95e6503907d3"
[[projects]]
branch = "v2"
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b"
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[[projects]]
branch = "master"
name = "mvdan.cc/sh"
packages = [
"interp",
"shell",
"syntax"
]
revision = "ca7561fd34910fd8575a3830d3cded291c0ce8b2"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "93839de063626661a216a313ab71e2ad920afb2528f69ca6110c2155276e6dab"
inputs-digest = "600bd482208fdedec60141bfaffe55eb403df077944bfdf5c007a33132c8ab5a"
solver-name = "gps-cdcl"
solver-version = 1

View File

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

697
README.md
View File

@@ -1,16 +1,21 @@
[![License](https://img.shields.io/github/license/go-task/task.svg)](https://github.com/go-task/task/blob/master/LICENSE)
[![Join the chat at https://gitter.im/go-task/task](https://img.shields.io/gitter/room/go-task/task.svg)](https://gitter.im/go-task/task?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Join Slack room](https://img.shields.io/badge/%23task%20on-gophers%20slack-blue.svg)](https://gophers.slack.com/messages/task)
[![Release Download Count](https://img.shields.io/github/downloads/go-task/task/total.svg)](http://www.somsubhra.com/github-release-stats/?username=go-task&repository=task)
[![Join Slack room](https://img.shields.io/badge/%23task-chat%20room-blue.svg)](https://gophers.slack.com/messages/task)
[![Build Status](https://travis-ci.org/go-task/task.svg?branch=master)](https://travis-ci.org/go-task/task)
# Task - Simple task runner / "Make" alternative
# Task - A task runner / simpler Make alternative written in Go
> We recently released version 2.0.0 of Task. The Taskfile changed a bit.
Please, check the [Taskfile versions](TASKFILE_VERSIONS.md) document to see
what changed and how to upgrade.
Task is a simple tool that allows you to easily run development and build
tasks. Task is written in Golang, but can be used to develop any language.
It aims to be simpler and easier to use then [GNU Make][make].
- [Installation](#installation)
- [Go](#go)
- [Homebrew](#homebrew)
- [Snap](#snap)
- [Binary](#binary)
- [Usage](#usage)
- [Environment](#environment)
- [OS specific task](#os-specific-task)
@@ -22,33 +27,74 @@ It aims to be simpler and easier to use then [GNU Make][make].
- [Dynamic variables](#dynamic-variables)
- [Go's template engine](#gos-template-engine)
- [Help](#help)
- [Silent mode](#silent-mode)
- [Watch tasks](#watch-tasks-experimental)
- [Examples](#examples)
- [Alternative task runners](#alternative-task-runners)
## Installation
### Go
If you have a [Golang][golang] environment setup, you can simply run:
```bash
go get -u -v github.com/go-task/task/cmd/task
```
### Homebrew
If you're on macOS and have [Homebrew][homebrew] installed, getting Task is
as simple as running:
```bash
brew install go-task/tap/go-task
```
### Snap
Task is available for [Snapcraft][snapcraft], but keep in mind that your
Linux distribution should allow classic confinement for Snaps to Task work
right:
```bash
sudo snap install task
```
### Install script
We also have a [install script][installscript], which is very useful on
scanarios like CIs. Many thanks to [godownloader][godownloader] for easily
generating this script.
```bash
curl -s https://raw.githubusercontent.com/go-task/task/master/install-task.sh | sh
```
### Binary
Or you can download the binary from the [releases][releases] page and add to
your `PATH`. DEB and RPM packages are also available.
The `task_checksums.txt` file contains the sha256 checksum for each file.
## Usage
Create a file called `Taskfile.yml` in the root of the project.
The `cmds` attribute should contains the commands of a task:
Create a file called `Taskfile.yml` in the root of your project.
The `cmds` attribute should contain the commands of a task.
The example below allows compiling a Go app and uses [Minify][minify] to concat
and minify multiple CSS files into a single one.
```yml
build:
cmds:
- go build -v -i main.go
version: '2'
assets:
cmds:
- gulp
tasks:
build:
cmds:
- go build -v -i main.go
assets:
cmds:
- minify -o public/style.css src/css
```
Running the tasks is as simple as running:
@@ -59,7 +105,7 @@ task assets build
Task uses [github.com/mvdan/sh](https://github.com/mvdan/sh), a native Go sh
interpreter. So you can write sh/bash commands and it will work even on
Windows, where `sh` or `bash` is usually not available. Just remember any
Windows, where `sh` or `bash` are usually not available. Just remember any
executable called must be available by the OS or in PATH.
If you ommit a task name, "default" will be assumed.
@@ -69,37 +115,48 @@ If you ommit a task name, "default" will be assumed.
You can specify environment variables that are added when running a command:
```yml
build:
cmds:
- echo $hallo
env:
hallo: welt
version: '2'
tasks:
build:
cmds:
- echo $hallo
env:
hallo: welt
```
### OS specific task
If you add a `Taskfile_{{GOOS}}` you can override or amend your taskfile based
on the operating system.
If you add a `Taskfile_{{GOOS}}.yml` you can override or amend your taskfile
based on the operating system.
Example:
Taskfile.yml:
```yml
build:
cmds:
- echo "default"
version: '2'
tasks:
build:
cmds:
- echo "default"
```
Taskfile_linux.yml:
```yml
build:
cmds:
- echo "linux"
tasks:
build:
cmds:
- echo "linux"
```
Will print out `linux` and not default
Will print out `linux` and not default.
It's also possible to have an OS specific `Taskvars.yml` file, like
`Taskvars_windows.yml`, `Taskfile_linux.yml`, or `Taskvars_darwin.yml`. See the
[variables section](#variables) below.
### Task directory
@@ -108,26 +165,33 @@ located. But you can easily make the task run in another folder informing
`dir`:
```yml
js:
dir: www/public/js
cmds:
- gulp
version: '2'
tasks:
serve:
dir: public/www
cmds:
# run http server
- caddy
```
### Task dependencies
You may have tasks that depends on others. Just pointing them on `deps` will
You may have tasks that depend on others. Just pointing them on `deps` will
make them run automatically before running the parent task:
```yml
build:
deps: [assets]
cmds:
- go build -v -i main.go
version: '2'
assets:
cmds:
- gulp
tasks:
build:
deps: [assets]
cmds:
- go build -v -i main.go
assets:
cmds:
- minify -o public/style.css src/css
```
In the above example, `assets` will always run right before `build` if you run
@@ -136,34 +200,45 @@ In the above example, `assets` will always run right before `build` if you run
A task can have only dependencies and no commands to group tasks together:
```yml
assets:
deps: [js, css]
version: '2'
js:
cmds:
- npm run buildjs
tasks:
assets:
deps: [js, css]
css:
cmds:
- npm run buildcss
js:
cmds:
- minify -o public/script.js src/js
css:
cmds:
- minify -o public/style.css src/css
```
If there are more than one dependency, they always run in parallel for better
If there is more than one dependency, they always run in parallel for better
performance.
Each task can only be run once. If it is included from another dependend task causing
a cyclomatic dependency, execution will be stopped.
If you want to pass information to dependencies, you can do that the same
manner as you would to [call another task](#calling-another-task):
```yml
task1:
deps: [task2]
version: '2'
task2:
deps: [task1]
tasks:
default:
deps:
- task: echo_sth
vars: {TEXT: "before 1"}
- task: echo_sth
vars: {TEXT: "before 2"}
cmds:
- echo "after"
echo_sth:
cmds:
- echo {{.TEXT}}
```
The above will fail with the message: "cyclic dependency detected".
### Calling another task
When a task has many dependencies, they are executed concurrently. This will
@@ -171,166 +246,230 @@ often result in a faster build pipeline. But in some situations you may need
to call other tasks serially. In this case, just use the following syntax:
```yml
main-task:
cmds:
- task: task-to-be-called
- task: another-task
- echo "Both done"
version: '2'
task-to-be-called:
cmds:
- echo "Task to be called"
tasks:
main-task:
cmds:
- task: task-to-be-called
- task: another-task
- echo "Both done"
another-task:
cmds:
- echo "Another task"
task-to-be-called:
cmds:
- echo "Task to be called"
another-task:
cmds:
- echo "Another task"
```
Overriding variables in the called task is as simple as informing `vars`
attribute:
```yml
main-task:
cmds:
- task: write-file
vars: {FILE: "hello.txt", CONTENT: "Hello!"}
- task: write-file
vars: {FILE: "world.txt", CONTENT: "World!"}
version: '2'
write-file:
cmds:
- echo "{{.CONTENT}}" > {{.FILE}}
tasks:
main-task:
cmds:
- task: write-file
vars: {FILE: "hello.txt", CONTENT: "Hello!"}
- task: write-file
vars: {FILE: "world.txt", CONTENT: "World!"}
write-file:
cmds:
- echo "{{.CONTENT}}" > {{.FILE}}
```
The above syntax is also supported in `deps`.
> NOTE: It's also possible to call a task without any param prefixing it
with `^`, but this syntax is deprecaded:
```yml
a-task:
cmds:
- ^another-task
another-task:
cmds:
- echo "Another task"
```
### Prevent unnecessary work
If a task generates something, you can inform Task the source and generated
files, so Task will prevent to run them if not necessary.
```yml
build:
deps: [js, css]
cmds:
- go build -v -i main.go
version: '2'
js:
cmds:
- npm run buildjs
sources:
- js/src/**/*.js
generates:
- public/bundle.js
tasks:
build:
deps: [js, css]
cmds:
- go build -v -i main.go
css:
cmds:
- npm run buildcss
sources:
- css/src/*.css
generates:
- public/bundle.css
js:
cmds:
- minify -o public/script.js src/js
sources:
- src/js/**/*.js
generates:
- public/script.js
css:
cmds:
- minify -o public/style.css src/css
sources:
- src/css/**/*.css
generates:
- public/style.css
```
`sources` and `generates` should be file patterns. When both are given, Task
will compare the modification date/time of the files to determine if it's
necessary to run the task. If not, it will just print
`sources` and `generates` can be files or file patterns. When both are given,
Task will compare the modification date/time of the files to determine if it's
necessary to run the task. If not, it will just print a message like
`Task "js" is up to date`.
If you prefer this check to be made by the content of the files, instead of
its timestamp, just set the `method` property to `checksum`.
You will probably want to ignore the `.task` folder in your `.gitignore` file
(It's there that Task stores the last checksum).
This feature is still experimental and can change until it's stable.
```yml
version: '2'
tasks:
build:
cmds:
- go build .
sources:
- ./*.go
generates:
- app{{exeExt}}
method: checksum
```
> TIP: method `none` skips any validation and always run the task.
Alternatively, you can inform a sequence of tests as `status`. If no error
is returned (exit status 0), the task is considered up-to-date:
```yml
generate-files:
cmds:
- mkdir directory
- touch directory/file1.txt
- touch directory/file2.txt
# test existence of files
status:
- test -d directory
- test -f directory/file1.txt
- test -f directory/file2.txt
version: '2'
tasks:
generate-files:
cmds:
- mkdir directory
- touch directory/file1.txt
- touch directory/file2.txt
# test existence of files
status:
- test -d directory
- test -f directory/file1.txt
- test -f directory/file2.txt
```
You can use `--force` or `-f` if you want to force a task to run even when
up-to-date.
Also, `task --status [tasks]...` will exit with a non-zero exit code if any of
the tasks are not up-to-date.
### Variables
When doing interpolation of variables, Task will look for the below.
They are listed below in order of importance (e.g. most important first):
- Variables declared locally in the task
- Variables given while calling a task from another.
(See [Calling another task](#calling-another-task) above)
- Environment variables
- Variables declared in the `vars:` option in the `Taskfile`
- Variables available in the `Taskvars.yml` file
- Variables declared locally in the task
- Environment variables
Example of overriding with environment variables:
Example of sending parameters with environment variables:
```bash
$ TASK_VARIABLE=a-value task do-something
```
Since some shells don't support above syntax to set environment variables
(Windows) tasks also accepts a similar style when not in the beginning of
the command. Variables given in this form are only visible to the task called
right before.
```bash
$ task write-file FILE=file.txt "CONTENT=Hello, World!" print "MESSAGE=All done!"
```
Example of locally declared vars:
```yml
version: '2'
tasks:
print-var:
cmds:
echo "{{.VAR}}"
vars:
VAR: Hello!
```
Example of global vars in a `Taskfile.yml`:
```yml
version: '2'
vars:
GREETING: Hello from Taskfile!
tasks:
greet:
cmds:
- echo "{{.GREETING}}"
```
Example of `Taskvars.yml` file:
```yml
PROJECT_NAME: My Project
DEV_MODE: production
GIT_COMMIT: $git log -n 1 --format=%h
GIT_COMMIT: {sh: git log -n 1 --format=%h}
```
Example of locally declared vars:
#### Variables expansion
Variables are expanded 2 times by default. You can change that by setting the
`expansions:` option. Change that will be necessary if you compose many
variables together:
```yml
print-var:
cmds:
echo "{{.VAR}}"
vars:
VAR: Hello!
```
version: '2'
> NOTE: It's also possible setting a variable globally using `set` attribute
in task, but this is deprecated:
expansions: 3
```yml
build:
deps: [set-message]
cmds:
- echo "Message: {{.MESSAGE}}"
vars:
FOO: foo
BAR: bar
BAZ: baz
FOOBAR: "{{.FOO}}{{.BAR}}"
FOOBARBAZ: "{{.FOOBAR}}{{.BAZ}}"
set-message:
cmds:
- echo "This is an important message"
set: MESSAGE
tasks:
default:
cmds:
- echo "{{.FOOBARBAZ}}"
```
#### Dynamic variables
If you prefix a variable with `$`, then the variable is considered a dynamic
variable. The value after the $-symbol will be treated as a command and the
output assigned.
The below syntax (`sh:` prop in a variable) is considered a dynamic variable.
The value will be treated as a command and the output assigned. If there is one
or more trailing newlines, the last newline will be trimmed.
```yml
build:
cmds:
- go build -ldflags="-X main.Version={{.LAST_GIT_COMMIT}}" main.go
vars:
LAST_GIT_COMMIT: $git log -n 1 --format=%h
version: '2'
tasks:
build:
cmds:
- go build -ldflags="-X main.Version={{.GIT_COMMIT}}" main.go
vars:
GIT_COMMIT:
sh: git log -n 1 --format=%h
```
This works for all types of variables.
@@ -338,15 +477,18 @@ This works for all types of variables.
### Go's template engine
Task parse commands as [Go's template engine][gotemplate] before executing
them. Variables are acessible through dot syntax (`.VARNAME`).
them. Variables are accessible through dot syntax (`.VARNAME`).
All functions by the Go's [sprig lib](http://masterminds.github.io/sprig/)
are available. The following example gets the current date in a given format:
```yml
print-date:
cmds:
- echo {{now | date "2006-01-02"}}
version: '2'
tasks:
print-date:
cmds:
- echo {{now | date "2006-01-02"}}
```
Task also adds the following functions:
@@ -355,78 +497,255 @@ Task also adds the following functions:
"darwin" (macOS) and "freebsd".
- `ARCH`: return the architecture Task was compiled to: "386", "amd64", "arm"
or "s390x".
- `ToSlash`: Does nothing on Unix, but on Windows converts a string from `\`
- `splitLines`: Splits Unix (\n) and Windows (\r\n) styled newlines.
- `catLines`: Replaces Unix (\n) and Windows (\r\n) styled newlines with a space.
- `toSlash`: Does nothing on Unix, but on Windows converts a string from `\`
path format to `/`.
- `FromSlash`: Oposite of `ToSlash`. Does nothing on Unix, but on Windows
- `fromSlash`: Oposite of `toSlash`. Does nothing on Unix, but on Windows
converts a string from `\` path format to `/`.
- `ExeExt`: Returns the right executable extension for the current OS
- `exeExt`: Returns the right executable extension for the current OS
(`".exe"` for Windows, `""` for others).
Example:
```yml
print-os:
cmds:
- echo '{{OS}} {{ARCH}}'
- echo '{{if eq OS "windows"}}windows-command{{else}}unix-command{{end}}'
# This will be path/to/file on Unix but path\to\file on Windows
- echo '{{FromSlash "path/to/file"}}'
version: '2'
tasks:
print-os:
cmds:
- echo '{{OS}} {{ARCH}}'
- echo '{{if eq OS "windows"}}windows-command{{else}}unix-command{{end}}'
# This will be path/to/file on Unix but path\to\file on Windows
- echo '{{fromSlash "path/to/file"}}'
enumerated-file:
vars:
CONTENT: |
foo
bar
cmds:
- |
cat << EOF > output.txt
{{range $i, $line := .CONTENT | splitLines -}}
{{printf "%3d" $i}}: {{$line}}
{{end}}EOF
```
### Help
Running `task help` lists all tasks with a description. The following taskfile:
Running `task --list` (or `task -l`) lists all tasks with a description.
The following taskfile:
```yml
build:
desc: Build the go binary.
cmds:
- go build -v -i main.go
version: '2'
test:
desc: Run all the go tests.
cmds:
- go test -race ./...
tasks:
build:
desc: Build the go binary.
cmds:
- go build -v -i main.go
js:
cmds:
- npm run buildjs
test:
desc: Run all the go tests.
cmds:
- go test -race ./...
css:
cmds:
- npm run buildcss
js:
cmds:
- minify -o public/script.js src/js
css:
cmds:
- minify -o public/style.css src/css
```
would print the following output:
```bash
build Build the go binary.
test Run all the go tests.
* build: Build the go binary.
* test: Run all the go tests.
```
## Watch tasks (experimental)
## Silent mode
If you give a `--watch` or `-w` argument, task will watch for files changes
Silent mode disables echoing of commands before Task runs it.
For the following Taskfile:
```yml
version: '2'
tasks:
echo:
cmds:
- echo "Print something"
```
Normally this will be print:
```sh
echo "Print something"
Print something
```
With silent mode on, the below will be print instead:
```sh
Print something
```
There's three ways to enable silent mode:
* At command level:
```yml
version: '2'
tasks:
echo:
cmds:
- cmd: echo "Print something"
silent: true
```
* At task level:
```yml
version: '2'
tasks:
echo:
cmds:
- echo "Print something"
silent: true
```
* Or globally with `--silent` or `-s` flag
If you want to suppress stdout instead, just redirect a command to `/dev/null`:
```yml
version: '2'
tasks:
echo:
cmds:
- echo "This will print nothing" > /dev/null
```
## Output syntax
By default, Task just redirect the STDOUT and STDERR of the running commands
to the shell in real time. This is good for having live feedback for log
printed by commands, but the output can become messy if you have multiple
commands running at the same time and printing lots of stuff.
To make this more customizable, there are currently three different output
options you can choose:
- `interleaved` (default)
- `group`
- `prefixed`
To choose another one, just set it to root in the Taskfile:
```yml
version: '2'
output: 'group'
tasks:
# ...
```
The `group` output will print the entire output of a command once, after it
finishes, so you won't have live feedback for commands that take a long time
to run.
The `prefix` output will prefix every line printed by a command with
`[task-name] ` as the prefix, but you can customize the prefix for a command
with the `prefix:` attribute:
```yml
version: '2'
output: prefixed
tasks:
default:
deps:
- task: print
vars: {TEXT: foo}
- task: print
vars: {TEXT: bar}
- task: print
vars: {TEXT: baz}
print:
cmds:
- echo "{{.TEXT}}"
prefix: "print-{{.TEXT}}"
silent: true
```
```bash
$ task default
[print-foo] foo
[print-bar] bar
[print-baz] baz
```
## Watch tasks
If you give a `--watch` or `-w` argument, task will watch for file changes
and run the task again. This requires the `sources` attribute to be given,
so task know which files to watch.
## Examples
The [go-task/examples][examples] intends to be a collection of Taskfiles for
various use cases.
(It still lacks many examples, though. Contributions are welcome).
## Alternative task runners
- YAML based:
- [tj/robo][robo]
- [dogtools/dog][dog]
- [goeuro/myke][myke]
- [dreadl0ck/zeus][zeus]
- [rliebz/tusk][tusk]
- Go based:
- [go-godo][godo]
- [markbates/grift][grift]
- [magefile/mage][mage]
- Make based:
- [tj/mmake][mmake]
### Sponsors
[![Sponsors](https://opencollective.com/task/sponsors.svg?width=890)][opencollective]
### Backers
[![Backers](https://opencollective.com/task/backers.svg?width=890)][opencollective]
### Contributors
[![Contributors](https://opencollective.com/task/contributors.svg?width=890)][contributors]
[make]: https://www.gnu.org/software/make/
[releases]: https://github.com/go-task/task/releases
[golang]: https://golang.org/
[gotemplate]: https://golang.org/pkg/text/template/
[robo]: https://github.com/tj/robo
[dog]: https://github.com/dogtools/dog
[myke]: https://github.com/goeuro/myke
[godo]: https://github.com/go-godo/godo
[zeus]: https://github.com/dreadl0ck/zeus
[tusk]: https://github.com/rliebz/tusk
[grift]: https://github.com/markbates/grift
[mage]: https://github.com/magefile/mage
[mmake]: https://github.com/tj/mmake
[sh]: https://github.com/mvdan/sh
[minify]: https://github.com/tdewolff/minify/tree/master/cmd/minify
[examples]: https://github.com/go-task/examples
[snapcraft]: https://snapcraft.io/
[homebrew]: https://brew.sh/
[installscript]: https://github.com/go-task/task/blob/master/install-task.sh
[godownloader]: https://github.com/goreleaser/godownloader
[opencollective]: https://opencollective.com/task
[contributors]: https://github.com/go-task/task/graphs/contributors

32
RELEASING_TASK.md Normal file
View File

@@ -0,0 +1,32 @@
# Releasing Task
The release process of Task is done is done with the help of
[GoReleaser][goreleaser]. You can test the release process locally by calling
the `test-release` task of the Taskfile.
The Travis CI should release automatically when a new
Git tag is pushed to master, either for the artifact uploading (raw executables
and DEB and RPM packages)
# Homebrew
To release a new version on the [Homebrew tap][homebrewtap] edit the
[Formula/go-task.rb][gotaskrb] file, updating with the new version, download
URL and sha256.
# Snapcraft
The exception is the publishing of a new version of the
[snap package][snappackage]. This current require two steps after publishing
the binaries:
* Updating the current version on [snapcraft.yaml][snapcraftyaml];
* Moving either the `i386` and `amd64` new artifacts to the stable channel on
the [Snapscraft dashboard][snapcraftdashboard]
[goreleaser]: https://goreleaser.com/#continuous_integration
[homebrewtap]: https://github.com/go-task/homebrew-tap
[gotaskrb]: https://github.com/go-task/homebrew-tap/blob/master/Formula/go-task.rb
[snappackage]: https://github.com/go-task/snap
[snapcraftyaml]: https://github.com/go-task/snap/blob/master/snap/snapcraft.yaml#L2
[snapcraftdashboard]: https://dashboard.snapcraft.io/

91
TASKFILE_VERSIONS.md Normal file
View File

@@ -0,0 +1,91 @@
# Taskfile version
The Taskfile syntax and features changed with time. This document explains what
changed on each version and how to upgrade your Taskfile.
# What the Taskfile version mean
The Taskfile version follows the Task version. E.g. the change to Taskfile
version `2` means that Task `v2.0.0` should be release to support it.
The `version:` key on Taskfile accepts a semver string, so either `2`, `2.0` or
`2.0.0` is accepted. You you choose to use `2.0` Task will not enable future
`2.1` features, but if you choose to use `2`, than any `2.x.x` features will be
available, but not `3.0.0+`.
## Version 1
In the first version of the `Taskfile`, the `version:` key was not available,
because the tasks was in the root of the YAML document. Like this:
```yml
echo:
cmds:
- echo "Hello, World!"
```
The variable priority order was also different:
1. Call variables
2. Environment
3. Task variables
4. `Taskvars.yml` variables
## Version 2.0
At version 2, we introduced the `version:` key, to allow us to envolve Task
with new features without breaking existing Taskfiles. The new syntax is as
follows:
```yml
version: '2'
tasks:
echo:
cmds:
- echo "Hello, World!"
```
Version 2 allows you to write global variables directly in the Taskfile,
if you don't want to create a `Taskvars.yml`:
```yml
version: '2'
vars:
GREETING: Hello, World!
tasks:
greet:
cmds:
- echo "{{.GREETING}}"
```
The variable priority order changed to the following:
1. Task variables
2. Call variables
3. Taskfile variables
4. Taskvars file variables
5. Environment variables
A new global option was added to configure the number of variables expansions
(which default to 2):
```yml
version: '2'
expansions: 3
vars:
FOO: foo
BAR: bar
BAZ: baz
FOOBAR: "{{.FOO}}{{.BAR}}"
FOOBARBAZ: "{{.FOOBAR}}{{.BAZ}}"
tasks:
default:
cmds:
- echo "{{.FOOBARBAZ}}"
```

View File

@@ -1,48 +1,95 @@
# compiles current source code and make "task" executable available on
# $GOPATH/bin/task{.exe}
install:
desc: Installs Task
cmds:
- go install -v -ldflags="-w -s -X main.version={{.GIT_COMMIT}}" ./cmd/task
version: '2'
dl-deps:
desc: Downloads cli dependencies
cmds:
- go get -u github.com/golang/lint/golint
- go get -u github.com/goreleaser/goreleaser
- go get -u github.com/golang/dep/cmd/dep
vars:
GIT_COMMIT:
sh: git log -n 1 --format=%h
update-deps:
desc: Updates dependencies
cmds:
- dep ensure -update
- dep prune
GO_PACKAGES:
.
./cmd/task
./internal/args
./internal/compiler
./internal/compiler/v1
./internal/compiler/v2
./internal/execext
./internal/logger
./internal/osext
./internal/output
./internal/status
./internal/taskfile
./internal/taskfile/version
./internal/templater
clean:
desc: Cleans temp files and folders
cmds:
- rm -rf dist/
tasks:
# compiles current source code and make "task" executable available on
# $GOPATH/bin/task{.exe}
install:
desc: Installs Task
cmds:
- go install -v -ldflags="-w -s -X main.version={{.GIT_COMMIT}}" ./cmd/task
env:
CGO_ENABLED: '0'
lint:
desc: Runs golint
cmds:
- golint .
- golint ./execext
- golint ./cmd/task
dl-deps:
desc: Downloads cli dependencies
cmds:
- task: go-get
vars: {REPO: github.com/golang/lint/golint}
- task: go-get
vars: {REPO: github.com/asticode/go-astitodo/astitodo}
- task: go-get
vars: {REPO: github.com/golang/dep/cmd/dep}
- task: go-get
vars: {REPO: github.com/goreleaser/goreleaser}
- task: go-get
vars: {REPO: github.com/goreleaser/godownloader}
test:
desc: Runs test suite
deps: [install]
cmds:
- go test -v
update-deps:
desc: Updates dependencies
cmds:
- dep ensure
- dep ensure -update
# https://github.com/goreleaser/goreleaser
release:
desc: Release Task
cmds:
- goreleaser
clean:
desc: Cleans temp files and folders
cmds:
- rm -rf dist/
test-release:
desc: Tests release process without publishing
cmds:
- goreleaser --skip-validate --skip-publish
lint:
desc: Runs golint
cmds:
- golint {{.GO_PACKAGES}}
silent: true
test:
desc: Runs test suite
deps: [install]
cmds:
- go test {{.GO_PACKAGES}}
test-release:
desc: Tests release process without publishing
cmds:
- goreleaser --snapshot --rm-dist
generate-install-script:
desc: Generate install script using https://githbub.com/goreleaser/godownloader
cmds:
- godownloader --repo go-task/task -o install-task.sh
todo:
desc: Prints TODO comments present in the code
cmds:
- astitodo {{.GO_PACKAGES}}
silent: true
ci:
cmds:
- task: go-get
vars: {REPO: github.com/golang/lint/golint}
- task: lint
- task: test
go-get:
cmds:
- go get -u {{.REPO}}

View File

@@ -1 +0,0 @@
GIT_COMMIT: $git log -n 1 --format=%h

View File

@@ -1,11 +1,14 @@
package main
import (
"fmt"
"context"
"log"
"os"
"os/signal"
"syscall"
"github.com/go-task/task"
"github.com/go-task/task/internal/args"
"github.com/spf13/pflag"
)
@@ -14,7 +17,7 @@ var (
version = "master"
)
const usage = `Usage: task [-ifwv] [--init] [--force] [--watch] [--verbose] [task...]
const usage = `Usage: task [-ilfwvsd] [--init] [--list] [--force] [--watch] [--verbose] [--silent] [--dir] [task...]
Runs the specified task(s). Falls back to the "default" task if no task name
was specified, or lists all tasks if an unknown task name was specified.
@@ -30,29 +33,40 @@ hello:
generates:
- output.txt
'''
Options:
`
func main() {
log.SetFlags(0)
log.SetOutput(os.Stderr)
pflag.Usage = func() {
fmt.Println(usage)
log.Print(usage)
pflag.PrintDefaults()
}
var (
versionFlag bool
init bool
list bool
status bool
force bool
watch bool
verbose bool
silent bool
dir string
)
pflag.BoolVar(&versionFlag, "version", false, "show Task version")
pflag.BoolVarP(&init, "init", "i", false, "creates a new Taskfile.yml in the current folder")
pflag.BoolVarP(&list, "list", "l", false, "lists tasks with description of current Taskfile")
pflag.BoolVar(&status, "status", false, "exits with non-zero exit code if any of the given tasks is not up-to-date")
pflag.BoolVarP(&force, "force", "f", false, "forces execution even when the task is up-to-date")
pflag.BoolVarP(&watch, "watch", "w", false, "enables watch of the given task")
pflag.BoolVarP(&verbose, "verbose", "v", false, "enables verbose mode")
pflag.BoolVarP(&silent, "silent", "s", false, "disables echoing")
pflag.StringVarP(&dir, "dir", "d", "", "sets directory of execution")
pflag.Parse()
if versionFlag {
@@ -65,7 +79,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
if err := task.InitTaskfile(wd); err != nil {
if err := task.InitTaskfile(os.Stdout, wd); err != nil {
log.Fatal(err)
}
return
@@ -75,22 +89,55 @@ func main() {
Force: force,
Watch: watch,
Verbose: verbose,
Silent: silent,
Dir: dir,
Context: getSignalContext(),
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
}
if err := e.ReadTaskfile(); err != nil {
if err := e.Setup(); err != nil {
log.Fatal(err)
}
args := pflag.Args()
if len(args) == 0 {
log.Println("task: No argument given, trying default task")
args = []string{"default"}
if list {
e.PrintTasksHelp()
return
}
if err := e.Run(args...); err != nil {
arguments := pflag.Args()
if len(arguments) == 0 {
log.Println("task: No argument given, trying default task")
arguments = []string{"default"}
}
calls, err := args.Parse(arguments...)
if err != nil {
log.Fatal(err)
}
if status {
if err = e.Status(calls...); err != nil {
log.Fatal(err)
}
return
}
if err := e.Run(calls...); err != nil {
log.Fatal(err)
}
}
func getSignalContext() context.Context {
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt, os.Kill, syscall.SIGTERM)
ctx, cancel := context.WithCancel(context.Background())
go func() {
sig := <-ch
log.Printf("task: signal received: %s", sig)
cancel()
}()
return ctx
}

24
completion/zsh/_task Normal file
View File

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

View File

@@ -1,29 +0,0 @@
package task
// HasCyclicDep checks if a task tree has any cyclic dependency
func (e *Executor) HasCyclicDep() bool {
visits := make(map[string]struct{}, len(e.Tasks))
var checkCyclicDep func(string, *Task) bool
checkCyclicDep = func(name string, t *Task) bool {
if _, ok := visits[name]; ok {
return false
}
visits[name] = struct{}{}
defer delete(visits, name)
for _, d := range t.Deps {
if !checkCyclicDep(d.Task, e.Tasks[d.Task]) {
return false
}
}
return true
}
for k, v := range e.Tasks {
if !checkCyclicDep(k, v) {
return true
}
}
return false
}

View File

@@ -1,40 +0,0 @@
package task_test
import (
"testing"
"github.com/go-task/task"
)
func TestCyclicDepCheck(t *testing.T) {
isCyclic := &task.Executor{
Tasks: task.Tasks{
"task-a": &task.Task{
Deps: []*task.Dep{&task.Dep{Task: "task-b"}},
},
"task-b": &task.Task{
Deps: []*task.Dep{&task.Dep{Task: "task-a"}},
},
},
}
if !isCyclic.HasCyclicDep() {
t.Error("Task should be cyclic")
}
isNotCyclic := &task.Executor{
Tasks: task.Tasks{
"task-a": &task.Task{
Deps: []*task.Dep{&task.Dep{Task: "task-c"}},
},
"task-b": &task.Task{
Deps: []*task.Dep{&task.Dep{Task: "task-c"}},
},
"task-c": &task.Task{},
},
}
if isNotCyclic.HasCyclicDep() {
t.Error("Task should not be cyclic")
}
}

View File

@@ -6,8 +6,6 @@ import (
)
var (
// ErrCyclicDependencyDetected is returned when a cyclic dependency was found in the Taskfile
ErrCyclicDependencyDetected = errors.New("task: cyclic dependency detected")
// ErrTaskfileAlreadyExists is returned on creating a Taskfile if one already exists
ErrTaskfileAlreadyExists = errors.New("task: A Taskfile already exists")
)
@@ -52,3 +50,18 @@ type cantWatchNoSourcesError struct {
func (err *cantWatchNoSourcesError) Error() string {
return fmt.Sprintf(`task: Can't watch task "%s" because it has no specified sources`, err.taskName)
}
// MaximumTaskCallExceededError is returned when a task is called too
// many times. In this case you probably have a cyclic dependendy or
// infinite loop
type MaximumTaskCallExceededError struct {
task string
}
func (e *MaximumTaskCallExceededError) Error() string {
return fmt.Sprintf(
`task: maximum task call exceeded (%d) for task "%s": probably an cyclic dep or infinite loop`,
MaximumTaskCall,
e.task,
)
}

View File

@@ -1,6 +0,0 @@
hello:
cmds:
- echo "I am going to write a file named 'output.txt' now."
- echo "hello" > output.txt
generates:
- output.txt

85
file.go
View File

@@ -1,85 +0,0 @@
package task
import (
"os"
"path/filepath"
"time"
"github.com/mattn/go-zglob"
)
func minTime(a, b time.Time) time.Time {
if !a.IsZero() && a.Before(b) {
return a
}
return b
}
func maxTime(a, b time.Time) time.Time {
if a.After(b) {
return a
}
return b
}
func getPatternsMinTime(dir string, patterns []string) (m time.Time, err error) {
for _, p := range patterns {
p = filepath.Join(dir, p)
mp, err := getPatternMinTime(p)
if err != nil {
return time.Time{}, err
}
m = minTime(m, mp)
}
return
}
func getPatternsMaxTime(dir string, patterns []string) (m time.Time, err error) {
for _, p := range patterns {
p = filepath.Join(dir, p)
mp, err := getPatternMaxTime(p)
if err != nil {
return time.Time{}, err
}
m = maxTime(m, mp)
}
return
}
func getPatternMinTime(pattern string) (minTime time.Time, err error) {
files, err := zglob.Glob(pattern)
if err != nil {
return time.Time{}, err
}
for _, f := range files {
info, err := os.Stat(f)
if err != nil {
return time.Time{}, err
}
modTime := info.ModTime()
if minTime.IsZero() || modTime.Before(minTime) {
minTime = modTime
}
}
return
}
func getPatternMaxTime(pattern string) (maxTime time.Time, err error) {
files, err := zglob.Glob(pattern)
if err != nil {
return time.Time{}, err
}
for _, f := range files {
info, err := os.Stat(f)
if err != nil {
return time.Time{}, err
}
modTime := info.ModTime()
if modTime.After(maxTime) {
maxTime = modTime
}
}
return
}

19
help.go
View File

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

32
init.go
View File

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

390
install-task.sh Executable file
View File

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

36
internal/args/args.go Normal file
View File

@@ -0,0 +1,36 @@
package args
import (
"errors"
"strings"
"github.com/go-task/task/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
func Parse(args ...string) ([]taskfile.Call, error) {
var calls []taskfile.Call
for _, arg := range args {
if !strings.Contains(arg, "=") {
calls = append(calls, taskfile.Call{Task: arg})
continue
}
if len(calls) < 1 {
return nil, ErrVariableWithoutTask
}
if calls[len(calls)-1].Vars == nil {
calls[len(calls)-1].Vars = make(taskfile.Vars)
}
pair := strings.SplitN(arg, "=", 2)
calls[len(calls)-1].Vars[pair[0]] = taskfile.Var{Static: pair[1]}
}
return calls, nil
}

View File

@@ -0,0 +1,70 @@
package args_test
import (
"fmt"
"testing"
"github.com/go-task/task/internal/args"
"github.com/go-task/task/internal/taskfile"
"github.com/stretchr/testify/assert"
)
func TestArgs(t *testing.T) {
tests := []struct {
Args []string
Expected []taskfile.Call
Err error
}{
{
Args: []string{"task-a", "task-b", "task-c"},
Expected: []taskfile.Call{
{Task: "task-a"},
{Task: "task-b"},
{Task: "task-c"},
},
},
{
Args: []string{"task-a", "FOO=bar", "task-b", "task-c", "BAR=baz", "BAZ=foo"},
Expected: []taskfile.Call{
{
Task: "task-a",
Vars: taskfile.Vars{
"FOO": taskfile.Var{Static: "bar"},
},
},
{Task: "task-b"},
{
Task: "task-c",
Vars: taskfile.Vars{
"BAR": taskfile.Var{Static: "baz"},
"BAZ": taskfile.Var{Static: "foo"},
},
},
},
},
{
Args: []string{"task-a", "CONTENT=with some spaces"},
Expected: []taskfile.Call{
{
Task: "task-a",
Vars: taskfile.Vars{
"CONTENT": taskfile.Var{Static: "with some spaces"},
},
},
},
},
{
Args: []string{"FOO=bar", "task-a"},
Err: args.ErrVariableWithoutTask,
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("TestArgs%d", i+1), func(t *testing.T) {
calls, err := args.Parse(test.Args...)
assert.Equal(t, test.Err, err)
assert.Equal(t, test.Expected, calls)
})
}
}

View File

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

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

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

View File

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

View File

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

View File

@@ -4,10 +4,11 @@ import (
"context"
"errors"
"io"
"os"
"strings"
"github.com/mvdan/sh/interp"
"github.com/mvdan/sh/syntax"
"mvdan.cc/sh/interp"
"mvdan.cc/sh/syntax"
)
// RunCommandOptions is the options for the RunCommand func
@@ -37,17 +38,29 @@ func RunCommand(opts *RunCommandOptions) error {
return err
}
r := interp.Runner{
Context: opts.Context,
File: p,
Dir: opts.Dir,
Env: opts.Env,
Stdin: opts.Stdin,
Stdout: opts.Stdout,
Stderr: opts.Stderr,
environ := opts.Env
if len(environ) == 0 {
environ = os.Environ()
}
if err = r.Run(); err != nil {
env, err := interp.EnvFromList(environ)
if err != nil {
return err
}
return nil
r := interp.Runner{
Context: opts.Context,
Dir: opts.Dir,
Env: env,
Exec: interp.DefaultExec,
Open: interp.OpenDevImpls(interp.DefaultOpen),
Stdin: opts.Stdin,
Stdout: opts.Stdout,
Stderr: opts.Stderr,
}
if err = r.Reset(); err != nil {
return err
}
return r.Run(p)
}

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

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

22
internal/osext/osext.go Normal file
View File

@@ -0,0 +1,22 @@
package osext
import (
"os"
"github.com/mitchellh/go-homedir"
)
// Expand is an improved version of os.ExpandEnv,
// that not only expand enrionment variable ($GOPATH/src/github.com/...)
// but also expands "~" as the home directory.
func Expand(s string) (string, error) {
s = os.ExpandEnv(s)
var err error
s, err = homedir.Expand(s)
if err != nil {
return "", err
}
return s, nil
}

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,87 @@
package status
import (
"crypto/md5"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
)
// Checksum validades if a task is up to date by calculating its source
// files checksum
type Checksum struct {
Dir string
Task string
Sources []string
}
// IsUpToDate implements the Checker interface
func (c *Checksum) IsUpToDate() (bool, error) {
checksumFile := c.checksumFilePath()
data, _ := ioutil.ReadFile(checksumFile)
oldMd5 := strings.TrimSpace(string(data))
sources, err := glob(c.Dir, c.Sources)
if err != nil {
return false, err
}
newMd5, err := c.checksum(sources...)
if err != nil {
return false, nil
}
_ = os.MkdirAll(filepath.Join(c.Dir, ".task", "checksum"), 0755)
if err = ioutil.WriteFile(checksumFile, []byte(newMd5+"\n"), 0644); err != nil {
return false, err
}
return oldMd5 == newMd5, nil
}
func (c *Checksum) checksum(files ...string) (string, error) {
h := md5.New()
for _, f := range files {
f, err := os.Open(f)
if err != nil {
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 {
return "", err
}
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
// OnError implements the Checker interface
func (c *Checksum) OnError() error {
return os.Remove(c.checksumFilePath())
}
func (c *Checksum) checksumFilePath() string {
return filepath.Join(c.Dir, ".task", "checksum", c.normalizeFilename(c.Task))
}
var checksumFilenameRegexp = regexp.MustCompile("[^A-z0-9]")
// replaces invalid caracters on filenames with "-"
func (*Checksum) normalizeFilename(f string) string {
return checksumFilenameRegexp.ReplaceAllString(f, "-")
}

View File

@@ -0,0 +1,21 @@
package status
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNormalizeFilename(t *testing.T) {
tests := []struct {
In, Out string
}{
{"foobarbaz", "foobarbaz"},
{"foo/bar/baz", "foo-bar-baz"},
{"foo@bar/baz", "foo-bar-baz"},
{"foo1bar2baz3", "foo1bar2baz3"},
}
for _, test := range tests {
assert.Equal(t, test.Out, (&Checksum{}).normalizeFilename(test.In))
}
}

29
internal/status/glob.go Normal file
View File

@@ -0,0 +1,29 @@
package status
import (
"path/filepath"
"sort"
"github.com/go-task/task/internal/osext"
"github.com/mattn/go-zglob"
)
func glob(dir string, globs []string) (files []string, err error) {
for _, g := range globs {
if !filepath.IsAbs(g) {
g = filepath.Join(dir, g)
}
g, err = osext.Expand(g)
if err != nil {
return nil, err
}
f, err := zglob.Glob(g)
if err != nil {
return nil, err
}
files = append(files, f...)
}
sort.Strings(files)
return
}

14
internal/status/none.go Normal file
View File

@@ -0,0 +1,14 @@
package status
// None is a no-op Checker
type None struct{}
// IsUpToDate implements the Checker interface
func (None) IsUpToDate() (bool, error) {
return false, nil
}
// OnError implements the Checker interface
func (None) OnError() error {
return nil
}

13
internal/status/status.go Normal file
View File

@@ -0,0 +1,13 @@
package status
var (
_ Checker = &Timestamp{}
_ Checker = &Checksum{}
_ Checker = None{}
)
// Checker is an interface that checks if the status is up-to-date
type Checker interface {
IsUpToDate() (bool, error)
OnError() error
}

View File

@@ -0,0 +1,85 @@
package status
import (
"os"
"time"
)
// Timestamp checks if any source change compared with the generated files,
// using file modifications timestamps.
type Timestamp struct {
Dir string
Sources []string
Generates []string
}
// IsUpToDate implements the Checker interface
func (t *Timestamp) IsUpToDate() (bool, error) {
if len(t.Sources) == 0 || len(t.Generates) == 0 {
return false, nil
}
sources, err := glob(t.Dir, t.Sources)
if err != nil {
return false, nil
}
generates, err := glob(t.Dir, t.Generates)
if err != nil {
return false, nil
}
sourcesMaxTime, err := getMaxTime(sources...)
if err != nil || sourcesMaxTime.IsZero() {
return false, nil
}
generatesMinTime, err := getMinTime(generates...)
if err != nil || generatesMinTime.IsZero() {
return false, nil
}
return !generatesMinTime.Before(sourcesMaxTime), nil
}
func getMinTime(files ...string) (time.Time, error) {
var t time.Time
for _, f := range files {
info, err := os.Stat(f)
if err != nil {
return time.Time{}, err
}
t = minTime(t, info.ModTime())
}
return t, nil
}
func getMaxTime(files ...string) (time.Time, error) {
var t time.Time
for _, f := range files {
info, err := os.Stat(f)
if err != nil {
return time.Time{}, err
}
t = maxTime(t, info.ModTime())
}
return t, nil
}
func minTime(a, b time.Time) time.Time {
if !a.IsZero() && a.Before(b) {
return a
}
return b
}
func maxTime(a, b time.Time) time.Time {
if a.After(b) {
return a
}
return b
}
// OnError implements the Checker interface
func (*Timestamp) OnError() error {
return nil
}

View File

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

View File

@@ -1,4 +1,4 @@
package task
package taskfile
import (
"errors"
@@ -7,9 +7,10 @@ import (
// Cmd is a task command
type Cmd struct {
Cmd string
Task string
Vars Vars
Cmd string
Silent bool
Task string
Vars Vars
}
// Dep is a task dependency
@@ -36,6 +37,15 @@ func (c *Cmd) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
return nil
}
var cmdStruct struct {
Cmd string
Silent bool
}
if err := unmarshal(&cmdStruct); err == nil && cmdStruct.Cmd != "" {
c.Cmd = cmdStruct.Cmd
c.Silent = cmdStruct.Silent
return nil
}
var taskCall struct {
Task string
Vars Vars

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

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

View File

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

View File

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

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

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

View File

@@ -0,0 +1,40 @@
package version
import (
"github.com/Masterminds/semver"
)
var (
v1 = mustVersion("1")
v2 = mustVersion("2")
v21 = mustVersion("2.1")
v22 = mustVersion("2.2")
)
// 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)
}
func mustVersion(s string) *semver.Version {
v, err := semver.NewVersion(s)
if err != nil {
panic(err)
}
return v
}

View File

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

View File

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

25
log.go
View File

@@ -1,25 +0,0 @@
package task
import (
"fmt"
)
func (e *Executor) println(args ...interface{}) {
fmt.Fprintln(e.Stdout, args...)
}
func (e *Executor) printfln(format string, args ...interface{}) {
fmt.Fprintf(e.Stdout, format+"\n", args...)
}
func (e *Executor) verbosePrintln(args ...interface{}) {
if e.Verbose {
e.println(args...)
}
}
func (e *Executor) verbosePrintfln(format string, args ...interface{}) {
if e.Verbose {
e.printfln(format, args...)
}
}

View File

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

85
status.go Normal file
View File

@@ -0,0 +1,85 @@
package task
import (
"context"
"fmt"
"github.com/go-task/task/internal/execext"
"github.com/go-task/task/internal/status"
"github.com/go-task/task/internal/taskfile"
)
// Status returns an error if any the of given tasks is not up-to-date
func (e *Executor) Status(calls ...taskfile.Call) error {
for _, call := range calls {
t, ok := e.Taskfile.Tasks[call.Task]
if !ok {
return &taskNotFoundError{taskName: call.Task}
}
isUpToDate, err := isTaskUpToDate(e.Context, t)
if err != nil {
return err
}
if !isUpToDate {
return fmt.Errorf(`task: Task "%s" is not up-to-date`, t.Task)
}
}
return nil
}
func isTaskUpToDate(ctx context.Context, t *taskfile.Task) (bool, error) {
if len(t.Status) > 0 {
return isTaskUpToDateStatus(ctx, t)
}
checker, err := getStatusChecker(t)
if err != nil {
return false, err
}
return checker.IsUpToDate()
}
func statusOnError(t *taskfile.Task) error {
checker, err := getStatusChecker(t)
if err != nil {
return err
}
return checker.OnError()
}
func getStatusChecker(t *taskfile.Task) (status.Checker, error) {
switch t.Method {
case "", "timestamp":
return &status.Timestamp{
Dir: t.Dir,
Sources: t.Sources,
Generates: t.Generates,
}, nil
case "checksum":
return &status.Checksum{
Dir: t.Dir,
Task: t.Task,
Sources: t.Sources,
}, nil
case "none":
return status.None{}, nil
default:
return nil, fmt.Errorf(`task: invalid method "%s"`, t.Method)
}
}
func isTaskUpToDateStatus(ctx context.Context, t *taskfile.Task) (bool, error) {
for _, s := range t.Status {
err := execext.RunCommand(&execext.RunCommandOptions{
Context: ctx,
Command: s,
Dir: t.Dir,
Env: getEnviron(t),
})
if err != nil {
return false, nil
}
}
return true, nil
}

369
task.go
View File

@@ -1,70 +1,94 @@
package task
import (
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"github.com/go-task/task/execext"
"github.com/go-task/task/internal/compiler"
compilerv1 "github.com/go-task/task/internal/compiler/v1"
compilerv2 "github.com/go-task/task/internal/compiler/v2"
"github.com/go-task/task/internal/execext"
"github.com/go-task/task/internal/logger"
"github.com/go-task/task/internal/output"
"github.com/go-task/task/internal/taskfile"
"github.com/go-task/task/internal/taskfile/version"
"github.com/Masterminds/semver"
"golang.org/x/sync/errgroup"
)
const (
// TaskFilePath is the default Taskfile
TaskFilePath = "Taskfile"
// MaximumTaskCall is the max number of times a task can be called.
// This exists to prevent infinite loops on cyclic dependencies
MaximumTaskCall = 100
)
// Executor executes a Taskfile
type Executor struct {
Tasks Tasks
Dir string
Force bool
Watch bool
Verbose bool
Taskfile *taskfile.Taskfile
Dir string
Force bool
Watch bool
Verbose bool
Silent bool
Context context.Context
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
taskvars Vars
watchingFiles map[string]struct{}
Logger *logger.Logger
Compiler compiler.Compiler
Output output.Output
dynamicCache Vars
muDynamicCache sync.Mutex
}
taskvars taskfile.Vars
// Vars is a string[string] variables map
type Vars map[string]string
// Tasks representas a group of tasks
type Tasks map[string]*Task
// Task represents a task
type Task struct {
Cmds []*Cmd
Deps []*Dep
Desc string
Sources []string
Generates []string
Status []string
Dir string
Vars Vars
Set string
Env Vars
taskCallCount map[string]*int32
}
// Run runs Task
func (e *Executor) Run(args ...string) error {
if e.HasCyclicDep() {
return ErrCyclicDependencyDetected
func (e *Executor) Run(calls ...taskfile.Call) error {
// check if given tasks exist
for _, c := range calls {
if _, ok := e.Taskfile.Tasks[c.Task]; !ok {
// FIXME: move to the main package
e.PrintTasksHelp()
return &taskNotFoundError{taskName: c.Task}
}
}
if e.Watch {
return e.watchTasks(calls...)
}
for _, c := range calls {
if err := e.RunTask(e.Context, c); err != nil {
return err
}
}
return nil
}
// Setup setups Executor's internal state
func (e *Executor) Setup() error {
if err := e.readTaskfile(); err != nil {
return err
}
v, err := semver.NewConstraint(e.Taskfile.Version)
if err != nil {
return fmt.Errorf(`task: could not parse taskfile version "%s": %v`, e.Taskfile.Version, err)
}
if e.Context == nil {
e.Context = context.Background()
}
if e.Stdin == nil {
e.Stdin = os.Stdin
}
@@ -74,242 +98,141 @@ func (e *Executor) Run(args ...string) error {
if e.Stderr == nil {
e.Stderr = os.Stderr
}
if e.dynamicCache == nil {
e.dynamicCache = make(Vars, 10)
e.Logger = &logger.Logger{
Stdout: e.Stdout,
Stderr: e.Stderr,
Verbose: e.Verbose,
}
switch {
case version.IsV1(v):
e.Compiler = &compilerv1.CompilerV1{
Dir: e.Dir,
Vars: e.taskvars,
Logger: e.Logger,
}
case version.IsV2(v), version.IsV21(v):
e.Compiler = &compilerv2.CompilerV2{
Dir: e.Dir,
Taskvars: e.taskvars,
TaskfileVars: e.Taskfile.Vars,
Expansions: e.Taskfile.Expansions,
Logger: e.Logger,
}
case version.IsV22(v):
return fmt.Errorf(`task: Taskfile versions greater than v2.1 not implemented in the version of Task`)
}
// check if given tasks exist
for _, a := range args {
if _, ok := e.Tasks[a]; !ok {
// FIXME: move to the main package
e.printExistingTasksHelp()
return &taskNotFoundError{taskName: a}
}
if !version.IsV21(v) && e.Taskfile.Output != "" {
return fmt.Errorf(`task: Taskfile option "output" is only available starting on Taskfile version v2.1`)
}
switch e.Taskfile.Output {
case "", "interleaved":
e.Output = output.Interleaved{}
case "group":
e.Output = output.Group{}
case "prefixed":
e.Output = output.Prefixed{}
default:
return fmt.Errorf(`task: output option "%s" not recognized`, e.Taskfile.Output)
}
if e.Watch {
if err := e.watchTasks(args...); err != nil {
return err
}
return nil
}
for _, a := range args {
if err := e.RunTask(context.Background(), a, nil); err != nil {
return err
}
e.taskCallCount = make(map[string]*int32, len(e.Taskfile.Tasks))
for k := range e.Taskfile.Tasks {
e.taskCallCount[k] = new(int32)
}
return nil
}
// RunTask runs a task by its name
func (e *Executor) RunTask(ctx context.Context, name string, vars Vars) error {
t, ok := e.Tasks[name]
if !ok {
return &taskNotFoundError{name}
func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
t, err := e.CompiledTask(call)
if err != nil {
return err
}
if !e.Watch && atomic.AddInt32(e.taskCallCount[call.Task], 1) >= MaximumTaskCall {
return &MaximumTaskCallExceededError{task: call.Task}
}
if err := e.runDeps(ctx, name, vars); err != nil {
if err := e.runDeps(ctx, t); err != nil {
return err
}
if !e.Force {
upToDate, err := e.isTaskUpToDate(ctx, name, vars)
upToDate, err := isTaskUpToDate(ctx, t)
if err != nil {
return err
}
if upToDate {
e.printfln(`task: Task "%s" is up to date`, name)
if !e.Silent {
e.Logger.Errf(`task: Task "%s" is up to date`, t.Task)
}
return nil
}
}
for i := range t.Cmds {
if err := e.runCommand(ctx, name, i, vars); err != nil {
return &taskRunError{name, err}
if err := e.runCommand(ctx, t, call, i); err != nil {
if err2 := statusOnError(t); err2 != nil {
e.Logger.VerboseErrf("task: error cleaning status on error: %v", err2)
}
return &taskRunError{t.Task, err}
}
}
return nil
}
func (e *Executor) runDeps(ctx context.Context, task string, vars Vars) error {
func (e *Executor) runDeps(ctx context.Context, t *taskfile.Task) error {
g, ctx := errgroup.WithContext(ctx)
t := e.Tasks[task]
for _, d := range t.Deps {
d := d
g.Go(func() error {
dep, err := e.ReplaceVariables(d.Task, task, vars)
if err != nil {
return err
}
if err = e.RunTask(ctx, dep, d.Vars); err != nil {
return err
}
return nil
return e.RunTask(ctx, taskfile.Call{Task: d.Task, Vars: d.Vars})
})
}
if err := g.Wait(); err != nil {
return err
}
return nil
return g.Wait()
}
func (e *Executor) isTaskUpToDate(ctx context.Context, task string, vars Vars) (bool, error) {
t := e.Tasks[task]
if len(t.Status) > 0 {
return e.isUpToDateStatus(ctx, task, vars)
}
return e.isUpToDateTimestamp(ctx, task, vars)
}
func (e *Executor) isUpToDateStatus(ctx context.Context, task string, vars Vars) (bool, error) {
t := e.Tasks[task]
environ, err := e.getEnviron(task, vars)
if err != nil {
return false, err
}
dir, err := e.getTaskDir(task, vars)
if err != nil {
return false, err
}
status, err := e.ReplaceSliceVariables(t.Status, task, vars)
if err != nil {
return false, err
}
for _, s := range status {
err = execext.RunCommand(&execext.RunCommandOptions{
Context: ctx,
Command: s,
Dir: dir,
Env: environ,
})
if err != nil {
return false, nil
}
}
return true, nil
}
func (e *Executor) isUpToDateTimestamp(ctx context.Context, task string, vars Vars) (bool, error) {
t := e.Tasks[task]
if len(t.Sources) == 0 || len(t.Generates) == 0 {
return false, nil
}
dir, err := e.getTaskDir(task, vars)
if err != nil {
return false, err
}
sources, err := e.ReplaceSliceVariables(t.Sources, task, vars)
if err != nil {
return false, err
}
generates, err := e.ReplaceSliceVariables(t.Generates, task, vars)
if err != nil {
return false, err
}
sourcesMaxTime, err := getPatternsMaxTime(dir, sources)
if err != nil || sourcesMaxTime.IsZero() {
return false, nil
}
generatesMinTime, err := getPatternsMinTime(dir, generates)
if err != nil || generatesMinTime.IsZero() {
return false, nil
}
return generatesMinTime.After(sourcesMaxTime), nil
}
func (e *Executor) runCommand(ctx context.Context, task string, i int, vars Vars) error {
t := e.Tasks[task]
func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfile.Call, i int) error {
cmd := t.Cmds[i]
if cmd.Cmd == "" {
return e.RunTask(ctx, cmd.Task, cmd.Vars)
}
c, err := e.ReplaceVariables(cmd.Cmd, task, vars)
if err != nil {
return err
}
dir, err := e.getTaskDir(task, vars)
if err != nil {
return err
}
envs, err := e.getEnviron(task, vars)
if err != nil {
return err
}
opts := &execext.RunCommandOptions{
Context: ctx,
Command: c,
Dir: dir,
Env: envs,
Stdin: e.Stdin,
Stderr: e.Stderr,
}
e.println(c)
if t.Set == "" {
opts.Stdout = e.Stdout
if err = execext.RunCommand(opts); err != nil {
return err
switch {
case cmd.Task != "":
return e.RunTask(ctx, taskfile.Call{Task: cmd.Task, Vars: cmd.Vars})
case cmd.Cmd != "":
if e.Verbose || (!cmd.Silent && !t.Silent && !e.Silent) {
e.Logger.Errf(cmd.Cmd)
}
} else {
buff := bytes.NewBuffer(nil)
opts.Stdout = buff
if err = execext.RunCommand(opts); err != nil {
return err
}
os.Setenv(t.Set, strings.TrimSpace(buff.String()))
stdOut := e.Output.WrapWriter(e.Stdout, t.Prefix)
stdErr := e.Output.WrapWriter(e.Stderr, t.Prefix)
defer stdOut.Close()
defer stdErr.Close()
return execext.RunCommand(&execext.RunCommandOptions{
Context: ctx,
Command: cmd.Cmd,
Dir: t.Dir,
Env: getEnviron(t),
Stdin: e.Stdin,
Stdout: stdOut,
Stderr: stdErr,
})
default:
return nil
}
return nil
}
func (e *Executor) getTaskDir(task string, vars Vars) (string, error) {
t := e.Tasks[task]
exeDir, err := e.ReplaceVariables(e.Dir, task, vars)
if err != nil {
return "", err
}
taskDir, err := e.ReplaceVariables(t.Dir, task, vars)
if err != nil {
return "", err
}
return filepath.Join(exeDir, taskDir), nil
}
func (e *Executor) getEnviron(task string, vars Vars) ([]string, error) {
t := e.Tasks[task]
func getEnviron(t *taskfile.Task) []string {
if t.Env == nil {
return nil, nil
return nil
}
envs := os.Environ()
for k, v := range t.Env {
env, err := e.ReplaceVariables(fmt.Sprintf("%s=%s", k, v), task, vars)
if err != nil {
return nil, err
}
envs = append(envs, env)
for k, v := range t.Env.ToStringMap() {
envs = append(envs, fmt.Sprintf("%s=%s", k, v))
}
return envs, nil
return envs
}

View File

@@ -2,6 +2,7 @@ package task_test
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
@@ -9,10 +10,197 @@ import (
"testing"
"github.com/go-task/task"
"github.com/go-task/task/internal/taskfile"
"github.com/stretchr/testify/assert"
)
// fileContentTest provides a basic reusable test-case for running a Taskfile
// and inspect generated files.
type fileContentTest struct {
Dir string
Target string
TrimSpace bool
Files map[string]string
}
func (fct fileContentTest) name(file string) string {
return fmt.Sprintf("target=%q,file=%q", fct.Target, file)
}
func (fct fileContentTest) Run(t *testing.T) {
for f := range fct.Files {
_ = os.Remove(filepath.Join(fct.Dir, f))
}
e := &task.Executor{
Dir: fct.Dir,
Stdout: ioutil.Discard,
Stderr: ioutil.Discard,
}
assert.NoError(t, e.Setup(), "e.Setup()")
assert.NoError(t, e.Run(taskfile.Call{Task: fct.Target}), "e.Run(target)")
for name, expectContent := range fct.Files {
t.Run(fct.name(name), func(t *testing.T) {
b, err := ioutil.ReadFile(filepath.Join(fct.Dir, name))
assert.NoError(t, err, "Error reading file")
s := string(b)
if fct.TrimSpace {
s = strings.TrimSpace(s)
}
assert.Equal(t, expectContent, s, "unexpected file content")
})
}
}
func TestEnv(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/env",
Target: "default",
TrimSpace: false,
Files: map[string]string{
"env.txt": "GOOS='linux' GOARCH='amd64' CGO_ENABLED='0'\n",
},
}
tt.Run(t)
}
func TestVarsV1(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/vars/v1",
Target: "default",
TrimSpace: true,
Files: map[string]string{
// hello task:
"foo.txt": "foo",
"bar.txt": "bar",
"baz.txt": "baz",
"tmpl_foo.txt": "foo",
"tmpl_bar.txt": "<no value>",
"tmpl_foo2.txt": "foo2",
"tmpl_bar2.txt": "bar2",
"shtmpl_foo.txt": "foo",
"shtmpl_foo2.txt": "foo2",
"nestedtmpl_foo.txt": "{{.FOO}}",
"nestedtmpl_foo2.txt": "foo2",
"foo2.txt": "foo2",
"bar2.txt": "bar2",
"baz2.txt": "baz2",
"tmpl2_foo.txt": "<no value>",
"tmpl2_foo2.txt": "foo2",
"tmpl2_bar.txt": "<no value>",
"tmpl2_bar2.txt": "<no value>",
"shtmpl2_foo.txt": "<no value>",
"shtmpl2_foo2.txt": "foo2",
"nestedtmpl2_foo2.txt": "{{.FOO2}}",
"override.txt": "bar",
},
}
tt.Run(t)
// Ensure identical results when running hello task directly.
tt.Target = "hello"
tt.Run(t)
}
func TestVarsV2(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/vars/v2",
Target: "default",
TrimSpace: true,
Files: map[string]string{
"foo.txt": "foo",
"bar.txt": "bar",
"baz.txt": "baz",
"tmpl_foo.txt": "foo",
"tmpl_bar.txt": "bar",
"tmpl_foo2.txt": "foo2",
"tmpl_bar2.txt": "bar2",
"shtmpl_foo.txt": "foo",
"shtmpl_foo2.txt": "foo2",
"nestedtmpl_foo.txt": "<no value>",
"nestedtmpl_foo2.txt": "foo2",
"foo2.txt": "foo2",
"bar2.txt": "bar2",
"baz2.txt": "baz2",
"tmpl2_foo.txt": "<no value>",
"tmpl2_foo2.txt": "foo2",
"tmpl2_bar.txt": "<no value>",
"tmpl2_bar2.txt": "bar2",
"shtmpl2_foo.txt": "<no value>",
"shtmpl2_foo2.txt": "foo2",
"nestedtmpl2_foo2.txt": "<no value>",
"override.txt": "bar",
"nested.txt": "Taskvars-TaskfileVars-TaskVars",
},
}
tt.Run(t)
// Ensure identical results when running hello task directly.
tt.Target = "hello"
tt.Run(t)
}
func TestMultilineVars(t *testing.T) {
for _, dir := range []string{"testdata/vars/v1/multiline", "testdata/vars/v2/multiline"} {
tt := fileContentTest{
Dir: dir,
Target: "default",
TrimSpace: false,
Files: map[string]string{
// Note:
// - task does not strip a trailing newline from var entries
// - task strips one trailing newline from shell output
// - the cat command adds a trailing newline
"echo_foobar.txt": "foo\nbar\n",
"echo_n_foobar.txt": "foo\nbar\n",
"echo_n_multiline.txt": "\n\nfoo\n bar\nfoobar\n\nbaz\n\n",
"var_multiline.txt": "\n\nfoo\n bar\nfoobar\n\nbaz\n\n\n",
"var_catlines.txt": " foo bar foobar baz \n",
"var_enumfile.txt": "0:\n1:\n2:foo\n3: bar\n4:foobar\n5:\n6:baz\n7:\n8:\n",
},
}
tt.Run(t)
}
}
func TestVarsInvalidTmpl(t *testing.T) {
const (
dir = "testdata/vars/v1"
target = "invalid-var-tmpl"
expectError = "template: :1: unexpected EOF"
)
e := &task.Executor{
Dir: dir,
Stdout: ioutil.Discard,
Stderr: ioutil.Discard,
}
assert.NoError(t, e.Setup(), "e.Setup()")
assert.EqualError(t, e.Run(taskfile.Call{Task: target}), expectError, "e.Run(target)")
}
func TestParams(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/params",
Target: "default",
TrimSpace: false,
Files: map[string]string{
"hello.txt": "Hello\n",
"world.txt": "World\n",
"exclamation.txt": "!\n",
"dep1.txt": "Dependence1\n",
"dep2.txt": "Dependence2\n",
"spanish.txt": "¡Holla mundo!\n",
"spanish-dep.txt": "¡Holla dependencia!\n",
"portuguese.txt": "Olá, mundo!\n",
"portuguese2.txt": "Olá, mundo!\n",
"german.txt": "Welt!\n",
},
}
tt.Run(t)
}
func TestDeps(t *testing.T) {
const dir = "testdata/deps"
@@ -40,8 +228,8 @@ func TestDeps(t *testing.T) {
Stdout: ioutil.Discard,
Stderr: ioutil.Discard,
}
assert.NoError(t, e.ReadTaskfile())
assert.NoError(t, e.Run("default"))
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(taskfile.Call{Task: "default"}))
for _, f := range files {
f = filepath.Join(dir, f)
@@ -51,73 +239,6 @@ func TestDeps(t *testing.T) {
}
}
func TestVars(t *testing.T) {
const dir = "testdata/vars"
files := []struct {
file string
content string
}{
{"foo.txt", "foo"},
{"bar.txt", "bar"},
{"foo2.txt", "foo2"},
{"bar2.txt", "bar2"},
{"equal.txt", "foo=bar"},
}
for _, f := range files {
_ = os.Remove(filepath.Join(dir, f.file))
}
e := &task.Executor{
Dir: dir,
Stdout: ioutil.Discard,
Stderr: ioutil.Discard,
}
assert.NoError(t, e.ReadTaskfile())
assert.NoError(t, e.Run("default"))
for _, f := range files {
d, err := ioutil.ReadFile(filepath.Join(dir, f.file))
if err != nil {
t.Errorf("Error reading %s: %v", f.file, err)
}
s := string(d)
s = strings.TrimSpace(s)
if s != f.content {
t.Errorf("File content should be %s but is %s", f.content, s)
}
}
}
func TestTaskCall(t *testing.T) {
const dir = "testdata/task_call"
files := []string{
"foo.txt",
"bar.txt",
}
for _, f := range files {
_ = os.Remove(filepath.Join(dir, f))
}
e := &task.Executor{
Dir: dir,
Stdout: ioutil.Discard,
Stderr: ioutil.Discard,
}
assert.NoError(t, e.ReadTaskfile())
assert.NoError(t, e.Run("default"))
for _, f := range files {
if _, err := os.Stat(filepath.Join(dir, f)); err != nil {
t.Error(err)
}
}
}
func TestStatus(t *testing.T) {
const dir = "testdata/status"
var file = filepath.Join(dir, "foo.txt")
@@ -128,27 +249,117 @@ func TestStatus(t *testing.T) {
t.Errorf("File should not exists: %v", err)
}
var buff bytes.Buffer
e := &task.Executor{
Dir: dir,
Stdout: ioutil.Discard,
Stderr: ioutil.Discard,
Stdout: &buff,
Stderr: &buff,
Silent: true,
}
assert.NoError(t, e.ReadTaskfile())
assert.NoError(t, e.Run("gen-foo"))
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(taskfile.Call{Task: "gen-foo"}))
if _, err := os.Stat(file); err != nil {
t.Errorf("File should exists: %v", err)
}
buff := bytes.NewBuffer(nil)
e.Stdout, e.Stderr = buff, buff
assert.NoError(t, e.Run("gen-foo"))
e.Silent = false
assert.NoError(t, e.Run(taskfile.Call{Task: "gen-foo"}))
if buff.String() != `task: Task "gen-foo" is up to date`+"\n" {
t.Errorf("Wrong output message: %s", buff.String())
}
}
func TestGenerates(t *testing.T) {
var srcTask = "sub/src.txt"
var relTask = "rel.txt"
var absTask = "abs.txt"
// This test does not work with a relative dir.
dir, err := filepath.Abs("testdata/generates")
assert.NoError(t, err)
var srcFile = filepath.Join(dir, srcTask)
for _, task := range []string{srcTask, relTask, absTask} {
path := filepath.Join(dir, task)
_ = os.Remove(path)
if _, err := os.Stat(path); err == nil {
t.Errorf("File should not exists: %v", err)
}
}
buff := bytes.NewBuffer(nil)
e := &task.Executor{
Dir: dir,
Stdout: buff,
Stderr: buff,
}
assert.NoError(t, e.Setup())
for _, theTask := range []string{relTask, absTask} {
var destFile = filepath.Join(dir, theTask)
var upToDate = fmt.Sprintf("task: Task \"%s\" is up to date\n", srcTask) +
fmt.Sprintf("task: Task \"%s\" is up to date\n", theTask)
// Run task for the first time.
assert.NoError(t, e.Run(taskfile.Call{Task: theTask}))
if _, err := os.Stat(srcFile); err != nil {
t.Errorf("File should exists: %v", err)
}
if _, err := os.Stat(destFile); err != nil {
t.Errorf("File should exists: %v", err)
}
// Ensure task was not incorrectly found to be up-to-date on first run.
if buff.String() == upToDate {
t.Errorf("Wrong output message: %s", buff.String())
}
buff.Reset()
// Re-run task to ensure it's now found to be up-to-date.
assert.NoError(t, e.Run(taskfile.Call{Task: theTask}))
if buff.String() != upToDate {
t.Errorf("Wrong output message: %s", buff.String())
}
buff.Reset()
}
}
func TestStatusChecksum(t *testing.T) {
const dir = "testdata/checksum"
files := []string{
"generated.txt",
".task/checksum/build",
}
for _, f := range files {
_ = os.Remove(filepath.Join(dir, f))
_, err := os.Stat(filepath.Join(dir, f))
assert.Error(t, err)
}
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(taskfile.Call{Task: "build"}))
for _, f := range files {
_, err := os.Stat(filepath.Join(dir, f))
assert.NoError(t, err)
}
buff.Reset()
assert.NoError(t, e.Run(taskfile.Call{Task: "build"}))
assert.Equal(t, `task: Task "build" is up to date`+"\n", buff.String())
}
func TestInit(t *testing.T) {
const dir = "testdata/init"
var file = filepath.Join(dir, "Taskfile.yml")
@@ -158,7 +369,7 @@ func TestInit(t *testing.T) {
t.Errorf("Taskfile.yml should not exists")
}
if err := task.InitTaskfile(dir); err != nil {
if err := task.InitTaskfile(ioutil.Discard, dir); err != nil {
t.Error(err)
}
@@ -167,34 +378,37 @@ func TestInit(t *testing.T) {
}
}
func TestParams(t *testing.T) {
const dir = "testdata/params"
var files = []struct {
file string
content string
}{
{"hello.txt", "Hello\n"},
{"world.txt", "World\n"},
{"exclamation.txt", "!\n"},
{"dep1.txt", "Dependence1\n"},
{"dep2.txt", "Dependence2\n"},
}
for _, f := range files {
_ = os.Remove(filepath.Join(dir, f.file))
}
func TestCyclicDep(t *testing.T) {
const dir = "testdata/cyclic"
e := task.Executor{
Dir: dir,
Stdout: ioutil.Discard,
Stderr: ioutil.Discard,
}
assert.NoError(t, e.ReadTaskfile())
assert.NoError(t, e.Run("default"))
assert.NoError(t, e.Setup())
assert.IsType(t, &task.MaximumTaskCallExceededError{}, e.Run(taskfile.Call{Task: "task-1"}))
}
for _, f := range files {
content, err := ioutil.ReadFile(filepath.Join(dir, f.file))
assert.NoError(t, err)
assert.Equal(t, f.content, string(content))
func TestTaskVersion(t *testing.T) {
tests := []struct {
Dir string
Version string
}{
{"testdata/version/v1", "1"},
{"testdata/version/v2", "2"},
}
for _, test := range tests {
t.Run(test.Dir, func(t *testing.T) {
e := task.Executor{
Dir: test.Dir,
Stdout: ioutil.Discard,
Stderr: ioutil.Discard,
}
assert.NoError(t, e.Setup())
assert.Equal(t, test.Version, e.Taskfile.Version)
assert.Equal(t, 2, len(e.Taskfile.Tasks))
})
}
}

74
taskfile.go Normal file
View File

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

2
testdata/checksum/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.task/
generated.txt

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

@@ -0,0 +1,8 @@
build:
cmds:
- cp ./source.txt ./generated.txt
sources:
- ./source.txt
generates:
- ./generated.txt
method: checksum

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

@@ -0,0 +1 @@
Hello, World!

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

@@ -0,0 +1,7 @@
task-1:
deps:
- task: task-2
task-2:
deps:
- task: task-1

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

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

10
testdata/env/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
default:
vars:
AMD64: amd64
env:
GOOS: linux
GOARCH: "{{.AMD64}}"
CGO_ENABLED:
sh: echo '0'
cmds:
- echo "GOOS='$GOOS' GOARCH='$GOARCH' CGO_ENABLED='$CGO_ENABLED'" > env.txt

31
testdata/generates/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
abs.txt:
desc: generates dest file based on absolute paths
deps:
- sub/src.txt
dir: sub
cmds:
- cat src.txt > '{{.BUILD_DIR}}/abs.txt'
sources:
- src.txt
generates:
- "{{.BUILD_DIR}}/abs.txt"
rel.txt:
desc: generates dest file based on relative paths
deps:
- sub/src.txt
dir: sub
cmds:
- cat src.txt > '../rel.txt'
sources:
- src.txt
generates:
- "../rel.txt"
sub/src.txt:
desc: generate source file
cmds:
- mkdir -p sub
- echo "hello world" > sub/src.txt
status:
- test -f sub/src.txt

1
testdata/generates/Taskvars.yml vendored Normal file
View File

@@ -0,0 +1 @@
BUILD_DIR: $pwd

View File

@@ -1,9 +1,15 @@
default:
vars:
SPANISH: ¡Holla mundo!
PORTUGUESE: "{{.PORTUGUESE_HELLO_WORLD}}"
GERMAN: "Welt!"
deps:
- task: write-file
vars: {CONTENT: Dependence1, FILE: dep1.txt}
- task: write-file
vars: {CONTENT: Dependence2, FILE: dep2.txt}
- task: write-file
vars: {CONTENT: "{{.SPANISH|replace \"mundo\" \"dependencia\"}}", FILE: spanish-dep.txt}
cmds:
- task: write-file
vars: {CONTENT: Hello, FILE: hello.txt}
@@ -11,7 +17,21 @@ default:
vars: {CONTENT: "$echo 'World'", FILE: world.txt}
- task: write-file
vars: {CONTENT: "!", FILE: exclamation.txt}
- task: write-file
vars: {CONTENT: "{{.SPANISH}}", FILE: spanish.txt}
- task: write-file
vars: {CONTENT: "{{.PORTUGUESE}}", FILE: portuguese.txt}
- task: write-file
vars: {CONTENT: "{{.GERMAN}}", FILE: german.txt}
- task: non-default
write-file:
cmds:
- echo {{.CONTENT}} > {{.FILE}}
non-default:
vars:
PORTUGUESE: "{{.PORTUGUESE_HELLO_WORLD}}"
cmds:
- task: write-file
vars: {CONTENT: "{{.PORTUGUESE}}", FILE: portuguese2.txt}

2
testdata/params/Taskvars.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
PORTUGUESE_HELLO_WORLD: Olá, mundo!
GERMAN: "Hello"

View File

@@ -1,20 +0,0 @@
default:
cmds:
- ^set-foo
- ^print
- ^set-bar
- ^print
print:
cmds:
- echo text > {{.FILE}}
set-foo:
set: FILE
cmds:
- echo foo.txt
set-bar:
set: FILE
cmds:
- echo bar.txt

View File

@@ -1,19 +0,0 @@
default:
deps: [hello]
hello:
deps: [set-equal]
cmds:
- echo {{.FOO}} > foo.txt
- echo {{.BAR}} > bar.txt
- echo {{.FOO2}} > foo2.txt
- echo {{.BAR2}} > bar2.txt
- echo {{.EQUAL}} > equal.txt
vars:
FOO: foo
BAR: $echo bar
set-equal:
set: EQUAL
cmds:
- echo foo=bar

View File

@@ -1,2 +0,0 @@
FOO2: foo2
BAR2: $echo bar2

48
testdata/vars/v1/Taskfile.yml vendored Normal file
View File

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

12
testdata/vars/v1/Taskvars.yml vendored Normal file
View File

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

43
testdata/vars/v1/multiline/Taskfile.yml vendored Normal file
View File

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

@@ -1,167 +0,0 @@
package task
import (
"bytes"
"errors"
"os"
"path/filepath"
"runtime"
"strings"
"text/template"
"github.com/go-task/task/execext"
"github.com/Masterminds/sprig"
)
var (
// TaskvarsFilePath file containing additional variables
TaskvarsFilePath = "Taskvars"
// ErrMultilineResultCmd is returned when a command returns multiline result
ErrMultilineResultCmd = errors.New("Got multiline result from command")
)
func (e *Executor) handleDynamicVariableContent(value string) (string, error) {
if !strings.HasPrefix(value, "$") {
return value, nil
}
e.muDynamicCache.Lock()
defer e.muDynamicCache.Unlock()
if result, ok := e.dynamicCache[value]; ok {
return result, nil
}
buff := bytes.NewBuffer(nil)
opts := &execext.RunCommandOptions{
Command: strings.TrimPrefix(value, "$"),
Dir: e.Dir,
Stdout: buff,
Stderr: e.Stderr,
}
if err := execext.RunCommand(opts); err != nil {
return "", err
}
result := buff.String()
result = strings.TrimSuffix(result, "\n")
if strings.ContainsRune(result, '\n') {
return "", ErrMultilineResultCmd
}
result = strings.TrimSpace(result)
e.verbosePrintfln(`task: dynamic variable: "%s", result: "%s"`, value, result)
e.dynamicCache[value] = result
return result, nil
}
func (e *Executor) getVariables(task string, vars Vars) (map[string]string, error) {
t := e.Tasks[task]
localVariables := make(map[string]string)
for key, value := range t.Vars {
val, err := e.handleDynamicVariableContent(value)
if err != nil {
return nil, err
}
localVariables[key] = val
}
if e.taskvars != nil {
for key, value := range e.taskvars {
val, err := e.handleDynamicVariableContent(value)
if err != nil {
return nil, err
}
localVariables[key] = val
}
}
for key, value := range getEnvironmentVariables() {
localVariables[key] = value
}
if vars != nil {
for k, v := range vars {
val, err := e.handleDynamicVariableContent(v)
if err != nil {
return nil, err
}
localVariables[k] = val
}
}
return localVariables, nil
}
var templateFuncs template.FuncMap
func init() {
taskFuncs := template.FuncMap{
"OS": func() string { return runtime.GOOS },
"ARCH": func() string { return runtime.GOARCH },
// historical reasons
"IsSH": func() bool { return true },
"FromSlash": func(path string) string {
return filepath.FromSlash(path)
},
"ToSlash": func(path string) string {
return filepath.ToSlash(path)
},
"ExeExt": func() string {
if runtime.GOOS == "windows" {
return ".exe"
}
return ""
},
}
templateFuncs = sprig.TxtFuncMap()
for k, v := range taskFuncs {
templateFuncs[k] = v
}
}
// ReplaceSliceVariables writes vars into initial string slice
func (e *Executor) ReplaceSliceVariables(initials []string, task string, vars Vars) ([]string, error) {
result := make([]string, len(initials))
for i, s := range initials {
var err error
result[i], err = e.ReplaceVariables(s, task, vars)
if err != nil {
return nil, err
}
}
return result, nil
}
// ReplaceVariables writes vars into initial string
func (e *Executor) ReplaceVariables(initial, task string, vars Vars) (string, error) {
vars, err := e.getVariables(task, vars)
if err != nil {
return "", err
}
templ, err := template.New("").Funcs(templateFuncs).Parse(initial)
if err != nil {
return "", err
}
b := bytes.NewBuffer(nil)
if err = templ.Execute(b, vars); err != nil {
return "", err
}
return b.String(), nil
}
// GetEnvironmentVariables returns environment variables as map
func getEnvironmentVariables() map[string]string {
var (
env = os.Environ()
m = make(map[string]string, len(env))
)
for _, e := range env {
keyVal := strings.SplitN(e, "=", 2)
key, val := keyVal[0], keyVal[1]
m[key] = val
}
return m
}

84
variables.go Normal file
View File

@@ -0,0 +1,84 @@
package task
import (
"path/filepath"
"github.com/go-task/task/internal/osext"
"github.com/go-task/task/internal/taskfile"
"github.com/go-task/task/internal/templater"
)
var (
// TaskvarsFilePath file containing additional variables.
TaskvarsFilePath = "Taskvars"
)
// CompiledTask returns a copy of a task, but replacing variables in almost all
// properties using the Go template package.
func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) {
origTask, ok := e.Taskfile.Tasks[call.Task]
if !ok {
return nil, &taskNotFoundError{call.Task}
}
vars, err := e.Compiler.GetVariables(origTask, call)
if err != nil {
return nil, err
}
r := templater.Templater{Vars: vars}
new := taskfile.Task{
Task: origTask.Task,
Desc: r.Replace(origTask.Desc),
Sources: r.ReplaceSlice(origTask.Sources),
Generates: r.ReplaceSlice(origTask.Generates),
Status: r.ReplaceSlice(origTask.Status),
Dir: r.Replace(origTask.Dir),
Vars: nil,
Env: r.ReplaceVars(origTask.Env),
Silent: origTask.Silent,
Method: r.Replace(origTask.Method),
Prefix: r.Replace(origTask.Prefix),
}
new.Dir, err = osext.Expand(new.Dir)
if err != nil {
return nil, err
}
if e.Dir != "" && !filepath.IsAbs(new.Dir) {
new.Dir = filepath.Join(e.Dir, new.Dir)
}
if new.Prefix == "" {
new.Prefix = new.Task
}
for k, v := range new.Env {
static, err := e.Compiler.HandleDynamicVar(v)
if err != nil {
return nil, err
}
new.Env[k] = taskfile.Var{Static: static}
}
if len(origTask.Cmds) > 0 {
new.Cmds = make([]*taskfile.Cmd, len(origTask.Cmds))
for i, cmd := range origTask.Cmds {
new.Cmds[i] = &taskfile.Cmd{
Task: r.Replace(cmd.Task),
Silent: cmd.Silent,
Cmd: r.Replace(cmd.Cmd),
Vars: r.ReplaceVars(cmd.Vars),
}
}
}
if len(origTask.Deps) > 0 {
new.Deps = make([]*taskfile.Dep, len(origTask.Deps))
for i, dep := range origTask.Deps {
new.Deps[i] = &taskfile.Dep{
Task: r.Replace(dep.Task),
Vars: r.ReplaceVars(dep.Vars),
}
}
}
return &new, r.Err()
}

View File

@@ -1,62 +0,0 @@
# 1.3.0 (2017-05-02)
## Added
- #45: Added json (un)marshaling support (thanks @mh-cbon)
- Stability marker. See https://masterminds.github.io/stability/
## Fixed
- #51: Fix handling of single digit tilde constraint (thanks @dgodd)
## Changed
- #55: The godoc icon moved from png to svg
# 1.2.3 (2017-04-03)
## Fixed
- #46: Fixed 0.x.x and 0.0.x in constraints being treated as *
# Release 1.2.2 (2016-12-13)
## Fixed
- #34: Fixed issue where hyphen range was not working with pre-release parsing.
# Release 1.2.1 (2016-11-28)
## Fixed
- #24: Fixed edge case issue where constraint "> 0" does not handle "0.0.1-alpha"
properly.
# Release 1.2.0 (2016-11-04)
## Added
- #20: Added MustParse function for versions (thanks @adamreese)
- #15: Added increment methods on versions (thanks @mh-cbon)
## Fixed
- Issue #21: Per the SemVer spec (section 9) a pre-release is unstable and
might not satisfy the intended compatibility. The change here ignores pre-releases
on constraint checks (e.g., ~ or ^) when a pre-release is not part of the
constraint. For example, `^1.2.3` will ignore pre-releases while
`^1.2.3-alpha` will include them.
# Release 1.1.1 (2016-06-30)
## Changed
- Issue #9: Speed up version comparison performance (thanks @sdboyer)
- Issue #8: Added benchmarks (thanks @sdboyer)
- Updated Go Report Card URL to new location
- Updated Readme to add code snippet formatting (thanks @mh-cbon)
- Updating tagging to v[SemVer] structure for compatibility with other tools.
# Release 1.1.0 (2016-03-11)
- Issue #2: Implemented validation to provide reasons a versions failed a
constraint.
# Release 1.0.1 (2015-12-31)
- Fixed #1: * constraint failing on valid versions.
# Release 1.0.0 (2015-10-20)
- Initial release

View File

@@ -1,36 +0,0 @@
.PHONY: setup
setup:
go get -u gopkg.in/alecthomas/gometalinter.v1
gometalinter.v1 --install
.PHONY: test
test: validate lint
@echo "==> Running tests"
go test -v
.PHONY: validate
validate:
@echo "==> Running static validations"
@gometalinter.v1 \
--disable-all \
--enable deadcode \
--severity deadcode:error \
--enable gofmt \
--enable gosimple \
--enable ineffassign \
--enable misspell \
--enable vet \
--tests \
--vendor \
--deadline 60s \
./... || exit_code=1
.PHONY: lint
lint:
@echo "==> Running linters"
@gometalinter.v1 \
--disable-all \
--enable golint \
--vendor \
--deadline 60s \
./... || :

View File

@@ -1,165 +0,0 @@
# SemVer
The `semver` package provides the ability to work with [Semantic Versions](http://semver.org) in Go. Specifically it provides the ability to:
* Parse semantic versions
* Sort semantic versions
* Check if a semantic version fits within a set of constraints
* Optionally work with a `v` prefix
[![Stability:
Active](https://masterminds.github.io/stability/active.svg)](https://masterminds.github.io/stability/active.html)
[![Build Status](https://travis-ci.org/Masterminds/semver.svg)](https://travis-ci.org/Masterminds/semver) [![Build status](https://ci.appveyor.com/api/projects/status/jfk66lib7hb985k8/branch/master?svg=true&passingText=windows%20build%20passing&failingText=windows%20build%20failing)](https://ci.appveyor.com/project/mattfarina/semver/branch/master) [![GoDoc](https://godoc.org/github.com/Masterminds/semver?status.svg)](https://godoc.org/github.com/Masterminds/semver) [![Go Report Card](https://goreportcard.com/badge/github.com/Masterminds/semver)](https://goreportcard.com/report/github.com/Masterminds/semver)
## Parsing Semantic Versions
To parse a semantic version use the `NewVersion` function. For example,
```go
v, err := semver.NewVersion("1.2.3-beta.1+build345")
```
If there is an error the version wasn't parseable. The version object has methods
to get the parts of the version, compare it to other versions, convert the
version back into a string, and get the original string. For more details
please see the [documentation](https://godoc.org/github.com/Masterminds/semver).
## Sorting Semantic Versions
A set of versions can be sorted using the [`sort`](https://golang.org/pkg/sort/)
package from the standard library. For example,
```go
raw := []string{"1.2.3", "1.0", "1.3", "2", "0.4.2",}
vs := make([]*semver.Version, len(raw))
for i, r := range raw {
v, err := semver.NewVersion(r)
if err != nil {
t.Errorf("Error parsing version: %s", err)
}
vs[i] = v
}
sort.Sort(semver.Collection(vs))
```
## Checking Version Constraints
Checking a version against version constraints is one of the most featureful
parts of the package.
```go
c, err := semver.NewConstraint(">= 1.2.3")
if err != nil {
// Handle constraint not being parseable.
}
v, _ := semver.NewVersion("1.3")
if err != nil {
// Handle version not being parseable.
}
// Check if the version meets the constraints. The a variable will be true.
a := c.Check(v)
```
## Basic Comparisons
There are two elements to the comparisons. First, a comparison string is a list
of comma separated and comparisons. These are then separated by || separated or
comparisons. For example, `">= 1.2, < 3.0.0 || >= 4.2.3"` is looking for a
comparison that's greater than or equal to 1.2 and less than 3.0.0 or is
greater than or equal to 4.2.3.
The basic comparisons are:
* `=`: equal (aliased to no operator)
* `!=`: not equal
* `>`: greater than
* `<`: less than
* `>=`: greater than or equal to
* `<=`: less than or equal to
_Note, according to the Semantic Version specification pre-releases may not be
API compliant with their release counterpart. It says,_
> _A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version._
_SemVer comparisons without a pre-release value will skip pre-release versions.
For example, `>1.2.3` will skip pre-releases when looking at a list of values
while `>1.2.3-alpha.1` will evaluate pre-releases._
## Hyphen Range Comparisons
There are multiple methods to handle ranges and the first is hyphens ranges.
These look like:
* `1.2 - 1.4.5` which is equivalent to `>= 1.2, <= 1.4.5`
* `2.3.4 - 4.5` which is equivalent to `>= 2.3.4, <= 4.5`
## Wildcards In Comparisons
The `x`, `X`, and `*` characters can be used as a wildcard character. This works
for all comparison operators. When used on the `=` operator it falls
back to the pack level comparison (see tilde below). For example,
* `1.2.x` is equivalent to `>= 1.2.0, < 1.3.0`
* `>= 1.2.x` is equivalent to `>= 1.2.0`
* `<= 2.x` is equivalent to `<= 3`
* `*` is equivalent to `>= 0.0.0`
## Tilde Range Comparisons (Patch)
The tilde (`~`) comparison operator is for patch level ranges when a minor
version is specified and major level changes when the minor number is missing.
For example,
* `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0`
* `~1` is equivalent to `>= 1, < 2`
* `~2.3` is equivalent to `>= 2.3, < 2.4`
* `~1.2.x` is equivalent to `>= 1.2.0, < 1.3.0`
* `~1.x` is equivalent to `>= 1, < 2`
## Caret Range Comparisons (Major)
The caret (`^`) comparison operator is for major level changes. This is useful
when comparisons of API versions as a major change is API breaking. For example,
* `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0`
* `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0`
* `^2.3` is equivalent to `>= 2.3, < 3`
* `^2.x` is equivalent to `>= 2.0.0, < 3`
# Validation
In addition to testing a version against a constraint, a version can be validated
against a constraint. When validation fails a slice of errors containing why a
version didn't meet the constraint is returned. For example,
```go
c, err := semver.NewConstraint("<= 1.2.3, >= 1.4")
if err != nil {
// Handle constraint not being parseable.
}
v, _ := semver.NewVersion("1.3")
if err != nil {
// Handle version not being parseable.
}
// Validate a version against a constraint.
a, msgs := c.Validate(v)
// a is false
for _, m := range msgs {
fmt.Println(m)
// Loops over the errors which would read
// "1.3 is greater than 1.2.3"
// "1.3 is less than 1.4"
}
```
# Contribute
If you find an issue or want to contribute please file an [issue](https://github.com/Masterminds/semver/issues)
or [create a pull request](https://github.com/Masterminds/semver/pulls).

View File

@@ -1,44 +0,0 @@
version: build-{build}.{branch}
clone_folder: C:\gopath\src\github.com\Masterminds\semver
shallow_clone: true
environment:
GOPATH: C:\gopath
platform:
- x64
install:
- go version
- go env
- go get -u gopkg.in/alecthomas/gometalinter.v1
- set PATH=%PATH%;%GOPATH%\bin
- gometalinter.v1.exe --install
build_script:
- go install -v ./...
test_script:
- "gometalinter.v1 \
--disable-all \
--enable deadcode \
--severity deadcode:error \
--enable gofmt \
--enable gosimple \
--enable ineffassign \
--enable misspell \
--enable vet \
--tests \
--vendor \
--deadline 60s \
./... || exit_code=1"
- "gometalinter.v1 \
--disable-all \
--enable golint \
--vendor \
--deadline 60s \
./... || :"
- go test -v
deploy: off

View File

@@ -233,12 +233,6 @@ func constraintNotEqual(v *Version, c *constraint) bool {
func constraintGreaterThan(v *Version, c *constraint) bool {
// An edge case the constraint is 0.0.0 and the version is 0.0.0-someprerelease
// exists. This that case.
if !isNonZero(c.con) && isNonZero(v) {
return true
}
// If there is a pre-release on the version but the constraint isn't looking
// for them assume that pre-releases are not compatible. See issue 21 for
// more details.
@@ -271,11 +265,6 @@ func constraintLessThan(v *Version, c *constraint) bool {
}
func constraintGreaterThanEqual(v *Version, c *constraint) bool {
// An edge case the constraint is 0.0.0 and the version is 0.0.0-someprerelease
// exists. This that case.
if !isNonZero(c.con) && isNonZero(v) {
return true
}
// If there is a pre-release on the version but the constraint isn't looking
// for them assume that pre-releases are not compatible. See issue 21 for
@@ -415,12 +404,3 @@ func rewriteRange(i string) string {
return o
}
// Detect if a version is not zero (0.0.0)
func isNonZero(v *Version) bool {
if v.Major() != 0 || v.Minor() != 0 || v.Patch() != 0 || v.Prerelease() != "" {
return true
}
return false
}

View File

@@ -64,14 +64,14 @@ func NewVersion(v string) (*Version, error) {
}
var temp int64
temp, err := strconv.ParseInt(m[1], 10, 32)
temp, err := strconv.ParseInt(m[1], 10, 64)
if err != nil {
return nil, fmt.Errorf("Error parsing version segment: %s", err)
}
sv.major = temp
if m[2] != "" {
temp, err = strconv.ParseInt(strings.TrimPrefix(m[2], "."), 10, 32)
temp, err = strconv.ParseInt(strings.TrimPrefix(m[2], "."), 10, 64)
if err != nil {
return nil, fmt.Errorf("Error parsing version segment: %s", err)
}
@@ -81,7 +81,7 @@ func NewVersion(v string) (*Version, error) {
}
if m[3] != "" {
temp, err = strconv.ParseInt(strings.TrimPrefix(m[3], "."), 10, 32)
temp, err = strconv.ParseInt(strings.TrimPrefix(m[3], "."), 10, 64)
if err != nil {
return nil, fmt.Errorf("Error parsing version segment: %s", err)
}
@@ -379,23 +379,43 @@ func comparePrePart(s, o string) int {
// When s or o are empty we can use the other in an attempt to determine
// the response.
if o == "" {
_, n := strconv.ParseInt(s, 10, 64)
if n != nil {
if s == "" {
if o != "" {
return -1
}
return 1
}
if s == "" {
_, n := strconv.ParseInt(o, 10, 64)
if n != nil {
if o == "" {
if s != "" {
return 1
}
return -1
}
if s > o {
// When comparing strings "99" is greater than "103". To handle
// cases like this we need to detect numbers and compare them.
oi, n1 := strconv.ParseInt(o, 10, 64)
si, n2 := strconv.ParseInt(s, 10, 64)
// The case where both are strings compare the strings
if n1 != nil && n2 != nil {
if s > o {
return 1
}
return -1
} else if n1 != nil {
// o is a string and s is a number
return -1
} else if n2 != nil {
// s is a string and o is a number
return 1
}
// Both are numbers
if si > oi {
return 1
}
return -1
}

View File

@@ -1,16 +0,0 @@
# Release 1.2.0 (2016-02-01)
- Added quote and squote
- Added b32enc and b32dec
- add now takes varargs
- biggest now takes varargs
# Release 1.1.0 (2015-12-29)
- Added #4: Added contains function. strings.Contains, but with the arguments
switched to simplify common pipelines. (thanks krancour)
- Added Travis-CI testing support
# Release 1.0.0 (2015-12-23)
- Initial release

View File

@@ -1,13 +0,0 @@
HAS_GLIDE := $(shell command -v glide;)
.PHONY: test
test:
go test -v .
.PHONY: setup
setup:
ifndef HAS_GLIDE
go get -u github.com/Masterminds/glide
endif
glide install

View File

@@ -1,81 +0,0 @@
# Sprig: Template functions for Go templates
[![Stability: Sustained](https://masterminds.github.io/stability/sustained.svg)](https://masterminds.github.io/stability/sustained.html)
[![Build Status](https://travis-ci.org/Masterminds/sprig.svg?branch=master)](https://travis-ci.org/Masterminds/sprig)
The Go language comes with a [built-in template
language](http://golang.org/pkg/text/template/), but not
very many template functions. This library provides a group of commonly
used template functions.
It is inspired by the template functions found in
[Twig](http://twig.sensiolabs.org/documentation) and also in various
JavaScript libraries, such as [underscore.js](http://underscorejs.org/).
## Usage
Template developers can read the [Sprig function documentation](http://masterminds.github.io/sprig/) to
learn about the >100 template functions available.
For Go developers wishing to include Sprig as a library in their programs,
API documentation is available [at GoDoc.org](http://godoc.org/github.com/Masterminds/sprig), but
read on for standard usage.
### Load the Sprig library
To load the Sprig `FuncMap`:
```go
import (
"github.com/Masterminds/sprig"
"html/template"
)
// This example illustrates that the FuncMap *must* be set before the
// templates themselves are loaded.
tpl := template.Must(
template.New("base").Funcs(sprig.FuncMap()).ParseGlob("*.html")
)
```
### Call the functions inside of templates
By convention, all functions are lowercase. This seems to follow the Go
idiom for template functions (as opposed to template methods, which are
TitleCase).
Example:
```
{{ "hello!" | upper | repeat 5 }}
```
Produces:
```
HELLO!HELLO!HELLO!HELLO!HELLO!
```
## Principles:
The following principles were used in deciding on which functions to add, and
determining how to implement them.
- Template functions should be used to build layout. Therefore, the following
types of operations are within the domain of template functions:
- Formatting
- Layout
- Simple type conversions
- Utilities that assist in handling common formatting and layout needs (e.g. arithmetic)
- Template functions should not return errors unless there is no way to print
a sensible value. For example, converting a string to an integer should not
produce an error if conversion fails. Instead, it should display a default
value that can be displayed.
- Simple math is necessary for grid layouts, pagers, and so on. Complex math
(anything other than arithmetic) should be done outside of templates.
- Template functions only deal with the data passed into them. They never retrieve
data from a source.
- Finally, do not override core Go template functions.

View File

@@ -8,16 +8,22 @@ import (
"crypto/hmac"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"math/big"
"net"
"time"
uuid "github.com/satori/go.uuid"
"github.com/google/uuid"
"golang.org/x/crypto/scrypt"
)
@@ -26,9 +32,14 @@ func sha256sum(input string) string {
return hex.EncodeToString(hash[:])
}
func sha1sum(input string) string {
hash := sha1.Sum([]byte(input))
return hex.EncodeToString(hash[:])
}
// uuidv4 provides a safe and secure UUID v4 implementation
func uuidv4() string {
return fmt.Sprintf("%s", uuid.NewV4())
return fmt.Sprintf("%s", uuid.New())
}
var master_password_seed = "com.lyndir.masterpassword"
@@ -146,3 +157,274 @@ func pemBlockForKey(priv interface{}) *pem.Block {
return nil
}
}
type certificate struct {
Cert string
Key string
}
func buildCustomCertificate(b64cert string, b64key string) (certificate, error) {
crt := certificate{}
cert, err := base64.StdEncoding.DecodeString(b64cert)
if err != nil {
return crt, errors.New("unable to decode base64 certificate")
}
key, err := base64.StdEncoding.DecodeString(b64key)
if err != nil {
return crt, errors.New("unable to decode base64 private key")
}
decodedCert, _ := pem.Decode(cert)
if decodedCert == nil {
return crt, errors.New("unable to decode certificate")
}
_, err = x509.ParseCertificate(decodedCert.Bytes)
if err != nil {
return crt, fmt.Errorf(
"error parsing certificate: decodedCert.Bytes: %s",
err,
)
}
decodedKey, _ := pem.Decode(key)
if decodedKey == nil {
return crt, errors.New("unable to decode key")
}
_, err = x509.ParsePKCS1PrivateKey(decodedKey.Bytes)
if err != nil {
return crt, fmt.Errorf(
"error parsing prive key: decodedKey.Bytes: %s",
err,
)
}
crt.Cert = string(cert)
crt.Key = string(key)
return crt, nil
}
func generateCertificateAuthority(
cn string,
daysValid int,
) (certificate, error) {
ca := certificate{}
template, err := getBaseCertTemplate(cn, nil, nil, daysValid)
if err != nil {
return ca, err
}
// Override KeyUsage and IsCA
template.KeyUsage = x509.KeyUsageKeyEncipherment |
x509.KeyUsageDigitalSignature |
x509.KeyUsageCertSign
template.IsCA = true
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return ca, fmt.Errorf("error generating rsa key: %s", err)
}
ca.Cert, ca.Key, err = getCertAndKey(template, priv, template, priv)
if err != nil {
return ca, err
}
return ca, nil
}
func generateSelfSignedCertificate(
cn string,
ips []interface{},
alternateDNS []interface{},
daysValid int,
) (certificate, error) {
cert := certificate{}
template, err := getBaseCertTemplate(cn, ips, alternateDNS, daysValid)
if err != nil {
return cert, err
}
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return cert, fmt.Errorf("error generating rsa key: %s", err)
}
cert.Cert, cert.Key, err = getCertAndKey(template, priv, template, priv)
if err != nil {
return cert, err
}
return cert, nil
}
func generateSignedCertificate(
cn string,
ips []interface{},
alternateDNS []interface{},
daysValid int,
ca certificate,
) (certificate, error) {
cert := certificate{}
decodedSignerCert, _ := pem.Decode([]byte(ca.Cert))
if decodedSignerCert == nil {
return cert, errors.New("unable to decode certificate")
}
signerCert, err := x509.ParseCertificate(decodedSignerCert.Bytes)
if err != nil {
return cert, fmt.Errorf(
"error parsing certificate: decodedSignerCert.Bytes: %s",
err,
)
}
decodedSignerKey, _ := pem.Decode([]byte(ca.Key))
if decodedSignerKey == nil {
return cert, errors.New("unable to decode key")
}
signerKey, err := x509.ParsePKCS1PrivateKey(decodedSignerKey.Bytes)
if err != nil {
return cert, fmt.Errorf(
"error parsing prive key: decodedSignerKey.Bytes: %s",
err,
)
}
template, err := getBaseCertTemplate(cn, ips, alternateDNS, daysValid)
if err != nil {
return cert, err
}
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return cert, fmt.Errorf("error generating rsa key: %s", err)
}
cert.Cert, cert.Key, err = getCertAndKey(
template,
priv,
signerCert,
signerKey,
)
if err != nil {
return cert, err
}
return cert, nil
}
func getCertAndKey(
template *x509.Certificate,
signeeKey *rsa.PrivateKey,
parent *x509.Certificate,
signingKey *rsa.PrivateKey,
) (string, string, error) {
derBytes, err := x509.CreateCertificate(
rand.Reader,
template,
parent,
&signeeKey.PublicKey,
signingKey,
)
if err != nil {
return "", "", fmt.Errorf("error creating certificate: %s", err)
}
certBuffer := bytes.Buffer{}
if err := pem.Encode(
&certBuffer,
&pem.Block{Type: "CERTIFICATE", Bytes: derBytes},
); err != nil {
return "", "", fmt.Errorf("error pem-encoding certificate: %s", err)
}
keyBuffer := bytes.Buffer{}
if err := pem.Encode(
&keyBuffer,
&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(signeeKey),
},
); err != nil {
return "", "", fmt.Errorf("error pem-encoding key: %s", err)
}
return string(certBuffer.Bytes()), string(keyBuffer.Bytes()), nil
}
func getBaseCertTemplate(
cn string,
ips []interface{},
alternateDNS []interface{},
daysValid int,
) (*x509.Certificate, error) {
ipAddresses, err := getNetIPs(ips)
if err != nil {
return nil, err
}
dnsNames, err := getAlternateDNSStrs(alternateDNS)
if err != nil {
return nil, err
}
return &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: cn,
},
IPAddresses: ipAddresses,
DNSNames: dnsNames,
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24 * time.Duration(daysValid)),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageServerAuth,
x509.ExtKeyUsageClientAuth,
},
BasicConstraintsValid: true,
}, nil
}
func getNetIPs(ips []interface{}) ([]net.IP, error) {
if ips == nil {
return []net.IP{}, nil
}
var ipStr string
var ok bool
var netIP net.IP
netIPs := make([]net.IP, len(ips))
for i, ip := range ips {
ipStr, ok = ip.(string)
if !ok {
return nil, fmt.Errorf("error parsing ip: %v is not a string", ip)
}
netIP = net.ParseIP(ipStr)
if netIP == nil {
return nil, fmt.Errorf("error parsing ip: %s", ipStr)
}
netIPs[i] = netIP
}
return netIPs, nil
}
func getAlternateDNSStrs(alternateDNS []interface{}) ([]string, error) {
if alternateDNS == nil {
return []string{}, nil
}
var dnsStr string
var ok bool
alternateDNSStrs := make([]string, len(alternateDNS))
for i, dns := range alternateDNS {
dnsStr, ok = dns.(string)
if !ok {
return nil, fmt.Errorf(
"error processing alternate dns name: %v is not a string",
dns,
)
}
alternateDNSStrs[i] = dnsStr
}
return alternateDNSStrs, nil
}

View File

@@ -51,3 +51,26 @@ func dateModify(fmt string, date time.Time) time.Time {
}
return date.Add(d)
}
func dateAgo(date interface{}) string {
var t time.Time
switch date := date.(type) {
default:
t = time.Now()
case time.Time:
t = date
case int64:
t = time.Unix(date, 0)
case int:
t = time.Unix(int64(date), 0)
}
// Drop resolution to seconds
duration := time.Since(t).Round(time.Second)
return duration.String()
}
func toDate(fmt, str string) time.Time {
t, _ := time.ParseInLocation(fmt, str, time.Local)
return t
}

View File

@@ -73,3 +73,12 @@ func toPrettyJson(v interface{}) string {
output, _ := json.MarshalIndent(v, "", " ")
return string(output)
}
// ternary returns the first value if the last value is true, otherwise returns the second value.
func ternary(vt interface{}, vf interface{}, v bool) interface{} {
if v {
return vt
}
return vf
}

View File

@@ -27,10 +27,12 @@ func pluck(key string, d ...map[string]interface{}) []interface{} {
return res
}
func keys(dict map[string]interface{}) []string {
func keys(dicts ...map[string]interface{}) []string {
k := []string{}
for key := range dict {
k = append(k, key)
for _, dict := range dicts {
for key := range dict {
k = append(k, key)
}
}
return k
}
@@ -75,10 +77,12 @@ func dict(v ...interface{}) map[string]interface{} {
return dict
}
func merge(dst map[string]interface{}, src map[string]interface{}) interface{} {
if err := mergo.Merge(&dst, src); err != nil {
// Swallow errors inside of a template.
return ""
func merge(dst map[string]interface{}, srcs ...map[string]interface{}) interface{} {
for _, src := range srcs {
if err := mergo.Merge(&dst, src); err != nil {
// Swallow errors inside of a template.
return ""
}
}
return dst
}

View File

@@ -48,6 +48,10 @@ String Functions
- randAlpha: Given a length, generate an alphabetic string
- randAscii: Given a length, generate a random ASCII string (symbols included)
- randNumeric: Given a length, generate a string of digits.
- swapcase: SwapCase swaps the case of a string using a word based algorithm. see https://godoc.org/github.com/Masterminds/goutils#SwapCase
- shuffle: Shuffle randomizes runes in a string and returns the result. It uses default random source in `math/rand`
- snakecase: convert all upper case characters in a string to underscore format.
- camelcase: convert all lower case characters behind underscores to upper case character
- wrap: Force a line wrap at the given width. `wrap 80 "imagine a longer string"`
- wrapWith: Wrap a line at the given length, but using 'sep' instead of a newline. `wrapWith 50, "<br>", $html`
- contains: strings.Contains, but with the arguments switched: `contains substr str`. (This simplifies common pipelines)
@@ -57,6 +61,7 @@ String Functions
- squote: Wrap string(s) in double quotation marks, does not escape content.
- cat: Concatenate strings, separating them by spaces. `cat $a $b $c`.
- indent: Indent a string using space characters. `indent 4 "foo\nbar"` produces " foo\n bar"
- nindent: Indent a string using space characters and prepend a new line. `indent 4 "foo\nbar"` produces "\n foo\n bar"
- replace: Replace an old with a new in a string: `$name | replace " " "-"`
- plural: Choose singular or plural based on length: `len $fish | plural "one anchovy" "many anchovies"`
- sha256sum: Generate a hex encoded sha256 hash of the input
@@ -84,7 +89,7 @@ Integer Slice Functions:
Conversions:
- atoi: Convert a string to an integer. 0 if the integer could not be parsed.
- in64: Convert a string or another numeric type to an int64.
- int64: Convert a string or another numeric type to an int64.
- int: Convert a string or another numeric type to an int.
- float64: Convert a string or another numeric type to a float64.
@@ -105,6 +110,9 @@ Defaults:
because it is an empty value.
- compact: Return a copy of a list with all of the empty values removed.
'list 0 1 2 "" | compact' will return '[1 2]'
- ternary: Given a value,'true | ternary "b" "c"' will return "b".
'false | ternary "b" "c"' will return '"c"'. Similar to the JavaScript ternary
operator.
OS:
- env: Resolve an environment variable
@@ -155,7 +163,7 @@ Data Structures:
be assigned the empty string. Non-string keys are converted to strings as
follows: []byte are converted, fmt.Stringers will have String() called.
errors will have Error() called. All others will be passed through
fmt.Sprtinf("%v").
fmt.Sprintf("%v").
Lists Functions:
@@ -184,7 +192,7 @@ These are used to manipulate dicts.
- hasKey: Takes a dict and a key, and returns boolean true if the key is in
the dict.
- pluck: Given a key and one or more maps, get all of the values for that key.
- keys: Get an array of all of the keys in a dict.
- keys: Get an array of all of the keys in one or more dicts.
- pick: Select just the given keys out of the dict, and return a new dict.
- omit: Return a dict without the given keys.

View File

@@ -98,6 +98,8 @@ var genericMap = map[string]interface{}{
"htmlDateInZone": htmlDateInZone,
"dateInZone": dateInZone,
"dateModify": dateModify,
"ago": dateAgo,
"toDate": toDate,
// Strings
"abbrev": abbrev,
@@ -137,8 +139,10 @@ var genericMap = map[string]interface{}{
"squote": squote,
"cat": cat,
"indent": indent,
"nindent": nindent,
"replace": replace,
"plural": plural,
"sha1sum": sha1sum,
"sha256sum": sha256sum,
"toString": strval,
@@ -199,6 +203,7 @@ var genericMap = map[string]interface{}{
"compact": compact,
"toJson": toJson,
"toPrettyJson": toPrettyJson,
"ternary": ternary,
// Reflection
"typeOf": typeOf,
@@ -246,11 +251,15 @@ var genericMap = map[string]interface{}{
"reverse": reverse,
"uniq": uniq,
"without": without,
"has": func(needle interface{}, haystack []interface{}) bool { return inList(haystack, needle) },
"has": has,
// Crypto:
"genPrivateKey": generatePrivateKey,
"derivePassword": derivePassword,
"genPrivateKey": generatePrivateKey,
"derivePassword": derivePassword,
"buildCustomCert": buildCustomCertificate,
"genCA": generateCertificateAuthority,
"genSelfSignedCert": generateSelfSignedCertificate,
"genSignedCert": generateSignedCertificate,
// UUIDs:
"uuidv4": uuidv4,
@@ -263,10 +272,10 @@ var genericMap = map[string]interface{}{
"fail": func(msg string) (string, error) { return "", errors.New(msg) },
// Regex
"regexMatch": regexMatch,
"regexFindAll": regexFindAll,
"regexFind": regexFind,
"regexReplaceAll": regexReplaceAll,
"regexMatch": regexMatch,
"regexFindAll": regexFindAll,
"regexFind": regexFind,
"regexReplaceAll": regexReplaceAll,
"regexReplaceAllLiteral": regexReplaceAllLiteral,
"regexSplit": regexSplit,
"regexSplit": regexSplit,
}

View File

@@ -1,33 +0,0 @@
hash: b9cc40bfd6dde74a94103b96700df1a9ab29a7fff5650216cf5a05f4fe72fb73
updated: 2017-05-02T16:01:04.617727646-06:00
imports:
- name: github.com/aokoli/goutils
version: 9c37978a95bd5c709a15883b6242714ea6709e64
- name: github.com/huandu/xstrings
version: 3959339b333561bf62a38b424fd41517c2c90f40
- name: github.com/imdario/mergo
version: 3e95a51e0639b4cf372f2ccf74c86749d747fbdc
- name: github.com/Masterminds/goutils
version: 45307ec16e3cd47cd841506c081f7afd8237d210
- name: github.com/Masterminds/semver
version: 59c29afe1a994eacb71c833025ca7acf874bb1da
- name: github.com/satori/go.uuid
version: 879c5887cd475cd7864858769793b2ceb0d44feb
- name: github.com/stretchr/testify
version: e3a8ff8ce36581f87a15341206f205b1da467059
subpackages:
- assert
- name: golang.org/x/crypto
version: d172538b2cfce0c13cee31e647d0367aa8cd2486
subpackages:
- pbkdf2
- scrypt
testImports:
- name: github.com/davecgh/go-spew
version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
subpackages:
- spew
- name: github.com/pmezard/go-difflib
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
subpackages:
- difflib

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