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

feat: Added support for lazy variables evaluation #1231

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions internal/compiler/v3/compiler_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ func (c *CompilerV3) getVariables(t *taskfile.Task, call *taskfile.Call, evaluat
return nil
}

if v.Lazy != nil {
v.Lazy.Dir = dir
v.Lazy.Sh = tr.Replace(v.Lazy.Sh)
v.Lazy.Stderr = c.Logger.Stderr
result.Set(k, taskfile.Var{Lazy: v.Lazy})
return nil
}

v = taskfile.Var{
Static: tr.Replace(v.Static),
Sh: tr.Replace(v.Sh),
Expand Down
6 changes: 6 additions & 0 deletions internal/templater/templater.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,16 @@ func (r *Templater) ReplaceVars(vars *taskfile.Vars) *taskfile.Vars {

var new taskfile.Vars
_ = vars.Range(func(k string, v taskfile.Var) error {
var lazy *taskfile.LazySh
if v.Lazy != nil {
v.Lazy.Sh = r.Replace(v.Lazy.Sh)
lazy = v.Lazy
}
new.Set(k, taskfile.Var{
Static: r.Replace(v.Static),
Live: v.Live,
Sh: r.Replace(v.Sh),
Lazy: lazy,
})
return nil
})
Expand Down
22 changes: 22 additions & 0 deletions task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ func TestVarsV3(t *testing.T) {
"missing-var.txt": "\n",
"var-order.txt": "ABCDEF\n",
"dependent-sh.txt": "123456\n",
"lazy-sh.txt": "LMNOPQ\n",
"with-call.txt": "Hi, ABC123!\n",
"from-dot-env.txt": "From .env file\n",
},
Expand Down Expand Up @@ -2166,3 +2167,24 @@ func TestForce(t *testing.T) {
})
}
}

func TestLazyVar(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/lazy_vars",
Target: "default",
Files: map[string]string{
"basic.txt": "123\n",
"env.txt": "1234\n",
"override-by-task.txt": "ABC\n",
"callee-deps.txt": "ABC\n",
"callee-task.txt": "ABC\n",
"var-in-dotenv-global.txt": "var_in_dot_env_1\n",
"var-in-dotenv-task.txt": "var_in_dot_env_2\n",
"not-exists.txt": "a\n",
"dir-name.txt": "lazy_vars\n",
"dir-name-in-child.txt": "lazy_vars\n",
"dir-name-in-child-with-vars.txt": "child_dir\n",
},
}
tt.Run(t)
}
60 changes: 59 additions & 1 deletion taskfile/var.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
package taskfile

