Compare commits

...

51 Commits

Author SHA1 Message Date
Andrey Nering
cd086228b2 v3.42.0 2025-03-08 22:34:07 -03:00
Andrey Nering
1b8b399c7e fix(changelog): add missing # to issue number 2025-03-08 22:32:55 -03:00
renovate[bot]
8426f84b18 chore(deps): update all non-major dependencies (#2097)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-07 08:24:04 +01:00
sblondon
14bbb324e5 doc: fix: remove a word (#2093)
The 'you' word has no sense here
2025-02-27 17:29:52 +00:00
Valentin Maerten
b9d202c491 chore: changelog for #2092 2025-02-26 18:07:31 +01:00
Valentin Maerten
c23c46e326 fix: include with dynamic vars (#2092) 2025-02-26 17:49:05 +01:00
Oleksandr Redko
a266fba93e chore: add linter mirror (#2060) 2025-02-24 09:06:54 -03:00
Pete Davison
fb631902ce refactor: run task through modernize tool (#2088) 2025-02-24 11:59:50 +00:00
Pete Davison
b14125bacd fix: remove debug line 2025-02-24 02:16:53 +00:00
Pete Davison
3c5782f4a4 chore: changelog for #2084 2025-02-23 18:31:18 +00:00
Pete Davison
60c8ee0ce6 refactor: ast.Call should be in main task package (#2084) 2025-02-23 18:30:42 +00:00
Pete Davison
cdaf69e03d chore: changelog for #2069 2025-02-23 18:18:22 +00:00
Pete Davison
d6234af49a feat: allow variable references in a matrix (#2069) 2025-02-23 18:13:56 +00:00
renovate[bot]
a31f2cf4a8 chore(deps): update all non-major dependencies (#2064) 2025-02-23 15:10:00 -03:00
Pete Davison
0dd6f78855 chore: changelog for #2086 2025-02-23 18:00:00 +00:00
Pete Davison
6f80777faf docs: getting started (#2086)
* docs: getting started

* docs: update intro with links to getting started docs
2025-02-23 17:56:55 +00:00
Valentin Maerten
8558e0c48a chore: changelog for #1982 2025-02-23 10:54:45 +01:00
Valentin Maerten
461714a899 feat: add a new .taskrc.yml to enable experiments (#1982) 2025-02-23 10:51:59 +01:00
Pete Davison
8a35033abc chore: changelog for #1798 2025-02-22 16:27:34 +00:00
Pete Davison
daf39a04bf feat: iterators (#1798)
* feat: update to github.com/elliotchance/orderedmap/v3

* refactor: better sort package

* feat: iterators

* chore: remove unnecessary code
2025-02-22 16:22:03 +00:00
Pete Davison
25f9299d0a chore: changelog for #2082 2025-02-22 16:09:53 +00:00
Pete Davison
4d15a8be8f feat: remove logger from taskfile package (#2082)
* refactor: remove logger from the taskfile node interface

* refactor: functional options on taskfile.Reader

* feat: use pass in debug/prompt functions to Reader rather than task Logger

* chore: reader docstrings

* fix: typo
2025-02-22 16:00:37 +00:00
Pete Davison
cbde4c33f8 chore: changelog for #2068 and #2052 2025-02-22 15:58:47 +00:00
Pete Davison
cdb6a3f70a feat: decoding improvements (#2068)
* refactor: moved/simplified snippets into its own file with tests

* refactor: move snippet to taskfile package

* feat: support snippets with line/col = 0

* feat: functional options for snippets

* feat: added option to hide snippet indicators

* feat: store raw lines for length calculations

* feat: add debug function for TaskfileDecodeError

* fix: decode errors from commands

* fix: schema for defer cmd calls

* fix: linting issues

* refactor: split var and vars into different files like other structures
2025-02-22 15:44:22 +00:00
Valentin Maerten
fb27318601 chore: changelog for #2052 2025-02-20 20:08:22 +01:00
Valentin Maerten
35ea4e0460 feat: display allowed values when vars are not provided (#2052) 2025-02-20 20:08:14 +01:00
Pete Davison
2b4d9bfba7 chore: changelog for #2059 2025-02-11 22:44:33 +00:00
Pete Davison
ce96447468 chore: bump minimum version to 1.23 (#2059)
* chore: bump minimum version to 1.23

* fix: version package for 1.24

* feat: update golangci-lint version
2025-02-11 22:43:17 +00:00
Pete Davison
e7a6de64cb chore: add package API changes to changelog and add gorelease tool (#2055)
* chore: add package API changes to changelog and add gorelease tool

* chore: use bullet points instead of a paragraph
2025-02-10 16:16:44 +00:00
Pete Davison
ff8c913ce7 chore: changelog and minor adjustments for #2018 2025-02-10 11:24:32 +00:00
Henrique Corrêa
0e23404d23 feat: specify --init filename/path (#2018)
* feat: specify init filename with --taskfile flag

previously, it was not possible to specify which filename to use when initializing a new Taskfile as it was hardcoded as "Taskfile.yml".

now the --taskfile flag specifies where to write the file to, and the first * contained in it will be replaced by "Taskfile", so `task -it *.yaml` will create a `Taskfile.yaml` file.

* docs: update CLI reference

* fix Flags header being inside tip admonition
* change -t flag's default column and add a description
* add Default Filenames section

* docs: revert adding Default Filenames section

I didn't realize it already existed elsewhere.

* refactor: use path instead of filepath on InitTaskFile

as requested to prevent ambiguity with the stdlib package.

* fix TestInit (incorrectly merged)

* docs: remove outdated info on --taskfile flag

* refactor task initialization changes

- remove const DefaultTaskInitFilename from taskfile/taskfile.go
- revert description of Entrypoint flag
- make InitTaskfile accept a path to either a file or a directory, and join the default Taskfile name+ext to it if it is a directory
- take the target file path from the first argument instead of the Entrypoint flag
- detect extension-only filenames (".yaml") instead of replacing "*" with "Taskfile"
- use different format in success log so that it makes sense at different paths than the current dir

* print colon instead of "at"

it's a lot cleaner in most cases.

* rewrite init tests

test both initializing to a directory path and a file path

* return final path from InitTaskfile

...and print it's relative representation

* fix lint error (ineffassign)

* use filepathext.TryAbsToRel() instead

* define and use filepathext.IsExtOnly()

* link to default filenames list in cli ref docs

(specifically in the --taskfile flag description)
2025-02-10 11:22:49 +00:00
renovate[bot]
65a64a01ee chore(deps): update all non-major dependencies (#2054)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 07:53:45 -03:00
Pete Davison
f6ec7444d5 chore: changelog for #2049 2025-02-08 23:16:41 +00:00
Pete Davison
6ce798e16c feat: experiments logging improvements (#2049)
* feat: warn when enabling inactive experiments

* feat: TASK_ environment prefix

* feat: calculate experiment enabled/active instead of storing

* refactor: rename GetTaskVar to GetTaskEnv

* feat: experiments tests
2025-02-08 23:02:51 +00:00
Pete Davison
be81885835 feat: stop task test installing task (#2050) 2025-02-08 23:02:22 +00:00
Valentin Maerten
69ac06170a chore: changelog for #2031 2025-02-08 17:34:43 +01:00
Valentin Maerten
c995fe6d11 fix(checker): use only one checker at the same time to improve perf (#2031)
* fix(checker): use only one checker at the same time to improve performance

* refactor

* fix test
2025-02-08 17:34:04 +01:00
Valentin Maerten
9009124192 chore: changelog for #2033 2025-02-08 17:31:01 +01:00
Valentin Maerten
80f96d67da fix: requires allowed values works with dynamic var (#2033) 2025-02-08 17:29:36 +01:00
Valentin Maerten
002b8c929a docs: fix a typo in dotenv section 2025-02-08 16:13:34 +01:00
Pete Davison
b5b1524d3a feat: variable inheritance tests (#2038) 2025-02-05 19:51:52 +00:00
renovate[bot]
3aee0a0519 chore(deps): update react monorepo to v19 (#2028)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-31 10:32:44 +01:00
Pete Davison
23df1f0c61 chore: changelog for #2007 2025-01-29 22:49:14 +00:00
Ukjae Jeong
edbb83f6de fix: HTTPNode.Location when building graph (#2007)
* Fix HTTPNode.Location when building graph

* Add test and fix cache
2025-01-29 22:46:43 +00:00
Pete Davison
c903d5c6f4 chore: changelog for #2011 2025-01-29 22:43:51 +00:00
Henrique Corrêa
88c4ba1740 feat: make Taskfile initialization less verbose by default (#2011)
* change what is printed when creating Taskfile

When using --init to create a new Taskfile, it used to print the whole contents of the file to the terminal, which was unnecessarily verbose (and honestly felt unintentional).

Now only the filename is printed by default and the --silent and --verbose flags can be used to control the behavior (print nothing or content + filename, respectively).

* include additional new line with -i -v

it looks slightly better in the terminal.

* print init success text in green

* fix TestInit, create and pass in a logger

* move logging outside of InitTaskfile

- revert API changes made to InitTaskfile
- make consts in init.go public so they can be accessed from task.go
- rename variable "logger" to "log" in task.go to fix conflict with logger package

* move TestInit into init_test.go file

as requested by pd93.
2025-01-29 22:41:17 +00:00
Valentin Maerten
7d4c52546a chore: add label to renovate's PRs 2025-01-29 21:46:37 +01:00
Pete Davison
f5121de468 docs: broken links 2025-01-26 00:56:25 +00:00
renovate[bot]
b5d573fbd9 chore(deps): update golang's deps (#2020)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-25 09:22:10 +01:00
renovate[bot]
888de0f8ef chore(deps): update website (#2021)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-25 09:14:26 +01:00
Valentin Maerten
09b11d343b chore: remove dependabot and put Renovate weekly (#2017) 2025-01-25 09:06:53 +01:00
151 changed files with 4534 additions and 2361 deletions

View File

@@ -1,24 +0,0 @@
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly
day: saturday
time: '08:00'
timezone: America/Sao_Paulo
labels:
- "area: dependencies"
- "lang: go"
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: saturday
time: '08:00'
timezone: America/Sao_Paulo
labels:
- "area: dependencies"
- "lang: javascript"

39
.github/renovate.json vendored
View File

@@ -3,46 +3,23 @@
"extends": [ "extends": [
"config:recommended", "config:recommended",
"group:allNonMajor", "group:allNonMajor",
"schedule:monthly" "schedule:weekly",
":semanticCommitTypeAll(chore)"
], ],
"mode": "full", "mode": "full",
"reviewers": ["team:developer"], "addLabels":["area: dependencies"],
"packageRules": [ "packageRules": [
{ {
"matchManagers": ["github-actions"], "matchManagers": ["github-actions"],
"groupName": "Github Action", "addLabels": ["area: github actions"]
"labels": ["area: github actions", "area: dependencies"],
"matchPackageNames": [
"*"
],
"matchUpdateTypes": [
"minor",
"patch"
]
}, },
{ {
"matchManagers": ["npm", "nvm"], "matchCategories": ["js", "node"],
"groupName": "Website", "addLabels": ["lang: javascript"]
"labels": ["lang: javascript", "area: dependencies"],
"matchPackageNames": [
"*"
],
"matchUpdateTypes": [
"minor",
"patch"
]
}, },
{ {
"matchManagers": ["gomod"], "matchCategories": ["golang"],
"groupName": "golang", "addLabels": ["lang: go"]
"labels": ["lang: go", "area: dependencies"],
"matchPackageNames": [
"*"
],
"matchUpdateTypes": [
"minor",
"patch"
]
} }
] ]
} }

View File

@@ -13,7 +13,7 @@ jobs:
name: Lint name: Lint
strategy: strategy:
matrix: matrix:
go-version: [1.22.x, 1.23.x] go-version: [1.23.x, 1.24.x]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
@@ -25,7 +25,7 @@ jobs:
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v6 uses: golangci/golangci-lint-action@v6
with: with:
version: v1.60.1 version: v1.64.2
lint-jsonschema: lint-jsonschema:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -15,7 +15,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: 1.22.x go-version: 1.23.x
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v6

View File

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

View File

@@ -9,10 +9,11 @@ linters:
- goimports - goimports
- gofmt - gofmt
- gofumpt - gofumpt
- mirror
- misspell - misspell
- noctx - noctx
- paralleltest - paralleltest
- tenv - usetesting
- thelper - thelper
- tparallel - tparallel
@@ -29,6 +30,8 @@ linters-settings:
desc: "Use github.com/go-task/task/v3/errors instead" desc: "Use github.com/go-task/task/v3/errors instead"
goimports: goimports:
local-prefixes: github.com/go-task local-prefixes: github.com/go-task
gofumpt:
module-path: github.com/go-task/task/v3
gofmt: gofmt:
rewrite-rules: rewrite-rules:
- pattern: 'interface{}' - pattern: 'interface{}'

2
.nvmrc
View File

@@ -1 +1 @@
22.12.0 22.14.0

View File

@@ -1,5 +1,88 @@
# Changelog # Changelog
## v3.42.0 - 2025-03-08
- Made `--init` less verbose by default and respect `--silent` and `--verbose`
flags (#2009, #2011 by @HeCorr).
- `--init` now accepts a file name or directory as an argument (#2008, #2018 by
@HeCorr).
- Fix a bug where an HTTP node's location was being mutated incorrectly (#2007
by @jeongukjae).
- Fixed a bug where allowed values didn't work with dynamic var (#2032, #2033 by
@vmaerten).
- Use only the relevant checker (timestamp or checksum) to improve performance
(#2029, #2031 by @vmaerten).
- Print warnings when attempting to enable an inactive experiment or an active
experiment with an invalid value (#1979, #2049 by @pd93).
- Refactored the experiments package and added tests (#2049 by @pd93).
- Show allowed values when a variable with an enum is missing (#2027, #2052 by
@vmaerten).
- Refactored how snippets in error work and added tests (#2068 by @pd93).
- Fixed a bug where errors decoding commands were sometimes unhelpful (#2068 by
@pd93).
- Fixed a bug in the Taskfile schema where `defer` statements in the shorthand
`cmds` syntax were not considered valid (#2068 by @pd93).
- Refactored how task sorting functions work (#1798 by @pd93).
- Added a new `.taskrc.yml` (or `.taskrc.yaml`) file to let users enable
experiments (similar to `.env`) (#1982 by @vmaerten).
- Added new [Getting Started docs](https://taskfile.dev/getting-started) (#2086
by @pd93).
- Allow `matrix` to use references to other variables (#2065, #2069 by @pd93).
- Fixed a bug where, when a dynamic variable is provided, even if it is not
used, all other variables become unavailable in the templating system within
the include (#2092 by @vmaerten).
#### Package API
Unlike our CLI tool,
[Task's package API is not currently stable](https://taskfile.dev/reference/package).
In an effort to ease the pain of breaking changes for our users, we will be
providing changelogs for our package API going forwards. The hope is that these
changes will provide a better long-term experience for our users and allow to
stabilize the API in the future. #121 now tracks this piece of work.
- Bumped the minimum required Go version to 1.23 (#2059 by @pd93).
- [`task.InitTaskfile`](https://pkg.go.dev/github.com/go-task/task/v3#InitTaskfile)
(#2011, ff8c913 by @HeCorr and @pd93)
- No longer accepts an `io.Writer` (output is now the caller's
responsibility).
- The path argument can now be a filename OR a directory.
- The function now returns the full path of the generated file.
- [`TaskfileDecodeError.WithFileInfo`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskfileDecodeError.WithFileInfo)
now accepts a string instead of the arguments required to generate a snippet
(#2068 by @pd93).
- The caller is now expected to create the snippet themselves (see below).
- [`TaskfileSnippet`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Snippet)
and related code moved from the `errors` package to the `taskfile` package
(#2068 by @pd93).
- Renamed `TaskMissingRequiredVars` to
[`TaskMissingRequiredVarsError`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskMissingRequiredVarsError)
(#2052 by @vmaerten).
- Renamed `TaskNotAllowedVars` to
[`TaskNotAllowedVarsError`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskNotAllowedVarsError)
(#2052 by @vmaerten).
- The
[`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)
is now constructed using the functional options pattern (#2082 by @pd93).
- Removed our internal `logger.Logger` from the entire `taskfile` package (#2082
by @pd93).
- Users are now expected to pass a custom debug/prompt functions into
[`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)
if they want this functionality by using the new
[`WithDebugFunc`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#WithDebugFunc)
and
[`WithPromptFunc`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#WithPromptFunc)
functional options.
- Remove `Range` functions in the `taskfile/ast` package in favour of new
iterator functions (#1798 by @pd93).
- `ast.Call` was moved from the `taskfile/ast` package to the main `task`
package (#2084 by @pd93).
- `ast.Tasks.FindMatchingTasks` was moved from the `taskfile/ast` package to the
`task.Executor.FindMatchingTasks` in the main `task` package (#2084 by @pd93).
- The `Compiler` and its `GetVariables` and `FastGetVariables` methods were
moved from the `internal/compiler` package to the main `task` package (#2084
by @pd93).
## v3.41.0 - 2025-01-18 ## v3.41.0 - 2025-01-18
- Fixed an issue where dynamic variables were not properly logged in verbose - Fixed an issue where dynamic variables were not properly logged in verbose

View File

@@ -98,21 +98,17 @@ tasks:
test: test:
desc: Runs test suite desc: Runs test suite
aliases: [t] aliases: [t]
deps: [install] sources:
- "**/*.go"
- "testdata/**/*"
cmds: cmds:
- go test {{catLines .GO_PACKAGES}} - go test ./...
vars:
GO_PACKAGES:
sh: go list ./...
test:all: test:all:
desc: Runs test suite with signals and watch tests included desc: Runs test suite with signals and watch tests included
deps: [install, sleepit:build] deps: [sleepit:build]
cmds: cmds:
- go test {{catLines .GO_PACKAGES}} -tags 'signals watch' - go test -tags 'signals watch' ./...
vars:
GO_PACKAGES:
sh: go list ./...
goreleaser:test: goreleaser:test:
desc: Tests release process without publishing desc: Tests release process without publishing
@@ -124,6 +120,22 @@ tasks:
cmds: cmds:
- go install github.com/goreleaser/goreleaser/v2@latest - go install github.com/goreleaser/goreleaser/v2@latest
gorelease:install:
desc: "Installs gorelease: https://pkg.go.dev/golang.org/x/exp/cmd/gorelease"
status:
- command -v gorelease
cmds:
- go install golang.org/x/exp/cmd/gorelease@latest
api:check:
desc: Checks what changes have been made to the public API
deps: [gorelease:install]
vars:
LATEST:
sh: git describe --tags --abbrev=0
cmds:
- gorelease -base={{.LATEST}}
release:*: release:*:
desc: Prepare the project for a new release desc: Prepare the project for a new release
summary: | summary: |
@@ -176,11 +188,3 @@ tasks:
desc: Publish release to npm desc: Publish release to npm
cmds: cmds:
- npm publish --access=public - npm publish --access=public
packages:
cmds:
- echo '{{.GO_PACKAGES}}'
vars:
GO_PACKAGES:
sh: go list ./...
silent: true

View File

@@ -3,17 +3,18 @@ package args
import ( import (
"strings" "strings"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
// Parse parses command line argument: tasks and global variables // Parse parses command line argument: tasks and global variables
func Parse(args ...string) ([]*ast.Call, *ast.Vars) { func Parse(args ...string) ([]*task.Call, *ast.Vars) {
calls := []*ast.Call{} calls := []*task.Call{}
globals := ast.NewVars() globals := ast.NewVars()
for _, arg := range args { for _, arg := range args {
if !strings.Contains(arg, "=") { if !strings.Contains(arg, "=") {
calls = append(calls, &ast.Call{Task: arg}) calls = append(calls, &task.Call{Task: arg})
continue continue
} }

View File

@@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/args" "github.com/go-task/task/v3/args"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
@@ -15,12 +16,12 @@ func TestArgs(t *testing.T) {
tests := []struct { tests := []struct {
Args []string Args []string
ExpectedCalls []*ast.Call ExpectedCalls []*task.Call
ExpectedGlobals *ast.Vars ExpectedGlobals *ast.Vars
}{ }{
{ {
Args: []string{"task-a", "task-b", "task-c"}, Args: []string{"task-a", "task-b", "task-c"},
ExpectedCalls: []*ast.Call{ ExpectedCalls: []*task.Call{
{Task: "task-a"}, {Task: "task-a"},
{Task: "task-b"}, {Task: "task-b"},
{Task: "task-c"}, {Task: "task-c"},
@@ -28,7 +29,7 @@ func TestArgs(t *testing.T) {
}, },
{ {
Args: []string{"task-a", "FOO=bar", "task-b", "task-c", "BAR=baz", "BAZ=foo"}, Args: []string{"task-a", "FOO=bar", "task-b", "task-c", "BAR=baz", "BAZ=foo"},
ExpectedCalls: []*ast.Call{ ExpectedCalls: []*task.Call{
{Task: "task-a"}, {Task: "task-a"},
{Task: "task-b"}, {Task: "task-b"},
{Task: "task-c"}, {Task: "task-c"},
@@ -56,7 +57,7 @@ func TestArgs(t *testing.T) {
}, },
{ {
Args: []string{"task-a", "CONTENT=with some spaces"}, Args: []string{"task-a", "CONTENT=with some spaces"},
ExpectedCalls: []*ast.Call{ ExpectedCalls: []*task.Call{
{Task: "task-a"}, {Task: "task-a"},
}, },
ExpectedGlobals: ast.NewVars( ExpectedGlobals: ast.NewVars(
@@ -70,7 +71,7 @@ func TestArgs(t *testing.T) {
}, },
{ {
Args: []string{"FOO=bar", "task-a", "task-b"}, Args: []string{"FOO=bar", "task-a", "task-b"},
ExpectedCalls: []*ast.Call{ ExpectedCalls: []*task.Call{
{Task: "task-a"}, {Task: "task-a"},
{Task: "task-b"}, {Task: "task-b"},
}, },
@@ -85,15 +86,15 @@ func TestArgs(t *testing.T) {
}, },
{ {
Args: nil, Args: nil,
ExpectedCalls: []*ast.Call{}, ExpectedCalls: []*task.Call{},
}, },
{ {
Args: []string{}, Args: []string{},
ExpectedCalls: []*ast.Call{}, ExpectedCalls: []*task.Call{},
}, },
{ {
Args: []string{"FOO=bar", "BAR=baz"}, Args: []string{"FOO=bar", "BAR=baz"},
ExpectedCalls: []*ast.Call{}, ExpectedCalls: []*task.Call{},
ExpectedGlobals: ast.NewVars( ExpectedGlobals: ast.NewVars(
&ast.VarElement{ &ast.VarElement{
Key: "FOO", Key: "FOO",

View File

@@ -1,9 +1,11 @@
package ast package task
import "github.com/go-task/task/v3/taskfile/ast"
// Call is the parameters to a task call // Call is the parameters to a task call
type Call struct { type Call struct {
Task string Task string
Vars *Vars Vars *ast.Vars
Silent bool Silent bool
Indirect bool // True if the task was called by another task Indirect bool // True if the task was called by another task
} }

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"path/filepath"
"strings" "strings"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@@ -13,6 +14,7 @@ import (
"github.com/go-task/task/v3/args" "github.com/go-task/task/v3/args"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/experiments" "github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/flags" "github.com/go-task/task/v3/internal/flags"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/sort" "github.com/go-task/task/v3/internal/sort"
@@ -44,7 +46,7 @@ func main() {
} }
func run() error { func run() error {
logger := &logger.Logger{ log := &logger.Logger{
Stdout: os.Stdout, Stdout: os.Stdout,
Stderr: os.Stderr, Stderr: os.Stderr,
Verbose: flags.Verbose, Verbose: flags.Verbose,
@@ -69,7 +71,7 @@ func run() error {
} }
if flags.Experiments { if flags.Experiments {
return experiments.List(logger) return log.PrintExperiments()
} }
if flags.Init { if flags.Init {
@@ -77,9 +79,28 @@ func run() error {
if err != nil { if err != nil {
return err return err
} }
if err := task.InitTaskfile(os.Stdout, wd); err != nil { args, _, err := getArgs()
if err != nil {
return err return err
} }
path := wd
if len(args) > 0 {
name := args[0]
if filepathext.IsExtOnly(name) {
name = filepathext.SmartJoin(filepath.Dir(name), "Taskfile"+filepath.Ext(name))
}
path = filepathext.SmartJoin(wd, name)
}
finalPath, err := task.InitTaskfile(path)
if err != nil {
return err
}
if !flags.Silent {
if flags.Verbose {
log.Outf(logger.Default, "%s\n", task.DefaultTaskfile)
}
log.Outf(logger.Green, "Taskfile created: %s\n", filepathext.TryAbsToRel(finalPath))
}
return nil return nil
} }
@@ -100,12 +121,16 @@ func run() error {
dir = home dir = home
} }
var taskSorter sort.TaskSorter if err := experiments.Validate(); err != nil {
log.Warnf("%s\n", err.Error())
}
var taskSorter sort.Sorter
switch flags.TaskSort { switch flags.TaskSort {
case "none": case "none":
taskSorter = &sort.Noop{} taskSorter = nil
case "alphanumeric": case "alphanumeric":
taskSorter = &sort.AlphaNumeric{} taskSorter = sort.AlphaNumeric
} }
e := task.Executor{ e := task.Executor{
@@ -145,9 +170,6 @@ func run() error {
if err != nil { if err != nil {
return err return err
} }
if experiments.AnyVariables.Enabled {
logger.Warnf("The 'Any Variables' experiment flag is no longer required to use non-map variable types. If you wish to use map variables, please use 'TASK_X_MAP_VARIABLES' instead. See https://github.com/go-task/task/issues/1585\n")
}
// If the download flag is specified, we should stop execution as soon as // If the download flag is specified, we should stop execution as soon as
// taskfile is downloaded // taskfile is downloaded
@@ -179,7 +201,7 @@ func run() error {
} }
var ( var (
calls []*ast.Call calls []*task.Call
globals *ast.Vars globals *ast.Vars
) )
@@ -192,7 +214,7 @@ func run() error {
// If there are no calls, run the default task instead // If there are no calls, run the default task instead
if len(calls) == 0 { if len(calls) == 0 {
calls = append(calls, &ast.Call{Task: "default"}) calls = append(calls, &task.Call{Task: "default"})
} }
globals.Set("CLI_ARGS", ast.Var{Value: cliArgs}) globals.Set("CLI_ARGS", ast.Var{Value: cliArgs})

View File

@@ -1,4 +1,4 @@
package compiler package task
import ( import (
"bytes" "bytes"
@@ -36,16 +36,16 @@ func (c *Compiler) GetTaskfileVariables() (*ast.Vars, error) {
return c.getVariables(nil, nil, true) return c.getVariables(nil, nil, true)
} }
func (c *Compiler) GetVariables(t *ast.Task, call *ast.Call) (*ast.Vars, error) { func (c *Compiler) GetVariables(t *ast.Task, call *Call) (*ast.Vars, error) {
return c.getVariables(t, call, true) return c.getVariables(t, call, true)
} }
func (c *Compiler) FastGetVariables(t *ast.Task, call *ast.Call) (*ast.Vars, error) { func (c *Compiler) FastGetVariables(t *ast.Task, call *Call) (*ast.Vars, error) {
return c.getVariables(t, call, false) return c.getVariables(t, call, false)
} }
func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool) (*ast.Vars, error) { func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) {
result := GetEnviron() result := env.GetEnviron()
specialVars, err := c.getSpecialVars(t, call) specialVars, err := c.getSpecialVars(t, call)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -103,30 +103,42 @@ func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool
taskRangeFunc = getRangeFunc(dir) taskRangeFunc = getRangeFunc(dir)
} }
if err := c.TaskfileEnv.Range(rangeFunc); err != nil { for k, v := range c.TaskfileEnv.All() {
return nil, err if err := rangeFunc(k, v); err != nil {
}
if err := c.TaskfileVars.Range(rangeFunc); err != nil {
return nil, err
}
if t != nil {
if err := t.IncludeVars.Range(rangeFunc); err != nil {
return nil, err return nil, err
} }
if err := t.IncludedTaskfileVars.Range(taskRangeFunc); err != nil { }
for k, v := range c.TaskfileVars.All() {
if err := rangeFunc(k, v); err != nil {
return nil, err return nil, err
} }
} }
if t != nil {
for k, v := range t.IncludeVars.All() {
if err := rangeFunc(k, v); err != nil {
return nil, err
}
}
for k, v := range t.IncludedTaskfileVars.All() {
if err := taskRangeFunc(k, v); err != nil {
return nil, err
}
}
}
if t == nil || call == nil { if t == nil || call == nil {
return result, nil return result, nil
} }
if err := call.Vars.Range(rangeFunc); err != nil { for k, v := range call.Vars.All() {
return nil, err if err := rangeFunc(k, v); err != nil {
return nil, err
}
} }
if err := t.Vars.Range(taskRangeFunc); err != nil { for k, v := range t.Vars.All() {
return nil, err if err := taskRangeFunc(k, v); err != nil {
return nil, err
}
} }
return result, nil return result, nil
@@ -184,7 +196,7 @@ func (c *Compiler) ResetCache() {
c.dynamicCache = nil c.dynamicCache = nil
} }
func (c *Compiler) getSpecialVars(t *ast.Task, call *ast.Call) (map[string]string, error) { func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, error) {
allVars := map[string]string{ allVars := map[string]string{
"TASK_EXE": filepath.ToSlash(os.Args[0]), "TASK_EXE": filepath.ToSlash(os.Args[0]),
"ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint), "ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint),

View File

@@ -2,36 +2,18 @@ package errors
import ( import (
"bytes" "bytes"
"embed" "cmp"
"errors" "errors"
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/quick"
"github.com/alecthomas/chroma/v2/styles"
"github.com/fatih/color" "github.com/fatih/color"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
//go:embed themes/*.xml
var embedded embed.FS
var typeErrorRegex = regexp.MustCompile(`line \d+: (.*)`) var typeErrorRegex = regexp.MustCompile(`line \d+: (.*)`)
func init() {
r, err := embedded.Open("themes/task.xml")
if err != nil {
panic(err)
}
style, err := chroma.NewXMLStyle(r)
if err != nil {
panic(err)
}
styles.Register(style)
}
type ( type (
TaskfileDecodeError struct { TaskfileDecodeError struct {
Message string Message string
@@ -39,15 +21,9 @@ type (
Line int Line int
Column int Column int
Tag string Tag string
Snippet TaskfileSnippet Snippet string
Err error Err error
} }
TaskfileSnippet struct {
Lines []string
StartLine int
EndLine int
Padding int
}
) )
func NewTaskfileDecodeError(err error, node *yaml.Node) *TaskfileDecodeError { func NewTaskfileDecodeError(err error, node *yaml.Node) *TaskfileDecodeError {
@@ -88,38 +64,44 @@ func (err *TaskfileDecodeError) Error() string {
} }
} }
fmt.Fprintln(buf, color.RedString("file: %s:%d:%d", err.Location, err.Line, err.Column)) fmt.Fprintln(buf, color.RedString("file: %s:%d:%d", err.Location, err.Line, err.Column))
fmt.Fprint(buf, err.Snippet)
return buf.String()
}
// Print the snippet func (err *TaskfileDecodeError) Debug() string {
maxLineNumberDigits := digits(err.Snippet.EndLine) const indentWidth = 2
lineNumberSpacer := strings.Repeat(" ", maxLineNumberDigits) buf := &bytes.Buffer{}
columnSpacer := strings.Repeat(" ", err.Column-1) fmt.Fprintln(buf, "TaskfileDecodeError:")
for i, line := range err.Snippet.Lines {
currentLine := err.Snippet.StartLine + i + 1
lineIndicator := " " // Recursively loop through the error chain and print any details
if currentLine == err.Line { var debug func(error, int)
lineIndicator = ">" debug = func(err error, indent int) {
} indentStr := strings.Repeat(" ", indent*indentWidth)
columnIndicator := "^"
// Print each line // Nothing left to unwrap
lineIndicator = color.RedString(lineIndicator) if err == nil {
columnIndicator = color.RedString(columnIndicator) fmt.Fprintf(buf, "%sEnd of chain\n", indentStr)
lineNumberFormat := fmt.Sprintf("%%%dd", maxLineNumberDigits) return
lineNumber := fmt.Sprintf(lineNumberFormat, currentLine)
fmt.Fprintf(buf, "%s %s | %s", lineIndicator, lineNumber, line)
// Print the column indicator
if currentLine == err.Line {
fmt.Fprintf(buf, "\n %s | %s%s", lineNumberSpacer, columnSpacer, columnIndicator)
} }
// If there are more lines to print, add a newline // Taskfile decode error
if i < len(err.Snippet.Lines)-1 { decodeErr := &TaskfileDecodeError{}
fmt.Fprintln(buf) if errors.As(err, &decodeErr) {
fmt.Fprintf(buf, "%s%s (%s:%d:%d)\n",
indentStr,
cmp.Or(decodeErr.Message, "<no_message>"),
decodeErr.Location,
decodeErr.Line,
decodeErr.Column,
)
debug(errors.Unwrap(err), indent+1)
return
} }
fmt.Fprintf(buf, "%s%s\n", indentStr, err)
debug(errors.Unwrap(err), indent+1)
} }
debug(err, 0)
return buf.String() return buf.String()
} }
@@ -141,23 +123,9 @@ func (err *TaskfileDecodeError) WithTypeMessage(t string) *TaskfileDecodeError {
return err return err
} }
func (err *TaskfileDecodeError) WithFileInfo(location string, b []byte, padding int) *TaskfileDecodeError { func (err *TaskfileDecodeError) WithFileInfo(location string, snippet string) *TaskfileDecodeError {
buf := &bytes.Buffer{}
if err := quick.Highlight(buf, string(b), "yaml", "terminal", "task"); err != nil {
buf.WriteString(string(b))
}
lines := strings.Split(buf.String(), "\n")
start := max(err.Line-1-padding, 0)
end := min(err.Line+padding, len(lines)-1)
err.Location = location err.Location = location
err.Snippet = TaskfileSnippet{ err.Snippet = snippet
Lines: lines[start:end],
StartLine: start,
EndLine: end,
Padding: padding,
}
return err return err
} }
@@ -168,12 +136,3 @@ func extractTypeErrorMessage(message string) string {
} }
return message return message
} }
func digits(number int) int {
count := 0
for number != 0 {
number /= 10
count += 1
}
return count
}

View File

@@ -141,21 +141,37 @@ func (err *TaskCancelledNoTerminalError) Code() int {
return CodeTaskCancelled return CodeTaskCancelled
} }
// TaskMissingRequiredVars is returned when a task is missing required variables. // TaskMissingRequiredVarsError is returned when a task is missing required variables.
type TaskMissingRequiredVars struct {
type MissingVar struct {
Name string
AllowedValues []string
}
type TaskMissingRequiredVarsError struct {
TaskName string TaskName string
MissingVars []string MissingVars []MissingVar
} }
func (err *TaskMissingRequiredVars) Error() string { func (v MissingVar) String() string {
if len(v.AllowedValues) == 0 {
return v.Name
}
return fmt.Sprintf("%s (allowed values: %v)", v.Name, v.AllowedValues)
}
func (err *TaskMissingRequiredVarsError) Error() string {
var vars []string
for _, v := range err.MissingVars {
vars = append(vars, v.String())
}
return fmt.Sprintf( return fmt.Sprintf(
`task: Task %q cancelled because it is missing required variables: %s`, `task: Task %q cancelled because it is missing required variables: %s`,
err.TaskName, err.TaskName,
strings.Join(err.MissingVars, ", "), strings.Join(vars, ", "))
)
} }
func (err *TaskMissingRequiredVars) Code() int { func (err *TaskMissingRequiredVarsError) Code() int {
return CodeTaskMissingRequiredVars return CodeTaskMissingRequiredVars
} }
@@ -165,12 +181,12 @@ type NotAllowedVar struct {
Name string Name string
} }
type TaskNotAllowedVars struct { type TaskNotAllowedVarsError struct {
TaskName string TaskName string
NotAllowedVars []NotAllowedVar NotAllowedVars []NotAllowedVar
} }
func (err *TaskNotAllowedVars) Error() string { func (err *TaskNotAllowedVarsError) Error() string {
var builder strings.Builder var builder strings.Builder
builder.WriteString(fmt.Sprintf("task: Task %q cancelled because it is missing required variables:\n", err.TaskName)) builder.WriteString(fmt.Sprintf("task: Task %q cancelled because it is missing required variables:\n", err.TaskName))
@@ -181,6 +197,6 @@ func (err *TaskNotAllowedVars) Error() string {
return builder.String() return builder.String()
} }
func (err *TaskNotAllowedVars) Code() int { func (err *TaskNotAllowedVarsError) Code() int {
return CodeTaskNotAllowedVars return CodeTaskNotAllowedVars
} }

47
go.mod
View File

@@ -1,61 +1,62 @@
module github.com/go-task/task/v3 module github.com/go-task/task/v3
go 1.22.0 go 1.23.0
require ( require (
github.com/Ladicle/tabwriter v1.0.0 github.com/Ladicle/tabwriter v1.0.0
github.com/Masterminds/semver/v3 v3.3.1 github.com/Masterminds/semver/v3 v3.3.1
github.com/alecthomas/chroma/v2 v2.14.0 github.com/alecthomas/chroma/v2 v2.15.0
github.com/chainguard-dev/git-urls v1.0.2 github.com/chainguard-dev/git-urls v1.0.2
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/dominikbraun/graph v0.23.0 github.com/dominikbraun/graph v0.23.0
github.com/elliotchance/orderedmap/v2 v2.7.0 github.com/elliotchance/orderedmap/v3 v3.1.0
github.com/fatih/color v1.18.0 github.com/fatih/color v1.18.0
github.com/go-git/go-billy/v5 v5.6.1 github.com/go-git/go-billy/v5 v5.6.2
github.com/go-git/go-git/v5 v5.13.1 github.com/go-git/go-git/v5 v5.14.0
github.com/go-task/slim-sprig/v3 v3.0.0 github.com/go-task/slim-sprig/v3 v3.0.0
github.com/go-task/template v0.1.0 github.com/go-task/template v0.1.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mattn/go-zglob v0.0.6 github.com/mattn/go-zglob v0.0.6
github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/otiai10/copy v1.14.0 github.com/otiai10/copy v1.14.1
github.com/radovskyb/watcher v1.0.7 github.com/radovskyb/watcher v1.0.7
github.com/sajari/fuzzy v1.0.0 github.com/sajari/fuzzy v1.0.0
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/zeebo/xxh3 v1.0.2 github.com/zeebo/xxh3 v1.0.2
golang.org/x/sync v0.10.0 golang.org/x/sync v0.12.0
golang.org/x/term v0.27.0 golang.org/x/term v0.30.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/v3 v3.10.0 mvdan.cc/sh/v3 v3.11.0
) )
require ( require (
dario.cat/mergo v1.0.0 // indirect dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.3 // indirect github.com/ProtonMail/go-crypto v1.1.5 // indirect
github.com/cloudflare/circl v1.3.7 // indirect github.com/cloudflare/circl v1.6.0 // indirect
github.com/cyphar/filepath-securejoin v0.3.6 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/otiai10/mint v1.6.3 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect github.com/skeema/knownhosts v1.3.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.31.0 // indirect golang.org/x/crypto v0.35.0 // indirect
golang.org/x/mod v0.18.0 // indirect golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.33.0 // indirect golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.28.0 // indirect golang.org/x/sys v0.31.0 // indirect
golang.org/x/tools v0.22.0 // indirect golang.org/x/tools v0.27.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
) )

94
go.sum
View File

@@ -7,12 +7,14 @@ github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lpr
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc=
github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
@@ -23,21 +25,25 @@ github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiw
github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o= github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ= github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM=
github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
github.com/elliotchance/orderedmap/v2 v2.7.0 h1:WHuf0DRo63uLnldCPp9ojm3gskYwEdIIfAUVG5KhoOc= github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
github.com/elliotchance/orderedmap/v2 v2.7.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q= github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
@@ -46,12 +52,14 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA= github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE= github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M= github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0=
github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc= github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
@@ -60,6 +68,8 @@ github.com/go-task/template v0.1.0 h1:ym/r2G937RZA1bsgiWedNnY9e5kxDT+3YcoAnuIetT
github.com/go-task/template v0.1.0/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k= github.com/go-task/template v0.1.0/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
@@ -92,12 +102,12 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -113,8 +123,10 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
@@ -129,17 +141,24 @@ github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -149,17 +168,22 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -172,3 +196,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/sh/v3 v3.10.0 h1:v9z7N1DLZ7owyLM/SXZQkBSXcwr2IGMm2LY2pmhVXj4= mvdan.cc/sh/v3 v3.10.0 h1:v9z7N1DLZ7owyLM/SXZQkBSXcwr2IGMm2LY2pmhVXj4=
mvdan.cc/sh/v3 v3.10.0/go.mod h1:z/mSSVyLFGZzqb3ZIKojjyqIx/xbmz/UHdCSv9HmqXY= mvdan.cc/sh/v3 v3.10.0/go.mod h1:z/mSSVyLFGZzqb3ZIKojjyqIx/xbmz/UHdCSv9HmqXY=
mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw=
mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg=

View File

@@ -128,18 +128,14 @@ func (e *Executor) ListTaskNames(allTasks bool) error {
w = e.Stdout w = e.Stdout
} }
// Get the list of tasks and sort them
tasks := e.Taskfile.Tasks.Values()
// Sort the tasks // Sort the tasks
if e.TaskSorter == nil { if e.TaskSorter == nil {
e.TaskSorter = &sort.AlphaNumericWithRootTasksFirst{} e.TaskSorter = sort.AlphaNumericWithRootTasksFirst
} }
e.TaskSorter.Sort(tasks)
// Create a list of task names // Create a list of task names
taskNames := make([]string, 0, e.Taskfile.Tasks.Len()) taskNames := make([]string, 0, e.Taskfile.Tasks.Len())
for _, task := range tasks { for task := range e.Taskfile.Tasks.Values(e.TaskSorter) {
if (allTasks || task.Desc != "") && !task.Internal { if (allTasks || task.Desc != "") && !task.Internal {
taskNames = append(taskNames, strings.TrimRight(task.Task, ":")) taskNames = append(taskNames, strings.TrimRight(task.Task, ":"))
for _, alias := range task.Aliases { for _, alias := range task.Aliases {

38
init.go
View File

@@ -1,15 +1,13 @@
package task package task
import ( import (
"fmt"
"io"
"os" "os"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
) )
const defaultTaskfile = `# https://taskfile.dev const DefaultTaskfile = `# https://taskfile.dev
version: '3' version: '3'
@@ -23,19 +21,31 @@ tasks:
silent: true silent: true
` `
const defaultTaskfileName = "Taskfile.yml" const defaultTaskFilename = "Taskfile.yml"
// InitTaskfile Taskfile creates a new Taskfile // InitTaskfile creates a new Taskfile at path.
func InitTaskfile(w io.Writer, dir string) error { //
f := filepathext.SmartJoin(dir, defaultTaskfileName) // path can be either a file path or a directory path.
// If path is a directory, path/Taskfile.yml will be created.
if _, err := os.Stat(f); err == nil { //
return errors.TaskfileAlreadyExistsError{} // The final file path is always returned and may be different from the input path.
func InitTaskfile(path string) (string, error) {
fi, err := os.Stat(path)
if err == nil && !fi.IsDir() {
return path, errors.TaskfileAlreadyExistsError{}
} }
if err := os.WriteFile(f, []byte(defaultTaskfile), 0o644); err != nil { if fi != nil && fi.IsDir() {
return err path = filepathext.SmartJoin(path, defaultTaskFilename)
// path was a directory, so check if Taskfile.yml exists in it
if _, err := os.Stat(path); err == nil {
return path, errors.TaskfileAlreadyExistsError{}
}
} }
fmt.Fprintf(w, "%s created in the current directory\n", defaultTaskfile)
return nil if err := os.WriteFile(path, []byte(DefaultTaskfile), 0o644); err != nil {
return path, err
}
return path, nil
} }

52
init_test.go Normal file
View File

@@ -0,0 +1,52 @@
package task_test
import (
"os"
"testing"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/internal/filepathext"
)
func TestInitDir(t *testing.T) {
t.Parallel()
const dir = "testdata/init"
file := filepathext.SmartJoin(dir, "Taskfile.yml")
_ = os.Remove(file)
if _, err := os.Stat(file); err == nil {
t.Errorf("Taskfile.yml should not exist")
}
if _, err := task.InitTaskfile(dir); err != nil {
t.Error(err)
}
if _, err := os.Stat(file); err != nil {
t.Errorf("Taskfile.yml should exist")
}
_ = os.Remove(file)
}
func TestInitFile(t *testing.T) {
t.Parallel()
const dir = "testdata/init"
file := filepathext.SmartJoin(dir, "Tasks.yml")
_ = os.Remove(file)
if _, err := os.Stat(file); err == nil {
t.Errorf("Tasks.yml should not exist")
}
if _, err := task.InitTaskfile(file); err != nil {
t.Error(err)
}
if _, err := os.Stat(file); err != nil {
t.Errorf("Tasks.yml should exist")
}
_ = os.Remove(file)
}

View File

@@ -1,20 +0,0 @@
package compiler
import (
"os"
"strings"
"github.com/go-task/task/v3/taskfile/ast"
)
// GetEnviron the all return all environment variables encapsulated on a
// ast.Vars
func GetEnviron() *ast.Vars {
m := ast.NewVars()
for _, e := range os.Environ() {
keyVal := strings.SplitN(e, "=", 2)
key, val := keyVal[0], keyVal[1]
m.Set(key, ast.Var{Value: val})
}
return m
}

View File

@@ -3,7 +3,7 @@ package deepcopy
import ( import (
"reflect" "reflect"
"github.com/elliotchance/orderedmap/v2" "github.com/elliotchance/orderedmap/v3"
) )
type Copier[T any] interface { type Copier[T any] interface {

21
internal/env/env.go vendored
View File

@@ -3,11 +3,26 @@ package env
import ( import (
"fmt" "fmt"
"os" "os"
"strings"
"github.com/go-task/task/v3/internal/experiments" "github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
const taskVarPrefix = "TASK_"
// GetEnviron the all return all environment variables encapsulated on a
// ast.Vars
func GetEnviron() *ast.Vars {
m := ast.NewVars()
for _, e := range os.Environ() {
keyVal := strings.SplitN(e, "=", 2)
key, val := keyVal[0], keyVal[1]
m.Set(key, ast.Var{Value: val})
}
return m
}
func Get(t *ast.Task) []string { func Get(t *ast.Task) []string {
if t.Env == nil { if t.Env == nil {
return nil return nil
@@ -23,7 +38,7 @@ func GetFromVars(env *ast.Vars) []string {
if !isTypeAllowed(v) { if !isTypeAllowed(v) {
continue continue
} }
if !experiments.EnvPrecedence.Enabled { if !experiments.EnvPrecedence.Enabled() {
if _, alreadySet := os.LookupEnv(k); alreadySet { if _, alreadySet := os.LookupEnv(k); alreadySet {
continue continue
} }
@@ -42,3 +57,7 @@ func isTypeAllowed(v any) bool {
return false return false
} }
} }
func GetTaskEnv(key string) string {
return os.Getenv(taskVarPrefix + key)
}

View File

@@ -0,0 +1,35 @@
package experiments
import (
"fmt"
"strconv"
"strings"
"github.com/go-task/task/v3/internal/slicesext"
)
type InvalidValueError struct {
Name string
AllowedValues []int
Value int
}
func (err InvalidValueError) Error() string {
return fmt.Sprintf(
"task: Experiment %q has an invalid value %q (allowed values: %s)",
err.Name,
err.Value,
strings.Join(slicesext.Convert(err.AllowedValues, strconv.Itoa), ", "),
)
}
type InactiveError struct {
Name string
}
func (err InactiveError) Error() string {
return fmt.Sprintf(
"task: Experiment %q is inactive and cannot be enabled",
err.Name,
)
}

View File

@@ -0,0 +1,62 @@
package experiments
import (
"fmt"
"slices"
"strconv"
)
type Experiment struct {
Name string // The name of the experiment.
AllowedValues []int // The values that can enable this experiment.
Value int // The version of the experiment that is enabled.
}
// New creates a new experiment with the given name and sets the values that can
// enable it.
func New(xName string, allowedValues ...int) Experiment {
value := experimentConfig.Experiments[xName]
if value == 0 {
value, _ = strconv.Atoi(getEnv(xName))
}
x := Experiment{
Name: xName,
AllowedValues: allowedValues,
Value: value,
}
xList = append(xList, x)
return x
}
func (x Experiment) Enabled() bool {
return slices.Contains(x.AllowedValues, x.Value)
}
func (x Experiment) Active() bool {
return len(x.AllowedValues) > 0
}
func (x Experiment) Valid() error {
if !x.Active() && x.Value != 0 {
return &InactiveError{
Name: x.Name,
}
}
if !x.Enabled() && x.Value != 0 {
return &InvalidValueError{
Name: x.Name,
AllowedValues: x.AllowedValues,
Value: x.Value,
}
}
return nil
}
func (x Experiment) String() string {
if x.Enabled() {
return fmt.Sprintf("on (%d)", x.Value)
}
return "off"
}

View File

@@ -0,0 +1,75 @@
package experiments_test
import (
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/go-task/task/v3/internal/experiments"
)
func TestNew(t *testing.T) {
const (
exampleExperiment = "EXAMPLE"
exampleExperimentEnv = "TASK_X_EXAMPLE"
)
tests := []struct {
name string
allowedValues []int
value int
wantEnabled bool
wantActive bool
wantValid error
}{
{
name: `[] allowed, value=""`,
wantEnabled: false,
wantActive: false,
},
{
name: `[] allowed, value="1"`,
value: 1,
wantEnabled: false,
wantActive: false,
wantValid: &experiments.InactiveError{
Name: exampleExperiment,
},
},
{
name: `[1] allowed, value=""`,
allowedValues: []int{1},
wantEnabled: false,
wantActive: true,
},
{
name: `[1] allowed, value="1"`,
allowedValues: []int{1},
value: 1,
wantEnabled: true,
wantActive: true,
},
{
name: `[1] allowed, value="2"`,
allowedValues: []int{1},
value: 2,
wantEnabled: false,
wantActive: true,
wantValid: &experiments.InvalidValueError{
Name: exampleExperiment,
AllowedValues: []int{1},
Value: 2,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv(exampleExperimentEnv, strconv.Itoa(tt.value))
x := experiments.New(exampleExperiment, tt.allowedValues...)
assert.Equal(t, exampleExperiment, x.Name)
assert.Equal(t, tt.wantEnabled, x.Enabled())
assert.Equal(t, tt.wantActive, x.Active())
assert.Equal(t, tt.wantValid, x.Valid())
})
}
}

View File

@@ -2,28 +2,28 @@ package experiments
import ( import (
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"github.com/Ladicle/tabwriter" "github.com/Masterminds/semver/v3"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/internal/logger"
) )
const envPrefix = "TASK_X_" const envPrefix = "TASK_X_"
type Experiment struct { var defaultConfigFilenames = []string{
Name string ".taskrc.yml",
Enabled bool ".taskrc.yaml",
Value string }
type experimentConfigFile struct {
Experiments map[string]int `yaml:"experiments"`
Version *semver.Version
} }
// A list of experiments.
var ( var (
GentleForce Experiment GentleForce Experiment
RemoteTaskfiles Experiment RemoteTaskfiles Experiment
@@ -32,32 +32,35 @@ var (
EnvPrecedence Experiment EnvPrecedence Experiment
) )
// An internal list of all the initialized experiments used for iterating.
var (
xList []Experiment
experimentConfig experimentConfigFile
)
func init() { func init() {
readDotEnv() readDotEnv()
GentleForce = New("GENTLE_FORCE") experimentConfig = readConfig()
RemoteTaskfiles = New("REMOTE_TASKFILES") GentleForce = New("GENTLE_FORCE", 1)
AnyVariables = New("ANY_VARIABLES", "1", "2") RemoteTaskfiles = New("REMOTE_TASKFILES", 1)
MapVariables = New("MAP_VARIABLES", "1", "2") AnyVariables = New("ANY_VARIABLES")
EnvPrecedence = New("ENV_PRECEDENCE") MapVariables = New("MAP_VARIABLES", 1, 2)
EnvPrecedence = New("ENV_PRECEDENCE", 1)
} }
func New(xName string, enabledValues ...string) Experiment { // Validate checks if any experiments have been enabled while being inactive.
if len(enabledValues) == 0 { // If one is found, the function returns an error.
enabledValues = []string{"1"} func Validate() error {
} for _, x := range List() {
value := getEnv(xName) if err := x.Valid(); err != nil {
return Experiment{ return err
Name: xName, }
Enabled: slices.Contains(enabledValues, value),
Value: value,
} }
return nil
} }
func (x Experiment) String() string { func List() []Experiment {
if x.Enabled { return xList
return fmt.Sprintf("on (%s)", x.Value)
}
return "off"
} }
func getEnv(xName string) string { func getEnv(xName string) string {
@@ -65,7 +68,7 @@ func getEnv(xName string) string {
return os.Getenv(envName) return os.Getenv(envName)
} }
func getEnvFilePath() string { func getFilePath(filename string) string {
// Parse the CLI flags again to get the directory/taskfile being run // Parse the CLI flags again to get the directory/taskfile being run
// We use a flagset here so that we can parse a subset of flags without exiting on error. // We use a flagset here so that we can parse a subset of flags without exiting on error.
var dir, taskfile string var dir, taskfile string
@@ -76,18 +79,18 @@ func getEnvFilePath() string {
_ = fs.Parse(os.Args[1:]) _ = fs.Parse(os.Args[1:])
// If the directory is set, find a .env file in that directory. // If the directory is set, find a .env file in that directory.
if dir != "" { if dir != "" {
return filepath.Join(dir, ".env") return filepath.Join(dir, filename)
} }
// If the taskfile is set, find a .env file in the directory containing the Taskfile. // If the taskfile is set, find a .env file in the directory containing the Taskfile.
if taskfile != "" { if taskfile != "" {
return filepath.Join(filepath.Dir(taskfile), ".env") return filepath.Join(filepath.Dir(taskfile), filename)
} }
// Otherwise just use the current working directory. // Otherwise just use the current working directory.
return ".env" return filename
} }
func readDotEnv() { func readDotEnv() {
env, _ := godotenv.Read(getEnvFilePath()) env, _ := godotenv.Read(getFilePath(".env"))
// If the env var is an experiment, set it. // If the env var is an experiment, set it.
for key, value := range env { for key, value := range env {
if strings.HasPrefix(key, envPrefix) { if strings.HasPrefix(key, envPrefix) {
@@ -96,17 +99,26 @@ func readDotEnv() {
} }
} }
func printExperiment(w io.Writer, l *logger.Logger, x Experiment) { func readConfig() experimentConfigFile {
l.FOutf(w, logger.Yellow, "* ") var cfg experimentConfigFile
l.FOutf(w, logger.Green, x.Name)
l.FOutf(w, logger.Default, ": \t%s\n", x.String())
}
func List(l *logger.Logger) error { var content []byte
w := tabwriter.NewWriter(os.Stdout, 0, 8, 0, ' ', 0) var err error
printExperiment(w, l, GentleForce) for _, filename := range defaultConfigFilenames {
printExperiment(w, l, RemoteTaskfiles) path := getFilePath(filename)
printExperiment(w, l, MapVariables) content, err = os.ReadFile(path)
printExperiment(w, l, EnvPrecedence) if err == nil {
return w.Flush() break
}
}
if err != nil {
return experimentConfigFile{}
}
if err := yaml.Unmarshal(content, &cfg); err != nil {
return experimentConfigFile{}
}
return cfg
} }

View File

@@ -55,3 +55,9 @@ func TryAbsToRel(abs string) string {
return rel return rel
} }
// IsExtOnly checks whether path points to a file with no name but with
// an extension, i.e. ".yaml"
func IsExtOnly(path string) bool {
return filepath.Base(path) == filepath.Ext(path)
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/experiments" "github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
@@ -79,7 +80,7 @@ func init() {
log.Print(usage) log.Print(usage)
pflag.PrintDefaults() pflag.PrintDefaults()
} }
offline, err := strconv.ParseBool(cmp.Or(os.Getenv("TASK_OFFLINE"), "false")) offline, err := strconv.ParseBool(cmp.Or(env.GetTaskEnv("OFFLINE"), "false"))
if err != nil { if err != nil {
offline = false offline = false
} }
@@ -115,7 +116,7 @@ func init() {
pflag.BoolVar(&Experiments, "experiments", false, "Lists all the available experiments and whether or not they are enabled.") pflag.BoolVar(&Experiments, "experiments", false, "Lists all the available experiments and whether or not they are enabled.")
// Gentle force experiment will override the force flag and add a new force-all flag // Gentle force experiment will override the force flag and add a new force-all flag
if experiments.GentleForce.Enabled { if experiments.GentleForce.Enabled() {
pflag.BoolVarP(&Force, "force", "f", false, "Forces execution of the directly called task.") pflag.BoolVarP(&Force, "force", "f", false, "Forces execution of the directly called task.")
pflag.BoolVar(&ForceAll, "force-all", false, "Forces execution of the called task and all its dependant tasks.") pflag.BoolVar(&ForceAll, "force-all", false, "Forces execution of the called task and all its dependant tasks.")
} else { } else {
@@ -123,7 +124,7 @@ func init() {
} }
// Remote Taskfiles experiment will adds the "download" and "offline" flags // Remote Taskfiles experiment will adds the "download" and "offline" flags
if experiments.RemoteTaskfiles.Enabled { if experiments.RemoteTaskfiles.Enabled() {
pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.") pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.")
pflag.BoolVar(&Offline, "offline", offline, "Forces Task to only use local or cached Taskfiles.") pflag.BoolVar(&Offline, "offline", offline, "Forces Task to only use local or cached Taskfiles.")
pflag.DurationVar(&Timeout, "timeout", time.Second*10, "Timeout for downloading remote Taskfiles.") pflag.DurationVar(&Timeout, "timeout", time.Second*10, "Timeout for downloading remote Taskfiles.")

View File

@@ -8,9 +8,12 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/Ladicle/tabwriter"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/internal/term" "github.com/go-task/task/v3/internal/term"
) )
@@ -19,70 +22,86 @@ var (
ErrNoTerminal = errors.New("no terminal") ErrNoTerminal = errors.New("no terminal")
) )
var (
attrsReset = envColor("COLOR_RESET", color.Reset)
attrsFgBlue = envColor("COLOR_BLUE", color.FgBlue)
attrsFgGreen = envColor("COLOR_GREEN", color.FgGreen)
attrsFgCyan = envColor("COLOR_CYAN", color.FgCyan)
attrsFgYellow = envColor("COLOR_YELLOW", color.FgYellow)
attrsFgMagenta = envColor("COLOR_MAGENTA", color.FgMagenta)
attrsFgRed = envColor("COLOR_RED", color.FgRed)
attrsFgHiBlue = envColor("COLOR_BRIGHT_BLUE", color.FgHiBlue)
attrsFgHiGreen = envColor("COLOR_BRIGHT_GREEN", color.FgHiGreen)
attrsFgHiCyan = envColor("COLOR_BRIGHT_CYAN", color.FgHiCyan)
attrsFgHiYellow = envColor("COLOR_BRIGHT_YELLOW", color.FgHiYellow)
attrsFgHiMagenta = envColor("COLOR_BRIGHT_MAGENTA", color.FgHiMagenta)
attrsFgHiRed = envColor("COLOR_BRIGHT_RED", color.FgHiRed)
)
type ( type (
Color func() PrintFunc Color func() PrintFunc
PrintFunc func(io.Writer, string, ...any) PrintFunc func(io.Writer, string, ...any)
) )
func Default() PrintFunc { func Default() PrintFunc {
return color.New(envColor("TASK_COLOR_RESET", color.Reset)...).FprintfFunc() return color.New(attrsReset...).FprintfFunc()
} }
func Blue() PrintFunc { func Blue() PrintFunc {
return color.New(envColor("TASK_COLOR_BLUE", color.FgBlue)...).FprintfFunc() return color.New(attrsFgBlue...).FprintfFunc()
} }
func Green() PrintFunc { func Green() PrintFunc {
return color.New(envColor("TASK_COLOR_GREEN", color.FgGreen)...).FprintfFunc() return color.New(attrsFgGreen...).FprintfFunc()
} }
func Cyan() PrintFunc { func Cyan() PrintFunc {
return color.New(envColor("TASK_COLOR_CYAN", color.FgCyan)...).FprintfFunc() return color.New(attrsFgCyan...).FprintfFunc()
} }
func Yellow() PrintFunc { func Yellow() PrintFunc {
return color.New(envColor("TASK_COLOR_YELLOW", color.FgYellow)...).FprintfFunc() return color.New(attrsFgYellow...).FprintfFunc()
} }
func Magenta() PrintFunc { func Magenta() PrintFunc {
return color.New(envColor("TASK_COLOR_MAGENTA", color.FgMagenta)...).FprintfFunc() return color.New(attrsFgMagenta...).FprintfFunc()
} }
func Red() PrintFunc { func Red() PrintFunc {
return color.New(envColor("TASK_COLOR_RED", color.FgRed)...).FprintfFunc() return color.New(attrsFgRed...).FprintfFunc()
} }
func BrightBlue() PrintFunc { func BrightBlue() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_BLUE", color.FgHiBlue)...).FprintfFunc() return color.New(attrsFgHiBlue...).FprintfFunc()
} }
func BrightGreen() PrintFunc { func BrightGreen() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_GREEN", color.FgHiGreen)...).FprintfFunc() return color.New(attrsFgHiGreen...).FprintfFunc()
} }
func BrightCyan() PrintFunc { func BrightCyan() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_CYAN", color.FgHiCyan)...).FprintfFunc() return color.New(attrsFgHiCyan...).FprintfFunc()
} }
func BrightYellow() PrintFunc { func BrightYellow() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_YELLOW", color.FgHiYellow)...).FprintfFunc() return color.New(attrsFgHiYellow...).FprintfFunc()
} }
func BrightMagenta() PrintFunc { func BrightMagenta() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_MAGENTA", color.FgHiMagenta)...).FprintfFunc() return color.New(attrsFgHiMagenta...).FprintfFunc()
} }
func BrightRed() PrintFunc { func BrightRed() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_RED", color.FgHiRed)...).FprintfFunc() return color.New(attrsFgHiRed...).FprintfFunc()
} }
func envColor(env string, defaultColor color.Attribute) []color.Attribute { func envColor(name string, defaultColor color.Attribute) []color.Attribute {
if os.Getenv("FORCE_COLOR") != "" { if os.Getenv("FORCE_COLOR") != "" {
color.NoColor = false color.NoColor = false
} }
// Fetch the environment variable // Fetch the environment variable
override := os.Getenv(env) override := env.GetTaskEnv(name)
// First, try splitting the string by commas (RGB shortcut syntax) and if it // First, try splitting the string by commas (RGB shortcut syntax) and if it
// matches, then prepend the 256-color foreground escape sequence. // matches, then prepend the 256-color foreground escape sequence.
@@ -195,3 +214,16 @@ func (l *Logger) Prompt(color Color, prompt string, defaultValue string, continu
return nil return nil
} }
func (l *Logger) PrintExperiments() error {
w := tabwriter.NewWriter(l.Stdout, 0, 8, 0, ' ', 0)
for _, x := range experiments.List() {
if !x.Active() {
continue
}
l.FOutf(w, Yellow, "* ")
l.FOutf(w, Green, x.Name)
l.FOutf(w, Default, ": \t%s\n", x.String())
}
return w.Flush()
}

View File

@@ -18,3 +18,15 @@ func UniqueJoin[T cmp.Ordered](ss ...[]T) []T {
slices.Sort(r) slices.Sort(r)
return slices.Compact(r) return slices.Compact(r)
} }
func Convert[T, U any](s []T, f func(T) U) []U {
// Create a new slice with the same length as the input slice
result := make([]U, len(s))
// Convert each element using the provided function
for i, v := range s {
result[i] = f(v)
}
return result
}

View File

@@ -0,0 +1,86 @@
package slicesext
import (
"math"
"strconv"
"testing"
)
func TestConvertIntToString(t *testing.T) {
t.Parallel()
input := []int{1, 2, 3, 4, 5}
expected := []string{"1", "2", "3", "4", "5"}
result := Convert(input, strconv.Itoa)
if len(result) != len(expected) {
t.Errorf("Expected length %d, got %d", len(expected), len(result))
}
for i := range expected {
if result[i] != expected[i] {
t.Errorf("At index %d: expected %v, got %v", i, expected[i], result[i])
}
}
}
func TestConvertStringToInt(t *testing.T) {
t.Parallel()
input := []string{"1", "2", "3", "4", "5"}
expected := []int{1, 2, 3, 4, 5}
result := Convert(input, func(s string) int {
n, _ := strconv.Atoi(s)
return n
})
if len(result) != len(expected) {
t.Errorf("Expected length %d, got %d", len(expected), len(result))
}
for i := range expected {
if result[i] != expected[i] {
t.Errorf("At index %d: expected %v, got %v", i, expected[i], result[i])
}
}
}
func TestConvertFloatToInt(t *testing.T) {
t.Parallel()
input := []float64{1.1, 2.2, 3.7, 4.5, 5.9}
expected := []int{1, 2, 4, 5, 6}
result := Convert(input, func(f float64) int {
return int(math.Round(f))
})
if len(result) != len(expected) {
t.Errorf("Expected length %d, got %d", len(expected), len(result))
}
for i := range expected {
if result[i] != expected[i] {
t.Errorf("At index %d: expected %v, got %v", i, expected[i], result[i])
}
}
}
func TestConvertEmptySlice(t *testing.T) {
t.Parallel()
input := []int{}
result := Convert(input, strconv.Itoa)
if len(result) != 0 {
t.Errorf("Expected empty slice, got length %d", len(result))
}
}
func TestConvertNilSlice(t *testing.T) {
t.Parallel()
var input []int
result := Convert(input, strconv.Itoa)
if result == nil {
t.Error("Expected non-nil empty slice, got nil")
}
if len(result) != 0 {
t.Errorf("Expected empty slice, got length %d", len(result))
}
}

View File

@@ -1,44 +1,39 @@
package sort package sort
import ( import (
"slices"
"sort" "sort"
"strings" "strings"
"github.com/go-task/task/v3/taskfile/ast"
) )
type TaskSorter interface { // A Sorter is any function that sorts a set of tasks.
Sort([]*ast.Task) type Sorter func(items []string, namespaces []string) []string
// AlphaNumeric sorts the JSON output so that tasks are in alpha numeric order
// by task name.
func AlphaNumeric(items []string, namespaces []string) []string {
slices.Sort(items)
return items
} }
type Noop struct{} // AlphaNumericWithRootTasksFirst sorts the JSON output so that tasks are in
// alpha numeric order by task name. It will also ensure that tasks that are not
func (s *Noop) Sort(tasks []*ast.Task) {} // namespaced will be listed before tasks that are. We detect this by searching
// for a ':' in the task name.
type AlphaNumeric struct{} func AlphaNumericWithRootTasksFirst(items []string, namespaces []string) []string {
if len(namespaces) > 0 {
// Tasks that are not namespaced should be listed before tasks that are. return AlphaNumeric(items, namespaces)
// We detect this by searching for a ':' in the task name. }
func (s *AlphaNumeric) Sort(tasks []*ast.Task) { sort.Slice(items, func(i, j int) bool {
sort.Slice(tasks, func(i, j int) bool { iContainsColon := strings.Contains(items[i], ":")
return tasks[i].Task < tasks[j].Task jContainsColon := strings.Contains(items[j], ":")
})
}
type AlphaNumericWithRootTasksFirst struct{}
// Tasks that are not namespaced should be listed before tasks that are.
// We detect this by searching for a ':' in the task name.
func (s *AlphaNumericWithRootTasksFirst) Sort(tasks []*ast.Task) {
sort.Slice(tasks, func(i, j int) bool {
iContainsColon := strings.Contains(tasks[i].Task, ":")
jContainsColon := strings.Contains(tasks[j].Task, ":")
if iContainsColon == jContainsColon { if iContainsColon == jContainsColon {
return tasks[i].Task < tasks[j].Task return items[i] < items[j]
} }
if !iContainsColon && jContainsColon { if !iContainsColon && jContainsColon {
return true return true
} }
return false return false
}) })
return items
} }

View File

@@ -4,39 +4,37 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/go-task/task/v3/taskfile/ast"
) )
func TestAlphaNumericWithRootTasksFirst_Sort(t *testing.T) { func TestAlphaNumericWithRootTasksFirst_Sort(t *testing.T) {
t.Parallel() t.Parallel()
task1 := &ast.Task{Task: "task1"} item1 := "a-item1"
task2 := &ast.Task{Task: "task2"} item2 := "m-item2"
task3 := &ast.Task{Task: "ns1:task3"} item3 := "ns1:item3"
task4 := &ast.Task{Task: "ns2:task4"} item4 := "ns2:item4"
task5 := &ast.Task{Task: "task5"} item5 := "z-item5"
task6 := &ast.Task{Task: "ns3:task6"} item6 := "ns3:item6"
tests := []struct { tests := []struct {
name string name string
tasks []*ast.Task items []string
want []*ast.Task want []string
}{ }{
{ {
name: "no namespace tasks sorted alphabetically first", name: "no namespace items sorted alphabetically first",
tasks: []*ast.Task{task3, task2, task1}, items: []string{item3, item2, item1},
want: []*ast.Task{task1, task2, task3}, want: []string{item1, item2, item3},
}, },
{ {
name: "namespace tasks sorted alphabetically after non-namespaced tasks", name: "namespace items sorted alphabetically after non-namespaced items",
tasks: []*ast.Task{task3, task4, task5}, items: []string{item3, item4, item5},
want: []*ast.Task{task5, task3, task4}, want: []string{item5, item3, item4},
}, },
{ {
name: "all tasks sorted alphabetically with root tasks first", name: "all items sorted alphabetically with root items first",
tasks: []*ast.Task{task6, task5, task4, task3, task2, task1}, items: []string{item6, item5, item4, item3, item2, item1},
want: []*ast.Task{task1, task2, task5, task3, task4, task6}, want: []string{item1, item2, item5, item3, item4, item6},
}, },
} }
@@ -44,9 +42,8 @@ func TestAlphaNumericWithRootTasksFirst_Sort(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
s := &AlphaNumericWithRootTasksFirst{} AlphaNumericWithRootTasksFirst(tt.items, nil)
s.Sort(tt.tasks) assert.Equal(t, tt.want, tt.items)
assert.Equal(t, tt.want, tt.tasks)
}) })
} }
} }
@@ -54,22 +51,22 @@ func TestAlphaNumericWithRootTasksFirst_Sort(t *testing.T) {
func TestAlphaNumeric_Sort(t *testing.T) { func TestAlphaNumeric_Sort(t *testing.T) {
t.Parallel() t.Parallel()
task1 := &ast.Task{Task: "task1"} item1 := "a-item1"
task2 := &ast.Task{Task: "task2"} item2 := "m-item2"
task3 := &ast.Task{Task: "ns1:task3"} item3 := "ns1:item3"
task4 := &ast.Task{Task: "ns2:task4"} item4 := "ns2:item4"
task5 := &ast.Task{Task: "task5"} item5 := "z-item5"
task6 := &ast.Task{Task: "ns3:task6"} item6 := "ns3:item6"
tests := []struct { tests := []struct {
name string name string
tasks []*ast.Task items []string
want []*ast.Task want []string
}{ }{
{ {
name: "all tasks sorted alphabetically", name: "all items sorted alphabetically",
tasks: []*ast.Task{task3, task2, task5, task1, task4, task6}, items: []string{item3, item2, item5, item1, item4, item6},
want: []*ast.Task{task3, task4, task6, task1, task2, task5}, want: []string{item1, item2, item3, item4, item6, item5},
}, },
} }
@@ -77,9 +74,8 @@ func TestAlphaNumeric_Sort(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
s := &AlphaNumeric{} AlphaNumeric(tt.items, nil)
s.Sort(tt.tasks) assert.Equal(t, tt.want, tt.items)
assert.Equal(t, tt.tasks, tt.want)
}) })
} }
} }

View File

@@ -7,10 +7,10 @@ import (
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
func PrintTasks(l *logger.Logger, t *ast.Taskfile, c []*ast.Call) { func PrintTasks(l *logger.Logger, t *ast.Taskfile, c []string) {
for i, call := range c { for i, call := range c {
PrintSpaceBetweenSummaries(l, i) PrintSpaceBetweenSummaries(l, i)
if task, ok := t.Tasks.Get(call.Task); ok { if task, ok := t.Tasks.Get(call); ok {
PrintTask(l, task) PrintTask(l, task)
} }
} }

View File

@@ -179,7 +179,8 @@ func TestPrintAllWithSpaces(t *testing.T) {
summary.PrintTasks(&l, summary.PrintTasks(&l,
&ast.Taskfile{Tasks: tasks}, &ast.Taskfile{Tasks: tasks},
[]*ast.Call{{Task: "t1"}, {Task: "t2"}, {Task: "t3"}}) []string{"t1", "t2", "t3"},
)
assert.True(t, strings.HasPrefix(buffer.String(), "task: t1")) assert.True(t, strings.HasPrefix(buffer.String(), "task: t1"))
assert.Contains(t, buffer.String(), "\n(task does not have description or summary)\n\n\ntask: t2") assert.Contains(t, buffer.String(), "\n(task does not have description or summary)\n\n\ntask: t2")

View File

@@ -1,6 +1,7 @@
package templater package templater
import ( import (
"maps"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
@@ -60,13 +61,9 @@ func init() {
cap += len(m) cap += len(m)
} }
result := make(map[string]any, cap) result := make(map[string]any, cap)
for k, v := range base { maps.Copy(result, base)
result[k] = v
}
for _, m := range v { for _, m := range v {
for k, v := range m { maps.Copy(result, m)
result[k] = v
}
} }
return result return result
}, },
@@ -84,7 +81,5 @@ func init() {
taskFuncs["ExeExt"] = taskFuncs["exeExt"] taskFuncs["ExeExt"] = taskFuncs["exeExt"]
templateFuncs = template.FuncMap(sprig.TxtFuncMap()) templateFuncs = template.FuncMap(sprig.TxtFuncMap())
for k, v := range taskFuncs { maps.Copy(templateFuncs, taskFuncs)
templateFuncs[k] = v
}
} }

View File

@@ -141,10 +141,9 @@ func ReplaceVarsWithExtra(vars *ast.Vars, cache *Cache, extra map[string]any) *a
} }
newVars := ast.NewVars() newVars := ast.NewVars()
_ = vars.Range(func(k string, v ast.Var) error { for k, v := range vars.All() {
newVars.Set(k, ReplaceVarWithExtra(v, cache, extra)) newVars.Set(k, ReplaceVarWithExtra(v, cache, extra))
return nil }
})
return newVars return newVars
} }

View File

@@ -12,7 +12,7 @@ var (
func init() { func init() {
info, ok := debug.ReadBuildInfo() info, ok := debug.ReadBuildInfo()
if !ok || info.Main.Version == "" { if !ok || info.Main.Version == "(devel)" || info.Main.Version == "" {
version = "unknown" version = "unknown"
} else { } else {
if version == "" { if version == "" {

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "@go-task/cli", "name": "@go-task/cli",
"version": "3.41.0", "version": "3.42.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {

View File

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

View File

@@ -12,33 +12,49 @@ func (e *Executor) areTaskRequiredVarsSet(t *ast.Task) error {
return nil return nil
} }
var missingVars []string var missingVars []errors.MissingVar
var notAllowedValuesVars []errors.NotAllowedVar
for _, requiredVar := range t.Requires.Vars { for _, requiredVar := range t.Requires.Vars {
value, ok := t.Vars.Get(requiredVar.Name) _, ok := t.Vars.Get(requiredVar.Name)
if !ok { if !ok {
missingVars = append(missingVars, requiredVar.Name) missingVars = append(missingVars, errors.MissingVar{
} else { Name: requiredVar.Name,
value, isString := value.Value.(string) AllowedValues: requiredVar.Enum,
if isString && requiredVar.Enum != nil && !slices.Contains(requiredVar.Enum, value) { })
notAllowedValuesVars = append(notAllowedValuesVars, errors.NotAllowedVar{
Value: value,
Enum: requiredVar.Enum,
Name: requiredVar.Name,
})
}
} }
} }
if len(missingVars) > 0 { if len(missingVars) > 0 {
return &errors.TaskMissingRequiredVars{ return &errors.TaskMissingRequiredVarsError{
TaskName: t.Name(), TaskName: t.Name(),
MissingVars: missingVars, MissingVars: missingVars,
} }
} }
return nil
}
func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
if t.Requires == nil || len(t.Requires.Vars) == 0 {
return nil
}
var notAllowedValuesVars []errors.NotAllowedVar
for _, requiredVar := range t.Requires.Vars {
varValue, _ := t.Vars.Get(requiredVar.Name)
value, isString := varValue.Value.(string)
if isString && requiredVar.Enum != nil && !slices.Contains(requiredVar.Enum, value) {
notAllowedValuesVars = append(notAllowedValuesVars, errors.NotAllowedVar{
Value: value,
Enum: requiredVar.Enum,
Name: requiredVar.Name,
})
}
}
if len(notAllowedValuesVars) > 0 { if len(notAllowedValuesVars) > 0 {
return &errors.TaskNotAllowedVars{ return &errors.TaskNotAllowedVarsError{
TaskName: t.Name(), TaskName: t.Name(),
NotAllowedVars: notAllowedValuesVars, NotAllowedVars: notAllowedValuesVars,
} }

View File

@@ -13,7 +13,7 @@ import (
"github.com/sajari/fuzzy" "github.com/sajari/fuzzy"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/compiler" "github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
@@ -55,7 +55,7 @@ func (e *Executor) Setup() error {
} }
func (e *Executor) getRootNode() (taskfile.Node, error) { func (e *Executor) getRootNode() (taskfile.Node, error) {
node, err := taskfile.NewRootNode(e.Logger, e.Entrypoint, e.Dir, e.Insecure, e.Timeout) node, err := taskfile.NewRootNode(e.Entrypoint, e.Dir, e.Insecure, e.Timeout)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -64,14 +64,21 @@ func (e *Executor) getRootNode() (taskfile.Node, error) {
} }
func (e *Executor) readTaskfile(node taskfile.Node) error { func (e *Executor) readTaskfile(node taskfile.Node) error {
debugFunc := func(s string) {
e.Logger.VerboseOutf(logger.Magenta, s)
}
promptFunc := func(s string) error {
return e.Logger.Prompt(logger.Yellow, s, "n", "y", "yes")
}
reader := taskfile.NewReader( reader := taskfile.NewReader(
node, node,
e.Insecure, taskfile.WithInsecure(e.Insecure),
e.Download, taskfile.WithDownload(e.Download),
e.Offline, taskfile.WithOffline(e.Offline),
e.Timeout, taskfile.WithTimeout(e.Timeout),
e.TempDir.Remote, taskfile.WithTempDir(e.TempDir.Remote),
e.Logger, taskfile.WithDebugFunc(debugFunc),
taskfile.WithPromptFunc(promptFunc),
) )
graph, err := reader.Read() graph, err := reader.Read()
if err != nil { if err != nil {
@@ -92,12 +99,9 @@ func (e *Executor) setupFuzzyModel() {
model.SetThreshold(1) // because we want to build grammar based on every task name model.SetThreshold(1) // because we want to build grammar based on every task name
var words []string var words []string
for _, taskName := range e.Taskfile.Tasks.Keys() { for name, task := range e.Taskfile.Tasks.All(nil) {
words = append(words, taskName) words = append(words, name)
words = slices.Concat(words, task.Aliases)
for _, task := range e.Taskfile.Tasks.Values() {
words = slices.Concat(words, task.Aliases)
}
} }
model.Train(words) model.Train(words)
@@ -109,13 +113,14 @@ func (e *Executor) setupTempDir() error {
return nil return nil
} }
if os.Getenv("TASK_TEMP_DIR") == "" { tempDir := env.GetTaskEnv("TEMP_DIR")
if tempDir == "" {
e.TempDir = TempDir{ e.TempDir = TempDir{
Remote: filepathext.SmartJoin(e.Dir, ".task"), Remote: filepathext.SmartJoin(e.Dir, ".task"),
Fingerprint: filepathext.SmartJoin(e.Dir, ".task"), Fingerprint: filepathext.SmartJoin(e.Dir, ".task"),
} }
} else if filepath.IsAbs(os.Getenv("TASK_TEMP_DIR")) || strings.HasPrefix(os.Getenv("TASK_TEMP_DIR"), "~") { } else if filepath.IsAbs(tempDir) || strings.HasPrefix(tempDir, "~") {
tempDir, err := execext.Expand(os.Getenv("TASK_TEMP_DIR")) tempDir, err := execext.Expand(tempDir)
if err != nil { if err != nil {
return err return err
} }
@@ -128,14 +133,15 @@ func (e *Executor) setupTempDir() error {
} else { } else {
e.TempDir = TempDir{ e.TempDir = TempDir{
Remote: filepathext.SmartJoin(e.Dir, os.Getenv("TASK_TEMP_DIR")), Remote: filepathext.SmartJoin(e.Dir, tempDir),
Fingerprint: filepathext.SmartJoin(e.Dir, os.Getenv("TASK_TEMP_DIR")), Fingerprint: filepathext.SmartJoin(e.Dir, tempDir),
} }
} }
if os.Getenv("TASK_REMOTE_DIR") != "" { remoteDir := env.GetTaskEnv("REMOTE_DIR")
if filepath.IsAbs(os.Getenv("TASK_REMOTE_DIR")) || strings.HasPrefix(os.Getenv("TASK_REMOTE_DIR"), "~") { if remoteDir != "" {
remoteTempDir, err := execext.Expand(os.Getenv("TASK_REMOTE_DIR")) if filepath.IsAbs(remoteDir) || strings.HasPrefix(remoteDir, "~") {
remoteTempDir, err := execext.Expand(remoteDir)
if err != nil { if err != nil {
return err return err
} }
@@ -191,7 +197,7 @@ func (e *Executor) setupCompiler() error {
} }
} }
e.Compiler = &compiler.Compiler{ e.Compiler = &Compiler{
Dir: e.Dir, Dir: e.Dir,
Entrypoint: e.Entrypoint, Entrypoint: e.Entrypoint,
UserWorkingDir: e.UserWorkingDir, UserWorkingDir: e.UserWorkingDir,
@@ -207,17 +213,21 @@ func (e *Executor) readDotEnvFiles() error {
return nil return nil
} }
env, err := taskfile.Dotenv(e.Compiler, e.Taskfile, e.Dir) vars, err := e.Compiler.GetTaskfileVariables()
if err != nil { if err != nil {
return err return err
} }
err = env.Range(func(key string, value ast.Var) error { env, err := taskfile.Dotenv(vars, e.Taskfile, e.Dir)
if _, ok := e.Taskfile.Env.Get(key); !ok { if err != nil {
e.Taskfile.Env.Set(key, value) return err
}
for k, v := range env.All() {
if _, ok := e.Taskfile.Env.Get(k); !ok {
e.Taskfile.Env.Set(k, v)
} }
return nil }
})
return err return err
} }
@@ -235,7 +245,7 @@ func (e *Executor) setupConcurrencyState() {
e.taskCallCount = make(map[string]*int32, e.Taskfile.Tasks.Len()) e.taskCallCount = make(map[string]*int32, e.Taskfile.Tasks.Len())
e.mkdirMutexMap = make(map[string]*sync.Mutex, e.Taskfile.Tasks.Len()) e.mkdirMutexMap = make(map[string]*sync.Mutex, e.Taskfile.Tasks.Len())
for _, k := range e.Taskfile.Tasks.Keys() { for k := range e.Taskfile.Tasks.Keys(nil) {
e.taskCallCount[k] = new(int32) e.taskCallCount[k] = new(int32)
e.mkdirMutexMap[k] = &sync.Mutex{} e.mkdirMutexMap[k] = &sync.Mutex{}
} }

View File

@@ -9,7 +9,7 @@ import (
) )
// Status returns an error if any the of given tasks is not up-to-date // Status returns an error if any the of given tasks is not up-to-date
func (e *Executor) Status(ctx context.Context, calls ...*ast.Call) error { func (e *Executor) Status(ctx context.Context, calls ...*Call) error {
for _, call := range calls { for _, call := range calls {
// Compile the task // Compile the task

82
task.go
View File

@@ -11,10 +11,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"mvdan.cc/sh/v3/interp"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/compiler"
"github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/fingerprint" "github.com/go-task/task/v3/internal/fingerprint"
@@ -28,6 +25,7 @@ import (
"github.com/sajari/fuzzy" "github.com/sajari/fuzzy"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"mvdan.cc/sh/v3/interp"
) )
const ( const (
@@ -71,10 +69,10 @@ type Executor struct {
Stderr io.Writer Stderr io.Writer
Logger *logger.Logger Logger *logger.Logger
Compiler *compiler.Compiler Compiler *Compiler
Output output.Output Output output.Output
OutputStyle ast.Output OutputStyle ast.Output
TaskSorter sort.TaskSorter TaskSorter sort.Sorter
UserWorkingDir string UserWorkingDir string
EnableVersionCheck bool EnableVersionCheck bool
@@ -87,8 +85,15 @@ type Executor struct {
executionHashesMutex sync.Mutex executionHashesMutex sync.Mutex
} }
// MatchingTask represents a task that matches a given call. It includes the
// task itself and a list of wildcards that were matched.
type MatchingTask struct {
Task *ast.Task
Wildcards []string
}
// Run runs Task // Run runs Task
func (e *Executor) Run(ctx context.Context, calls ...*ast.Call) error { func (e *Executor) Run(ctx context.Context, calls ...*Call) error {
// check if given tasks exist // check if given tasks exist
for _, call := range calls { for _, call := range calls {
task, err := e.GetTask(call) task, err := e.GetTask(call)
@@ -150,7 +155,7 @@ func (e *Executor) Run(ctx context.Context, calls ...*ast.Call) error {
return nil return nil
} }
func (e *Executor) splitRegularAndWatchCalls(calls ...*ast.Call) (regularCalls []*ast.Call, watchCalls []*ast.Call, err error) { func (e *Executor) splitRegularAndWatchCalls(calls ...*Call) (regularCalls []*Call, watchCalls []*Call, err error) {
for _, c := range calls { for _, c := range calls {
t, err := e.GetTask(c) t, err := e.GetTask(c)
if err != nil { if err != nil {
@@ -167,7 +172,7 @@ func (e *Executor) splitRegularAndWatchCalls(calls ...*ast.Call) (regularCalls [
} }
// RunTask runs a task by its name // RunTask runs a task by its name
func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error { func (e *Executor) RunTask(ctx context.Context, call *Call) error {
t, err := e.FastCompiledTask(call) t, err := e.FastCompiledTask(call)
if err != nil { if err != nil {
return err return err
@@ -185,6 +190,11 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
if err != nil { if err != nil {
return err return err
} }
if err := e.areTaskRequiredVarsAllowedValuesSet(t); err != nil {
return err
}
if !e.Watch && atomic.AddInt32(e.taskCallCount[t.Task], 1) >= MaximumTaskCall { if !e.Watch && atomic.AddInt32(e.taskCallCount[t.Task], 1) >= MaximumTaskCall {
return &errors.TaskCalledTooManyTimesError{ return &errors.TaskCalledTooManyTimesError{
TaskName: t.Task, TaskName: t.Task,
@@ -312,7 +322,7 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
for _, d := range t.Deps { for _, d := range t.Deps {
d := d d := d
g.Go(func() error { g.Go(func() error {
err := e.RunTask(ctx, &ast.Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true}) err := e.RunTask(ctx, &Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true})
if err != nil { if err != nil {
return err return err
} }
@@ -323,7 +333,7 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
return g.Wait() return g.Wait()
} }
func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int, deferredExitCode *uint8) { func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, deferredExitCode *uint8) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@@ -348,7 +358,7 @@ func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int, deferredExitC
} }
} }
func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *ast.Call, i int) error { func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i int) error {
cmd := t.Cmds[i] cmd := t.Cmds[i]
switch { switch {
@@ -356,7 +366,7 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *ast.Call,
reacquire := e.releaseConcurrencyLimit() reacquire := e.releaseConcurrencyLimit()
defer reacquire() defer reacquire()
err := e.RunTask(ctx, &ast.Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true}) err := e.RunTask(ctx, &Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true})
if err != nil { if err != nil {
return err return err
} }
@@ -442,12 +452,39 @@ func (e *Executor) startExecution(ctx context.Context, t *ast.Task, execute func
return execute(ctx) return execute(ctx)
} }
// FindMatchingTasks returns a list of tasks that match the given call. A task
// matches a call if its name is equal to the call's task name or if it matches
// a wildcard pattern. The function returns a list of MatchingTask structs, each
// containing a task and a list of wildcards that were matched.
func (e *Executor) FindMatchingTasks(call *Call) []*MatchingTask {
if call == nil {
return nil
}
var matchingTasks []*MatchingTask
// If there is a direct match, return it
if task, ok := e.Taskfile.Tasks.Get(call.Task); ok {
matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil})
return matchingTasks
}
// Attempt a wildcard match
// For now, we can just nil check the task before each loop
for _, value := range e.Taskfile.Tasks.All(nil) {
if match, wildcards := value.WildcardMatch(call.Task); match {
matchingTasks = append(matchingTasks, &MatchingTask{
Task: value,
Wildcards: wildcards,
})
}
}
return matchingTasks
}
// GetTask will return the task with the name matching the given call from the taskfile. // GetTask will return the task with the name matching the given call from the taskfile.
// If no task is found, it will search for tasks with a matching alias. // If no task is found, it will search for tasks with a matching alias.
// If multiple tasks contain the same alias or no matches are found an error is returned. // If multiple tasks contain the same alias or no matches are found an error is returned.
func (e *Executor) GetTask(call *ast.Call) (*ast.Task, error) { func (e *Executor) GetTask(call *Call) (*ast.Task, error) {
// Search for a matching task // Search for a matching task
matchingTasks := e.Taskfile.Tasks.FindMatchingTasks(call) matchingTasks := e.FindMatchingTasks(call)
switch len(matchingTasks) { switch len(matchingTasks) {
case 0: // Carry on case 0: // Carry on
case 1: case 1:
@@ -470,7 +507,7 @@ func (e *Executor) GetTask(call *ast.Call) (*ast.Task, error) {
// If didn't find one, search for a task with a matching alias // If didn't find one, search for a task with a matching alias
var matchingTask *ast.Task var matchingTask *ast.Task
var aliasedTasks []string var aliasedTasks []string
for _, task := range e.Taskfile.Tasks.Values() { for task := range e.Taskfile.Tasks.Values(nil) {
if slices.Contains(task.Aliases, call.Task) { if slices.Contains(task.Aliases, call.Task) {
aliasedTasks = append(aliasedTasks, task.Task) aliasedTasks = append(aliasedTasks, task.Task)
matchingTask = task matchingTask = task
@@ -506,8 +543,13 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
// Create an error group to wait for each task to be compiled // Create an error group to wait for each task to be compiled
var g errgroup.Group var g errgroup.Group
// Sort the tasks
if e.TaskSorter == nil {
e.TaskSorter = sort.AlphaNumericWithRootTasksFirst
}
// Filter tasks based on the given filter functions // Filter tasks based on the given filter functions
for _, task := range e.Taskfile.Tasks.Values() { for task := range e.Taskfile.Tasks.Values(e.TaskSorter) {
var shouldFilter bool var shouldFilter bool
for _, filter := range filters { for _, filter := range filters {
if filter(task) { if filter(task) {
@@ -522,7 +564,7 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
// Compile the list of tasks // Compile the list of tasks
for i := range tasks { for i := range tasks {
g.Go(func() error { g.Go(func() error {
compiledTask, err := e.FastCompiledTask(&ast.Call{Task: tasks[i].Task}) compiledTask, err := e.FastCompiledTask(&Call{Task: tasks[i].Task})
if err != nil { if err != nil {
return err return err
} }
@@ -536,12 +578,6 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
return nil, err return nil, err
} }
// Sort the tasks
if e.TaskSorter == nil {
e.TaskSorter = &sort.AlphaNumericWithRootTasksFirst{}
}
e.TaskSorter.Sort(tasks)
return tasks, nil return tasks, nil
} }

File diff suppressed because it is too large Load Diff

View File

@@ -51,18 +51,53 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
return nil return nil
case yaml.MappingNode: case yaml.MappingNode:
// A command with additional options
var cmdStruct struct { var cmdStruct struct {
Cmd string Cmd string
Task string
For *For For *For
Silent bool Silent bool
Set []string Set []string
Shopt []string Shopt []string
Vars *Vars
IgnoreError bool `yaml:"ignore_error"` IgnoreError bool `yaml:"ignore_error"`
Defer *Defer
Platforms []*Platform Platforms []*Platform
} }
if err := node.Decode(&cmdStruct); err == nil && cmdStruct.Cmd != "" { if err := node.Decode(&cmdStruct); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
if cmdStruct.Defer != nil {
// A deferred command
if cmdStruct.Defer.Cmd != "" {
c.Defer = true
c.Cmd = cmdStruct.Defer.Cmd
c.Silent = cmdStruct.Silent
return nil
}
// A deferred task call
if cmdStruct.Defer.Task != "" {
c.Defer = true
c.Task = cmdStruct.Defer.Task
c.Vars = cmdStruct.Defer.Vars
c.Silent = cmdStruct.Defer.Silent
return nil
}
return nil
}
// A task call
if cmdStruct.Task != "" {
c.Task = cmdStruct.Task
c.Vars = cmdStruct.Vars
c.For = cmdStruct.For
c.Silent = cmdStruct.Silent
return nil
}
// A command with additional options
if cmdStruct.Cmd != "" {
c.Cmd = cmdStruct.Cmd c.Cmd = cmdStruct.Cmd
c.For = cmdStruct.For c.For = cmdStruct.For
c.Silent = cmdStruct.Silent c.Silent = cmdStruct.Silent
@@ -73,45 +108,6 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
return nil return nil
} }
// A deferred command
var deferredCmd struct {
Defer string
Silent bool
}
if err := node.Decode(&deferredCmd); err == nil && deferredCmd.Defer != "" {
c.Defer = true
c.Cmd = deferredCmd.Defer
c.Silent = deferredCmd.Silent
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
c.Silent = deferredCall.Defer.Silent
return nil
}
// A task call
var taskCall struct {
Task string
Vars *Vars
For *For
Silent bool
}
if err := node.Decode(&taskCall); err == nil && taskCall.Task != "" {
c.Task = taskCall.Task
c.Vars = taskCall.Vars
c.For = taskCall.For
c.Silent = taskCall.Silent
return nil
}
return errors.NewTaskfileDecodeError(nil, node).WithMessage("invalid keys in command") return errors.NewTaskfileDecodeError(nil, node).WithMessage("invalid keys in command")
} }

45
taskfile/ast/defer.go Normal file
View File

@@ -0,0 +1,45 @@
package ast
import (
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
)
type Defer struct {
Cmd string
Task string
Vars *Vars
Silent bool
}
func (d *Defer) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.ScalarNode:
var cmd string
if err := node.Decode(&cmd); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
d.Cmd = cmd
return nil
case yaml.MappingNode:
var deferStruct struct {
Defer string
Task string
Vars *Vars
Silent bool
}
if err := node.Decode(&deferStruct); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
d.Cmd = deferStruct.Defer
d.Task = deferStruct.Task
d.Vars = deferStruct.Vars
d.Silent = deferStruct.Silent
return nil
}
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("defer")
}

View File

@@ -116,14 +116,5 @@ func (tfg *TaskfileGraph) Merge() (*Taskfile, error) {
return nil, err return nil, err
} }
_ = rootVertex.Taskfile.Tasks.Range(func(name string, task *Task) error {
if task == nil {
task = &Task{}
rootVertex.Taskfile.Tasks.Set(name, task)
}
task.Task = name
return nil
})
return rootVertex.Taskfile, nil return rootVertex.Taskfile, nil
} }

View File

@@ -1,9 +1,10 @@
package ast package ast
import ( import (
"iter"
"sync" "sync"
"github.com/elliotchance/orderedmap/v2" "github.com/elliotchance/orderedmap/v3"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
@@ -84,19 +85,31 @@ func (includes *Includes) Set(key string, value *Include) bool {
return includes.om.Set(key, value) return includes.om.Set(key, value)
} }
// All returns an iterator that loops over all task key-value pairs.
// Range calls the provided function for each include in the map. The function // Range calls the provided function for each include in the map. The function
// receives the include's key and value as arguments. If the function returns // receives the include's key and value as arguments. If the function returns
// an error, the iteration stops and the error is returned. // an error, the iteration stops and the error is returned.
func (includes *Includes) Range(f func(k string, v *Include) error) error { func (includes *Includes) All() iter.Seq2[string, *Include] {
if includes == nil || includes.om == nil { if includes == nil || includes.om == nil {
return nil return func(yield func(string, *Include) bool) {}
} }
for pair := includes.om.Front(); pair != nil; pair = pair.Next() { return includes.om.AllFromFront()
if err := f(pair.Key, pair.Value); err != nil { }
return err
} // Keys returns an iterator that loops over all task keys.
func (includes *Includes) Keys() iter.Seq[string] {
if includes == nil || includes.om == nil {
return func(yield func(string) bool) {}
} }
return nil return includes.om.Keys()
}
// Values returns an iterator that loops over all task values.
func (includes *Includes) Values() iter.Seq[*Include] {
if includes == nil || includes.om == nil {
return func(yield func(*Include) bool) {}
}
return includes.om.Values()
} }
// UnmarshalYAML implements the yaml.Unmarshaler interface. // UnmarshalYAML implements the yaml.Unmarshaler interface.

View File

@@ -1,22 +1,34 @@
package ast package ast
import ( import (
"github.com/elliotchance/orderedmap/v2" "iter"
"github.com/elliotchance/orderedmap/v3"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"
) )
type Matrix struct { type (
om *orderedmap.OrderedMap[string, []any] // Matrix is an ordered map of variable names to arrays of values.
} Matrix struct {
om *orderedmap.OrderedMap[string, *MatrixRow]
type MatrixElement orderedmap.Element[string, []any] }
// A MatrixElement is a key-value pair that is used for initializing a
// Matrix structure.
MatrixElement orderedmap.Element[string, *MatrixRow]
// A MatrixRow list of values for a matrix key or a reference to another
// variable.
MatrixRow struct {
Ref string
Value []any
}
)
func NewMatrix(els ...*MatrixElement) *Matrix { func NewMatrix(els ...*MatrixElement) *Matrix {
matrix := &Matrix{ matrix := &Matrix{
om: orderedmap.NewOrderedMap[string, []any](), om: orderedmap.NewOrderedMap[string, *MatrixRow](),
} }
for _, el := range els { for _, el := range els {
matrix.Set(el.Key, el.Value) matrix.Set(el.Key, el.Value)
@@ -31,33 +43,45 @@ func (matrix *Matrix) Len() int {
return matrix.om.Len() return matrix.om.Len()
} }
func (matrix *Matrix) Get(key string) ([]any, bool) { func (matrix *Matrix) Get(key string) (*MatrixRow, bool) {
if matrix == nil || matrix.om == nil { if matrix == nil || matrix.om == nil {
return nil, false return nil, false
} }
return matrix.om.Get(key) return matrix.om.Get(key)
} }
func (matrix *Matrix) Set(key string, value []any) bool { func (matrix *Matrix) Set(key string, value *MatrixRow) bool {
if matrix == nil { if matrix == nil {
matrix = NewMatrix() matrix = NewMatrix()
} }
if matrix.om == nil { if matrix.om == nil {
matrix.om = orderedmap.NewOrderedMap[string, []any]() matrix.om = orderedmap.NewOrderedMap[string, *MatrixRow]()
} }
return matrix.om.Set(key, value) return matrix.om.Set(key, value)
} }
func (matrix *Matrix) Range(f func(k string, v []any) error) error { // All returns an iterator that loops over all task key-value pairs.
func (matrix *Matrix) All() iter.Seq2[string, *MatrixRow] {
if matrix == nil || matrix.om == nil { if matrix == nil || matrix.om == nil {
return nil return func(yield func(string, *MatrixRow) bool) {}
} }
for pair := matrix.om.Front(); pair != nil; pair = pair.Next() { return matrix.om.AllFromFront()
if err := f(pair.Key, pair.Value); err != nil { }
return err
} // Keys returns an iterator that loops over all task keys.
func (matrix *Matrix) Keys() iter.Seq[string] {
if matrix == nil || matrix.om == nil {
return func(yield func(string) bool) {}
} }
return nil return matrix.om.Keys()
}
// Values returns an iterator that loops over all task values.
func (matrix *Matrix) Values() iter.Seq[*MatrixRow] {
if matrix == nil || matrix.om == nil {
return func(yield func(*MatrixRow) bool) {}
}
return matrix.om.Values()
} }
func (matrix *Matrix) DeepCopy() *Matrix { func (matrix *Matrix) DeepCopy() *Matrix {
@@ -79,14 +103,36 @@ func (matrix *Matrix) UnmarshalYAML(node *yaml.Node) error {
keyNode := node.Content[i] keyNode := node.Content[i]
valueNode := node.Content[i+1] valueNode := node.Content[i+1]
// Decode the value node into a Matrix struct switch valueNode.Kind {
var v []any case yaml.SequenceNode:
if err := valueNode.Decode(&v); err != nil { // Decode the value node into a Matrix struct
return errors.NewTaskfileDecodeError(err, node) var v []any
} if err := valueNode.Decode(&v); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
// Add the task to the ordered map // Add the row to the ordered map
matrix.Set(keyNode.Value, v) matrix.Set(keyNode.Value, &MatrixRow{
Value: v,
})
case yaml.MappingNode:
// Decode the value node into a Matrix struct
var refStruct struct {
Ref string
}
if err := valueNode.Decode(&refStruct); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
// Add the reference to the ordered map
matrix.Set(keyNode.Value, &MatrixRow{
Ref: refStruct.Ref,
})
default:
return errors.NewTaskfileDecodeError(nil, node).WithMessage("matrix values must be an array or a reference")
}
} }
return nil return nil
} }

View File

@@ -2,15 +2,17 @@ package ast
import ( import (
"fmt" "fmt"
"iter"
"slices" "slices"
"strings" "strings"
"sync" "sync"
"github.com/elliotchance/orderedmap/v2" "github.com/elliotchance/orderedmap/v3"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/sort"
) )
type ( type (
@@ -22,12 +24,6 @@ type (
// A TaskElement is a key-value pair that is used for initializing a Tasks // A TaskElement is a key-value pair that is used for initializing a Tasks
// structure. // structure.
TaskElement orderedmap.Element[string, *Task] TaskElement orderedmap.Element[string, *Task]
// MatchingTask represents a task that matches a given call. It includes the
// task itself and a list of wildcards that were matched.
MatchingTask struct {
Task *Task
Wildcards []string
}
) )
// NewTasks creates a new instance of Tasks and initializes it with the provided // NewTasks creates a new instance of Tasks and initializes it with the provided
@@ -79,81 +75,53 @@ func (tasks *Tasks) Set(key string, value *Task) bool {
return tasks.om.Set(key, value) return tasks.om.Set(key, value)
} }
// Range calls the provided function for each task in the map. The function // All returns an iterator that loops over all task key-value pairs in the order
// receives the task's key and value as arguments. If the function returns an // specified by the sorter.
// error, the iteration stops and the error is returned. func (t *Tasks) All(sorter sort.Sorter) iter.Seq2[string, *Task] {
func (tasks *Tasks) Range(f func(k string, v *Task) error) error { if t == nil || t.om == nil {
if tasks == nil || tasks.om == nil { return func(yield func(string, *Task) bool) {}
return nil
} }
for pair := tasks.om.Front(); pair != nil; pair = pair.Next() { if sorter == nil {
if err := f(pair.Key, pair.Value); err != nil { return t.om.AllFromFront()
return err }
return func(yield func(string, *Task) bool) {
for _, key := range sorter(slices.Collect(t.om.Keys()), nil) {
el := t.om.GetElement(key)
if !yield(el.Key, el.Value) {
return
}
} }
} }
return nil
} }
// Keys returns a slice of all the keys in the Tasks map. // Keys returns an iterator that loops over all task keys in the order specified
func (tasks *Tasks) Keys() []string { // by the sorter.
if tasks == nil { func (t *Tasks) Keys(sorter sort.Sorter) iter.Seq[string] {
return nil return func(yield func(string) bool) {
} for k := range t.All(sorter) {
defer tasks.mutex.RUnlock() if !yield(k) {
tasks.mutex.RLock() return
var keys []string }
for pair := tasks.om.Front(); pair != nil; pair = pair.Next() {
keys = append(keys, pair.Key)
}
return keys
}
// Values returns a slice of all the values in the Tasks map.
func (tasks *Tasks) Values() []*Task {
if tasks == nil {
return nil
}
defer tasks.mutex.RUnlock()
tasks.mutex.RLock()
var values []*Task
for pair := tasks.om.Front(); pair != nil; pair = pair.Next() {
values = append(values, pair.Value)
}
return values
}
// FindMatchingTasks returns a list of tasks that match the given call. A task
// matches a call if its name is equal to the call's task name or if it matches
// a wildcard pattern. The function returns a list of MatchingTask structs, each
// containing a task and a list of wildcards that were matched.
func (t *Tasks) FindMatchingTasks(call *Call) []*MatchingTask {
if call == nil {
return nil
}
var matchingTasks []*MatchingTask
// If there is a direct match, return it
if task, ok := t.Get(call.Task); ok {
matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil})
return matchingTasks
}
// Attempt a wildcard match
// For now, we can just nil check the task before each loop
_ = t.Range(func(key string, value *Task) error {
if match, wildcards := value.WildcardMatch(call.Task); match {
matchingTasks = append(matchingTasks, &MatchingTask{
Task: value,
Wildcards: wildcards,
})
} }
return nil }
}) }
return matchingTasks
// Values returns an iterator that loops over all task values in the order
// specified by the sorter.
func (t *Tasks) Values(sorter sort.Sorter) iter.Seq[*Task] {
return func(yield func(*Task) bool) {
for _, v := range t.All(sorter) {
if !yield(v) {
return
}
}
}
} }
func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars) error { func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars) error {
defer t2.mutex.RUnlock() defer t2.mutex.RUnlock()
t2.mutex.RLock() t2.mutex.RLock()
err := t2.Range(func(name string, v *Task) error { for name, v := range t2.All(nil) {
// We do a deep copy of the task struct here to ensure that no data can // We do a deep copy of the task struct here to ensure that no data can
// be changed elsewhere once the taskfile is merged. // be changed elsewhere once the taskfile is merged.
task := v.DeepCopy() task := v.DeepCopy()
@@ -162,9 +130,9 @@ func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars)
task.Internal = task.Internal || (include != nil && include.Internal) task.Internal = task.Internal || (include != nil && include.Internal)
taskName := name taskName := name
// if the task is in the exclude list, don't add it to the merged taskfile and early return // if the task is in the exclude list, don't add it to the merged taskfile
if slices.Contains(include.Excludes, name) { if slices.Contains(include.Excludes, name) {
return nil continue
} }
if !include.Flatten { if !include.Flatten {
@@ -219,9 +187,7 @@ func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars)
} }
// Add the task to the merged taskfile // Add the task to the merged taskfile
t1.Set(taskName, task) t1.Set(taskName, task)
}
return nil
})
// If the included Taskfile has a default task, is not flattened and the // If the included Taskfile has a default task, is not flattened and the
// parent namespace has no task with a matching name, we can add an alias so // parent namespace has no task with a matching name, we can add an alias so
@@ -239,7 +205,7 @@ func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars)
} }
} }
return err return nil
} }
func (t *Tasks) UnmarshalYAML(node *yaml.Node) error { func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {

View File

@@ -2,169 +2,13 @@ package ast
import ( import (
"strings" "strings"
"sync"
"github.com/elliotchance/orderedmap/v2"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
"github.com/go-task/task/v3/internal/experiments" "github.com/go-task/task/v3/internal/experiments"
) )
type (
// Vars is an ordered map of variable names to values.
Vars struct {
om *orderedmap.OrderedMap[string, Var]
mutex sync.RWMutex
}
// A VarElement is a key-value pair that is used for initializing a Vars
// structure.
VarElement orderedmap.Element[string, Var]
)
// NewVars creates a new instance of Vars and initializes it with the provided
// set of elements, if any. The elements are added in the order they are passed.
func NewVars(els ...*VarElement) *Vars {
vars := &Vars{
om: orderedmap.NewOrderedMap[string, Var](),
}
for _, el := range els {
vars.Set(el.Key, el.Value)
}
return vars
}
// Len returns the number of variables in the Vars map.
func (vars *Vars) Len() int {
if vars == nil || vars.om == nil {
return 0
}
defer vars.mutex.RUnlock()
vars.mutex.RLock()
return vars.om.Len()
}
// Get returns the value the the variable with the provided key and a boolean
// that indicates if the value was found or not. If the value is not found, the
// returned variable is a zero value and the bool is false.
func (vars *Vars) Get(key string) (Var, bool) {
if vars == nil || vars.om == nil {
return Var{}, false
}
defer vars.mutex.RUnlock()
vars.mutex.RLock()
return vars.om.Get(key)
}
// Set sets the value of the variable with the provided key to the provided
// value. If the variable already exists, its value is updated. If the variable
// does not exist, it is created.
func (vars *Vars) Set(key string, value Var) bool {
if vars == nil {
vars = NewVars()
}
if vars.om == nil {
vars.om = orderedmap.NewOrderedMap[string, Var]()
}
defer vars.mutex.Unlock()
vars.mutex.Lock()
return vars.om.Set(key, value)
}
// Range calls the provided function for each variable in the map. The function
// receives the variable's key and value as arguments. If the function returns
// an error, the iteration stops and the error is returned.
func (vars *Vars) Range(f func(k string, v Var) error) error {
if vars == nil || vars.om == nil {
return nil
}
for pair := vars.om.Front(); pair != nil; pair = pair.Next() {
if err := f(pair.Key, pair.Value); err != nil {
return err
}
}
return nil
}
// ToCacheMap converts Vars to an unordered map containing only the static
// variables
func (vars *Vars) ToCacheMap() (m map[string]any) {
defer vars.mutex.RUnlock()
vars.mutex.RLock()
m = make(map[string]any, vars.Len())
for pair := vars.om.Front(); pair != nil; pair = pair.Next() {
if pair.Value.Sh != nil && *pair.Value.Sh != "" {
// Dynamic variable is not yet resolved; trigger
// <no value> to be used in templates.
return nil
}
if pair.Value.Live != nil {
m[pair.Key] = pair.Value.Live
} else {
m[pair.Key] = pair.Value.Value
}
}
return
}
// Merge loops over other and merges it values with the variables in vars. If
// the include parameter is not nil and its it is an advanced import, the
// directory is set set to the value of the include parameter.
func (vars *Vars) Merge(other *Vars, include *Include) {
if vars == nil || vars.om == nil || other == nil {
return
}
defer other.mutex.RUnlock()
other.mutex.RLock()
for pair := other.om.Front(); pair != nil; pair = pair.Next() {
if include != nil && include.AdvancedImport {
pair.Value.Dir = include.Dir
}
vars.om.Set(pair.Key, pair.Value)
}
}
func (vs *Vars) DeepCopy() *Vars {
if vs == nil {
return nil
}
defer vs.mutex.RUnlock()
vs.mutex.RLock()
return &Vars{
om: deepcopy.OrderedMap(vs.om),
}
}
func (vs *Vars) UnmarshalYAML(node *yaml.Node) error {
if vs == nil || vs.om == nil {
*vs = *NewVars()
}
vs.om = orderedmap.NewOrderedMap[string, Var]()
switch node.Kind {
case yaml.MappingNode:
// NOTE: orderedmap does not have an unmarshaler, so we have to decode
// the map manually. We increment over 2 values at a time and assign
// them as a key-value pair.
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
// Decode the value node into a Task struct
var v Var
if err := valueNode.Decode(&v); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
// Add the task to the ordered map
vs.Set(keyNode.Value, v)
}
return nil
}
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("vars")
}
// Var represents either a static or dynamic variable. // Var represents either a static or dynamic variable.
type Var struct { type Var struct {
Value any Value any
@@ -175,10 +19,10 @@ type Var struct {
} }
func (v *Var) UnmarshalYAML(node *yaml.Node) error { func (v *Var) UnmarshalYAML(node *yaml.Node) error {
if experiments.MapVariables.Enabled { if experiments.MapVariables.Enabled() {
// This implementation is not backwards-compatible and replaces the 'sh' key with map variables // This implementation is not backwards-compatible and replaces the 'sh' key with map variables
if experiments.MapVariables.Value == "1" { if experiments.MapVariables.Value == 1 {
var value any var value any
if err := node.Decode(&value); err != nil { if err := node.Decode(&value); err != nil {
return errors.NewTaskfileDecodeError(err, node) return errors.NewTaskfileDecodeError(err, node)
@@ -199,7 +43,7 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
} }
// This implementation IS backwards-compatible and keeps the 'sh' key and allows map variables to be added under the `map` key // This implementation IS backwards-compatible and keeps the 'sh' key and allows map variables to be added under the `map` key
if experiments.MapVariables.Value == "2" { if experiments.MapVariables.Value == 2 {
switch node.Kind { switch node.Kind {
case yaml.MappingNode: case yaml.MappingNode:
key := node.Content[0].Value key := node.Content[0].Value

174
taskfile/ast/vars.go Normal file
View File

@@ -0,0 +1,174 @@
package ast
import (
"iter"
"sync"
"github.com/elliotchance/orderedmap/v3"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
)
type (
// Vars is an ordered map of variable names to values.
Vars struct {
om *orderedmap.OrderedMap[string, Var]
mutex sync.RWMutex
}
// A VarElement is a key-value pair that is used for initializing a Vars
// structure.
VarElement orderedmap.Element[string, Var]
)
// NewVars creates a new instance of Vars and initializes it with the provided
// set of elements, if any. The elements are added in the order they are passed.
func NewVars(els ...*VarElement) *Vars {
vars := &Vars{
om: orderedmap.NewOrderedMap[string, Var](),
}
for _, el := range els {
vars.Set(el.Key, el.Value)
}
return vars
}
// Len returns the number of variables in the Vars map.
func (vars *Vars) Len() int {
if vars == nil || vars.om == nil {
return 0
}
defer vars.mutex.RUnlock()
vars.mutex.RLock()
return vars.om.Len()
}
// Get returns the value the the variable with the provided key and a boolean
// that indicates if the value was found or not. If the value is not found, the
// returned variable is a zero value and the bool is false.
func (vars *Vars) Get(key string) (Var, bool) {
if vars == nil || vars.om == nil {
return Var{}, false
}
defer vars.mutex.RUnlock()
vars.mutex.RLock()
return vars.om.Get(key)
}
// Set sets the value of the variable with the provided key to the provided
// value. If the variable already exists, its value is updated. If the variable
// does not exist, it is created.
func (vars *Vars) Set(key string, value Var) bool {
if vars == nil {
vars = NewVars()
}
if vars.om == nil {
vars.om = orderedmap.NewOrderedMap[string, Var]()
}
defer vars.mutex.Unlock()
vars.mutex.Lock()
return vars.om.Set(key, value)
}
// All returns an iterator that loops over all task key-value pairs.
func (vars *Vars) All() iter.Seq2[string, Var] {
if vars == nil || vars.om == nil {
return func(yield func(string, Var) bool) {}
}
return vars.om.AllFromFront()
}
// Keys returns an iterator that loops over all task keys.
func (vars *Vars) Keys() iter.Seq[string] {
if vars == nil || vars.om == nil {
return func(yield func(string) bool) {}
}
return vars.om.Keys()
}
// Values returns an iterator that loops over all task values.
func (vars *Vars) Values() iter.Seq[Var] {
if vars == nil || vars.om == nil {
return func(yield func(Var) bool) {}
}
return vars.om.Values()
}
// ToCacheMap converts Vars to an unordered map containing only the static
// variables
func (vars *Vars) ToCacheMap() (m map[string]any) {
defer vars.mutex.RUnlock()
vars.mutex.RLock()
m = make(map[string]any, vars.Len())
for k, v := range vars.All() {
if v.Sh != nil && *v.Sh != "" {
// Dynamic variable is not yet resolved; trigger
// <no value> to be used in templates.
continue
}
if v.Live != nil {
m[k] = v.Live
} else {
m[k] = v.Value
}
}
return
}
// Merge loops over other and merges it values with the variables in vars. If
// the include parameter is not nil and its it is an advanced import, the
// directory is set set to the value of the include parameter.
func (vars *Vars) Merge(other *Vars, include *Include) {
if vars == nil || vars.om == nil || other == nil {
return
}
defer other.mutex.RUnlock()
other.mutex.RLock()
for pair := other.om.Front(); pair != nil; pair = pair.Next() {
if include != nil && include.AdvancedImport {
pair.Value.Dir = include.Dir
}
vars.om.Set(pair.Key, pair.Value)
}
}
func (vs *Vars) DeepCopy() *Vars {
if vs == nil {
return nil
}
defer vs.mutex.RUnlock()
vs.mutex.RLock()
return &Vars{
om: deepcopy.OrderedMap(vs.om),
}
}
func (vs *Vars) UnmarshalYAML(node *yaml.Node) error {
if vs == nil || vs.om == nil {
*vs = *NewVars()
}
vs.om = orderedmap.NewOrderedMap[string, Var]()
switch node.Kind {
case yaml.MappingNode:
// NOTE: orderedmap does not have an unmarshaler, so we have to decode
// the map manually. We increment over 2 values at a time and assign
// them as a key-value pair.
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
// Decode the value node into a Task struct
var v Var
if err := valueNode.Decode(&v); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
// Add the task to the ordered map
vs.Set(keyNode.Value, v)
}
return nil
}
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("vars")
}

View File

@@ -6,22 +6,16 @@ import (
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/go-task/task/v3/internal/compiler"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
func Dotenv(c *compiler.Compiler, tf *ast.Taskfile, dir string) (*ast.Vars, error) { func Dotenv(vars *ast.Vars, tf *ast.Taskfile, dir string) (*ast.Vars, error) {
if len(tf.Dotenv) == 0 { if len(tf.Dotenv) == 0 {
return nil, nil return nil, nil
} }
vars, err := c.GetTaskfileVariables()
if err != nil {
return nil, err
}
env := ast.NewVars() env := ast.NewVars()
cache := &templater.Cache{Vars: vars} cache := &templater.Cache{Vars: vars}

View File

@@ -11,7 +11,6 @@ import (
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/experiments" "github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/internal/logger"
) )
type Node interface { type Node interface {
@@ -26,7 +25,6 @@ type Node interface {
} }
func NewRootNode( func NewRootNode(
l *logger.Logger,
entrypoint string, entrypoint string,
dir string, dir string,
insecure bool, insecure bool,
@@ -37,11 +35,10 @@ func NewRootNode(
if entrypoint == "-" { if entrypoint == "-" {
return NewStdinNode(dir) return NewStdinNode(dir)
} }
return NewNode(l, entrypoint, dir, insecure, timeout) return NewNode(entrypoint, dir, insecure, timeout)
} }
func NewNode( func NewNode(
l *logger.Logger,
entrypoint string, entrypoint string,
dir string, dir string,
insecure bool, insecure bool,
@@ -58,13 +55,13 @@ func NewNode(
case "git": case "git":
node, err = NewGitNode(entrypoint, dir, insecure, opts...) node, err = NewGitNode(entrypoint, dir, insecure, opts...)
case "http", "https": case "http", "https":
node, err = NewHTTPNode(l, entrypoint, dir, insecure, timeout, opts...) node, err = NewHTTPNode(entrypoint, dir, insecure, timeout, opts...)
default: default:
node, err = NewFileNode(l, entrypoint, dir, opts...) node, err = NewFileNode(entrypoint, dir, opts...)
} }
if node.Remote() && !experiments.RemoteTaskfiles.Enabled { if node.Remote() && !experiments.RemoteTaskfiles.Enabled() {
return nil, errors.New("task: Remote taskfiles are not enabled. You can read more about this experiment and how to enable it at https://taskfile.dev/experiments/remote-taskfiles") return nil, errors.New("task: Remote taskfiles are not enabled. You can read more about this experiment and how to enable it at https://taskfile.dev/experiments/remote-taskfiles")
} }
return node, err return node, err

View File

@@ -9,7 +9,6 @@ import (
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger"
) )
// A FileNode is a node that reads a taskfile from the local filesystem. // A FileNode is a node that reads a taskfile from the local filesystem.
@@ -18,10 +17,10 @@ type FileNode struct {
Entrypoint string Entrypoint string
} }
func NewFileNode(l *logger.Logger, entrypoint, dir string, opts ...NodeOption) (*FileNode, error) { func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error) {
var err error var err error
base := NewBaseNode(dir, opts...) base := NewBaseNode(dir, opts...)
entrypoint, base.dir, err = resolveFileNodeEntrypointAndDir(l, entrypoint, base.dir) entrypoint, base.dir, err = resolveFileNodeEntrypointAndDir(entrypoint, base.dir)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -50,10 +49,10 @@ func (node *FileNode) Read(ctx context.Context) ([]byte, error) {
// resolveFileNodeEntrypointAndDir resolves checks the values of entrypoint and dir and // resolveFileNodeEntrypointAndDir resolves checks the values of entrypoint and dir and
// populates them with default values if necessary. // populates them with default values if necessary.
func resolveFileNodeEntrypointAndDir(l *logger.Logger, entrypoint, dir string) (string, string, error) { func resolveFileNodeEntrypointAndDir(entrypoint, dir string) (string, string, error) {
var err error var err error
if entrypoint != "" { if entrypoint != "" {
entrypoint, err = Exists(l, entrypoint) entrypoint, err = Exists(entrypoint)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
@@ -68,7 +67,7 @@ func resolveFileNodeEntrypointAndDir(l *logger.Logger, entrypoint, dir string) (
return "", "", err return "", "", err
} }
} }
entrypoint, err = ExistsWalk(l, dir) entrypoint, err = ExistsWalk(dir)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }

View File

@@ -11,19 +11,17 @@ import (
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger"
) )
// An HTTPNode is a node that reads a Taskfile from a remote location via HTTP. // An HTTPNode is a node that reads a Taskfile from a remote location via HTTP.
type HTTPNode struct { type HTTPNode struct {
*BaseNode *BaseNode
URL *url.URL URL *url.URL // stores url pointing actual remote file. (e.g. with Taskfile.yml)
logger *logger.Logger entrypoint string // stores entrypoint url. used for building graph vertices.
timeout time.Duration timeout time.Duration
} }
func NewHTTPNode( func NewHTTPNode(
l *logger.Logger,
entrypoint string, entrypoint string,
dir string, dir string,
insecure bool, insecure bool,
@@ -40,15 +38,15 @@ func NewHTTPNode(
} }
return &HTTPNode{ return &HTTPNode{
BaseNode: base, BaseNode: base,
URL: url, URL: url,
timeout: timeout, entrypoint: entrypoint,
logger: l, timeout: timeout,
}, nil }, nil
} }
func (node *HTTPNode) Location() string { func (node *HTTPNode) Location() string {
return node.URL.String() return node.entrypoint
} }
func (node *HTTPNode) Remote() bool { func (node *HTTPNode) Remote() bool {
@@ -56,7 +54,7 @@ func (node *HTTPNode) Remote() bool {
} }
func (node *HTTPNode) Read(ctx context.Context) ([]byte, error) { func (node *HTTPNode) Read(ctx context.Context) ([]byte, error) {
url, err := RemoteExists(ctx, node.logger, node.URL, node.timeout) url, err := RemoteExists(ctx, node.URL, node.timeout)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -119,6 +117,6 @@ func (node *HTTPNode) ResolveDir(dir string) (string, error) {
} }
func (node *HTTPNode) FilenameAndLastDir() (string, string) { func (node *HTTPNode) FilenameAndLastDir() (string, string) {
dir, filename := filepath.Split(node.URL.Path) dir, filename := filepath.Split(node.entrypoint)
return filepath.Base(dir), filename return filepath.Base(dir), filename
} }

View File

@@ -12,9 +12,8 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/compiler" "github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/filepathext" "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/templater"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
@@ -28,40 +27,115 @@ Continue?`
Continue?` Continue?`
) )
// A Reader will recursively read Taskfiles from a given source using a directed type (
// acyclic graph (DAG). // ReaderDebugFunc is a function that is called when the reader wants to
type Reader struct { // log debug messages
graph *ast.TaskfileGraph ReaderDebugFunc func(string)
node Node // ReaderPromptFunc is a function that is called when the reader wants to
insecure bool // prompt the user in some way
download bool ReaderPromptFunc func(string) error
offline bool // ReaderOption is a function that configures a Reader.
timeout time.Duration ReaderOption func(*Reader)
tempDir string // A Reader will recursively read Taskfiles from a given source using a directed
logger *logger.Logger // acyclic graph (DAG).
promptMutex sync.Mutex Reader struct {
} graph *ast.TaskfileGraph
node Node
insecure bool
download bool
offline bool
timeout time.Duration
tempDir string
debugFunc ReaderDebugFunc
promptFunc ReaderPromptFunc
promptMutex sync.Mutex
}
)
// NewReader constructs a new Taskfile Reader using the given Node and options.
func NewReader( func NewReader(
node Node, node Node,
insecure bool, opts ...ReaderOption,
download bool,
offline bool,
timeout time.Duration,
tempDir string,
logger *logger.Logger,
) *Reader { ) *Reader {
return &Reader{ reader := &Reader{
graph: ast.NewTaskfileGraph(), graph: ast.NewTaskfileGraph(),
node: node, node: node,
insecure: insecure, insecure: false,
download: download, download: false,
offline: offline, offline: false,
timeout: timeout, timeout: time.Second * 10,
tempDir: tempDir, tempDir: os.TempDir(),
logger: logger, debugFunc: nil,
promptFunc: nil,
promptMutex: sync.Mutex{}, promptMutex: sync.Mutex{},
} }
for _, opt := range opts {
opt(reader)
}
return reader
}
// WithInsecure enables insecure connections when reading remote taskfiles. By
// default, insecure connections are rejected.
func WithInsecure(insecure bool) ReaderOption {
return func(r *Reader) {
r.insecure = insecure
}
}
// WithDownload forces the reader to download a fresh copy of the taskfile from
// the remote source.
func WithDownload(download bool) ReaderOption {
return func(r *Reader) {
r.download = download
}
}
// WithOffline stops the reader from being able to make network connections.
// It will still be able to read local files and cached copies of remote files.
func WithOffline(offline bool) ReaderOption {
return func(r *Reader) {
r.offline = offline
}
}
// WithTimeout sets the timeout for reading remote taskfiles. By default, the
// timeout is set to 10 seconds.
func WithTimeout(timeout time.Duration) ReaderOption {
return func(r *Reader) {
r.timeout = timeout
}
}
// WithTempDir sets the temporary directory to be used by the reader. By
// default, the reader uses `os.TempDir()`.
func WithTempDir(tempDir string) ReaderOption {
return func(r *Reader) {
r.tempDir = tempDir
}
}
// WithDebugFunc sets the debug function to be used by the reader. If set, this
// function will be called with debug messages. This can be useful if the caller
// wants to log debug messages from the reader. By default, no debug function is
// set and the logs are not written.
func WithDebugFunc(debugFunc ReaderDebugFunc) ReaderOption {
return func(r *Reader) {
r.debugFunc = debugFunc
}
}
// WithPromptFunc sets the prompt function to be used by the reader. If set,
// this function will be called with prompt messages. The function should
// optionally log the message to the user and return nil if the prompt is
// accepted and the execution should continue. Otherwise, it should return an
// error which describes why the the prompt was rejected. This can then be
// caught and used later when calling the Read method. By default, no prompt
// function is set and all prompts are automatically accepted.
func WithPromptFunc(promptFunc ReaderPromptFunc) ReaderOption {
return func(r *Reader) {
r.promptFunc = promptFunc
}
} }
func (r *Reader) Read() (*ast.TaskfileGraph, error) { func (r *Reader) Read() (*ast.TaskfileGraph, error) {
@@ -73,6 +147,19 @@ func (r *Reader) Read() (*ast.TaskfileGraph, error) {
return r.graph, nil return r.graph, nil
} }
func (r *Reader) debugf(format string, a ...any) {
if r.debugFunc != nil {
r.debugFunc(fmt.Sprintf(format, a...))
}
}
func (r *Reader) promptf(format string, a ...any) error {
if r.promptFunc != nil {
return r.promptFunc(fmt.Sprintf(format, a...))
}
return nil
}
func (r *Reader) include(node Node) error { func (r *Reader) include(node Node) error {
// Create a new vertex for the Taskfile // Create a new vertex for the Taskfile
vertex := &ast.TaskfileVertex{ vertex := &ast.TaskfileVertex{
@@ -100,8 +187,8 @@ func (r *Reader) include(node Node) error {
var g errgroup.Group var g errgroup.Group
// Loop over each included taskfile // Loop over each included taskfile
_ = vertex.Taskfile.Includes.Range(func(namespace string, include *ast.Include) error { for _, include := range vertex.Taskfile.Includes.All() {
vars := compiler.GetEnviron() vars := env.GetEnviron()
vars.Merge(vertex.Taskfile.Vars, nil) vars.Merge(vertex.Taskfile.Vars, nil)
// Start a goroutine to process each included Taskfile // Start a goroutine to process each included Taskfile
g.Go(func() error { g.Go(func() error {
@@ -132,7 +219,7 @@ func (r *Reader) include(node Node) error {
return err return err
} }
includeNode, err := NewNode(r.logger, entrypoint, include.Dir, r.insecure, r.timeout, includeNode, err := NewNode(entrypoint, include.Dir, r.insecure, r.timeout,
WithParent(node), WithParent(node),
) )
if err != nil { if err != nil {
@@ -177,8 +264,7 @@ func (r *Reader) include(node Node) error {
} }
return err return err
}) })
return nil }
})
// Wait for all the go routines to finish // Wait for all the go routines to finish
return g.Wait() return g.Wait()
@@ -193,9 +279,14 @@ func (r *Reader) readNode(node Node) (*ast.Taskfile, error) {
var tf ast.Taskfile var tf ast.Taskfile
if err := yaml.Unmarshal(b, &tf); err != nil { if err := yaml.Unmarshal(b, &tf); err != nil {
// Decode the taskfile and add the file info the any errors // Decode the taskfile and add the file info the any errors
taskfileInvalidErr := &errors.TaskfileDecodeError{} taskfileDecodeErr := &errors.TaskfileDecodeError{}
if errors.As(err, &taskfileInvalidErr) { if errors.As(err, &taskfileDecodeErr) {
return nil, taskfileInvalidErr.WithFileInfo(node.Location(), b, 2) snippet := NewSnippet(b,
SnippetWithLine(taskfileDecodeErr.Line),
SnippetWithColumn(taskfileDecodeErr.Column),
SnippetWithPadding(2),
)
return nil, taskfileDecodeErr.WithFileInfo(node.Location(), snippet.String())
} }
return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err} return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err}
} }
@@ -207,7 +298,7 @@ func (r *Reader) readNode(node Node) (*ast.Taskfile, error) {
// Set the taskfile/task's locations // Set the taskfile/task's locations
tf.Location = node.Location() tf.Location = node.Location()
for _, task := range tf.Tasks.Values() { for task := range tf.Tasks.Values(nil) {
// If the task is not defined, create a new one // If the task is not defined, create a new one
if task == nil { if task == nil {
task = &ast.Task{} task = &ast.Task{}
@@ -241,7 +332,7 @@ func (r *Reader) loadNodeContent(node Node) ([]byte, error) {
} else if err != nil { } else if err != nil {
return nil, err return nil, err
} }
r.logger.VerboseOutf(logger.Magenta, "task: [%s] Fetched cached copy\n", node.Location()) r.debugf("task: [%s] Fetched cached copy\n", node.Location())
return cached, nil return cached, nil
} }
@@ -265,14 +356,14 @@ func (r *Reader) loadNodeContent(node Node) ([]byte, error) {
} else if err != nil { } else if err != nil {
return nil, err return nil, err
} }
r.logger.VerboseOutf(logger.Magenta, "task: [%s] Network timeout. Fetched cached copy\n", node.Location()) r.debugf("task: [%s] Network timeout. Fetched cached copy\n", node.Location())
return cached, nil return cached, nil
} else if err != nil { } else if err != nil {
return nil, err return nil, err
} }
r.logger.VerboseOutf(logger.Magenta, "task: [%s] Fetched remote copy\n", node.Location()) r.debugf("task: [%s] Fetched remote copy\n", node.Location())
// Get the checksums // Get the checksums
checksum := checksum(b) checksum := checksum(b)
@@ -281,17 +372,17 @@ func (r *Reader) loadNodeContent(node Node) ([]byte, error) {
var prompt string var prompt string
if cachedChecksum == "" { if cachedChecksum == "" {
// If the checksum doesn't exist, prompt the user to continue // If the checksum doesn't exist, prompt the user to continue
prompt = fmt.Sprintf(taskfileUntrustedPrompt, node.Location()) prompt = taskfileUntrustedPrompt
} else if checksum != cachedChecksum { } else if checksum != cachedChecksum {
// If there is a cached hash, but it doesn't match the expected hash, prompt the user to continue // If there is a cached hash, but it doesn't match the expected hash, prompt the user to continue
prompt = fmt.Sprintf(taskfileChangedPrompt, node.Location()) prompt = taskfileChangedPrompt
} }
if prompt != "" { if prompt != "" {
if err := func() error { if err := func() error {
r.promptMutex.Lock() r.promptMutex.Lock()
defer r.promptMutex.Unlock() defer r.promptMutex.Unlock()
return r.logger.Prompt(logger.Yellow, prompt, "n", "y", "yes") return r.promptf(prompt, node.Location())
}(); err != nil { }(); err != nil {
return nil, &errors.TaskfileNotTrustedError{URI: node.Location()} return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
} }
@@ -302,7 +393,7 @@ func (r *Reader) loadNodeContent(node Node) ([]byte, error) {
} }
// Cache the file // Cache the file
r.logger.VerboseOutf(logger.Magenta, "task: [%s] Caching downloaded file\n", node.Location()) r.debugf("task: [%s] Caching downloaded file\n", node.Location())
if err = cache.write(node, b); err != nil { if err = cache.write(node, b); err != nil {
return nil, err return nil, err
} }

148
taskfile/snippet.go Normal file
View File

@@ -0,0 +1,148 @@
package taskfile
import (
"bytes"
"embed"
"fmt"
"strings"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/quick"
"github.com/alecthomas/chroma/v2/styles"
"github.com/fatih/color"
)
//go:embed themes/*.xml
var embedded embed.FS
const (
lineIndicator = ">"
columnIndicator = "^"
)
func init() {
r, err := embedded.Open("themes/task.xml")
if err != nil {
panic(err)
}
style, err := chroma.NewXMLStyle(r)
if err != nil {
panic(err)
}
styles.Register(style)
}
type (
SnippetOption func(*Snippet)
Snippet struct {
linesRaw []string
linesHighlighted []string
start int
end int
line int
column int
padding int
noIndicators bool
}
)
// NewSnippet creates a new snippet from a byte slice and a line and column
// number. The line and column numbers should be 1-indexed. For example, the
// first character in the file would be 1:1 (line 1, column 1). The padding
// determines the number of lines to include before and after the chosen line.
func NewSnippet(b []byte, opts ...SnippetOption) *Snippet {
snippet := &Snippet{}
for _, opt := range opts {
opt(snippet)
}
// Syntax highlight the input and split it into lines
buf := &bytes.Buffer{}
if err := quick.Highlight(buf, string(b), "yaml", "terminal", "task"); err != nil {
buf.Write(b)
}
linesRaw := strings.Split(string(b), "\n")
linesHighlighted := strings.Split(buf.String(), "\n")
// Work out the start and end lines of the snippet
snippet.start = max(snippet.line-snippet.padding, 1)
snippet.end = min(snippet.line+snippet.padding, len(linesRaw)-1)
snippet.linesRaw = linesRaw[snippet.start-1 : snippet.end]
snippet.linesHighlighted = linesHighlighted[snippet.start-1 : snippet.end]
return snippet
}
func SnippetWithLine(line int) SnippetOption {
return func(snippet *Snippet) {
snippet.line = line
}
}
func SnippetWithColumn(column int) SnippetOption {
return func(snippet *Snippet) {
snippet.column = column
}
}
func SnippetWithPadding(padding int) SnippetOption {
return func(snippet *Snippet) {
snippet.padding = padding
}
}
func SnippetWithNoIndicators() SnippetOption {
return func(snippet *Snippet) {
snippet.noIndicators = true
}
}
func (snippet *Snippet) String() string {
buf := &bytes.Buffer{}
maxLineNumberDigits := digits(snippet.end)
lineNumberFormat := fmt.Sprintf("%%%dd", maxLineNumberDigits)
lineNumberSpacer := strings.Repeat(" ", maxLineNumberDigits)
lineIndicatorSpacer := strings.Repeat(" ", len(lineIndicator))
columnSpacer := strings.Repeat(" ", max(snippet.column-1, 0))
// Loop over each line in the snippet
for i, lineHighlighted := range snippet.linesHighlighted {
if i > 0 {
fmt.Fprintln(buf)
}
currentLine := snippet.start + i
lineNumber := fmt.Sprintf(lineNumberFormat, currentLine)
// If this is a padding line or indicators are disabled, print it as normal
if currentLine != snippet.line || snippet.noIndicators {
fmt.Fprintf(buf, "%s %s | %s", lineIndicatorSpacer, lineNumber, lineHighlighted)
continue
}
// Otherwise, print the line with indicators
fmt.Fprintf(buf, "%s %s | %s", color.RedString(lineIndicator), lineNumber, lineHighlighted)
// Only print the column indicator if the column is in bounds
if snippet.column > 0 && snippet.column <= len(snippet.linesRaw[i]) {
fmt.Fprintf(buf, "\n%s %s | %s%s", lineIndicatorSpacer, lineNumberSpacer, columnSpacer, color.RedString(columnIndicator))
}
}
// If there are lines, but no line is selected, print the column indicator under all the lines
if len(snippet.linesHighlighted) > 0 && snippet.line == 0 && snippet.column > 0 {
fmt.Fprintf(buf, "\n%s %s | %s%s", lineIndicatorSpacer, lineNumberSpacer, columnSpacer, color.RedString(columnIndicator))
}
return buf.String()
}
func digits(number int) int {
count := 0
for number != 0 {
number /= 10
count += 1
}
return count
}

289
taskfile/snippet_test.go Normal file
View File

@@ -0,0 +1,289 @@
package taskfile
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
)
const sample = `version: 3
tasks:
default:
vars:
FOO: foo
BAR: bar
cmds:
- echo "{{.FOO}}"
- echo "{{.BAR}}"
`
func TestNewSnippet(t *testing.T) {
t.Parallel()
tests := []struct {
name string
b []byte
opts []SnippetOption
want *Snippet
}{
{
name: "first line, first column",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(1),
SnippetWithColumn(1),
},
want: &Snippet{
linesRaw: []string{
"version: 3",
},
linesHighlighted: []string{
"\x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
start: 1,
end: 1,
line: 1,
column: 1,
padding: 0,
},
},
{
name: "first line, first column, padding=2",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(1),
SnippetWithColumn(1),
SnippetWithPadding(2),
},
want: &Snippet{
linesRaw: []string{
"version: 3",
"",
"tasks:",
},
linesHighlighted: []string{
"\x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
"\x1b[1m\x1b[30m\x1b[0m",
"\x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
start: 1,
end: 3,
line: 1,
column: 1,
padding: 2,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := NewSnippet(tt.b, tt.opts...)
require.Equal(t, tt.want, got)
})
}
}
func TestSnippetString(t *testing.T) {
t.Parallel()
tests := []struct {
name string
b []byte
opts []SnippetOption
want string
}{
{
name: "empty",
b: []byte{},
opts: []SnippetOption{
SnippetWithLine(1),
SnippetWithColumn(1),
},
want: "",
},
{
name: "0th line, 0th column (no indicators)",
b: []byte(sample),
want: "",
},
{
name: "1st line, 0th column (line indicator only)",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(1),
},
want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
{
name: "0th line, 1st column (column indicator only)",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithColumn(1),
},
want: "",
},
{
name: "0th line, 1st column, padding=2 (column indicator only)",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithColumn(1),
SnippetWithPadding(2),
},
want: " 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 2 | \x1b[1m\x1b[30m\x1b[0m\n | ^",
},
{
name: "1st line, 1st column",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(1),
SnippetWithColumn(1),
},
want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^",
},
{
name: "1st line, 10th column",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(1),
SnippetWithColumn(10),
},
want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^",
},
{
name: "1st line, 1st column, padding=2",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(1),
SnippetWithColumn(1),
SnippetWithPadding(2),
},
want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 2 | \x1b[1m\x1b[30m\x1b[0m\n 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
{
name: "1st line, 10th column, padding=2",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(1),
SnippetWithColumn(10),
SnippetWithPadding(2),
},
want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 2 | \x1b[1m\x1b[30m\x1b[0m\n 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
{
name: "5th line, 1st column",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(5),
SnippetWithColumn(1),
},
want: "> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^",
},
{
name: "5th line, 5th column",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(5),
SnippetWithColumn(5),
},
want: "> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^",
},
{
name: "5th line, 5th column, padding=2",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(5),
SnippetWithColumn(5),
SnippetWithPadding(2),
},
want: " 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 4 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mdefault\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 6 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mFOO\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mfoo\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 7 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mBAR\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mbar\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
{
name: "5th line, 5th column, padding=2, no indicators",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(5),
SnippetWithColumn(5),
SnippetWithPadding(2),
SnippetWithNoIndicators(),
},
want: " 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 4 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mdefault\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 6 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mFOO\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mfoo\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 7 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mBAR\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mbar\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
{
name: "10th line, 1st column",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(10),
SnippetWithColumn(1),
},
want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^",
},
{
name: "10th line, 23rd column",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(10),
SnippetWithColumn(23),
},
want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^",
},
{
name: "10th line, 24th column (out of bounds)",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(10),
SnippetWithColumn(24),
},
want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
{
name: "10th line, 23rd column, padding=2",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(10),
SnippetWithColumn(23),
SnippetWithPadding(2),
},
want: " 8 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mcmds\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 9 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.FOO}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^",
},
{
name: "5th line, 5th column, padding=100",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(5),
SnippetWithColumn(5),
SnippetWithPadding(100),
},
want: " 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 2 | \x1b[1m\x1b[30m\x1b[0m\n 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 4 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mdefault\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 6 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mFOO\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mfoo\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 7 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mBAR\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mbar\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 8 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mcmds\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 9 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.FOO}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
{
name: "11th line (out of bounds), 1st column",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(11),
SnippetWithColumn(1),
},
want: "",
},
{
name: "11th line (out of bounds), 1st column, padding=2",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(11),
SnippetWithColumn(1),
SnippetWithPadding(2),
},
want: " 9 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.FOO}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
snippet := NewSnippet(tt.b, tt.opts...)
got := snippet.String()
if strings.Contains(got, "\t") {
t.Fatalf("tab character found in snippet - check the sample string")
}
require.Equal(t, tt.want, got)
})
}
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/sysinfo" "github.com/go-task/task/v3/internal/sysinfo"
) )
@@ -41,7 +40,7 @@ var (
// at the given URL with any of the default Taskfile files names. If any of // at the given URL with any of the default Taskfile files names. If any of
// these match a file, the first matching path will be returned. If no files are // these match a file, the first matching path will be returned. If no files are
// found, an error will be returned. // found, an error will be returned.
func RemoteExists(ctx context.Context, l *logger.Logger, u *url.URL, timeout time.Duration) (*url.URL, error) { func RemoteExists(ctx context.Context, u *url.URL, timeout time.Duration) (*url.URL, error) {
// Create a new HEAD request for the given URL to check if the resource exists // Create a new HEAD request for the given URL to check if the resource exists
req, err := http.NewRequestWithContext(ctx, "HEAD", u.String(), nil) req, err := http.NewRequestWithContext(ctx, "HEAD", u.String(), nil)
if err != nil { if err != nil {
@@ -89,7 +88,6 @@ func RemoteExists(ctx context.Context, l *logger.Logger, u *url.URL, timeout tim
// If the request was successful, return the URL // If the request was successful, return the URL
if resp.StatusCode == http.StatusOK { if resp.StatusCode == http.StatusOK {
l.VerboseOutf(logger.Magenta, "task: [%s] Not found - Using alternative (%s)\n", alt.String(), taskfile)
return alt, nil return alt, nil
} }
} }
@@ -102,7 +100,7 @@ func RemoteExists(ctx context.Context, l *logger.Logger, u *url.URL, timeout tim
// given path with any of the default Taskfile files names. If any of these // given path with any of the default Taskfile files names. If any of these
// match a file, the first matching path will be returned. If no files are // match a file, the first matching path will be returned. If no files are
// found, an error will be returned. // found, an error will be returned.
func Exists(l *logger.Logger, path string) (string, error) { func Exists(path string) (string, error) {
fi, err := os.Stat(path) fi, err := os.Stat(path)
if err != nil { if err != nil {
return "", err return "", err
@@ -117,7 +115,6 @@ func Exists(l *logger.Logger, path string) (string, error) {
for _, taskfile := range defaultTaskfiles { for _, taskfile := range defaultTaskfiles {
alt := filepathext.SmartJoin(path, taskfile) alt := filepathext.SmartJoin(path, taskfile)
if _, err := os.Stat(alt); err == nil { if _, err := os.Stat(alt); err == nil {
l.VerboseOutf(logger.Magenta, "task: [%s] Not found - Using alternative (%s)\n", path, taskfile)
return filepath.Abs(alt) return filepath.Abs(alt)
} }
} }
@@ -130,14 +127,14 @@ func Exists(l *logger.Logger, path string) (string, error) {
// calling the exists function until it finds a file or reaches the root // calling the exists function until it finds a file or reaches the root
// directory. On supported operating systems, it will also check if the user ID // directory. On supported operating systems, it will also check if the user ID
// of the directory changes and abort if it does. // of the directory changes and abort if it does.
func ExistsWalk(l *logger.Logger, path string) (string, error) { func ExistsWalk(path string) (string, error) {
origPath := path origPath := path
owner, err := sysinfo.Owner(path) owner, err := sysinfo.Owner(path)
if err != nil { if err != nil {
return "", err return "", err
} }
for { for {
fpath, err := Exists(l, path) fpath, err := Exists(path)
if err == nil { if err == nil {
return fpath, nil return fpath, nil
} }

View File

@@ -1,10 +1,16 @@
version: '3' version: '3'
tasks: tasks:
build: build-checksum:
sources: sources:
- ./source.txt - ./source.txt
cmds: cmds:
- echo "{{.CHECKSUM}}" - echo "{{.CHECKSUM}}"
- echo "{{.TIMESTAMP.Unix}}"
- echo "{{.TIMESTAMP}}" build-ts:
method: timestamp
sources:
- ./source.txt
cmds:
- echo '{{.TIMESTAMP.Unix}}'
- echo '{{.TIMESTAMP}}'

View File

@@ -1,5 +1,10 @@
version: "3" version: "3"
vars:
OS_VAR: ["windows", "linux", "darwin"]
ARCH_VAR: ["amd64", "arm64"]
NOT_A_LIST: "not a list"
tasks: tasks:
# Loop over a list of values # Loop over a list of values
loop-explicit: loop-explicit:
@@ -15,6 +20,26 @@ tasks:
ARCH: ["amd64", "arm64"] ARCH: ["amd64", "arm64"]
cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}" cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
loop-matrix-ref:
cmds:
- for:
matrix:
OS:
ref: .OS_VAR
ARCH:
ref: .ARCH_VAR
cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
loop-matrix-ref-error:
cmds:
- for:
matrix:
OS:
ref: .OS_VAR
ARCH:
ref: .NOT_A_LIST
cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
# Loop over the task's sources # Loop over the task's sources
loop-sources: loop-sources:
sources: sources:

View File

@@ -1,5 +1,10 @@
version: "3" version: "3"
vars:
OS_VAR: ["windows", "linux", "darwin"]
ARCH_VAR: ["amd64", "arm64"]
NOT_A_LIST: "not a list"
tasks: tasks:
# Loop over a list of values # Loop over a list of values
loop-explicit: loop-explicit:
@@ -19,6 +24,30 @@ tasks:
vars: vars:
TEXT: "{{.ITEM.OS}}/{{.ITEM.ARCH}}" TEXT: "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
loop-matrix-ref:
deps:
- for:
matrix:
OS:
ref: .OS_VAR
ARCH:
ref: .ARCH_VAR
task: echo
vars:
TEXT: "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
loop-matrix-ref-error:
deps:
- for:
matrix:
OS:
ref: .OS_VAR
ARCH:
ref: .NOT_A_LIST
task: echo
vars:
TEXT: "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
# Loop over the task's sources # Loop over the task's sources
loop-sources: loop-sources:
sources: sources:

View File

@@ -0,0 +1,10 @@
version: "3"
vars:
INCLUDE: include
FOO:
sh : echo bar
includes:
included1:
taskfile: '{{.INCLUDE}}/Taskfile.include.yml'

View File

@@ -0,0 +1 @@
version: "3"

View File

@@ -7,7 +7,7 @@ tasks:
missing-var: missing-var:
requires: requires:
vars: vars:
- foo - FOO
cmd: echo "{{.foo}}" cmd: echo "{{.foo}}"
var-defined-in-task: var-defined-in-task:
@@ -19,13 +19,24 @@ tasks:
cmd: echo "{{.FOO}}" cmd: echo "{{.FOO}}"
validation-var-dynamic:
vars:
FOO:
sh: echo "one"
requires:
vars:
- name: FOO
enum: ['one', 'two']
validation-var: validation-var:
requires: requires:
vars: vars:
- name: foo - ENV
- name: FOO
enum: ['one', 'two'] enum: ['one', 'two']
require-before-compile: require-before-compile:
requires: requires:
vars: [ MY_VAR ] vars: [ MY_VAR ]

View File

@@ -1,10 +1,16 @@
version: '3' version: '3'
tasks: tasks:
build: build-checksum:
sources: sources:
- ./source.txt - ./source.txt
status: status:
- echo "{{.CHECKSUM}}" - echo "{{.CHECKSUM}}"
build-ts:
method: timestamp
sources:
- ./source.txt
status:
- echo '{{.TIMESTAMP.Unix}}' - echo '{{.TIMESTAMP.Unix}}'
- echo '{{.TIMESTAMP}}' - echo '{{.TIMESTAMP}}'

View File

@@ -0,0 +1,11 @@
version: '3'
silent: true
dotenv:
- 'global.env'
tasks:
default:
cmds:
- 'echo "{{.VAR}}"'
- 'echo "$ENV"'

View File

@@ -0,0 +1,2 @@
VAR=entrypoint-global-dotenv
ENV=entrypoint-global-dotenv

View File

@@ -0,0 +1,15 @@
version: '3'
silent: true
dotenv:
- 'global.env'
vars:
VAR: entrypoint-global-vars
env:
ENV: entrypoint-global-vars
tasks:
default:
cmds:
- 'echo "{{.VAR}}"'
- 'echo "$ENV"'

View File

@@ -0,0 +1,2 @@
VAR=entrypoint-global-dotenv
ENV=entrypoint-global-dotenv

View File

@@ -0,0 +1,25 @@
version: '3'
silent: true
dotenv:
- 'global.env'
vars:
VAR: entrypoint-global-vars
env:
ENV: entrypoint-global-vars
tasks:
default:
dotenv:
- 'task.env'
cmds:
- task: called-task
vars:
VAR: entrypoint-task-call-vars
called-task:
dotenv:
- 'called-task.env'
cmds:
- 'echo "{{.VAR}}"'
- 'echo "$ENV"'

View File

@@ -0,0 +1,2 @@
VAR=entrypoint-task-call-dotenv
ENV=entrypoint-task-call-dotenv

View File

@@ -0,0 +1,2 @@
VAR=entrypoint-global-dotenv
ENV=entrypoint-global-dotenv

View File

@@ -0,0 +1,2 @@
VAR=entrypoint-task-dotenv
ENV=entrypoint-task-dotenv

View File

@@ -0,0 +1,27 @@
version: '3'
silent: true
dotenv:
- 'global.env'
vars:
VAR: entrypoint-global-vars
env:
ENV: entrypoint-global-vars
tasks:
default:
dotenv:
- 'task.env'
cmds:
- task: called-task
vars:
VAR: entrypoint-task-call-vars
called-task:
vars:
VAR: entrypoint-task-call-task-vars
env:
ENV: entrypoint-task-call-task-vars
cmds:
- 'echo "{{.VAR}}"'
- 'echo "$ENV"'

View File

@@ -0,0 +1,2 @@
VAR=entrypoint-global-dotenv
ENV=entrypoint-global-dotenv

View File

@@ -0,0 +1,2 @@
VAR=entrypoint-task-dotenv
ENV=entrypoint-task-dotenv

View File

@@ -0,0 +1,23 @@
version: '3'
silent: true
dotenv:
- 'global.env'
vars:
VAR: entrypoint-global-vars
env:
ENV: entrypoint-global-vars
tasks:
default:
dotenv:
- 'task.env'
cmds:
- task: called-task
vars:
VAR: entrypoint-task-call-vars
called-task:
cmds:
- 'echo "{{.VAR}}"'
- 'echo "$ENV"'

View File

@@ -0,0 +1,2 @@
VAR=entrypoint-global-dotenv
ENV=entrypoint-global-dotenv

View File

@@ -0,0 +1,2 @@
VAR=entrypoint-task-dotenv
ENV=entrypoint-task-dotenv

View File

@@ -0,0 +1,17 @@
version: '3'
silent: true
dotenv:
- 'global.env'
vars:
VAR: entrypoint-global-vars
env:
ENV: entrypoint-global-vars
tasks:
default:
dotenv:
- 'task.env'
cmds:
- 'echo "{{.VAR}}"'
- 'echo "$ENV"'

View File

@@ -0,0 +1,2 @@
VAR=entrypoint-global-dotenv
ENV=entrypoint-global-dotenv

View File

@@ -0,0 +1,2 @@
VAR=entrypoint-task-dotenv
ENV=entrypoint-task-dotenv

View File

@@ -0,0 +1,21 @@
version: '3'
silent: true
dotenv:
- 'global.env'
vars:
VAR: entrypoint-global-vars
env:
ENV: entrypoint-global-vars
tasks:
default:
dotenv:
- 'task.env'
vars:
VAR: entrypoint-task-vars
env:
ENV: entrypoint-task-vars
cmds:
- 'echo "{{.VAR}}"'
- 'echo "$ENV"'

View File

@@ -0,0 +1,2 @@
VAR=entrypoint-global-dotenv
ENV=entrypoint-global-dotenv

View File

@@ -0,0 +1,2 @@
VAR=entrypoint-task-dotenv
ENV=entrypoint-task-dotenv

View File

@@ -0,0 +1,12 @@
version: '3'
silent: true
dotenv:
- 'global.env'
vars:
VAR: entrypoint-global-vars
env:
ENV: entrypoint-global-vars
includes:
included: included.yml

View File

@@ -0,0 +1,2 @@
VAR=entrypoint-global-dotenv
ENV=entrypoint-global-dotenv

View File

@@ -0,0 +1,13 @@
version: '3'
silent: true
vars:
VAR: included-global-vars
env:
ENV: included-global-vars
tasks:
default:
cmds:
- 'echo "{{.VAR}}"'
- 'echo "$ENV"'

View File

@@ -0,0 +1,12 @@
version: '3'
silent: true
dotenv:
- 'global.env'
vars:
VAR: entrypoint-global-vars
env:
ENV: entrypoint-global-vars
includes:
included: included.yml

View File

@@ -0,0 +1,2 @@
VAR=entrypoint-global-dotenv
ENV=entrypoint-global-dotenv

View File

@@ -0,0 +1,21 @@
version: '3'
silent: true
vars:
VAR: included-global-vars
env:
ENV: included-global-vars
tasks:
default:
dotenv:
- 'task.env'
cmds:
- task: called-task
vars:
VAR: included-task-call-vars
called-task:
cmds:
- 'echo "{{.VAR}}"'
- 'echo "$ENV"'

View File

@@ -0,0 +1,2 @@
VAR=included-task-dotenv
ENV=included-task-dotenv

View File

@@ -0,0 +1,12 @@
version: '3'
silent: true
dotenv:
- 'global.env'
vars:
VAR: entrypoint-global-vars
env:
ENV: entrypoint-global-vars
includes:
included: included.yml

View File

@@ -0,0 +1,2 @@
VAR=entrypoint-global-dotenv
ENV=entrypoint-global-dotenv

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