Compare commits

...

49 Commits

Author SHA1 Message Date
Andrey Nering
1920ee38c3 v3.21.0 2023-02-22 22:12:48 -03:00
João Pedro
ec2110e58f Add new TASK_VERSION special variable
Closes #1014
Closes #990
2023-02-22 22:08:38 -03:00
Andrey Nering
12a1cd6f62 Docs: Update API page 2023-02-16 21:16:43 -03:00
Aleksandr Komlev
9af056e746 Add FORCE_COLOR env support (#1003) 2023-02-16 21:12:44 -03:00
Pete Davison
c8fe450623 Merge pull request #1010 from go-task/go-1-20
chore: update to go 1.20
2023-02-13 13:33:51 +00:00
Pete Davison
ab1fe742f3 chore: update to go 1.20 2023-02-13 13:28:49 +00:00
Pete Davison
28c5f4a635 Merge pull request #1007 from go-task/fix-tasks-being-incorrectly-marked-as-internal
fix: tasks being incorrectly marked as internal
2023-02-10 18:14:01 +00:00
Pete Davison
74f69a21cd fix: tasks being incorrectly marked as internal 2023-02-10 17:02:11 +00:00
dependabot[bot]
e23dacd6d4 build(deps): bump github.com/joho/godotenv from 1.4.0 to 1.5.1 (#1002)
Bumps [github.com/joho/godotenv](https://github.com/joho/godotenv) from 1.4.0 to 1.5.1.
- [Release notes](https://github.com/joho/godotenv/releases)
- [Commits](https://github.com/joho/godotenv/compare/v1.4.0...v1.5.1)

---
updated-dependencies:
- dependency-name: github.com/joho/godotenv
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-06 09:41:37 -03:00
Andrey Nering
58d582941b Documentation: Remove mentions of Minify in favor of esbuild 2023-02-04 20:29:10 -03:00
dependabot[bot]
3bbc51949c build(deps): bump http-cache-semantics from 4.1.0 to 4.1.1 in /docs (#1000)
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-04 18:51:05 -03:00
Andrey Nering
69e0254a99 Website: Update 2023-01-29 15:42:59 -03:00
Andrey Nering
1091a914bd Documentation: version: '2' -> version: '3'
https://github.com/go-task/task/pull/929#discussion_r1082370557
2023-01-29 15:39:45 -03:00
dependabot[bot]
ecc65a218e build(deps): bump github.com/fatih/color from 1.13.0 to 1.14.1 (#995)
Bumps [github.com/fatih/color](https://github.com/fatih/color) from 1.13.0 to 1.14.1.
- [Release notes](https://github.com/fatih/color/releases)
- [Commits](https://github.com/fatih/color/compare/v1.13.0...v1.14.1)

---
updated-dependencies:
- dependency-name: github.com/fatih/color
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-28 08:07:12 -03:00
dependabot[bot]
426ed7eff6 build(deps): bump ua-parser-js from 0.7.31 to 0.7.33 in /docs (#991)
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.31 to 0.7.33.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.31...0.7.33)

---
updated-dependencies:
- dependency-name: ua-parser-js
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-27 09:55:18 -03:00
Andrey Nering
73aba36309 v3.20.0 2023-01-14 17:34:15 -03:00
Andrey Nering
cb393ccd3a Add CHANGELOG entry + small adjustments to #977 2023-01-14 17:18:26 -03:00
Amin Yahyaabadi
347fcf9f67 fix: avoid reruns when the timestamp method is used (#977) 2023-01-14 17:17:36 -03:00
Andrey Nering
fce7575b03 Add README entry for #982 2023-01-14 16:48:04 -03:00
Pete Davison
2da7ddc399 chore: optimize task filtering (#982) 2023-01-14 16:45:52 -03:00
Pete Davison
1c1be683ab feat: set and shopt directives (#929)
Co-authored-by: Andrey Nering <andrey@nering.com.br>
2023-01-14 16:41:56 -03:00
Andrey Nering
4be1050234 Optimize the Taskfile a bit
`go list ./...` takes quite a few seconds to run. Let's restrict it to the
tasks that actually use it.
2023-01-06 21:41:18 -03:00
Andrey Nering
2efb3533ec Add CHANGELOG + improvements to #980
Closes #978
2023-01-06 21:39:57 -03:00
Lea Anthony
aa6c7e4b94 Add support for 'platforms' in both task and command (#980) 2023-01-06 21:38:35 -03:00
Andrey Nering
63c50d13ee Website/README: Add link to the Mastodon account 2023-01-01 21:24:16 -03:00
Andrey Nering
c1e127e42f Website: Update outdated URL 2022-12-31 14:28:37 -03:00
dependabot[bot]
9e38e8a4db build(deps): bump json5 from 2.2.1 to 2.2.2 in /docs (#972)
Bumps [json5](https://github.com/json5/json5) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.2.1...v2.2.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-31 14:15:20 -03:00
Andrey Nering
b4c95d6b0b v3.19.1 2022-12-31 14:03:55 -03:00
Pete Davison
c4766e2611 fix: add missing nil check (#971)
Co-authored-by: Andrey Nering <andrey@nering.com.br>
2022-12-31 13:54:26 -03:00
Pete Davison
796097e3ab fix: watch interval (#970) 2022-12-31 13:48:49 -03:00
Pete Davison
c7d9efebf9 Merge pull request #967 from go-task/gha-issue-closed
feat: add action for when an issue is closed
2022-12-23 16:34:40 -06:00
Pete Davison
8f4306d321 feat: add action for when an issue is closed 2022-12-23 22:31:02 +00:00
Pete Davison
435f086cb7 Merge pull request #965 from go-task/fix-interval-schema
fix: incorrect schema type for interval
2022-12-22 18:36:15 -06:00
Pete Davison
01c9158120 fix: incorrect schema type for interval
- Fixes #962
2022-12-23 00:34:08 +00:00
Andrey Nering
e235d77d64 Add CHANGELOG to #964 2022-12-22 21:27:19 -03:00
Henrique Corrêa
dbe8131b75 Close Taskfile after reading it (#964)
This should fix issues preventing modifications to the Taskfile while tasks are still running, like switching git branches for example.

See #963.
2022-12-22 21:23:17 -03:00
Pete Davison
0a9d76515e Merge pull request #957 from go-task/faq
FAQ doc
2022-12-19 19:58:06 +00:00
Pete Davison
0ce1af9ee0 WIP: FAQ doc 2022-12-19 19:51:22 +00:00
Andrey Nering
c4452d2698 Website > Installation: Document official Homebrew go-task package 2022-12-18 22:41:42 -03:00
Pete Davison
491888f6c0 feat: improve unmarshal error handling and use v3 yaml interface everywhere (#959) 2022-12-18 22:11:31 -03:00
Pete Davison
e4158dc5e4 feat: add local-prefixes flag to goimports linter (#958) 2022-12-18 22:06:09 -03:00
Andrey Nering
0307ca8ac6 Website: Upgrade Docusaurus (#956) 2022-12-17 11:43:43 -03:00
Andrey Nering
156a273351 go.mod: Pin released v3.6.0 version of mvdan/sh 2022-12-17 11:18:23 -03:00
Andrey Nering
d6d51a2f8b Prevent TestFileWatcherInterval from running on CI
This test can fail intermittently. It's fine to run it only locally.

We were already doing this for TestSignalSentToProcessGroup.
2022-12-17 11:08:41 -03:00
Andrey Nering
a98b41d657 Add goimports as a linter 2022-12-17 11:02:17 -03:00
Andrey Nering
87ec78fbaa Cleanup: Remove duplicated internal/sleepit/main.go file 2022-12-17 11:01:54 -03:00
Andrey Nering
957bff4b89 CHANGELOG + small improvements to #936 2022-12-17 10:35:30 -03:00
David Alpert
321f7b59d8 Add --json flag to be used by editor extensions (#936) 2022-12-17 10:31:00 -03:00
Andrey Nering
41a9316523 Website: Update "Donate" page + FUNDING.yml 2022-12-07 21:33:49 -03:00
72 changed files with 3555 additions and 1474 deletions

4
.github/FUNDING.yml vendored
View File

@@ -1,3 +1,3 @@
github: andreynering
github: [andreynering, pd93]
open_collective: task
custom: 'https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=GSVDU63RKG45A&currency_code=USD&source=url'
custom: https://taskfile.dev/donate/

View File

@@ -10,6 +10,7 @@ jobs:
steps:
- uses: actions/github-script@v6
with:
github-token: ${{secrets.GH_PAT}}
script: |
const issue = await github.rest.issues.get({
owner: context.repo.owner,

29
.github/workflows/issue-closed.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: issue closed
on:
issues:
types: [closed]
jobs:
issue-closed:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
with:
github-token: ${{secrets.GH_PAT}}
script: |
const labels = await github.paginate(
github.rest.issues.listLabelsOnIssue, {
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
}
)
if (labels.find(label => label.name === 'needs triage')) {
github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'needs triage'
})
}

View File

@@ -5,11 +5,12 @@ on:
types: [opened]
jobs:
needs-triage:
issue-needs-triage:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
with:
github-token: ${{secrets.GH_PAT}}
script: |
const labels = await github.paginate(
github.rest.issues.listLabelsOnIssue, {

View File

@@ -14,11 +14,11 @@ jobs:
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.18.x
go-version: 1.20.x
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.46.1
version: v1.51.1

View File

@@ -15,7 +15,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19.x
go-version: 1.20.x
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2

View File

@@ -13,7 +13,7 @@ jobs:
name: Test
strategy:
matrix:
go-version: [1.18.x, 1.19.x]
go-version: [1.19.x, 1.20.x]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{matrix.platform}}
steps:

12
.golangci.yml Normal file
View File

@@ -0,0 +1,12 @@
# NOTE(@andreynering): The linters listed here are additions on top of
# those enabled by default:
#
# https://golangci-lint.run/usage/linters/#enabled-by-default
linters:
enable:
- goimports
linters-settings:
goimports:
local-prefixes: github.com/go-task/task

View File

@@ -6,15 +6,15 @@ build:
- darwin
- linux
goarch:
- 386
- '386'
- amd64
- arm
- arm64
goarm:
- 6
- '6'
ignore:
- goos: darwin
goarch: 386
goarch: '386'
env:
- CGO_ENABLED=0
mod_timestamp: '{{ .CommitTimestamp }}'

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
18.12.1

View File

@@ -1,5 +1,42 @@
# Changelog
## v3.21.0 - 2023-02-22
- Added new `TASK_VERSION` special variable
([#990](https://github.com/go-task/task/issues/990), [#1014](https://github.com/go-task/task/pull/1014) by @ja1code).
- Fixed a bug where tasks were sometimes incorrectly marked as internal ([#1007](https://github.com/go-task/task/pull/1007) by @pd93).
- Update to Go 1.20 (bump minimum version to 1.19) ([#1010](https://github.com/go-task/task/pull/1010) by @pd93)
- Added environment variable `FORCE_COLOR` support to force color output. Usefull for environments without TTY ([#1003](https://github.com/go-task/task/pull/1003) by @automation-stack)
## v3.20.0 - 2023-01-14
- Improve behavior and performance of status checking when using the
`timestamp` mode
([#976](https://github.com/go-task/task/issues/976), [#977](https://github.com/go-task/task/pull/977) by @aminya).
- Performance optimizations were made for large Taskfiles
([#982](https://github.com/go-task/task/pull/982) by @pd93).
- Add ability to configure options for the [`set`](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html)
and [`shopt`](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html) builtins
([#908](https://github.com/go-task/task/issues/908), [#929](https://github.com/go-task/task/pull/929) by @pd93, [Documentation](http://taskfile.dev/usage/#set-and-shopt)).
- Add new `platforms:` attribute to `task` and `cmd`, so it's now possible to
choose in which platforms that given task or command will be run on. Possible
values are operating system (GOOS), architecture (GOARCH) or a combination of
the two. Example: `platforms: [linux]`, `platforms: [amd64]` or
`platforms: [linux/amd64]`. Other platforms will be skipped
([#978](https://github.com/go-task/task/issues/978), [#980](https://github.com/go-task/task/pull/980) by @leaanthony).
## v3.19.1 - 2022-12-31
- Small bug fix: closing `Taskfile.yml` once we're done reading it
([#963](https://github.com/go-task/task/issues/963), [#964](https://github.com/go-task/task/pull/964) by @HeCorr).
- Fixes a bug in v2 that caused a panic when using a `Taskfile_{{OS}}.yml` file
([#961](https://github.com/go-task/task/issues/961), [#971](https://github.com/go-task/task/pull/971) by @pd93).
- Fixed a bug where watch intervals set in the Taskfile were not being respected ([#969](https://github.com/go-task/task/pull/969), [#970](https://github.com/go-task/task/pull/970) by @pd93)
- Add `--json` flag (alias `-j`) with the intent to improve support for code
editors and add room to other possible integrations. This is basic for now,
but we plan to add more info in the near future
([#936](https://github.com/go-task/task/pull/936) by @davidalpert, [#764](https://github.com/go-task/task/issues/764)).
## v3.19.0 - 2022-12-05
- Installation via npm now supports [pnpm](https://pnpm.io/) as well

View File

@@ -10,7 +10,7 @@
</p>
<p>
<a href="https://taskfile.dev/installation/">Installation</a> | <a href="https://taskfile.dev/usage/">Documentation</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a>
<a href="https://taskfile.dev/installation/">Installation</a> | <a href="https://taskfile.dev/usage/">Documentation</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://fosstodon.org/@task">Mastodon</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a>
</p>
</div>

View File

@@ -6,13 +6,6 @@ includes:
taskfile: ./docs
dir: ./docs
vars:
GIT_COMMIT:
sh: git log -n 1 --format=%h
GO_PACKAGES:
sh: go list ./...
env:
CGO_ENABLED: '0'
@@ -28,7 +21,11 @@ tasks:
sources:
- './**/*.go'
cmds:
- go install -v -ldflags="-w -s -X main.version={{.GIT_COMMIT}}" ./cmd/task
- go install -v -ldflags="-w -s -X '{{.VERSION_VAR}}={{.GIT_COMMIT}}'" ./cmd/task
vars:
VERSION_VAR: github.com/go-task/task/v3/internal/version.version
GIT_COMMIT:
sh: git log -n 1 --format=%h
mod:
desc: Downloads and tidy Go modules
@@ -47,6 +44,7 @@ tasks:
aliases: [l]
sources:
- './**/*.go'
- .golangci.yml
cmds:
- golangci-lint run
@@ -72,12 +70,18 @@ tasks:
deps: [install]
cmds:
- go test {{catLines .GO_PACKAGES}}
vars:
GO_PACKAGES:
sh: go list ./...
test:signals:
desc: Runs test suite with signals tests included
test:all:
desc: Runs test suite with signals and watch tests included
deps: [install, sleepit:build]
cmds:
- go test {{catLines .GO_PACKAGES}} -tags signals
- go test {{catLines .GO_PACKAGES}} -tags 'signals watch'
vars:
GO_PACKAGES:
sh: go list ./...
test-release:
desc: Tests release process without publishing
@@ -92,7 +96,7 @@ tasks:
- rm {{.FILE}}
- 'echo "---" >> {{.FILE}}'
- 'echo "slug: /changelog/" >> {{.FILE}}'
- 'echo "sidebar_position: 6" >> {{.FILE}}'
- 'echo "sidebar_position: 7" >> {{.FILE}}'
- 'echo "---" >> {{.FILE}}'
- 'echo "" >> {{.FILE}}'
- 'cat CHANGELOG.md >> {{.FILE}}'
@@ -105,4 +109,11 @@ tasks:
packages:
cmds:
- echo '{{.GO_PACKAGES}}'
vars:
GO_PACKAGES:
sh: go list ./...
silent: true
foo:
cmds:
- echo "{{.TASK_VERSION}}"

View File

@@ -130,11 +130,11 @@ func supervisor(
// The goroutine will prepend its prints with the prefix `name`.
// The goroutine will simulate some work and will terminate when one of the following
// conditions happens:
// 1. When `howlong` is elapsed. This case will be signaled on the `workerDone` channel.
// 2. When something happens on channel `canceled`. Note that this simulates real-life,
// so cancellation is not instantaneous: if the caller wants a synchronous cancel,
// it should send a message; if instead it wants an asynchronous cancel, it should
// close the channel.
// 1. When `howlong` is elapsed. This case will be signaled on the `workerDone` channel.
// 2. When something happens on channel `canceled`. Note that this simulates real-life,
// so cancellation is not instantaneous: if the caller wants a synchronous cancel,
// it should send a message; if instead it wants an asynchronous cancel, it should
// close the channel.
func worker(
canceled <-chan struct{},
howlong time.Duration,

View File

@@ -6,8 +6,8 @@ import (
"log"
"os"
"path/filepath"
"runtime/debug"
"strings"
"time"
"github.com/spf13/pflag"
"mvdan.cc/sh/v3/syntax"
@@ -15,13 +15,10 @@ import (
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/args"
"github.com/go-task/task/v3/internal/logger"
ver "github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile"
)
var (
version = ""
)
const usage = `Usage: task [-ilfwvsd] [--init] [--list] [--force] [--watch] [--verbose] [--silent] [--dir] [--taskfile] [--dry] [--summary] [task...]
Runs the specified task(s). Falls back to the "default" task if no task name
@@ -59,6 +56,7 @@ func main() {
init bool
list bool
listAll bool
listJson bool
status bool
force bool
watch bool
@@ -73,7 +71,7 @@ func main() {
entrypoint string
output taskfile.Output
color bool
interval string
interval time.Duration
)
pflag.BoolVar(&versionFlag, "version", false, "show Task version")
@@ -81,6 +79,7 @@ func main() {
pflag.BoolVarP(&init, "init", "i", false, "creates a new Taskfile.yaml in the current folder")
pflag.BoolVarP(&list, "list", "l", false, "lists tasks with description of current Taskfile")
pflag.BoolVarP(&listAll, "list-all", "a", false, "lists tasks with or without a description")
pflag.BoolVarP(&listJson, "json", "j", false, "formats task list as json")
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")
@@ -97,11 +96,11 @@ func main() {
pflag.StringVar(&output.Group.End, "output-group-end", "", "message template to print after a task's grouped output")
pflag.BoolVarP(&color, "color", "c", true, "colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable")
pflag.IntVarP(&concurrency, "concurrency", "C", 0, "limit number tasks to run concurrently")
pflag.StringVarP(&interval, "interval", "I", "5s", "interval to watch for changes")
pflag.DurationVarP(&interval, "interval", "I", 0, "interval to watch for changes")
pflag.Parse()
if versionFlag {
fmt.Printf("Task version: %s\n", getVersion())
fmt.Printf("Task version: %s\n", ver.GetVersion())
return
}
@@ -162,7 +161,12 @@ func main() {
OutputStyle: output,
}
if (list || listAll) && silent {
var listOptions = task.NewListOptions(list, listAll, listJson)
if err := listOptions.Validate(); err != nil {
log.Fatal(err)
}
if (listOptions.ShouldListTasks()) && silent {
e.ListTaskNames(listAll)
return
}
@@ -176,16 +180,9 @@ func main() {
return
}
if list {
if ok := e.ListTasks(task.FilterOutInternal(), task.FilterOutNoDesc()); !ok {
e.Logger.Outf(logger.Yellow, "task: No tasks with description available. Try --list-all to list all tasks")
}
return
}
if listAll {
if ok := e.ListTasks(task.FilterOutInternal()); !ok {
e.Logger.Outf(logger.Yellow, "task: No tasks available")
if listOptions.ShouldListTasks() {
if foundTasks, err := e.ListTasks(listOptions); !foundTasks || err != nil {
os.Exit(1)
}
return
}
@@ -254,21 +251,3 @@ func getArgs() ([]string, string, error) {
}
return args[:doubleDashPos], strings.Join(quotedCliArgs, " "), nil
}
func getVersion() string {
if version != "" {
return version
}
info, ok := debug.ReadBuildInfo()
if !ok || info.Main.Version == "" {
return "unknown"
}
version = info.Main.Version
if info.Main.Sum != "" {
version += fmt.Sprintf(" (%s)", info.Main.Sum)
}
return version
}

View File

@@ -35,3 +35,8 @@ tasks:
summary: Requires GIT_USER and GIT_PASS envs to be previous set
cmds:
- npx docusaurus deploy
upgrade:
desc: Upgrade Docusaurus
cmds:
- yarn upgrade @docusaurus/core@latest @docusaurus/preset-classic@latest

View File

@@ -1,11 +1,13 @@
const GITHUB_URL = 'https://github.com/go-task/task';
const TWITTER_URL = 'https://twitter.com/taskfiledev';
const MASTODON_URL = 'https://fosstodon.org/@task';
const DISCORD_URL = 'https://discord.gg/6TY36E39UK';
const CHINESE_URL = 'https://task-zh.readthedocs.io/zh_CN/latest/';
module.exports = {
GITHUB_URL,
TWITTER_URL,
CHINESE_URL,
DISCORD_URL,
CHINESE_URL
GITHUB_URL,
MASTODON_URL,
TWITTER_URL
};

View File

@@ -58,6 +58,7 @@ There are some special variables that is available on the templating system:
| `USER_WORKING_DIR` | The absolute path of the directory `task` was called from. |
| `CHECKSUM` | The checksum of the files listed in `sources`. Only available within the `status` prop and if method is set to `checksum`. |
| `TIMESTAMP` | The date object of the greatest timestamp of the files listes in `sources`. Only available within the `status` prop and if method is set to `timestamp`. |
| `TASK_VERSION` | The current version of task. |
## ENV
@@ -73,6 +74,7 @@ Some environment variables can be overriden to adjust Task behavior.
| `TASK_COLOR_YELLOW` | `33` | Color used for yellow. |
| `TASK_COLOR_MAGENTA` | `35` | Color used for magenta. |
| `TASK_COLOR_RED` | `31` | Color used for red. |
| `FORCE_COLOR` | | Force color output usage. |
## Schema
@@ -91,6 +93,8 @@ Some environment variables can be overriden to adjust Task behavior.
| `dotenv` | `[]string` | | A list of `.env` file paths to be parsed. |
| `run` | `string` | `always` | Default 'run' option for this Taskfile. Available options: `always`, `once` and `when_changed`. |
| `interval` | `string` | `5s` | Sets a different watch interval when using `--watch`, the default being 5 seconds. This string should be a valid [Go Duration](https://pkg.go.dev/time#ParseDuration). |
| `set` | `[]string` | | Specify options for the [`set` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html). |
| `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html). |
### Include
@@ -139,6 +143,9 @@ includes:
| `prefix` | `string` | | Defines a string to prefix the output of tasks running in parallel. Only used when the output mode is `prefixed`. |
| `ignore_error` | `bool` | `false` | Continue execution if errors happen while executing commands. |
| `run` | `string` | The one declared globally in the Taskfile or `always` | Specifies whether the task should run again or not if called more than once. Available options: `always`, `once` and `when_changed`. |
| `platforms` | `[]string` | All platforms | Specifies which platforms the task should be run on. [Valid GOOS and GOARCH values allowed](https://github.com/golang/go/blob/master/src/go/build/syslist.go). Task will be skipped otherwise. |
| `set` | `[]string` | | Specify options for the [`set` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html). |
| `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html). |
:::info
@@ -189,6 +196,9 @@ tasks:
| `vars` | [`map[string]Variable`](#variable) | | Optional additional variables to be passed to the referenced task. Only relevant when setting `task` instead of `cmd`. |
| `ignore_error` | `bool` | `false` | Continue execution if errors happen while executing the command. |
| `defer` | `string` | | Alternative to `cmd`, but schedules the command to be executed at the end of this task instead of immediately. This cannot be used together with `cmd`. |
| `platforms` | `[]string` | All platforms | Specifies which platforms the command should be run on. [Valid GOOS and GOARCH values allowed](https://github.com/golang/go/blob/master/src/go/build/syslist.go). Command will be skipped otherwise. |
| `set` | `[]string` | | Specify options for the [`set` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html). |
| `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html). |
:::info

View File

@@ -1,10 +1,47 @@
---
slug: /changelog/
sidebar_position: 6
sidebar_position: 7
---
# Changelog
## v3.21.0 - 2023-02-22
- Added new `TASK_VERSION` special variable
([#990](https://github.com/go-task/task/issues/990), [#1014](https://github.com/go-task/task/pull/1014) by @ja1code).
- Fixed a bug where tasks were sometimes incorrectly marked as internal ([#1007](https://github.com/go-task/task/pull/1007) by @pd93).
- Update to Go 1.20 (bump minimum version to 1.19) ([#1010](https://github.com/go-task/task/pull/1010) by @pd93)
- Added environment variable `FORCE_COLOR` support to force color output. Usefull for environments without TTY ([#1003](https://github.com/go-task/task/pull/1003) by @automation-stack)
## v3.20.0 - 2023-01-14
- Improve behavior and performance of status checking when using the
`timestamp` mode
([#976](https://github.com/go-task/task/issues/976), [#977](https://github.com/go-task/task/pull/977) by @aminya).
- Performance optimizations were made for large Taskfiles
([#982](https://github.com/go-task/task/pull/982) by @pd93).
- Add ability to configure options for the [`set`](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html)
and [`shopt`](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html) builtins
([#908](https://github.com/go-task/task/issues/908), [#929](https://github.com/go-task/task/pull/929) by @pd93, [Documentation](http://taskfile.dev/usage/#set-and-shopt)).
- Add new `platforms:` attribute to `task` and `cmd`, so it's now possible to
choose in which platforms that given task or command will be run on. Possible
values are operating system (GOOS), architecture (GOARCH) or a combination of
the two. Example: `platforms: [linux]`, `platforms: [amd64]` or
`platforms: [linux/amd64]`. Other platforms will be skipped
([#978](https://github.com/go-task/task/issues/978), [#980](https://github.com/go-task/task/pull/980) by @leaanthony).
## v3.19.1 - 2022-12-31
- Small bug fix: closing `Taskfile.yml` once we're done reading it
([#963](https://github.com/go-task/task/issues/963), [#964](https://github.com/go-task/task/pull/964) by @HeCorr).
- Fixes a bug in v2 that caused a panic when using a `Taskfile_{{OS}}.yml` file
([#961](https://github.com/go-task/task/issues/961), [#971](https://github.com/go-task/task/pull/971) by @pd93).
- Fixed a bug where watch intervals set in the Taskfile were not being respected ([#969](https://github.com/go-task/task/pull/969), [#970](https://github.com/go-task/task/pull/970) by @pd93)
- Add `--json` flag (alias `-j`) with the intent to improve support for code
editors and add room to other possible integrations. This is basic for now,
but we plan to add more info in the near future
([#936](https://github.com/go-task/task/pull/936) by @davidalpert, [#764](https://github.com/go-task/task/issues/764)).
## v3.19.0 - 2022-12-05
- Installation via npm now supports [pnpm](https://pnpm.io/) as well

View File

@@ -1,6 +1,6 @@
---
slug: /community/
sidebar_position: 6
sidebar_position: 8
---
# Community

View File

@@ -1,6 +1,6 @@
---
slug: /contributing/
sidebar_position: 7
sidebar_position: 9
---
# Contributing

View File

@@ -1,6 +1,6 @@
---
slug: /donate/
sidebar_position: 10
sidebar_position: 12
---
# Donate
@@ -11,10 +11,23 @@ channels listed below.
This is just a way of saying "thank you", it won't give you any benefits like
higher priority on issues or something similar.
Companies who donate at least $50/month will be featured as a "Gold Sponsor"
in the website homepage and on the GitHub repository README. Make contact with
[@andreynering] with the logo you want to be shown.
Suspect businesses (gambling, casinos, etc) won't be allowed, though.
## GitHub Sponsors
The preferred way to donate to the maintainers is via GitHub Sponsors.
Just use the following links to do your donation:
- [@andreynering](https://github.com/sponsors/andreynering)
- [@pd93](https://github.com/sponsors/pd93)
## Open Collective
Task is on [Open Collective](https://opencollective.com/task) and you have
these options to donate:
If you prefer [Open Collective](https://opencollective.com/task) you can donate
by using these links:
- [$2 per month](https://opencollective.com/task/contribute/backer-4034/checkout)
- [$5 per month](https://opencollective.com/task/contribute/supporter-8404/checkout)
@@ -22,15 +35,15 @@ these options to donate:
- [$50 per month](https://opencollective.com/task/contribute/sponsor-28775/checkout)
- [Custom value - One-time donation option supported](https://opencollective.com/task/donate)
## GitHub Sponsors
- [@andreynering](https://github.com/sponsors/andreynering)
## PayPal
You can donate to [@andreynering] via PayPal as well:
- [Any value - One-time donation](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=GSVDU63RKG45A&currency_code=USD&source=url)
## PIX (Brazil only)
If you're Brazilian, you can donate any value by
And if you're Brazilian, you can also donate to [@andreynering] via PIX by
[using this QR Code](/img/pix.png).
[@andreynering]: https://github.com/andreynering

54
docs/docs/faq.md Normal file
View File

@@ -0,0 +1,54 @@
---
slug: /faq/
sidebar_position: 5
---
# FAQ
This page contains a list of frequently asked questions about Task.
- [Why won't my task update my shell environment?](#why-wont-my-task-update-my-shell-environment)
- ['x' builtin command doesn't work on Windows](#x-builtin-command-doesnt-work-on-windows)
## Why won't my task update my shell environment?
This is a limitation of how shells work. Task runs as a subprocess of your
current shell, so it can't change the environment of the shell that started it.
This limitation is shared by other task runners and build tools too.
A common way to work around this is to create a task that will generate output
that can be parsed by your shell. For example, to set an environment variable on
your shell you can write a task like this:
```yaml
my-shell-env:
cmds:
- echo "export FOO=foo"
- echo "export BAR=bar"
```
Now run `eval $(task my-shell-env)` and the variables `$FOO` and `$BAR` will be
available in your shell.
## 'x' builtin command doesn't work on Windows
The default shell on Windows (`cmd` and `powershell`) do not have commands like
`rm` and `cp` available as builtins. This means that these commands won't work.
If you want to make your Taskfile fully cross-platform, you'll need to work
around this limitation using one of the following methods:
- Use the `{{OS}}` function to run an OS-specific script.
- Use something like `{{if eq OS "windows"}}powershell {{end}}<my_cmd>` to
detect windows and run the command in Powershell directly.
- Use a shell on Windows that supports these commands as builtins, such as [Git
Bash] or [WSL].
We want to make improvements to this part of Task and the issues below track
this work. Constructive comments and contributions are very welcome!
- [#197](https://github.com/go-task/task/issues/197)
- [mvdan/sh#93](https://github.com/mvdan/sh/issues/93)
- [mvdan/sh#97](https://github.com/mvdan/sh/issues/97)
[Git Bash]: https://gitforwindows.org/
[WSL]: https://learn.microsoft.com/en-us/windows/wsl/install

View File

@@ -18,6 +18,15 @@ Task is as simple as running:
brew install go-task/tap/go-task
```
The above Formula is [maintained by ourselves](https://github.com/go-task/homebrew-tap/blob/master/Formula/go-task.rb).
Recently, Task was also made available [on the official Homebrew repository](https://formulae.brew.sh/formula/go-task),
so you also have that option if you prefer:
```bash
brew install go-task
```
### Snap
Task is available in [Snapcraft][snapcraft], but keep in mind that your

View File

@@ -1,6 +1,6 @@
---
slug: /releasing/
sidebar_position: 8
sidebar_position: 10
---
# Releasing
@@ -41,7 +41,7 @@ the [Snapcraft dashboard][snapcraftdashboard].
Scoop is a command-line package manager for the Windows operating system.
Scoop package manifests are maintained by the community.
Scoop owners usually take care of updating versions there by editing [this file](https://github.com/lukesampson/scoop-extras/blob/master/bucket/task.json).
Scoop owners usually take care of updating versions there by editing [this file](https://github.com/ScoopInstaller/Main/blob/master/bucket/task.json).
If you think its Task version is outdated, open an issue to let us know.
# Nix

View File

@@ -1,6 +1,6 @@
---
slug: /styleguide/
sidebar_position: 5
sidebar_position: 6
---
# Styleguide

View File

@@ -1,6 +1,6 @@
---
slug: /taskfile-versions/
sidebar_position: 9
sidebar_position: 11
---
# Taskfile Versions

View File

@@ -9,7 +9,7 @@ sidebar_position: 3
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
The example below allows compiling a Go app and uses [esbuild](https://esbuild.github.io/) to concat
and minify multiple CSS files into a single one.
```yaml
@@ -22,7 +22,7 @@ tasks:
assets:
cmds:
- minify -o public/style.css src/css
- esbuild --bundle --minify css/index.css > public/bundle.css
```
Running the tasks is as simple as running:
@@ -384,7 +384,7 @@ tasks:
assets:
cmds:
- minify -o public/style.css src/css
- esbuild --bundle --minify css/index.css > public/bundle.css
```
In the above example, `assets` will always run right before `build` if you run
@@ -401,11 +401,11 @@ tasks:
js:
cmds:
- minify -o public/script.js src/js
- esbuild --bundle --minify js/index.js > public/bundle.js
css:
cmds:
- minify -o public/style.css src/css
- esbuild --bundle --minify css/index.css > public/bundle.css
```
If there is more than one dependency, they always run in parallel for better
@@ -439,6 +439,78 @@ tasks:
- echo {{.TEXT}}
```
## Platform specific tasks and commands
If you want to restrict the running of tasks to explicit platforms, this can be achieved
using the `platforms:` key. Tasks can be restricted to a specific OS, architecture or a
combination of both.
On a mismatch, the task or command will be skipped, and no error will be thrown.
The values allowed as OS or Arch are valid `GOOS` and `GOARCH` values, as
defined by the Go language
[here](https://github.com/golang/go/blob/master/src/go/build/syslist.go).
The `build-windows` task below will run only on Windows, and on any architecture:
```yaml
version: '3'
tasks:
build-windows:
platforms: [windows]
cmds:
- echo 'Running command on Windows'
```
This can be restricted to a specific architecture as follows:
```yaml
version: '3'
tasks:
build-windows-amd64:
platforms: [windows/amd64]
cmds:
- echo 'Running command on Windows (amd64)'
```
It is also possible to restrict the task to specific architectures:
```yaml
version: '3'
tasks:
build-amd64:
platforms: [amd64]
cmds:
- echo 'Running command on amd64'
```
Multiple platforms can be specified as follows:
```yaml
version: '3'
tasks:
build:
platforms: [windows/amd64, darwin]
cmds:
- echo 'Running command on Windows (amd64) and macOS'
```
Individual commands can also be restricted to specific platforms:
```yaml
version: '3'
tasks:
build:
cmds:
- cmd: echo 'Running command on Windows (amd64) and macOS'
platforms: [windows/amd64, darwin]
- cmd: echo 'Running on all platforms'
```
## Calling another task
When a task has many dependencies, they are executed concurrently. This will
@@ -511,19 +583,19 @@ tasks:
js:
cmds:
- minify -o public/script.js src/js
- esbuild --bundle --minify js/index.js > public/bundle.js
sources:
- src/js/**/*.js
generates:
- public/script.js
- public/bundle.js
css:
cmds:
- minify -o public/style.css src/css
- esbuild --bundle --minify css/index.css > public/bundle.css
sources:
- src/css/**/*.css
generates:
- public/style.css
- public/bundle.css
```
`sources` and `generates` can be files or file patterns. When given,
@@ -595,9 +667,9 @@ The method `none` skips any validation and always run the task.
:::info
For the `checksum` (default) method to work, it is only necessary to
inform the source files, but if you want to use the `timestamp` method, you
also need to inform the generated files with `generates`.
For the `checksum` (default) or `timestamp` method to work, it is only necessary to
inform the source files.
When the `timestamp` method is used, the last time of the running the task is considered as a generate.
:::
@@ -987,11 +1059,11 @@ tasks:
js:
cmds:
- minify -o public/script.js src/js
- esbuild --bundle --minify js/index.js > public/bundle.js
css:
cmds:
- minify -o public/style.css src/css
- esbuild --bundle --minify css/index.css > public/bundle.css
```
would print the following output:
@@ -1348,6 +1420,31 @@ tasks:
- ./app{{exeExt}} -h localhost -p 8080
```
## `set` and `shopt`
It's possible to specify options to the
[`set`](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html)
and [`shopt`](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html)
builtins. This can be added at global, task or command level.
```yaml
version: '3'
set: [pipefail]
shopt: [globstar]
tasks:
# `globstar` required for double star globs to work
default: echo **/*.go
```
:::info
Keep in mind that not all options are available in the
[shell interpreter library](https://github.com/mvdan/sh) that Task uses.
:::
## Watch tasks
With the flags `--watch` or `-w` task will watch for file changes
@@ -1359,4 +1456,3 @@ either setting `interval: '500ms'` in the root of the Taskfile passing it
as an argument like `--interval=500ms`.
[gotemplate]: https://golang.org/pkg/text/template/
[minify]: https://github.com/tdewolff/minify/tree/master/cmd/minify

View File

@@ -2,10 +2,11 @@
// Note: type annotations allow type checking and IDEs autocompletion
const {
GITHUB_URL,
TWITTER_URL,
CHINESE_URL,
DISCORD_URL,
CHINESE_URL
GITHUB_URL,
MASTODON_URL,
TWITTER_URL
} = require('./constants');
const lightCodeTheme = require('./src/themes/prismLight');
const darkCodeTheme = require('./src/themes/prismDark');
@@ -108,6 +109,11 @@ const config = {
label: 'Twitter',
position: 'right'
},
{
href: MASTODON_URL,
label: 'Mastodon',
position: 'right'
},
{
href: DISCORD_URL,
label: 'Discord',
@@ -146,6 +152,10 @@ const config = {
label: 'Twitter',
href: TWITTER_URL
},
{
label: 'Mastodon',
href: MASTODON_URL
},
{
label: 'Discord',
href: DISCORD_URL

View File

@@ -14,8 +14,8 @@
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "2.0.0-beta.20",
"@docusaurus/preset-classic": "2.0.0-beta.20",
"@docusaurus/core": "^2.2.0",
"@docusaurus/preset-classic": "^2.2.0",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.1.1",
"prism-react-renderer": "^1.3.1",

View File

@@ -108,6 +108,20 @@
"description": "The directory in which this task should run. Defaults to the current working directory.",
"type": "string"
},
"set": {
"description": "Enables POSIX shell options for all of a task's commands. See https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/3/set"
}
},
"shopt": {
"description": "Enables Bash shell options for all of a task's commands. See https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/3/shopt"
}
},
"vars": {
"description": "A set of variables that can be used in the task.",
"$ref": "#/definitions/3/vars"
@@ -155,6 +169,13 @@
"run": {
"description": "Specifies whether the task should run again or not if called more than once. Available options: `always`, `once` and `when_changed`.",
"$ref": "#/definitions/3/run"
},
"platforms": {
"description": "Specifies which platforms the task should be run on.",
"type": "array",
"items": {
"type": "string"
}
}
}
},
@@ -177,6 +198,14 @@
}
]
},
"set": {
"type": "string",
"enum": ["allexport", "a", "errexit", "e", "noexec", "n", "noglob", "f", "nounset", "u", "xtrace", "x", "pipefail"]
},
"shopt": {
"type": "string",
"enum": ["expand_aliases", "globstar", "nullglob"]
},
"vars": {
"type": "object",
"patternProperties": {
@@ -226,6 +255,20 @@
"description": "Silent mode disables echoing of command before Task runs it",
"type": "boolean"
},
"set": {
"description": "Enables POSIX shell options for this command. See https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/3/set"
}
},
"shopt": {
"description": "Enables Bash shell options for this command. See https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/3/shopt"
}
},
"ignore_error": {
"description": "Prevent command from aborting the execution of task even after receiving a status code of 1",
"type": "boolean"
@@ -233,6 +276,13 @@
"defer": {
"description": "",
"type": "boolean"
},
"platforms": {
"description": "Specifies which platforms the command should be run on.",
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false,
@@ -357,6 +407,20 @@
"description": "Default 'silent' options for this Taskfile. If `false`, can be overidden with `true` in a task by task basis.",
"type": "boolean"
},
"set": {
"description": "Enables POSIX shell options for all commands in the Taskfile. See https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/3/set"
}
},
"shopt": {
"description": "Enables Bash shell options for all commands in the Taskfile. See https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/3/shopt"
}
},
"dotenv": {
"type": "array",
"description": "A list of `.env` file paths to be parsed.",
@@ -370,7 +434,8 @@
},
"interval": {
"description": "Sets a different watch interval when using `--watch`, the default being 5 seconds. This string should be a valid Go duration: https://pkg.go.dev/time#ParseDuration.",
"$ref": "#/definitions/3/run"
"type": "string",
"pattern": "^[0-9]+(?:m|s|ms)$"
}
},
"additionalProperties": false,

File diff suppressed because it is too large Load Diff

20
go.mod
View File

@@ -1,29 +1,29 @@
module github.com/go-task/task/v3
require (
github.com/fatih/color v1.13.0
github.com/fatih/color v1.14.1
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0
github.com/joho/godotenv v1.4.0
github.com/joho/godotenv v1.5.1
github.com/mattn/go-zglob v0.0.4
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/radovskyb/watcher v1.0.7
github.com/sajari/fuzzy v1.0.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.1
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/exp v0.0.0-20230212135524-a684f29349b6
golang.org/x/sync v0.1.0
gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/v3 v3.6.0-0.dev.0.20220704111049-a6e3029cd899
mvdan.cc/sh/v3 v3.6.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/term v0.3.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)
go 1.18
go 1.19

50
go.sum
View File

@@ -1,22 +1,22 @@
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss=
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-zglob v0.0.4 h1:LQi2iOm0/fGgu80AioIJ/1j9w9Oh+9DZ39J4VAGzHQM=
github.com/mattn/go-zglob v0.0.4/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
@@ -25,7 +25,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -40,15 +40,15 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 h1:RjggHMcaTVp0LOVZcW0bo8alwHrOaCrGUDgfWUHhnN4=
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/exp v0.0.0-20230212135524-a684f29349b6 h1:Ic9KukPQ7PegFzHckNiMTQXGgEszA7mY2Fn4ZMtnMbw=
golang.org/x/exp v0.0.0-20230212135524-a684f29349b6/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -56,5 +56,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/sh/v3 v3.6.0-0.dev.0.20220704111049-a6e3029cd899 h1:nwm4t5PtLlFd/H342GP50CtYf7vyMCOZkPx3g9shO0c=
mvdan.cc/sh/v3 v3.6.0-0.dev.0.20220704111049-a6e3029cd899/go.mod h1:1JcoyAKm1lZw/2bZje/iYKWicU/KMd0rsyJeKHnsK4E=
mvdan.cc/sh/v3 v3.6.0 h1:gtva4EXJ0dFNvl5bHjcUEvws+KRcDslT8VKheTYkbGU=
mvdan.cc/sh/v3 v3.6.0/go.mod h1:U4mhtBLZ32iWhif5/lD+ygy1zrgaQhUu+XFy7C8+TTA=

106
help.go
View File

@@ -1,6 +1,8 @@
package task
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
@@ -9,16 +11,85 @@ import (
"strings"
"text/tabwriter"
"github.com/go-task/task/v3/internal/editors"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/taskfile"
)
// ListOptions collects list-related options
type ListOptions struct {
ListOnlyTasksWithDescriptions bool
ListAllTasks bool
FormatTaskListAsJSON bool
}
// NewListOptions creates a new ListOptions instance
func NewListOptions(list, listAll, listAsJson bool) ListOptions {
return ListOptions{
ListOnlyTasksWithDescriptions: list,
ListAllTasks: listAll,
FormatTaskListAsJSON: listAsJson,
}
}
// ShouldListTasks returns true if one of the options to list tasks has been set to true
func (o ListOptions) ShouldListTasks() bool {
return o.ListOnlyTasksWithDescriptions || o.ListAllTasks
}
// Validate validates that the collection of list-related options are in a valid configuration
func (o ListOptions) Validate() error {
if o.ListOnlyTasksWithDescriptions && o.ListAllTasks {
return fmt.Errorf("task: cannot use --list and --list-all at the same time")
}
if o.FormatTaskListAsJSON && !o.ShouldListTasks() {
return fmt.Errorf("task: --json only applies to --list or --list-all")
}
return nil
}
// Filters returns the slice of FilterFunc which filters a list
// of taskfile.Task according to the given ListOptions
func (o ListOptions) Filters() []FilterFunc {
filters := []FilterFunc{FilterOutInternal}
if o.ListOnlyTasksWithDescriptions {
filters = append(filters, FilterOutNoDesc)
}
return filters
}
// ListTasks prints a list of tasks.
// Tasks that match the given filters will be excluded from the list.
// The function returns a boolean indicating whether or not tasks were found.
func (e *Executor) ListTasks(filters ...FilterFunc) bool {
tasks := e.GetTaskList(filters...)
// The function returns a boolean indicating whether tasks were found
// and an error if one was encountered while preparing the output.
func (e *Executor) ListTasks(o ListOptions) (bool, error) {
tasks, err := e.GetTaskList(o.Filters()...)
if err != nil {
return false, err
}
if o.FormatTaskListAsJSON {
output, err := e.ToEditorOutput(tasks)
if err != nil {
return false, err
}
encoder := json.NewEncoder(e.Stdout)
encoder.SetIndent("", " ")
if err := encoder.Encode(output); err != nil {
return false, err
}
return len(tasks) > 0, nil
}
if len(tasks) == 0 {
return false
if o.ListOnlyTasksWithDescriptions {
e.Logger.Outf(logger.Yellow, "task: No tasks with description available. Try --list-all to list all tasks")
} else if o.ListAllTasks {
e.Logger.Outf(logger.Yellow, "task: No tasks available")
}
return false, nil
}
e.Logger.Outf(logger.Default, "task: Available tasks for this project:")
@@ -31,10 +102,12 @@ func (e *Executor) ListTasks(filters ...FilterFunc) bool {
if len(task.Aliases) > 0 {
e.Logger.FOutf(w, logger.Cyan, "\t(aliases: %s)", strings.Join(task.Aliases, ", "))
}
fmt.Fprint(w, "\n")
_, _ = fmt.Fprint(w, "\n")
}
w.Flush()
return true
if err := w.Flush(); err != nil {
return false, err
}
return true, nil
}
// ListTaskNames prints only the task names in a Taskfile.
@@ -69,3 +142,22 @@ func (e *Executor) ListTaskNames(allTasks bool) {
fmt.Fprintln(w, t)
}
}
func (e *Executor) ToEditorOutput(tasks []*taskfile.Task) (*editors.Output, error) {
o := &editors.Output{
Tasks: make([]editors.Task, len(tasks)),
}
for i, t := range tasks {
upToDate, err := e.isTaskUpToDate(context.Background(), t)
if err != nil {
return nil, err
}
o.Tasks[i] = editors.Task{
Name: t.Name(),
Desc: t.Desc,
Summary: t.Summary,
UpToDate: upToDate,
}
}
return o, nil
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile"
)
@@ -184,6 +185,7 @@ func (c *CompilerV3) getSpecialVars(t *taskfile.Task) (map[string]string, error)
"ROOT_DIR": c.Dir,
"TASKFILE_DIR": taskfileDir,
"USER_WORKING_DIR": c.UserWorkingDir,
"TASK_VERSION": version.GetVersion(),
}, nil
}

View File

@@ -0,0 +1,14 @@
package editors
// Output wraps task list output for use in editor integrations (e.g. VSCode, etc)
type Output struct {
Tasks []Task `json:"tasks"`
}
// Task describes a single task
type Task struct {
Name string `json:"name"`
Desc string `json:"desc"`
Summary string `json:"summary"`
UpToDate bool `json:"up_to_date"`
}

View File

@@ -3,6 +3,7 @@ package execext
import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
@@ -17,12 +18,14 @@ import (
// RunCommandOptions is the options for the RunCommand func
type RunCommandOptions struct {
Command string
Dir string
Env []string
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
Command string
Dir string
Env []string
PosixOpts []string
BashOpts []string
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
}
var (
@@ -36,9 +39,18 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
return ErrNilOptions
}
p, err := syntax.NewParser().Parse(strings.NewReader(opts.Command), "")
if err != nil {
return err
// Set "-e" or "errexit" by default
opts.PosixOpts = append(opts.PosixOpts, "e")
// Format POSIX options into a slice that mvdan/sh understands
var params []string
for _, opt := range opts.PosixOpts {
if len(opt) == 1 {
params = append(params, fmt.Sprintf("-%s", opt))
} else {
params = append(params, "-o")
params = append(params, opt)
}
}
environ := opts.Env
@@ -47,7 +59,7 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
}
r, err := interp.New(
interp.Params("-e"),
interp.Params(params...),
interp.Env(expand.ListEnviron(environ...)),
interp.ExecHandler(interp.DefaultExecHandler(15*time.Second)),
interp.OpenHandler(openHandler),
@@ -58,6 +70,25 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
return err
}
parser := syntax.NewParser()
// Run any shopt commands
if len(opts.BashOpts) > 0 {
shoptCmdStr := fmt.Sprintf("shopt -s %s", strings.Join(opts.BashOpts, " "))
shoptCmd, err := parser.Parse(strings.NewReader(shoptCmdStr), "")
if err != nil {
return err
}
if err := r.Run(ctx, shoptCmd); err != nil {
return err
}
}
// Run the user-defined command
p, err := parser.Parse(strings.NewReader(opts.Command), "")
if err != nil {
return err
}
return r.Run(ctx, p)
}

62
internal/goext/meta.go Normal file
View File

@@ -0,0 +1,62 @@
package goext
// NOTE(@andreynering): The lists in this file were copied from:
//
// https://github.com/golang/go/blob/master/src/go/build/syslist.go
func IsKnownOS(str string) bool {
_, known := knownOS[str]
return known
}
func IsKnownArch(str string) bool {
_, known := knownArch[str]
return known
}
var knownOS = map[string]struct{}{
"aix": {},
"android": {},
"darwin": {},
"dragonfly": {},
"freebsd": {},
"hurd": {},
"illumos": {},
"ios": {},
"js": {},
"linux": {},
"nacl": {},
"netbsd": {},
"openbsd": {},
"plan9": {},
"solaris": {},
"windows": {},
"zos": {},
}
var knownArch = map[string]struct{}{
"386": {},
"amd64": {},
"amd64p32": {},
"arm": {},
"armbe": {},
"arm64": {},
"arm64be": {},
"loong64": {},
"mips": {},
"mipsle": {},
"mips64": {},
"mips64le": {},
"mips64p32": {},
"mips64p32le": {},
"ppc": {},
"ppc64": {},
"ppc64le": {},
"riscv": {},
"riscv64": {},
"s390": {},
"s390x": {},
"sparc": {},
"sparc64": {},
"wasm": {},
}

View File

@@ -34,6 +34,10 @@ func Red() PrintFunc {
}
func envColor(env string, defaultColor color.Attribute) color.Attribute {
if os.Getenv("FORCE_COLOR") != "" {
color.NoColor = false
}
override, err := strconv.Atoi(os.Getenv(env))
if err == nil {
return color.Attribute(override)

View File

@@ -6,11 +6,11 @@ import (
"io"
"testing"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile"
"github.com/stretchr/testify/assert"
"github.com/go-task/task/v3/internal/output"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile"
)
func TestInterleaved(t *testing.T) {

View File

@@ -1,176 +0,0 @@
// This code is released under the MIT License
// Copyright (c) 2020 Marco Molteni and the timeit contributors.
package main
import (
"flag"
"fmt"
"os"
"os/signal"
"time"
)
const usage = `sleepit: sleep for the specified duration, optionally handling signals
When the line "sleepit: ready" is printed, it means that it is safe to send signals to it
Usage: sleepit <command> [<args>]
Commands
default Use default action: on reception of SIGINT terminate abruptly
handle Handle signals: on reception of SIGINT perform cleanup before exiting
version Show the sleepit version`
var (
// Filled by the linker.
fullVersion = "unknown" // example: v0.0.9-8-g941583d027-dirty
)
func main() {
os.Exit(run(os.Args[1:]))
}
func run(args []string) int {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, usage)
return 2
}
defaultCmd := flag.NewFlagSet("default", flag.ExitOnError)
defaultSleep := defaultCmd.Duration("sleep", 5*time.Second, "Sleep duration")
handleCmd := flag.NewFlagSet("handle", flag.ExitOnError)
handleSleep := handleCmd.Duration("sleep", 5*time.Second, "Sleep duration")
handleCleanup := handleCmd.Duration("cleanup", 5*time.Second, "Cleanup duration")
handleTermAfter := handleCmd.Int("term-after", 0,
"Terminate immediately after `N` signals.\n"+
"Default is to terminate only when the cleanup phase has completed.")
versionCmd := flag.NewFlagSet("version", flag.ExitOnError)
switch args[0] {
case "default":
_ = defaultCmd.Parse(args[1:])
if len(defaultCmd.Args()) > 0 {
fmt.Fprintf(os.Stderr, "default: unexpected arguments: %v\n", defaultCmd.Args())
return 2
}
return supervisor(*defaultSleep, 0, 0, nil)
case "handle":
_ = handleCmd.Parse(args[1:])
if *handleTermAfter == 1 {
fmt.Fprintf(os.Stderr, "handle: term-after cannot be 1\n")
return 2
}
if len(handleCmd.Args()) > 0 {
fmt.Fprintf(os.Stderr, "handle: unexpected arguments: %v\n", handleCmd.Args())
return 2
}
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt) // Ctrl-C -> SIGINT
return supervisor(*handleSleep, *handleCleanup, *handleTermAfter, sigCh)
case "version":
_ = versionCmd.Parse(args[1:])
if len(versionCmd.Args()) > 0 {
fmt.Fprintf(os.Stderr, "version: unexpected arguments: %v\n", versionCmd.Args())
return 2
}
fmt.Printf("sleepit version %s\n", fullVersion)
return 0
default:
fmt.Fprintln(os.Stderr, usage)
return 2
}
}
func supervisor(
sleep time.Duration,
cleanup time.Duration,
termAfter int,
sigCh <-chan os.Signal,
) int {
fmt.Printf("sleepit: ready\n")
fmt.Printf("sleepit: PID=%d sleep=%v cleanup=%v\n",
os.Getpid(), sleep, cleanup)
cancelWork := make(chan struct{})
workerDone := worker(cancelWork, sleep, "work")
cancelCleaner := make(chan struct{})
var cleanerDone <-chan struct{}
sigCount := 0
for {
select {
case sig := <-sigCh:
sigCount++
fmt.Printf("sleepit: got signal=%s count=%d\n", sig, sigCount)
if sigCount == 1 {
// since `cancelWork` is unbuffered, sending will be synchronous:
// we are ensured that the worker has terminated before starting cleanup.
// This is important in some real-life situations.
cancelWork <- struct{}{}
cleanerDone = worker(cancelCleaner, cleanup, "cleanup")
}
if sigCount == termAfter {
cancelCleaner <- struct{}{}
return 4
}
case <-workerDone:
return 0
case <-cleanerDone:
return 3
}
}
}
// Start a worker goroutine and return immediately a `workerDone` channel.
// The goroutine will prepend its prints with the prefix `name`.
// The goroutine will simulate some work and will terminate when one of the following
// conditions happens:
// 1. When `howlong` is elapsed. This case will be signaled on the `workerDone` channel.
// 2. When something happens on channel `canceled`. Note that this simulates real-life,
// so cancellation is not instantaneous: if the caller wants a synchronous cancel,
// it should send a message; if instead it wants an asynchronous cancel, it should
// close the channel.
func worker(
canceled <-chan struct{},
howlong time.Duration,
name string,
) <-chan struct{} {
workerDone := make(chan struct{})
deadline := time.Now().Add(howlong)
go func() {
fmt.Printf("sleepit: %s started\n", name)
for {
select {
case <-canceled:
fmt.Printf("sleepit: %s canceled\n", name)
return
default:
if doSomeWork(deadline) {
fmt.Printf("sleepit: %s done\n", name) // <== NOTE THIS LINE
workerDone <- struct{}{}
return
}
}
}
}()
return workerDone
}
// Do some work and then return, so that the caller can decide wether to continue or not.
// Return true when all work is done.
func doSomeWork(deadline time.Time) bool {
if time.Now().After(deadline) {
return true
}
timeout := 100 * time.Millisecond
time.Sleep(timeout)
return false
}

View File

@@ -0,0 +1,20 @@
package slicesext
import (
"golang.org/x/exp/constraints"
"golang.org/x/exp/slices"
)
func UniqueJoin[T constraints.Ordered](ss ...[]T) []T {
var length int
for _, s := range ss {
length += len(s)
}
r := make([]T, length)
var i int
for _, s := range ss {
i += copy(r[i:], s)
}
slices.Sort(r)
return slices.Compact(r)
}

View File

@@ -85,6 +85,7 @@ func (c *Checksum) checksum(files ...string) (string, error) {
if _, err = io.Copy(h, f); err != nil {
return "", err
}
f.Close()
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
@@ -109,12 +110,12 @@ func (*Checksum) Kind() string {
}
func (c *Checksum) checksumFilePath() string {
return filepath.Join(c.TempDir, "checksum", c.normalizeFilename(c.Task))
return filepath.Join(c.TempDir, "checksum", normalizeFilename(c.Task))
}
var checksumFilenameRegexp = regexp.MustCompile("[^A-z0-9]")
// replaces invalid caracters on filenames with "-"
func (*Checksum) normalizeFilename(f string) string {
func normalizeFilename(f string) string {
return checksumFilenameRegexp.ReplaceAllString(f, "-")
}

View File

@@ -16,6 +16,6 @@ func TestNormalizeFilename(t *testing.T) {
{"foo1bar2baz3", "foo1bar2baz3"},
}
for _, test := range tests {
assert.Equal(t, test.Out, (&Checksum{}).normalizeFilename(test.In))
assert.Equal(t, test.Out, normalizeFilename(test.In))
}
}

View File

@@ -2,20 +2,24 @@ package status
import (
"os"
"path/filepath"
"time"
)
// Timestamp checks if any source change compared with the generated files,
// using file modifications timestamps.
type Timestamp struct {
TempDir string
Task string
Dir string
Sources []string
Generates []string
Dry bool
}
// IsUpToDate implements the Checker interface
func (t *Timestamp) IsUpToDate() (bool, error) {
if len(t.Sources) == 0 || len(t.Generates) == 0 {
if len(t.Sources) == 0 {
return false, nil
}
@@ -28,17 +32,51 @@ func (t *Timestamp) IsUpToDate() (bool, error) {
return false, nil
}
sourcesMaxTime, err := getMaxTime(sources...)
if err != nil || sourcesMaxTime.IsZero() {
timestampFile := t.timestampFilePath()
// If the file exists, add the file path to the generates.
// If the generate file is old, the task will be executed.
_, err = os.Stat(timestampFile)
if err == nil {
generates = append(generates, timestampFile)
} else {
// Create the timestamp file for the next execution when the file does not exist.
if !t.Dry {
if err := os.MkdirAll(filepath.Dir(timestampFile), 0o755); err != nil {
return false, err
}
f, err := os.Create(timestampFile)
if err != nil {
return false, err
}
f.Close()
}
}
taskTime := time.Now()
// Compare the time of the generates and sources. If the generates are old, the task will be executed.
// Get the max time of the generates.
generateMaxTime, err := getMaxTime(generates...)
if err != nil || generateMaxTime.IsZero() {
return false, nil
}
generatesMinTime, err := getMinTime(generates...)
if err != nil || generatesMinTime.IsZero() {
// Check if any of the source files is newer than the max time of the generates.
shouldUpdate, err := anyFileNewerThan(sources, generateMaxTime)
if err != nil {
return false, nil
}
return !generatesMinTime.Before(sourcesMaxTime), nil
// Modify the metadata of the file to the the current time.
if !t.Dry {
if err := os.Chtimes(timestampFile, taskTime, taskTime); err != nil {
return false, err
}
}
return !shouldUpdate, nil
}
func (t *Timestamp) Kind() string {
@@ -64,18 +102,6 @@ func (t *Timestamp) Value() (interface{}, error) {
return 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 {
@@ -88,13 +114,6 @@ func getMaxTime(files ...string) (time.Time, error) {
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
@@ -102,7 +121,26 @@ func maxTime(a, b time.Time) time.Time {
return b
}
// If the modification time of any of the files is newer than the the given time, returns true.
// This function is lazy, as it stops when it finds a file newer than the given time.
func anyFileNewerThan(files []string, givenTime time.Time) (bool, error) {
for _, f := range files {
info, err := os.Stat(f)
if err != nil {
return false, err
}
if info.ModTime().After(givenTime) {
return true, nil
}
}
return false, nil
}
// OnError implements the Checker interface
func (*Timestamp) OnError() error {
return nil
}
func (t *Timestamp) timestampFilePath() string {
return filepath.Join(t.TempDir, "timestamp", normalizeFilename(t.Task))
}

View File

@@ -0,0 +1,25 @@
package version
import (
"fmt"
"runtime/debug"
)
var version = ""
func GetVersion() string {
if version != "" {
return version
}
info, ok := debug.ReadBuildInfo()
if !ok || info.Main.Version == "" {
return "unknown"
}
ver := info.Main.Version
if info.Main.Sum != "" {
ver += fmt.Sprintf(" (%s)", info.Main.Sum)
}
return ver
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@go-task/cli",
"version": "3.19.0",
"version": "3.21.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@go-task/cli",
"version": "3.19.0",
"version": "3.21.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@go-task/cli",
"version": "3.19.0",
"version": "3.21.0",
"description": "A task runner / simpler Make alternative written in Go",
"scripts": {
"postinstall": "go-npm install",

View File

@@ -87,9 +87,12 @@ func (e *Executor) getStatusChecker(t *taskfile.Task) (status.Checker, error) {
func (e *Executor) timestampChecker(t *taskfile.Task) status.Checker {
return &status.Timestamp{
TempDir: e.TempDir,
Task: t.Name(),
Dir: t.Dir,
Sources: t.Sources,
Generates: t.Generates,
Dry: e.Dry,
}
}

128
task.go
View File

@@ -5,15 +5,18 @@ import (
"fmt"
"io"
"os"
"runtime"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/go-task/task/v3/internal/compiler"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/output"
"github.com/go-task/task/v3/internal/slicesext"
"github.com/go-task/task/v3/internal/summary"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile"
@@ -45,7 +48,7 @@ type Executor struct {
Parallel bool
Color bool
Concurrency int
Interval string
Interval time.Duration
Stdin io.Reader
Stdout io.Writer
@@ -72,12 +75,20 @@ func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error {
for _, call := range calls {
task, err := e.GetTask(call)
if err != nil {
e.ListTasks(FilterOutInternal(), FilterOutNoDesc())
if _, ok := err.(*taskNotFoundError); ok {
if _, err := e.ListTasks(ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil {
return err
}
}
return err
}
if task.Internal {
e.ListTasks(FilterOutInternal(), FilterOutNoDesc())
if _, ok := err.(*taskNotFoundError); ok {
if _, err := e.ListTasks(ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil {
return err
}
}
return &taskInternalError{taskName: call.Task}
}
}
@@ -126,6 +137,11 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
defer release()
return e.startExecution(ctx, t, func(ctx context.Context) error {
if !shouldRunOnCurrentPlatform(t.Platforms) {
e.Logger.VerboseOutf(logger.Yellow, `task: "%s" not for current platform - ignored`, call.Task)
return nil
}
e.Logger.VerboseErrf(logger.Magenta, `task: "%s" started`, call.Task)
if err := e.runDeps(ctx, t); err != nil {
return err
@@ -243,6 +259,11 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
}
return nil
case cmd.Cmd != "":
if !shouldRunOnCurrentPlatform(cmd.Platforms) {
e.Logger.VerboseOutf(logger.Yellow, `task: [%s] %s not for current platform - ignored`, t.Name(), cmd.Cmd)
return nil
}
if e.Verbose || (!cmd.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) {
e.Logger.Errf(logger.Green, "task: [%s] %s", t.Name(), cmd.Cmd)
}
@@ -263,17 +284,19 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
stdOut, stdErr, close := outputWrapper.WrapWriter(e.Stdout, e.Stderr, t.Prefix, outputTemplater)
defer func() {
if err := close(); err != nil {
e.Logger.Errf(logger.Red, "task: unable to close writter: %v", err)
e.Logger.Errf(logger.Red, "task: unable to close writer: %v", err)
}
}()
err = execext.RunCommand(ctx, &execext.RunCommandOptions{
Command: cmd.Cmd,
Dir: t.Dir,
Env: getEnviron(t),
Stdin: e.Stdin,
Stdout: stdOut,
Stderr: stdErr,
Command: cmd.Cmd,
Dir: t.Dir,
Env: getEnviron(t),
PosixOpts: slicesext.UniqueJoin(e.Taskfile.Set, t.Set, cmd.Set),
BashOpts: slicesext.UniqueJoin(e.Taskfile.Shopt, t.Shopt, cmd.Shopt),
Stdin: e.Stdin,
Stdout: stdOut,
Stderr: stdErr,
})
if execext.IsExitError(err) && cmd.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v", t.Name(), err)
@@ -377,26 +400,42 @@ func (e *Executor) GetTask(call taskfile.Call) (*taskfile.Task, error) {
return matchingTask, nil
}
type FilterFunc func(tasks []*taskfile.Task) []*taskfile.Task
type FilterFunc func(task *taskfile.Task) bool
func (e *Executor) GetTaskList(filters ...FilterFunc) []*taskfile.Task {
func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*taskfile.Task, error) {
tasks := make([]*taskfile.Task, 0, len(e.Taskfile.Tasks))
// Create an error group to wait for each task to be compiled
var g errgroup.Group
// Fetch and compile the list of tasks
for _, task := range e.Taskfile.Tasks {
compiledTask, err := e.FastCompiledTask(taskfile.Call{Task: task.Task})
if err == nil {
task = compiledTask
}
tasks = append(tasks, task)
for key := range e.Taskfile.Tasks {
task := e.Taskfile.Tasks[key]
g.Go(func() error {
// Check if we should filter the task
for _, filter := range filters {
if filter(task) {
return nil
}
}
// Compile the task
compiledTask, err := e.FastCompiledTask(taskfile.Call{Task: task.Task})
if err == nil {
task = compiledTask
}
tasks = append(tasks, task)
return nil
})
}
// Filter the tasks
for _, filter := range filters {
tasks = filter(tasks)
// Wait for all the go routines to finish
if err := g.Wait(); err != nil {
return nil, err
}
// Sort the tasks
// Sort the tasks.
// Tasks that are not namespaced should be listed before tasks that are.
// We detect this by searching for a ':' in the task name.
sort.Slice(tasks, func(i, j int) bool {
@@ -411,38 +450,27 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) []*taskfile.Task {
return false
})
return tasks
}
// Filter is a generic task filtering function. It will remove each task in the
// slice where the result of the given function is true.
func Filter(f func(task *taskfile.Task) bool) FilterFunc {
return func(tasks []*taskfile.Task) []*taskfile.Task {
shift := 0
for _, task := range tasks {
if !f(task) {
tasks[shift] = task
shift++
}
}
// This loop stops any memory leaks
for j := shift; j < len(tasks); j++ {
tasks[j] = nil
}
return slices.Clip(tasks[:shift])
}
return tasks, nil
}
// FilterOutNoDesc removes all tasks that do not contain a description.
func FilterOutNoDesc() FilterFunc {
return Filter(func(task *taskfile.Task) bool {
return task.Desc == ""
})
func FilterOutNoDesc(task *taskfile.Task) bool {
return task.Desc == ""
}
// FilterOutInternal removes all tasks that are marked as internal.
func FilterOutInternal() FilterFunc {
return Filter(func(task *taskfile.Task) bool {
return task.Internal
})
func FilterOutInternal(task *taskfile.Task) bool {
return task.Internal
}
func shouldRunOnCurrentPlatform(platforms []*taskfile.Platform) bool {
if len(platforms) == 0 {
return true
}
for _, p := range platforms {
if (p.OS == "" || p.OS == runtime.GOOS) && (p.Arch == "" || p.Arch == runtime.GOARCH) {
return true
}
}
return false
}

View File

@@ -10,7 +10,6 @@ import (
"runtime"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
@@ -191,11 +190,13 @@ func TestSpecialVars(t *testing.T) {
assert.Contains(t, output, "root/TASK=print")
assert.Contains(t, output, "root/ROOT_DIR="+toAbs("testdata/special_vars"))
assert.Contains(t, output, "root/TASKFILE_DIR="+toAbs("testdata/special_vars"))
assert.Contains(t, output, "root/TASK_VERSION=unknown")
// Included Taskfile
assert.Contains(t, output, "included/TASK=included:print")
assert.Contains(t, output, "included/ROOT_DIR="+toAbs("testdata/special_vars"))
assert.Contains(t, output, "included/TASKFILE_DIR="+toAbs("testdata/special_vars/included"))
assert.Contains(t, output, "included/TASK_VERSION=unknown")
}
func TestVarsInvalidTmpl(t *testing.T) {
@@ -606,7 +607,9 @@ func TestNoLabelInList(t *testing.T) {
Stderr: &buff,
}
assert.NoError(t, e.Setup())
e.ListTasks(task.FilterOutInternal(), task.FilterOutNoDesc())
if _, err := e.ListTasks(task.ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil {
t.Error(err)
}
assert.Contains(t, buff.String(), "foo")
}
@@ -624,7 +627,9 @@ func TestListAllShowsNoDesc(t *testing.T) {
assert.NoError(t, e.Setup())
var title string
e.ListTasks(task.FilterOutInternal())
if _, err := e.ListTasks(task.ListOptions{ListAllTasks: true}); err != nil {
t.Error(err)
}
for _, title = range []string{
"foo",
"voo",
@@ -646,7 +651,9 @@ func TestListCanListDescOnly(t *testing.T) {
}
assert.NoError(t, e.Setup())
e.ListTasks(task.FilterOutInternal(), task.FilterOutNoDesc())
if _, err := e.ListTasks(task.ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil {
t.Error(err)
}
var title string
assert.Contains(t, buff.String(), "foo")
@@ -1034,7 +1041,7 @@ func TestIncludesInternal(t *testing.T) {
}{
{"included internal task via task", "task-1", false, "Hello, World!\n"},
{"included internal task via dep", "task-2", false, "Hello, World!\n"},
{"included internal direct", "included:task-3", true, ""},
{"included internal direct", "included:task-3", true, "task: No tasks with description available. Try --list-all to list all tasks\n"},
}
for _, test := range tests {
@@ -1643,67 +1650,6 @@ func TestEvaluateSymlinksInPaths(t *testing.T) {
assert.NoError(t, err)
}
func TestFileWatcherInterval(t *testing.T) {
const dir = "testdata/watcher_interval"
expectedOutput := strings.TrimSpace(`
task: Started watching for tasks: default
task: [default] echo "Hello, World!"
Hello, World!
task: [default] echo "Hello, World!"
Hello, World!
`)
var buff bytes.Buffer
e := &task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
Watch: true,
}
assert.NoError(t, e.Setup())
buff.Reset()
err := os.MkdirAll(filepathext.SmartJoin(dir, "src"), 0755)
assert.NoError(t, err)
err = os.WriteFile(filepathext.SmartJoin(dir, "src/a"), []byte("test"), 0644)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
err := e.Run(ctx, taskfile.Call{Task: "default"})
if err != nil {
return
}
}
}
}(ctx)
time.Sleep(10 * time.Millisecond)
err = os.WriteFile(filepathext.SmartJoin(dir, "src/a"), []byte("test updated"), 0644)
if err != nil {
t.Fatal(err)
}
time.Sleep(700 * time.Millisecond)
cancel()
assert.Equal(t, expectedOutput, strings.TrimSpace(buff.String()))
buff.Reset()
err = os.RemoveAll(filepathext.SmartJoin(dir, ".task"))
assert.NoError(t, err)
err = os.RemoveAll(filepathext.SmartJoin(dir, "src"))
assert.NoError(t, err)
}
func TestTaskfileWalk(t *testing.T) {
tests := []struct {
name string
@@ -1752,3 +1698,99 @@ func TestUserWorkingDirectory(t *testing.T) {
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"}))
assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String())
}
func TestPlatforms(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/platforms",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build-" + runtime.GOOS}))
assert.Equal(t, fmt.Sprintf("task: [build-%s] echo 'Running task on %s'\nRunning task on %s\n", runtime.GOOS, runtime.GOOS, runtime.GOOS), buff.String())
}
func TestPOSIXShellOptsGlobalLevel(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/shopts/global_level",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
err := e.Run(context.Background(), taskfile.Call{Task: "pipefail"})
assert.NoError(t, err)
assert.Equal(t, "pipefail\ton\n", buff.String())
}
func TestPOSIXShellOptsTaskLevel(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/shopts/task_level",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
err := e.Run(context.Background(), taskfile.Call{Task: "pipefail"})
assert.NoError(t, err)
assert.Equal(t, "pipefail\ton\n", buff.String())
}
func TestPOSIXShellOptsCommandLevel(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/shopts/command_level",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
err := e.Run(context.Background(), taskfile.Call{Task: "pipefail"})
assert.NoError(t, err)
assert.Equal(t, "pipefail\ton\n", buff.String())
}
func TestBashShellOptsGlobalLevel(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/shopts/global_level",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
err := e.Run(context.Background(), taskfile.Call{Task: "globstar"})
assert.NoError(t, err)
assert.Equal(t, "globstar\ton\n", buff.String())
}
func TestBashShellOptsTaskLevel(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/shopts/task_level",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
err := e.Run(context.Background(), taskfile.Call{Task: "globstar"})
assert.NoError(t, err)
assert.Equal(t, "globstar\ton\n", buff.String())
}
func TestBashShellOptsCommandLevel(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/shopts/command_level",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
err := e.Run(context.Background(), taskfile.Call{Task: "globstar"})
assert.NoError(t, err)
assert.Equal(t, "globstar\ton\n", buff.String())
}

View File

@@ -1,13 +1,22 @@
package taskfile
import (
"fmt"
"gopkg.in/yaml.v3"
)
// Cmd is a task command
type Cmd struct {
Cmd string
Silent bool
Task string
Set []string
Shopt []string
Vars *Vars
IgnoreError bool
Defer bool
Platforms []*Platform
}
// Dep is a task dependency
@@ -16,68 +25,99 @@ type Dep struct {
Vars *Vars
}
// UnmarshalYAML implements yaml.Unmarshaler interface
func (c *Cmd) UnmarshalYAML(unmarshal func(interface{}) error) error {
var cmd string
if err := unmarshal(&cmd); err == nil {
func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.ScalarNode:
var cmd string
if err := node.Decode(&cmd); err != nil {
return err
}
c.Cmd = cmd
return nil
case yaml.MappingNode:
// A command with additional options
var cmdStruct struct {
Cmd string
Silent bool
Set []string
Shopt []string
IgnoreError bool `yaml:"ignore_error"`
Platforms []*Platform
}
if err := node.Decode(&cmdStruct); err == nil && cmdStruct.Cmd != "" {
c.Cmd = cmdStruct.Cmd
c.Silent = cmdStruct.Silent
c.Set = cmdStruct.Set
c.Shopt = cmdStruct.Shopt
c.IgnoreError = cmdStruct.IgnoreError
c.Platforms = cmdStruct.Platforms
return nil
}
// A deferred command
var deferredCmd struct {
Defer string
}
if err := node.Decode(&deferredCmd); err == nil && deferredCmd.Defer != "" {
c.Defer = true
c.Cmd = deferredCmd.Defer
return nil
}
// A deferred task call
var deferredCall struct {
Defer Call
}
if err := node.Decode(&deferredCall); err == nil && deferredCall.Defer.Task != "" {
c.Defer = true
c.Task = deferredCall.Defer.Task
c.Vars = deferredCall.Defer.Vars
return nil
}
// A task call
var taskCall struct {
Task string
Vars *Vars
}
if err := node.Decode(&taskCall); err == nil && taskCall.Task != "" {
c.Task = taskCall.Task
c.Vars = taskCall.Vars
return nil
}
return fmt.Errorf("yaml: line %d: invalid keys in command", node.Line)
}
var cmdStruct struct {
Cmd string
Silent bool
IgnoreError bool `yaml:"ignore_error"`
}
if err := unmarshal(&cmdStruct); err == nil && cmdStruct.Cmd != "" {
c.Cmd = cmdStruct.Cmd
c.Silent = cmdStruct.Silent
c.IgnoreError = cmdStruct.IgnoreError
return nil
}
var deferredCmd struct {
Defer string
}
if err := unmarshal(&deferredCmd); err == nil && deferredCmd.Defer != "" {
c.Defer = true
c.Cmd = deferredCmd.Defer
return nil
}
var deferredCall struct {
Defer Call
}
if err := unmarshal(&deferredCall); err == nil && deferredCall.Defer.Task != "" {
c.Defer = true
c.Task = deferredCall.Defer.Task
c.Vars = deferredCall.Defer.Vars
return nil
}
var taskCall struct {
Task string
Vars *Vars
}
if err := unmarshal(&taskCall); err != nil {
return err
}
c.Task = taskCall.Task
c.Vars = taskCall.Vars
return nil
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into command", node.Line, node.ShortTag())
}
// UnmarshalYAML implements yaml.Unmarshaler interface
func (d *Dep) UnmarshalYAML(unmarshal func(interface{}) error) error {
var task string
if err := unmarshal(&task); err == nil {
func (d *Dep) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.ScalarNode:
var task string
if err := node.Decode(&task); err != nil {
return err
}
d.Task = task
return nil
case yaml.MappingNode:
var taskCall struct {
Task string
Vars *Vars
}
if err := node.Decode(&taskCall); err != nil {
return err
}
d.Task = taskCall.Task
d.Vars = taskCall.Vars
return nil
}
var taskCall struct {
Task string
Vars *Vars
}
if err := unmarshal(&taskCall); err != nil {
return err
}
d.Task = taskCall.Task
d.Vars = taskCall.Vars
return nil
return fmt.Errorf("cannot unmarshal %s into dependency", node.ShortTag())
}

View File

@@ -1,7 +1,6 @@
package taskfile
import (
"errors"
"fmt"
"path/filepath"
@@ -32,24 +31,26 @@ type IncludedTaskfiles struct {
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (tfs *IncludedTaskfiles) UnmarshalYAML(node *yaml.Node) error {
if node.Kind != yaml.MappingNode {
return errors.New("task: includes is not a map")
}
switch node.Kind {
// NOTE(@andreynering): on this style of custom unmarsheling,
// even number contains the keys, while odd numbers contains
// the values.
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
case yaml.MappingNode:
// NOTE(@andreynering): on this style of custom unmarshalling,
// even number contains the keys, while odd numbers contains
// the values.
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
var v IncludedTaskfile
if err := valueNode.Decode(&v); err != nil {
return err
var v IncludedTaskfile
if err := valueNode.Decode(&v); err != nil {
return err
}
tfs.Set(keyNode.Value, v)
}
tfs.Set(keyNode.Value, v)
return nil
}
return nil
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into included taskfiles", node.Line, node.ShortTag())
}
// Len returns the length of the map
@@ -60,14 +61,6 @@ func (tfs *IncludedTaskfiles) Len() int {
return len(tfs.Keys)
}
// Merge merges the given IncludedTaskfiles into the caller one
func (tfs *IncludedTaskfiles) Merge(other *IncludedTaskfiles) {
_ = other.Range(func(key string, value IncludedTaskfile) error {
tfs.Set(key, value)
return nil
})
}
// Set sets a value to a given key
func (tfs *IncludedTaskfiles) Set(key string, includedTaskfile IncludedTaskfile) {
if tfs.Mapping == nil {
@@ -92,33 +85,40 @@ func (tfs *IncludedTaskfiles) Range(yield func(key string, includedTaskfile Incl
return nil
}
// UnmarshalYAML implements yaml.Unmarshaler interface
func (it *IncludedTaskfile) UnmarshalYAML(unmarshal func(interface{}) error) error {
var str string
if err := unmarshal(&str); err == nil {
func (it *IncludedTaskfile) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.ScalarNode:
var str string
if err := node.Decode(&str); err != nil {
return err
}
it.Taskfile = str
return nil
case yaml.MappingNode:
var includedTaskfile struct {
Taskfile string
Dir string
Optional bool
Internal bool
Aliases []string
Vars *Vars
}
if err := node.Decode(&includedTaskfile); err != nil {
return err
}
it.Taskfile = includedTaskfile.Taskfile
it.Dir = includedTaskfile.Dir
it.Optional = includedTaskfile.Optional
it.Internal = includedTaskfile.Internal
it.Aliases = includedTaskfile.Aliases
it.AdvancedImport = true
it.Vars = includedTaskfile.Vars
return nil
}
var includedTaskfile struct {
Taskfile string
Dir string
Optional bool
Internal bool
Aliases []string
Vars *Vars
}
if err := unmarshal(&includedTaskfile); err != nil {
return err
}
it.Taskfile = includedTaskfile.Taskfile
it.Dir = includedTaskfile.Dir
it.Optional = includedTaskfile.Optional
it.Internal = includedTaskfile.Internal
it.Aliases = includedTaskfile.Aliases
it.AdvancedImport = true
it.Vars = includedTaskfile.Vars
return nil
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into included taskfile", node.Line, node.ShortTag())
}
// DeepCopy creates a new instance of IncludedTaskfile and copies

View File

@@ -5,7 +5,7 @@ import (
"strings"
)
// NamespaceSeparator contains the character that separates namescapes
// NamespaceSeparator contains the character that separates namespaces
const NamespaceSeparator = ":"
// Merge merges the second Taskfile into the first
@@ -21,11 +21,6 @@ func Merge(t1, t2 *Taskfile, includedTaskfile *IncludedTaskfile, namespaces ...s
t1.Output = t2.Output
}
if t1.Includes == nil {
t1.Includes = &IncludedTaskfiles{}
}
t1.Includes.Merge(t2.Includes)
if t1.Vars == nil {
t1.Vars = &Vars{}
}
@@ -45,7 +40,7 @@ func Merge(t1, t2 *Taskfile, includedTaskfile *IncludedTaskfile, namespaces ...s
// Set the task to internal if EITHER the included task or the included
// taskfile are marked as internal
task.Internal = task.Internal || includedTaskfile.Internal
task.Internal = task.Internal || (includedTaskfile != nil && includedTaskfile.Internal)
// Add namespaces to dependencies, commands and aliases
for _, dep := range task.Deps {

View File

@@ -2,6 +2,8 @@ package taskfile
import (
"fmt"
"gopkg.in/yaml.v3"
)
// Output of the Task output
@@ -17,28 +19,35 @@ func (s *Output) IsSet() bool {
return s.Name != ""
}
// UnmarshalYAML implements yaml.Unmarshaler
// It accepts a scalar node representing the Output.Name or a mapping node representing the OutputGroup.
func (s *Output) UnmarshalYAML(unmarshal func(interface{}) error) error {
var name string
if err := unmarshal(&name); err == nil {
func (s *Output) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.ScalarNode:
var name string
if err := node.Decode(&name); err != nil {
return err
}
s.Name = name
return nil
case yaml.MappingNode:
var tmp struct {
Group *OutputGroup
}
if err := node.Decode(&tmp); err != nil {
return fmt.Errorf("task: output style must be a string or mapping with a \"group\" key: %w", err)
}
if tmp.Group == nil {
return fmt.Errorf("task: output style must have the \"group\" key when in mapping form")
}
*s = Output{
Name: "group",
Group: *tmp.Group,
}
return nil
}
var tmp struct {
Group *OutputGroup
}
if err := unmarshal(&tmp); err != nil {
return fmt.Errorf("task: output style must be a string or mapping with a \"group\" key: %w", err)
}
if tmp.Group == nil {
return fmt.Errorf("task: output style must have the \"group\" key when in mapping form")
}
*s = Output{
Name: "group",
Group: *tmp.Group,
}
return nil
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into output", node.Line, node.ShortTag())
}
// OutputGroup is the style options specific to the Group style.

90
taskfile/platforms.go Normal file
View File

@@ -0,0 +1,90 @@
package taskfile
import (
"fmt"
"strings"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/internal/goext"
)
// Platform represents GOOS and GOARCH values
type Platform struct {
OS string
Arch string
}
type ErrInvalidPlatform struct {
Platform string
}
func (err *ErrInvalidPlatform) Error() string {
return fmt.Sprintf(`task: Invalid platform "%s"`, err.Platform)
}
// UnmarshalYAML implements yaml.Unmarshaler interface.
func (p *Platform) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.ScalarNode:
var platform string
if err := node.Decode(&platform); err != nil {
return err
}
if err := p.parsePlatform(platform); err != nil {
return err
}
return nil
}
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into platform", node.Line, node.ShortTag())
}
// parsePlatform takes a string representing an OS/Arch combination (or either on their own)
// and parses it into the Platform struct. It returns an error if the input string is invalid.
// Valid combinations for input: OS, Arch, OS/Arch
func (p *Platform) parsePlatform(input string) error {
splitValues := strings.Split(input, "/")
if len(splitValues) > 2 {
return &ErrInvalidPlatform{Platform: input}
}
if err := p.parseOsOrArch(splitValues[0]); err != nil {
return &ErrInvalidPlatform{Platform: input}
}
if len(splitValues) == 2 {
if err := p.parseArch(splitValues[1]); err != nil {
return &ErrInvalidPlatform{Platform: input}
}
}
return nil
}
// parseOsOrArch will check if the given input is a valid OS or Arch value.
// If so, it will store it. If not, an error is returned
func (p *Platform) parseOsOrArch(osOrArch string) error {
if osOrArch == "" {
return fmt.Errorf("task: Blank OS/Arch value provided")
}
if goext.IsKnownOS(osOrArch) {
p.OS = osOrArch
return nil
}
if goext.IsKnownArch(osOrArch) {
p.Arch = osOrArch
return nil
}
return fmt.Errorf("task: Invalid OS/Arch value provided (%s)", osOrArch)
}
func (p *Platform) parseArch(arch string) error {
if arch == "" {
return fmt.Errorf("task: Blank Arch value provided")
}
if p.Arch != "" {
return fmt.Errorf("task: Multiple Arch values provided")
}
if goext.IsKnownArch(arch) {
p.Arch = arch
return nil
}
return fmt.Errorf("task: Invalid Arch value provided (%s)", arch)
}

View File

@@ -0,0 +1,49 @@
package taskfile
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestPlatformParsing(t *testing.T) {
tests := []struct {
Input string
ExpectedOS string
ExpectedArch string
Error string
}{
{Input: "windows", ExpectedOS: "windows", ExpectedArch: ""},
{Input: "linux", ExpectedOS: "linux", ExpectedArch: ""},
{Input: "darwin", ExpectedOS: "darwin", ExpectedArch: ""},
{Input: "386", ExpectedOS: "", ExpectedArch: "386"},
{Input: "amd64", ExpectedOS: "", ExpectedArch: "amd64"},
{Input: "arm64", ExpectedOS: "", ExpectedArch: "arm64"},
{Input: "windows/386", ExpectedOS: "windows", ExpectedArch: "386"},
{Input: "windows/amd64", ExpectedOS: "windows", ExpectedArch: "amd64"},
{Input: "windows/arm64", ExpectedOS: "windows", ExpectedArch: "arm64"},
{Input: "invalid", Error: `task: Invalid platform "invalid"`},
{Input: "invalid/invalid", Error: `task: Invalid platform "invalid/invalid"`},
{Input: "windows/invalid", Error: `task: Invalid platform "windows/invalid"`},
{Input: "invalid/amd64", Error: `task: Invalid platform "invalid/amd64"`},
}
for _, test := range tests {
t.Run(test.Input, func(t *testing.T) {
var p Platform
err := p.parsePlatform(test.Input)
if test.Error != "" {
assert.Error(t, err)
assert.Equal(t, test.Error, err.Error())
} else {
assert.NoError(t, err)
assert.Equal(t, test.ExpectedOS, p.OS)
assert.Equal(t, test.ExpectedArch, p.Arch)
}
})
}
}

View File

@@ -3,6 +3,8 @@ package taskfile
import (
"errors"
"fmt"
"gopkg.in/yaml.v3"
)
var (
@@ -17,29 +19,33 @@ type Precondition struct {
}
// UnmarshalYAML implements yaml.Unmarshaler interface.
func (p *Precondition) UnmarshalYAML(unmarshal func(interface{}) error) error {
var cmd string
func (p *Precondition) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
if err := unmarshal(&cmd); err == nil {
case yaml.ScalarNode:
var cmd string
if err := node.Decode(&cmd); err != nil {
return err
}
p.Sh = cmd
p.Msg = fmt.Sprintf("`%s` failed", cmd)
return nil
case yaml.MappingNode:
var sh struct {
Sh string
Msg string
}
if err := node.Decode(&sh); err != nil {
return err
}
p.Sh = sh.Sh
p.Msg = sh.Msg
if p.Msg == "" {
p.Msg = fmt.Sprintf("%s failed", sh.Sh)
}
return nil
}
var sh struct {
Sh string
Msg string
}
if err := unmarshal(&sh); err != nil {
return err
}
p.Sh = sh.Sh
p.Msg = sh.Msg
if p.Msg == "" {
p.Msg = fmt.Sprintf("%s failed", sh.Sh)
}
return nil
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into precondition", node.Line, node.ShortTag())
}

View File

@@ -197,6 +197,8 @@ func readTaskfile(file string) (*taskfile.Taskfile, error) {
if err != nil {
return nil, err
}
defer f.Close()
var t taskfile.Taskfile
if err := yaml.NewDecoder(f).Decode(&t); err != nil {
return nil, fmt.Errorf("task: Failed to parse %s:\n%w", filepathext.TryAbsToRel(file), err)

View File

@@ -1,5 +1,11 @@
package taskfile
import (
"fmt"
"gopkg.in/yaml.v3"
)
// Tasks represents a group of tasks
type Tasks map[string]*Task
@@ -17,6 +23,8 @@ type Task struct {
Status []string
Preconditions []*Precondition
Dir string
Set []string
Shopt []string
Vars *Vars
Env *Vars
Dotenv []string
@@ -30,6 +38,7 @@ type Task struct {
IncludeVars *Vars
IncludedTaskfileVars *Vars
IncludedTaskfile *IncludedTaskfile
Platforms []*Platform
}
func (t *Task) Name() string {
@@ -39,67 +48,86 @@ func (t *Task) Name() string {
return t.Task
}
func (t *Task) UnmarshalYAML(unmarshal func(interface{}) error) error {
var cmd Cmd
if err := unmarshal(&cmd); err == nil && cmd.Cmd != "" {
func (t *Task) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
// Shortcut syntax for a task with a single command
case yaml.ScalarNode:
var cmd Cmd
if err := node.Decode(&cmd); err != nil {
return err
}
t.Cmds = append(t.Cmds, &cmd)
return nil
}
var cmds []*Cmd
if err := unmarshal(&cmds); err == nil && len(cmds) > 0 {
// Shortcut syntax for a simple task with a list of commands
case yaml.SequenceNode:
var cmds []*Cmd
if err := node.Decode(&cmds); err != nil {
return err
}
t.Cmds = cmds
return nil
// Full task object
case yaml.MappingNode:
var task struct {
Cmds []*Cmd
Deps []*Dep
Label string
Desc string
Summary string
Aliases []string
Sources []string
Generates []string
Status []string
Preconditions []*Precondition
Dir string
Set []string
Shopt []string
Vars *Vars
Env *Vars
Dotenv []string
Silent bool
Interactive bool
Internal bool
Method string
Prefix string
IgnoreError bool `yaml:"ignore_error"`
Run string
Platforms []*Platform
}
if err := node.Decode(&task); err != nil {
return err
}
t.Cmds = task.Cmds
t.Deps = task.Deps
t.Label = task.Label
t.Desc = task.Desc
t.Summary = task.Summary
t.Aliases = task.Aliases
t.Sources = task.Sources
t.Generates = task.Generates
t.Status = task.Status
t.Preconditions = task.Preconditions
t.Dir = task.Dir
t.Set = task.Set
t.Shopt = task.Shopt
t.Vars = task.Vars
t.Env = task.Env
t.Dotenv = task.Dotenv
t.Silent = task.Silent
t.Interactive = task.Interactive
t.Internal = task.Internal
t.Method = task.Method
t.Prefix = task.Prefix
t.IgnoreError = task.IgnoreError
t.Run = task.Run
t.Platforms = task.Platforms
return nil
}
var task struct {
Cmds []*Cmd
Deps []*Dep
Label string
Desc string
Summary string
Aliases []string
Sources []string
Generates []string
Status []string
Preconditions []*Precondition
Dir string
Vars *Vars
Env *Vars
Dotenv []string
Silent bool
Interactive bool
Internal bool
Method string
Prefix string
IgnoreError bool `yaml:"ignore_error"`
Run string
}
if err := unmarshal(&task); err != nil {
return err
}
t.Cmds = task.Cmds
t.Deps = task.Deps
t.Label = task.Label
t.Desc = task.Desc
t.Aliases = task.Aliases
t.Summary = task.Summary
t.Sources = task.Sources
t.Generates = task.Generates
t.Status = task.Status
t.Preconditions = task.Preconditions
t.Dir = task.Dir
t.Vars = task.Vars
t.Env = task.Env
t.Dotenv = task.Dotenv
t.Silent = task.Silent
t.Interactive = task.Interactive
t.Internal = task.Internal
t.Method = task.Method
t.Prefix = task.Prefix
t.IgnoreError = task.IgnoreError
t.Run = task.Run
return nil
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into task", node.Line, node.ShortTag())
}
// DeepCopy creates a new instance of Task and copies
@@ -118,6 +146,8 @@ func (t *Task) DeepCopy() *Task {
Status: deepCopySlice(t.Status),
Preconditions: deepCopySlice(t.Preconditions),
Dir: t.Dir,
Set: deepCopySlice(t.Set),
Shopt: deepCopySlice(t.Shopt),
Vars: t.Vars.DeepCopy(),
Env: t.Env.DeepCopy(),
Dotenv: deepCopySlice(t.Dotenv),
@@ -131,6 +161,7 @@ func (t *Task) DeepCopy() *Task {
IncludeVars: t.IncludeVars.DeepCopy(),
IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(),
IncludedTaskfile: t.IncludedTaskfile.DeepCopy(),
Platforms: deepCopySlice(t.Platforms),
}
return c
}

View File

@@ -3,6 +3,9 @@ package taskfile
import (
"fmt"
"strconv"
"time"
"gopkg.in/yaml.v3"
)
// Taskfile represents a Taskfile.yml
@@ -12,59 +15,67 @@ type Taskfile struct {
Output Output
Method string
Includes *IncludedTaskfiles
Set []string
Shopt []string
Vars *Vars
Env *Vars
Tasks Tasks
Silent bool
Dotenv []string
Run string
Interval string
Interval time.Duration
}
// UnmarshalYAML implements yaml.Unmarshaler interface
func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error {
var taskfile struct {
Version string
Expansions int
Output Output
Method string
Includes *IncludedTaskfiles
Vars *Vars
Env *Vars
Tasks Tasks
Silent bool
Dotenv []string
Run string
Interval string
func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.MappingNode:
var taskfile struct {
Version string
Expansions int
Output Output
Method string
Includes *IncludedTaskfiles
Set []string
Shopt []string
Vars *Vars
Env *Vars
Tasks Tasks
Silent bool
Dotenv []string
Run string
Interval time.Duration
}
if err := node.Decode(&taskfile); err != nil {
return err
}
tf.Version = taskfile.Version
tf.Expansions = taskfile.Expansions
tf.Output = taskfile.Output
tf.Method = taskfile.Method
tf.Includes = taskfile.Includes
tf.Set = taskfile.Set
tf.Shopt = taskfile.Shopt
tf.Vars = taskfile.Vars
tf.Env = taskfile.Env
tf.Tasks = taskfile.Tasks
tf.Silent = taskfile.Silent
tf.Dotenv = taskfile.Dotenv
tf.Run = taskfile.Run
tf.Interval = taskfile.Interval
if tf.Expansions <= 0 {
tf.Expansions = 2
}
if tf.Vars == nil {
tf.Vars = &Vars{}
}
if tf.Env == nil {
tf.Env = &Vars{}
}
return nil
}
if err := unmarshal(&taskfile); err != nil {
return err
}
tf.Version = taskfile.Version
tf.Expansions = taskfile.Expansions
tf.Output = taskfile.Output
tf.Method = taskfile.Method
tf.Includes = taskfile.Includes
tf.Vars = taskfile.Vars
tf.Env = taskfile.Env
tf.Tasks = taskfile.Tasks
tf.Silent = taskfile.Silent
tf.Dotenv = taskfile.Dotenv
tf.Run = taskfile.Run
tf.Interval = taskfile.Interval
if tf.Expansions <= 0 {
tf.Expansions = 2
}
if tf.Vars == nil {
tf.Vars = &Vars{}
}
if tf.Env == nil {
tf.Env = &Vars{}
}
return nil
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into taskfile", node.Line, node.ShortTag())
}
// ParsedVersion returns the version as a float64

View File

@@ -1,7 +1,7 @@
package taskfile
import (
"errors"
"fmt"
"golang.org/x/exp/slices"
"gopkg.in/yaml.v3"
@@ -13,26 +13,27 @@ type Vars struct {
Mapping map[string]Var
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (vs *Vars) UnmarshalYAML(node *yaml.Node) error {
if node.Kind != yaml.MappingNode {
return errors.New("task: vars is not a map")
}
switch node.Kind {
// NOTE(@andreynering): on this style of custom unmarsheling,
// even number contains the keys, while odd numbers contains
// the values.
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
case yaml.MappingNode:
// NOTE(@andreynering): on this style of custom unmarshalling,
// even number contains the keys, while odd numbers contains
// the values.
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
var v Var
if err := valueNode.Decode(&v); err != nil {
return err
var v Var
if err := valueNode.Decode(&v); err != nil {
return err
}
vs.Set(keyNode.Value, v)
}
vs.Set(keyNode.Value, v)
return nil
}
return nil
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into variables", node.Line, node.ShortTag())
}
// DeepCopy creates a new instance of Vars and copies
@@ -116,20 +117,27 @@ type Var struct {
Dir string
}
// UnmarshalYAML implements yaml.Unmarshaler interface.
func (v *Var) UnmarshalYAML(unmarshal func(interface{}) error) error {
var str string
if err := unmarshal(&str); err == nil {
func (v *Var) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.ScalarNode:
var str string
if err := node.Decode(&str); err != nil {
return err
}
v.Static = str
return nil
case yaml.MappingNode:
var sh struct {
Sh string
}
if err := node.Decode(&sh); err != nil {
return err
}
v.Sh = sh.Sh
return nil
}
var sh struct {
Sh string
}
if err := unmarshal(&sh); err != nil {
return err
}
v.Sh = sh.Sh
return nil
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into variable", node.Line, node.ShortTag())
}

55
testdata/platforms/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
version: '3'
tasks:
build-windows:
platforms: [windows]
cmds:
- echo 'Running task on windows'
build-darwin:
platforms: [darwin]
cmds:
- echo 'Running task on darwin'
build-linux:
platforms: [linux]
cmds:
- echo 'Running task on linux'
build-freebsd:
platforms: [freebsd]
cmds:
- echo 'Running task on freebsd'
build-blank-os:
platforms: []
cmds:
- echo 'Running command'
build-multiple:
platforms: []
cmds:
- cmd: echo 'Running command'
- cmd: echo 'Running on Windows'
platforms: [windows]
- cmd: echo 'Running on Darwin'
platforms: [darwin]
build-amd64:
platforms: [amd64]
cmds:
- echo "Running command on amd64"
build-arm64:
platforms: [arm64]
cmds:
- echo "Running command on arm64"
build-mixed:
cmds:
- cmd: echo 'building on windows/arm64'
platforms: [windows/arm64]
- cmd: echo 'building on linux/amd64'
platforms: [linux/amd64]
- cmd: echo 'building on darwin'
platforms: [darwin]

View File

@@ -0,0 +1,14 @@
version: '3'
silent: true
tasks:
pipefail:
cmds:
- cmd: set -o | grep pipefail
set: [pipefail]
globstar:
cmds:
- cmd: shopt | grep globstar
shopt: [globstar]

View File

@@ -0,0 +1,14 @@
version: '3'
silent: true
set: [pipefail]
shopt: [globstar]
tasks:
pipefail:
cmds:
- set -o | grep pipefail
globstar:
cmds:
- shopt | grep globstar

14
testdata/shopts/task_level/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
version: '3'
silent: true
tasks:
pipefail:
set: [pipefail]
cmds:
- set -o | grep pipefail
globstar:
shopt: [globstar]
cmds:
- shopt | grep globstar

View File

@@ -16,3 +16,4 @@ tasks:
- echo root/TASK={{.TASK}}
- echo root/ROOT_DIR={{.ROOT_DIR}}
- echo root/TASKFILE_DIR={{.TASKFILE_DIR}}
- echo root/TASK_VERSION={{.TASK_VERSION}}

View File

@@ -6,3 +6,4 @@ tasks:
- echo included/TASK={{.TASK}}
- echo included/ROOT_DIR={{.ROOT_DIR}}
- echo included/TASKFILE_DIR={{.TASKFILE_DIR}}
- echo included/TASK_VERSION={{.TASK_VERSION}}

View File

@@ -56,6 +56,8 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
Sources: r.ReplaceSlice(origTask.Sources),
Generates: r.ReplaceSlice(origTask.Generates),
Dir: r.Replace(origTask.Dir),
Set: origTask.Set,
Shopt: origTask.Shopt,
Vars: nil,
Env: nil,
Dotenv: r.ReplaceSlice(origTask.Dotenv),
@@ -68,6 +70,7 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
Run: r.Replace(origTask.Run),
IncludeVars: origTask.IncludeVars,
IncludedTaskfileVars: origTask.IncludedTaskfileVars,
Platforms: origTask.Platforms,
}
new.Dir, err = execext.Expand(new.Dir)
if err != nil {
@@ -124,12 +127,15 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
continue
}
new.Cmds = append(new.Cmds, &taskfile.Cmd{
Task: r.Replace(cmd.Task),
Silent: cmd.Silent,
Cmd: r.Replace(cmd.Cmd),
Silent: cmd.Silent,
Task: r.Replace(cmd.Task),
Set: cmd.Set,
Shopt: cmd.Shopt,
Vars: r.ReplaceVars(cmd.Vars),
IgnoreError: cmd.IgnoreError,
Defer: cmd.Defer,
Platforms: cmd.Platforms,
})
}
}

View File

@@ -10,10 +10,11 @@ import (
"syscall"
"time"
"github.com/radovskyb/watcher"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/status"
"github.com/go-task/task/v3/taskfile"
"github.com/radovskyb/watcher"
)
const defaultWatchInterval = 5 * time.Second
@@ -37,23 +38,14 @@ func (e *Executor) watchTasks(calls ...taskfile.Call) error {
}()
}
var watchIntervalString string
if e.Interval != "" {
watchIntervalString = e.Interval
} else if e.Taskfile.Interval != "" {
watchIntervalString = e.Taskfile.Interval
}
watchInterval := defaultWatchInterval
if watchIntervalString != "" {
var err error
watchInterval, err = parseWatchInterval(watchIntervalString)
if err != nil {
cancel()
return err
}
var watchInterval time.Duration
switch {
case e.Interval != 0:
watchInterval = e.Interval
case e.Taskfile.Interval != 0:
watchInterval = e.Taskfile.Interval
default:
watchInterval = defaultWatchInterval
}
e.Logger.VerboseOutf(logger.Green, "task: Watching for changes every %v", watchInterval)
@@ -185,11 +177,3 @@ func (e *Executor) registerWatchedFiles(w *watcher.Watcher, calls ...taskfile.Ca
func shouldIgnoreFile(path string) bool {
return strings.Contains(path, "/.git") || strings.Contains(path, "/.task") || strings.Contains(path, "/node_modules")
}
func parseWatchInterval(watchInterval string) (time.Duration, error) {
v, err := time.ParseDuration(watchInterval)
if err != nil {
return 0, fmt.Errorf(`task: Could not parse watch interval "%s": %v`, watchInterval, err)
}
return v, nil
}

80
watch_test.go Normal file
View File

@@ -0,0 +1,80 @@
//go:build watch
// +build watch
package task_test
import (
"bytes"
"context"
"os"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/taskfile"
)
func TestFileWatcherInterval(t *testing.T) {
const dir = "testdata/watcher_interval"
expectedOutput := strings.TrimSpace(`
task: Started watching for tasks: default
task: [default] echo "Hello, World!"
Hello, World!
task: [default] echo "Hello, World!"
Hello, World!
`)
var buff bytes.Buffer
e := &task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
Watch: true,
}
assert.NoError(t, e.Setup())
buff.Reset()
err := os.MkdirAll(filepathext.SmartJoin(dir, "src"), 0755)
assert.NoError(t, err)
err = os.WriteFile(filepathext.SmartJoin(dir, "src/a"), []byte("test"), 0644)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
err := e.Run(ctx, taskfile.Call{Task: "default"})
if err != nil {
return
}
}
}
}(ctx)
time.Sleep(10 * time.Millisecond)
err = os.WriteFile(filepathext.SmartJoin(dir, "src/a"), []byte("test updated"), 0644)
if err != nil {
t.Fatal(err)
}
time.Sleep(700 * time.Millisecond)
cancel()
assert.Equal(t, expectedOutput, strings.TrimSpace(buff.String()))
buff.Reset()
err = os.RemoveAll(filepathext.SmartJoin(dir, ".task"))
assert.NoError(t, err)
err = os.RemoveAll(filepathext.SmartJoin(dir, "src"))
assert.NoError(t, err)
}