import (
"bytes"
"context"
"fmt"
"io"
"os"
"strings"

"golang.org/x/sync/singleflight"

"github.com/go-task/task/v3/internal/execext"

"gopkg.in/yaml.v3"

Expand All @@ -26,6 +35,8 @@ func (vs *Vars) ToCacheMap() (m map[string]any) {

if v.Live != nil {
m[k] = v.Live
} else if v.Lazy != nil {
m[k] = v.Lazy
} else {
m[k] = v.Static
}
Expand Down Expand Up @@ -75,6 +86,7 @@ type Var struct {
Live any
Sh string
Dir string
Lazy *LazySh
}

func (v *Var) UnmarshalYAML(node *yaml.Node) error {
Expand All @@ -90,14 +102,60 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {

case yaml.MappingNode:
var sh struct {
Sh string
Sh string
Lazy bool
}
if err := node.Decode(&sh); err != nil {
return err
}
if sh.Lazy {
v.Lazy = &LazySh{
Sh: sh.Sh,
Sf: &singleflight.Group{},
Stderr: os.Stderr,
}
return nil
}
v.Sh = sh.Sh
return nil
}

return fmt.Errorf("yaml: line %d: cannot unmarshal %s into variable", node.Line, node.ShortTag())
}

type LazySh struct {
Sh string
Dir string
Val string
Done bool
Sf *singleflight.Group
Stderr io.Writer
}

func (l *LazySh) String() string {
val, err, _ := l.Sf.Do("", func() (any, error) {
if l.Done {
return l.Val, nil
}

var stdout bytes.Buffer
opts := &execext.RunCommandOptions{
Command: l.Sh,
Dir: l.Dir,
Stdout: &stdout,
Stderr: l.Stderr,
}
if err := execext.RunCommand(context.Background(), opts); err != nil {
return "", fmt.Errorf(`task: Command "%s" failed: %s`, opts.Command, err)
}
result := strings.TrimSuffix(stdout.String(), "\r\n")
result = strings.TrimSuffix(result, "\n")
l.Val = result
l.Done = true
return result, nil
})
if err != nil {
panic(err)
}
return val.(string)
}
1 change: 1 addition & 0 deletions testdata/lazy_vars/.env.global
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VAR_IN_DOTENV=var_in_dot_env_1
1 change: 1 addition & 0 deletions testdata/lazy_vars/.env.task
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VAR_IN_DOTENV=var_in_dot_env_2
1 change: 1 addition & 0 deletions testdata/lazy_vars/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.txt
108 changes: 108 additions & 0 deletions testdata/lazy_vars/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
version: '3'

vars:
VAR_MUST_NOT_EXEC: {sh: 'echo "this should not be executed" && exit 1', lazy: true}
VAR_1: {sh: echo 1, lazy: true}
VAR_2: {sh: echo 2, lazy: true}
VAR_3: {sh: echo 3, lazy: true}
VAR_4: {sh: echo 4, lazy: true}
VAR_NO: {sh: 'echo "{{.VAR_1}}{{.VAR_2}}{{.VAR_3}} Root VAR_NO" && exit 1', lazy: true}

VAR_A: {sh: 'echo NO_A && exit 1', lazy: true}
VAR_B: {sh: 'echo NO_B && exit 1', lazy: true}
VAR_C: {sh: 'echo NO_C && exit 1', lazy: true}

VAR_a: {sh: 'echo {{.VAR_NOT_EXISTS}}a', lazy: true}

VAR_DIR: {sh: 'basename "$(pwd)"', lazy: true}

VAR_DOTENV_GLOBAL: {sh: 'echo global', lazy: true}

env:
ENV_1: "{{.VAR_1}}"
ENV_2:
sh: echo "{{.VAR_2}}"

tasks:
default:
- task: basic
- task: env
- task: override-by-task
- task: var-in-dotenv-global
- task: var-in-dotenv-task
- task: not-exists
- task: dir-name
- task: dir-name-in-child
- task: dir-name-in-child-with-vars

basic:
vars:
VAR_2: {sh: 'echo "{{.VAR_1}}2"', lazy: true}
VAR_NO: {sh: 'echo "Basic VAR_NO" && exit 1', lazy: true}
cmd: echo "{{.VAR_2}}3" > basic.txt

env:
env:
ENV_3: "{{.VAR_3}}"
ENV_4:
sh: echo "{{.VAR_4}}"
cmd: echo "${ENV_1}${ENV_2}${ENV_3}${ENV_4}" > env.txt

override-by-task:
deps:
- task: callee-deps
vars:
VAR_A: A
VAR_B: {sh: 'echo B', lazy: true}
VAR_C: {sh: 'echo C', lazy: true}
vars:
VAR_A: A
VAR_B: {sh: 'echo B', lazy: true}
VAR_C: {sh: 'echo C', lazy: true}
cmds:
- task: callee-task
vars:
VAR_A: A
VAR_B: {sh: 'echo B', lazy: true}
VAR_C: {sh: 'echo C', lazy: true}
- echo "{{.VAR_A}}{{.VAR_B}}{{.VAR_C}}" > override-by-task.txt

callee-deps:
internal: true
vars:
VAR_B: '{{default "NO" .VAR_B}}'
cmd: echo "{{.VAR_A}}{{.VAR_B}}{{.VAR_C}}" > callee-deps.txt

callee-task:
internal: true
vars:
VAR_B: '{{default "NO" .VAR_B}}'
cmd: echo "{{.VAR_A}}{{.VAR_B}}{{.VAR_C}}" > callee-task.txt

var-in-dotenv-global:
dotenv: [ ".env.{{.VAR_DOTENV_GLOBAL}}" ]
cmd: echo "$VAR_IN_DOTENV" > var-in-dotenv-global.txt

var-in-dotenv-task:
dotenv: [ ".env.{{.VAR_DOTENV_TASK}}" ]
vars:
VAR_DOTENV_TASK: {sh: 'echo task', lazy: true}
cmd: echo "$VAR_IN_DOTENV" > var-in-dotenv-task.txt

not-exists:
vars:
VAR_NOT_EXISTS: exists
cmd: echo "{{.VAR_a}}" > not-exists.txt

dir-name:
cmd: echo "{{.VAR_DIR}}" > dir-name.txt

dir-name-in-child:
dir: child_dir
cmd: echo "{{.VAR_DIR}}" > ../dir-name-in-child.txt

dir-name-in-child-with-vars:
dir: child_dir
vars:
VAR_DIR: {sh: 'basename "$(pwd)"'}
cmd: echo "{{.VAR_DIR}}" > ../dir-name-in-child-with-vars.txt
Empty file.
15 changes: 15 additions & 0 deletions testdata/vars/v3/Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,18 @@ vars:
VAR_2: {sh: 'echo "{{.VAR_1}}2"'}
VAR_3: {sh: 'echo "{{.VAR_2}}3"'}

VAR_MUST_NOT_EXEC: {sh: 'echo "this should not be executed" && exit 1', lazy: true}
VAR_L: {sh: 'echo L', lazy: true}
VAR_M: {sh: 'echo "{{.VAR_L}}M"', lazy: true}
VAR_N: {sh: 'echo "{{.VAR_M}}N"', lazy: true}
VAR_O: {sh: 'echo "this should not be executed" && exit 1"', lazy: true}

tasks:
default:
- task: missing-var
- task: var-order
- task: dependent-sh
- task: lazy-sh
- task: with-call
- task: from-dot-env

Expand All @@ -37,6 +44,14 @@ tasks:
cmds:
- echo '{{.VAR_6}}' > dependent-sh.txt

lazy-sh:
vars:
VAR_O: {sh: 'echo "{{.VAR_N}}O"', lazy: true}
VAR_P: {sh: 'echo "{{.VAR_O}}P"', lazy: true}
VAR_Q: {sh: 'echo "{{.VAR_P}}Q"', lazy: true}
cmds:
- echo '{{.VAR_Q}}' > lazy-sh.txt

with-call:
- task: called-task
vars:
Expand Down