mirror of
https://github.com/go-task/task.git
synced 2026-06-11 09:51:50 +00:00
When calling a task with vars (e.g., `task: name` with `vars:`), those vars were not being applied in scoped mode. This fix adds call.Vars to the variable resolution chain. Variable priority (lowest to highest): 1. Root Taskfile vars 2. Include Taskfile vars 3. Include passthrough vars 4. Task vars 5. Call vars (NEW) 6. CLI vars
365 lines
9.6 KiB
Go
365 lines
9.6 KiB
Go
package task
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/go-task/task/v3/experiments"
|
|
"github.com/go-task/task/v3/internal/env"
|
|
"github.com/go-task/task/v3/internal/execext"
|
|
"github.com/go-task/task/v3/internal/filepathext"
|
|
"github.com/go-task/task/v3/internal/logger"
|
|
"github.com/go-task/task/v3/internal/templater"
|
|
"github.com/go-task/task/v3/internal/version"
|
|
"github.com/go-task/task/v3/taskfile/ast"
|
|
)
|
|
|
|
type Compiler struct {
|
|
Dir string
|
|
Entrypoint string
|
|
UserWorkingDir string
|
|
|
|
TaskfileEnv *ast.Vars
|
|
TaskfileVars *ast.Vars
|
|
CLIVars *ast.Vars // CLI vars passed via command line (e.g., task foo VAR=value)
|
|
Graph *ast.TaskfileGraph
|
|
|
|
Logger *logger.Logger
|
|
|
|
dynamicCache map[string]string
|
|
muDynamicCache sync.Mutex
|
|
}
|
|
|
|
func (c *Compiler) GetTaskfileVariables() (*ast.Vars, error) {
|
|
return c.getVariables(nil, nil, true)
|
|
}
|
|
|
|
func (c *Compiler) GetVariables(t *ast.Task, call *Call) (*ast.Vars, error) {
|
|
return c.getVariables(t, call, true)
|
|
}
|
|
|
|
func (c *Compiler) FastGetVariables(t *ast.Task, call *Call) (*ast.Vars, error) {
|
|
return c.getVariables(t, call, false)
|
|
}
|
|
|
|
func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) {
|
|
// In scoped mode, OS env vars are in {{.env.XXX}} namespace, not at root
|
|
// In legacy mode, they are at root level
|
|
scopedMode := experiments.ScopedTaskfiles.Enabled() && t != nil && t.Location != nil && c.Graph != nil
|
|
var result *ast.Vars
|
|
if scopedMode {
|
|
result = ast.NewVars()
|
|
} else {
|
|
result = env.GetEnviron()
|
|
}
|
|
|
|
specialVars, err := c.getSpecialVars(t, call)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for k, v := range specialVars {
|
|
result.Set(k, ast.Var{Value: v})
|
|
}
|
|
|
|
getRangeFunc := func(dir string) func(k string, v ast.Var) error {
|
|
return func(k string, v ast.Var) error {
|
|
cache := &templater.Cache{Vars: result}
|
|
// Replace values
|
|
newVar := templater.ReplaceVar(v, cache)
|
|
// 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
|
|
// Preserve the Sh field so it can be displayed in summary
|
|
if !evaluateShVars && newVar.Value == nil {
|
|
result.Set(k, ast.Var{Value: "", Sh: newVar.Sh})
|
|
return nil
|
|
}
|
|
// If the variable should not be evaluated and it is set, we can set it and return
|
|
if !evaluateShVars {
|
|
result.Set(k, ast.Var{Value: newVar.Value, Sh: newVar.Sh})
|
|
return nil
|
|
}
|
|
// Now we can check for errors since we've handled all the cases when we don't want to evaluate
|
|
if err := cache.Err(); err != nil {
|
|
return err
|
|
}
|
|
// If the variable is already set, we can set it and return
|
|
if newVar.Value != nil || newVar.Sh == nil {
|
|
result.Set(k, ast.Var{Value: newVar.Value})
|
|
return nil
|
|
}
|
|
// If the variable is dynamic, we need to resolve it first
|
|
static, err := c.HandleDynamicVar(newVar, dir, env.GetFromVars(result))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result.Set(k, ast.Var{Value: static})
|
|
return nil
|
|
}
|
|
}
|
|
rangeFunc := getRangeFunc(c.Dir)
|
|
|
|
var taskRangeFunc func(k string, v ast.Var) error
|
|
if t != nil {
|
|
// NOTE(@andreynering): We're manually joining these paths here because
|
|
// this is the raw task, not the compiled one.
|
|
cache := &templater.Cache{Vars: result}
|
|
dir := templater.Replace(t.Dir, cache)
|
|
if err := cache.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
dir = filepathext.SmartJoin(c.Dir, dir)
|
|
taskRangeFunc = getRangeFunc(dir)
|
|
}
|
|
|
|
// When scoped includes is enabled, resolve vars from DAG instead of merged vars
|
|
if scopedMode {
|
|
// Get root Taskfile for inheritance (parent vars are always accessible)
|
|
rootVertex, err := c.Graph.Root()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// === ENV NAMESPACE ===
|
|
// Create a separate map for environment variables
|
|
// Accessible via {{.env.VAR}} in templates
|
|
envMap := make(map[string]any)
|
|
|
|
// 1. OS environment variables
|
|
for _, e := range os.Environ() {
|
|
k, v, _ := strings.Cut(e, "=")
|
|
envMap[k] = v
|
|
}
|
|
|
|
// Helper to resolve env vars and add to envMap
|
|
resolveEnvToMap := func(k string, v ast.Var, dir string) error {
|
|
cache := &templater.Cache{Vars: result}
|
|
newVar := templater.ReplaceVar(v, cache)
|
|
if err := cache.Err(); err != nil {
|
|
return err
|
|
}
|
|
// Static value
|
|
if newVar.Value != nil || newVar.Sh == nil {
|
|
if newVar.Value != nil {
|
|
envMap[k] = newVar.Value
|
|
}
|
|
return nil
|
|
}
|
|
// Dynamic value (sh:)
|
|
if evaluateShVars {
|
|
// Build env slice for sh execution (includes envMap values)
|
|
envSlice := os.Environ()
|
|
for ek, ev := range envMap {
|
|
if s, ok := ev.(string); ok {
|
|
envSlice = append(envSlice, fmt.Sprintf("%s=%s", ek, s))
|
|
}
|
|
}
|
|
static, err := c.HandleDynamicVar(newVar, dir, envSlice)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
envMap[k] = static
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 2. Root taskfile env
|
|
for k, v := range rootVertex.Taskfile.Env.All() {
|
|
if err := resolveEnvToMap(k, v, c.Dir); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// === VARS (at root level) ===
|
|
// Apply root vars
|
|
for k, v := range rootVertex.Taskfile.Vars.All() {
|
|
if err := rangeFunc(k, v); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// If task is from an included Taskfile (not the root), get its vars from the DAG
|
|
if t.Location.Taskfile != rootVertex.URI {
|
|
includeVertex, err := c.Graph.Vertex(t.Location.Taskfile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Apply include's env to envMap (overrides root's env)
|
|
for k, v := range includeVertex.Taskfile.Env.All() {
|
|
if err := resolveEnvToMap(k, v, filepathext.SmartJoin(c.Dir, t.Dir)); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
// Apply include's vars (overrides root's vars)
|
|
for k, v := range includeVertex.Taskfile.Vars.All() {
|
|
if err := taskRangeFunc(k, v); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply IncludeVars (vars passed via includes: section)
|
|
if t.IncludeVars != nil {
|
|
for k, v := range t.IncludeVars.All() {
|
|
if err := rangeFunc(k, v); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply task-level vars
|
|
if call != nil {
|
|
for k, v := range t.Vars.All() {
|
|
if err := taskRangeFunc(k, v); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
// Apply call vars (vars passed when calling a task)
|
|
for k, v := range call.Vars.All() {
|
|
if err := taskRangeFunc(k, v); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// CLI vars have highest priority - applied last to override everything
|
|
for k, v := range c.CLIVars.All() {
|
|
if err := rangeFunc(k, v); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Inject env namespace into result
|
|
result.Set("env", ast.Var{Value: envMap})
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// === LEGACY MODE ===
|
|
// Legacy behavior: use merged vars
|
|
for k, v := range c.TaskfileEnv.All() {
|
|
if err := rangeFunc(k, v); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
for k, v := range c.TaskfileVars.All() {
|
|
if err := rangeFunc(k, v); err != nil {
|
|
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 {
|
|
return result, nil
|
|
}
|
|
|
|
// Legacy order: CLI vars, then task vars (task vars override CLI)
|
|
for k, v := range call.Vars.All() {
|
|
if err := rangeFunc(k, v); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
for k, v := range t.Vars.All() {
|
|
if err := taskRangeFunc(k, v); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Compiler) HandleDynamicVar(v ast.Var, dir string, e []string) (string, error) {
|
|
c.muDynamicCache.Lock()
|
|
defer c.muDynamicCache.Unlock()
|
|
|
|
// If the variable is not dynamic or it is empty, return an empty string
|
|
if v.Sh == nil || *v.Sh == "" {
|
|
return "", nil
|
|
}
|
|
|
|
if c.dynamicCache == nil {
|
|
c.dynamicCache = make(map[string]string, 30)
|
|
}
|
|
if result, ok := c.dynamicCache[*v.Sh]; ok {
|
|
return result, nil
|
|
}
|
|
|
|
// NOTE(@andreynering): If a var have a specific dir, use this instead
|
|
if v.Dir != "" {
|
|
dir = v.Dir
|
|
}
|
|
|
|
var stdout bytes.Buffer
|
|
opts := &execext.RunCommandOptions{
|
|
Command: *v.Sh,
|
|
Dir: dir,
|
|
Stdout: &stdout,
|
|
Stderr: c.Logger.Stderr,
|
|
Env: e,
|
|
}
|
|
if err := execext.RunCommand(context.Background(), opts); err != nil {
|
|
return "", fmt.Errorf(`task: Command "%s" failed: %s`, opts.Command, err)
|
|
}
|
|
|
|
// Trim a single trailing newline from the result to make most command
|
|
// output easier to use in shell commands.
|
|
result := strings.TrimSuffix(stdout.String(), "\r\n")
|
|
result = strings.TrimSuffix(result, "\n")
|
|
|
|
c.dynamicCache[*v.Sh] = result
|
|
c.Logger.VerboseErrf(logger.Magenta, "task: dynamic variable: %q result: %q\n", *v.Sh, result)
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// ResetCache clear the dynamic variables cache
|
|
func (c *Compiler) ResetCache() {
|
|
c.muDynamicCache.Lock()
|
|
defer c.muDynamicCache.Unlock()
|
|
|
|
c.dynamicCache = nil
|
|
}
|
|
|
|
func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, error) {
|
|
allVars := map[string]string{
|
|
"TASK_EXE": filepath.ToSlash(os.Args[0]),
|
|
"ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint),
|
|
"ROOT_DIR": c.Dir,
|
|
"USER_WORKING_DIR": c.UserWorkingDir,
|
|
"TASK_VERSION": version.GetVersion(),
|
|
}
|
|
if t != nil {
|
|
allVars["TASK"] = t.Task
|
|
allVars["TASK_DIR"] = filepathext.SmartJoin(c.Dir, t.Dir)
|
|
allVars["TASKFILE"] = t.Location.Taskfile
|
|
allVars["TASKFILE_DIR"] = filepath.Dir(t.Location.Taskfile)
|
|
} else {
|
|
allVars["TASK"] = ""
|
|
allVars["TASK_DIR"] = ""
|
|
allVars["TASKFILE"] = ""
|
|
allVars["TASKFILE_DIR"] = ""
|
|
}
|
|
if call != nil {
|
|
allVars["ALIAS"] = call.Task
|
|
} else {
|
|
allVars["ALIAS"] = ""
|
|
}
|
|
|
|
return allVars, nil
|
|
}
|