Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support any variables #1421

Merged
merged 13 commits into from
Dec 21, 2023
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

- Added
[Any Variables experiment](https://taskfile.dev/experiments/any_variables)
(#1415, #1421 by @pd93).
- Added `aliases` to `--json` flag output (#1430, #1431 by @pd93)

## v3.32.0 - 2023-11-29
Expand Down
6 changes: 3 additions & 3 deletions args/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func ParseV3(args ...string) ([]taskfile.Call, *taskfile.Vars) {
}

name, value := splitVar(arg)
globals.Set(name, taskfile.Var{Static: value})
globals.Set(name, taskfile.Var{Value: value})
}

return calls, globals
Expand All @@ -37,13 +37,13 @@ func ParseV2(args ...string) ([]taskfile.Call, *taskfile.Vars) {

if len(calls) < 1 {
name, value := splitVar(arg)
globals.Set(name, taskfile.Var{Static: value})
globals.Set(name, taskfile.Var{Value: value})
} else {
if calls[len(calls)-1].Vars == nil {
calls[len(calls)-1].Vars = &taskfile.Vars{}
}
name, value := splitVar(arg)
calls[len(calls)-1].Vars.Set(name, taskfile.Var{Static: value})
calls[len(calls)-1].Vars.Set(name, taskfile.Var{Value: value})
}
}

Expand Down
28 changes: 14 additions & 14 deletions args/args_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ func TestArgsV3(t *testing.T) {
ExpectedGlobals: &taskfile.Vars{
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"FOO": {Static: "bar"},
"BAR": {Static: "baz"},
"BAZ": {Static: "foo"},
"FOO": {Value: "bar"},
"BAR": {Value: "baz"},
"BAZ": {Value: "foo"},
},
[]string{"FOO", "BAR", "BAZ"},
),
Expand All @@ -51,7 +51,7 @@ func TestArgsV3(t *testing.T) {
ExpectedGlobals: &taskfile.Vars{
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"CONTENT": {Static: "with some spaces"},
"CONTENT": {Value: "with some spaces"},
},
[]string{"CONTENT"},
),
Expand All @@ -66,7 +66,7 @@ func TestArgsV3(t *testing.T) {
ExpectedGlobals: &taskfile.Vars{
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"FOO": {Static: "bar"},
"FOO": {Value: "bar"},
},
[]string{"FOO"},
),
Expand All @@ -86,8 +86,8 @@ func TestArgsV3(t *testing.T) {
ExpectedGlobals: &taskfile.Vars{
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"FOO": {Static: "bar"},
"BAR": {Static: "baz"},
"FOO": {Value: "bar"},
"BAR": {Value: "baz"},
},
[]string{"FOO", "BAR"},
),
Expand Down Expand Up @@ -130,7 +130,7 @@ func TestArgsV2(t *testing.T) {
Vars: &taskfile.Vars{
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"FOO": {Static: "bar"},
"FOO": {Value: "bar"},
},
[]string{"FOO"},
),
Expand All @@ -143,8 +143,8 @@ func TestArgsV2(t *testing.T) {
Vars: &taskfile.Vars{
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"BAR": {Static: "baz"},
"BAZ": {Static: "foo"},
"BAR": {Value: "baz"},
"BAZ": {Value: "foo"},
},
[]string{"BAR", "BAZ"},
),
Expand All @@ -161,7 +161,7 @@ func TestArgsV2(t *testing.T) {
Vars: &taskfile.Vars{
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"CONTENT": {Static: "with some spaces"},
"CONTENT": {Value: "with some spaces"},
},
[]string{"CONTENT"},
),
Expand All @@ -178,7 +178,7 @@ func TestArgsV2(t *testing.T) {
ExpectedGlobals: &taskfile.Vars{
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"FOO": {Static: "bar"},
"FOO": {Value: "bar"},
},
[]string{"FOO"},
),
Expand All @@ -198,8 +198,8 @@ func TestArgsV2(t *testing.T) {
ExpectedGlobals: &taskfile.Vars{
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"FOO": {Static: "bar"},
"BAR": {Static: "baz"},
"FOO": {Value: "bar"},
"BAR": {Value: "baz"},
},
[]string{"FOO", "BAR"},
),
Expand Down
2 changes: 1 addition & 1 deletion cmd/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ func run() error {
calls = append(calls, taskfile.Call{Task: "default", Direct: true})
}

globals.Set("CLI_ARGS", taskfile.Var{Static: cliArgs})
globals.Set("CLI_ARGS", taskfile.Var{Value: cliArgs})
e.Taskfile.Vars.Merge(globals)

if !flags.watch {
Expand Down
105 changes: 105 additions & 0 deletions docs/docs/experiments/any_variables.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
slug: /experiments/any-variables/
---

# Any Variables

- Issue: [#1415](https://github.com/go-task/task/issues/1415)
- Environment variable: `TASK_X_ANY_VARIABLES=1`
- Breaks:
- Dynamically defined variables (using the `sh` keyword)

Currently, Task only supports string variables. This experiment allows you to
specify and use the following variable types:

- `string`
- `bool`
- `int`
- `float`
- `array`
- `map`

This allows you to have a lot more flexibility in how you use variables in
Task's templating engine. For example:

Evaluating booleans:

```yaml
version: 3

tasks:
foo:
vars:
BOOL: false
cmds:
- '{{ if .BOOL }}echo foo{{ end}}'
```

Arithmetic:

```yaml
version: 3

tasks:
foo:
vars:
INT: 10
FLOAT: 3.14159
cmds:
- 'echo {{ add .INT .FLOAT }}'
```

Loops:

```yaml
version: 3

tasks:
foo:
vars:
ARRAY: [1, 2, 3]
cmds:
- 'echo {{ range .ARRAY }}{{ . }}{{ end }}'
```

etc.

## Migration

Taskfiles with dynamically defined variables via the `sh` subkey will no longer
work with this experiment enabled. In order to keep using dynamically defined
variables, you will need to migrate your Taskfile to use the new syntax.

Previously, you might have defined a dynamic variable like this:

```yaml
version: 3

task:
foo:
vars:
CALCULATED_VAR:
sh: 'echo hello'
cmds:
- 'echo {{ .CALCULATED_VAR }}'
```

With this experiment enabled, you will need to remove the `sh` subkey and define
your command as a string that begins with a `$`. This will instruct Task to
interpret the string as a command instead of a literal value and the variable
will be populated with the output of the command. For example:

```yaml
version: 3

task:
foo:
vars:
CALCULATED_VAR: '$echo hello'
cmds:
- 'echo {{ .CALCULATED_VAR }}'
```

If your current Taskfile contains a string variable that begins with a `$`, you
will now need to escape the `$` with a backslash (`\`) to stop Task from
executing it as a command.
2 changes: 1 addition & 1 deletion docs/static/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@
"^.*$": {
"anyOf": [
{
"type": ["boolean", "integer", "null", "number", "string"]
"type": ["boolean", "integer", "null", "number", "string", "object", "array"]
},
{
"$ref": "#/definitions/3/dynamic_var"
Expand Down
2 changes: 1 addition & 1 deletion internal/compiler/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func GetEnviron() *taskfile.Vars {
for _, e := range os.Environ() {
keyVal := strings.SplitN(e, "=", 2)
key, val := keyVal[0], keyVal[1]
m.Set(key, taskfile.Var{Static: val})
m.Set(key, taskfile.Var{Value: val})
}
return m
}
27 changes: 17 additions & 10 deletions internal/compiler/v2/compiler_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (c *CompilerV2) GetVariables(t *taskfile.Task, call taskfile.Call) (*taskfi
c: c,
vars: compiler.GetEnviron(),
}
vr.vars.Set("TASK", taskfile.Var{Static: t.Task})
vr.vars.Set("TASK", taskfile.Var{Value: t.Task})

for _, vars := range []*taskfile.Vars{c.Taskvars, c.TaskfileVars, call.Vars, t.Vars} {
for i := 0; i < c.Expansions; i++ {
Expand All @@ -72,26 +72,33 @@ func (vr *varResolver) merge(vars *taskfile.Vars) {
}
tr := templater.Templater{Vars: vr.vars}
_ = vars.Range(func(k string, v taskfile.Var) error {
v = taskfile.Var{
Static: tr.Replace(v.Static),
Sh: tr.Replace(v.Sh),
// Replace values
newVar := taskfile.Var{}
switch value := v.Value.(type) {
case string:
newVar.Value = tr.Replace(value)
default:
newVar.Value = value
}
static, err := vr.c.HandleDynamicVar(v, "")
newVar.Sh = tr.Replace(v.Sh)
// If the variable is not dynamic, we can set it and return
if newVar.Value != nil || newVar.Sh == "" {
vr.vars.Set(k, taskfile.Var{Value: newVar.Value})
return nil
}
// If the variable is dynamic, we need to resolve it first
static, err := vr.c.HandleDynamicVar(newVar, "")
if err != nil {
vr.err = err
return err
}
vr.vars.Set(k, taskfile.Var{Static: static})
vr.vars.Set(k, taskfile.Var{Value: static})
return nil
})
vr.err = tr.Err()
}

func (c *CompilerV2) HandleDynamicVar(v taskfile.Var, _ string) (string, error) {
if v.Static != "" || v.Sh == "" {
return v.Static, nil
}

c.muDynamicCache.Lock()
defer c.muDynamicCache.Unlock()

Expand Down
41 changes: 24 additions & 17 deletions internal/compiler/v3/compiler_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,32 +51,43 @@ func (c *CompilerV3) getVariables(t *taskfile.Task, call *taskfile.Call, evaluat
return nil, err
}
for k, v := range specialVars {
result.Set(k, taskfile.Var{Static: v})
result.Set(k, taskfile.Var{Value: v})
}
}

getRangeFunc := func(dir string) func(k string, v taskfile.Var) error {
return func(k string, v taskfile.Var) error {
tr := templater.Templater{Vars: result, RemoveNoValue: true}

if !evaluateShVars {
result.Set(k, taskfile.Var{Static: tr.Replace(v.Static)})
return nil
}

v = taskfile.Var{
Static: tr.Replace(v.Static),
Sh: tr.Replace(v.Sh),
Dir: v.Dir,
// Replace values
newVar := taskfile.Var{}
switch value := v.Value.(type) {
case string:
newVar.Value = tr.Replace(value)
default:
newVar.Value = value
}
newVar.Sh = tr.Replace(v.Sh)
newVar.Dir = v.Dir
if err := tr.Err(); err != nil {
return err
}
static, err := c.HandleDynamicVar(v, dir)
// If the variable should not be evaluated, but is nil, set it to an empty string
// This stops empty interface errors when using the templater to replace values later
if !evaluateShVars && newVar.Value == nil {
result.Set(k, taskfile.Var{Value: ""})
return nil
}
// If the variable is not dynamic, we can set it and return
if !evaluateShVars || newVar.Value != nil || newVar.Sh == "" {
result.Set(k, taskfile.Var{Value: newVar.Value})
return nil
}
// If the variable is dynamic, we need to resolve it first
static, err := c.HandleDynamicVar(newVar, dir)
if err != nil {
return err
}
result.Set(k, taskfile.Var{Static: static})
result.Set(k, taskfile.Var{Value: static})
return nil
}
}
Expand Down Expand Up @@ -125,10 +136,6 @@ func (c *CompilerV3) getVariables(t *taskfile.Task, call *taskfile.Call, evaluat
}

func (c *CompilerV3) HandleDynamicVar(v taskfile.Var, dir string) (string, error) {
if v.Static != "" || v.Sh == "" {
return v.Static, nil
}

c.muDynamicCache.Lock()
defer c.muDynamicCache.Unlock()

Expand Down