variables.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. package task
  2. import (
  3. "os"
  4. "path/filepath"
  5. "strings"
  6. "github.com/joho/godotenv"
  7. "github.com/go-task/task/v3/errors"
  8. "github.com/go-task/task/v3/internal/execext"
  9. "github.com/go-task/task/v3/internal/filepathext"
  10. "github.com/go-task/task/v3/internal/fingerprint"
  11. "github.com/go-task/task/v3/internal/omap"
  12. "github.com/go-task/task/v3/internal/templater"
  13. "github.com/go-task/task/v3/taskfile/ast"
  14. )
  15. // CompiledTask returns a copy of a task, but replacing variables in almost all
  16. // properties using the Go template package.
  17. func (e *Executor) CompiledTask(call *ast.Call) (*ast.Task, error) {
  18. return e.compiledTask(call, true)
  19. }
  20. // FastCompiledTask is like CompiledTask, but it skippes dynamic variables.
  21. func (e *Executor) FastCompiledTask(call *ast.Call) (*ast.Task, error) {
  22. return e.compiledTask(call, false)
  23. }
  24. func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task, error) {
  25. origTask, err := e.GetTask(call)
  26. if err != nil {
  27. return nil, err
  28. }
  29. var vars *ast.Vars
  30. if evaluateShVars {
  31. vars, err = e.Compiler.GetVariables(origTask, call)
  32. } else {
  33. vars, err = e.Compiler.FastGetVariables(origTask, call)
  34. }
  35. if err != nil {
  36. return nil, err
  37. }
  38. cache := &templater.Cache{Vars: vars}
  39. new := ast.Task{
  40. Task: origTask.Task,
  41. Label: templater.Replace(origTask.Label, cache),
  42. Desc: templater.Replace(origTask.Desc, cache),
  43. Prompt: templater.Replace(origTask.Prompt, cache),
  44. Summary: templater.Replace(origTask.Summary, cache),
  45. Aliases: origTask.Aliases,
  46. Sources: templater.ReplaceGlobs(origTask.Sources, cache),
  47. Generates: templater.ReplaceGlobs(origTask.Generates, cache),
  48. Dir: templater.Replace(origTask.Dir, cache),
  49. Set: origTask.Set,
  50. Shopt: origTask.Shopt,
  51. Vars: nil,
  52. Env: nil,
  53. Dotenv: templater.Replace(origTask.Dotenv, cache),
  54. Silent: origTask.Silent,
  55. Interactive: origTask.Interactive,
  56. Internal: origTask.Internal,
  57. Method: templater.Replace(origTask.Method, cache),
  58. Prefix: templater.Replace(origTask.Prefix, cache),
  59. IgnoreError: origTask.IgnoreError,
  60. Run: templater.Replace(origTask.Run, cache),
  61. IncludeVars: origTask.IncludeVars,
  62. IncludedTaskfileVars: origTask.IncludedTaskfileVars,
  63. Platforms: origTask.Platforms,
  64. Location: origTask.Location,
  65. Requires: origTask.Requires,
  66. Watch: origTask.Watch,
  67. Namespace: origTask.Namespace,
  68. }
  69. new.Dir, err = execext.Expand(new.Dir)
  70. if err != nil {
  71. return nil, err
  72. }
  73. if e.Dir != "" {
  74. new.Dir = filepathext.SmartJoin(e.Dir, new.Dir)
  75. }
  76. if new.Prefix == "" {
  77. new.Prefix = new.Task
  78. }
  79. dotenvEnvs := &ast.Vars{}
  80. if len(new.Dotenv) > 0 {
  81. for _, dotEnvPath := range new.Dotenv {
  82. dotEnvPath = filepathext.SmartJoin(new.Dir, dotEnvPath)
  83. if _, err := os.Stat(dotEnvPath); os.IsNotExist(err) {
  84. continue
  85. }
  86. envs, err := godotenv.Read(dotEnvPath)
  87. if err != nil {
  88. return nil, err
  89. }
  90. for key, value := range envs {
  91. if ok := dotenvEnvs.Exists(key); !ok {
  92. dotenvEnvs.Set(key, ast.Var{Value: value})
  93. }
  94. }
  95. }
  96. }
  97. new.Env = &ast.Vars{}
  98. new.Env.Merge(templater.ReplaceVars(e.Taskfile.Env, cache), nil)
  99. new.Env.Merge(templater.ReplaceVars(dotenvEnvs, cache), nil)
  100. new.Env.Merge(templater.ReplaceVars(origTask.Env, cache), nil)
  101. if evaluateShVars {
  102. err = new.Env.Range(func(k string, v ast.Var) error {
  103. // If the variable is not dynamic, we can set it and return
  104. if v.Value != nil || v.Sh == "" {
  105. new.Env.Set(k, ast.Var{Value: v.Value})
  106. return nil
  107. }
  108. static, err := e.Compiler.HandleDynamicVar(v, new.Dir)
  109. if err != nil {
  110. return err
  111. }
  112. new.Env.Set(k, ast.Var{Value: static})
  113. return nil
  114. })
  115. if err != nil {
  116. return nil, err
  117. }
  118. }
  119. if len(origTask.Cmds) > 0 {
  120. new.Cmds = make([]*ast.Cmd, 0, len(origTask.Cmds))
  121. for _, cmd := range origTask.Cmds {
  122. if cmd == nil {
  123. continue
  124. }
  125. if cmd.For != nil {
  126. list, keys, err := itemsFromFor(cmd.For, new.Dir, new.Sources, vars, origTask.Location)
  127. if err != nil {
  128. return nil, err
  129. }
  130. // Name the iterator variable
  131. var as string
  132. if cmd.For.As != "" {
  133. as = cmd.For.As
  134. } else {
  135. as = "ITEM"
  136. }
  137. // Create a new command for each item in the list
  138. for i, loopValue := range list {
  139. extra := map[string]any{
  140. as: loopValue,
  141. }
  142. if len(keys) > 0 {
  143. extra["KEY"] = keys[i]
  144. }
  145. newCmd := cmd.DeepCopy()
  146. newCmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
  147. newCmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)
  148. newCmd.Vars = templater.ReplaceVarsWithExtra(cmd.Vars, cache, extra)
  149. new.Cmds = append(new.Cmds, newCmd)
  150. }
  151. continue
  152. }
  153. // Defer commands are replaced in a lazy manner because
  154. // we need to include EXIT_CODE.
  155. if cmd.Defer {
  156. new.Cmds = append(new.Cmds, cmd.DeepCopy())
  157. continue
  158. }
  159. newCmd := cmd.DeepCopy()
  160. newCmd.Cmd = templater.Replace(cmd.Cmd, cache)
  161. newCmd.Task = templater.Replace(cmd.Task, cache)
  162. newCmd.Vars = templater.ReplaceVars(cmd.Vars, cache)
  163. new.Cmds = append(new.Cmds, newCmd)
  164. }
  165. }
  166. if len(origTask.Deps) > 0 {
  167. new.Deps = make([]*ast.Dep, 0, len(origTask.Deps))
  168. for _, dep := range origTask.Deps {
  169. if dep == nil {
  170. continue
  171. }
  172. if dep.For != nil {
  173. list, keys, err := itemsFromFor(dep.For, new.Dir, new.Sources, vars, origTask.Location)
  174. if err != nil {
  175. return nil, err
  176. }
  177. // Name the iterator variable
  178. var as string
  179. if dep.For.As != "" {
  180. as = dep.For.As
  181. } else {
  182. as = "ITEM"
  183. }
  184. // Create a new command for each item in the list
  185. for i, loopValue := range list {
  186. extra := map[string]any{
  187. as: loopValue,
  188. }
  189. if len(keys) > 0 {
  190. extra["KEY"] = keys[i]
  191. }
  192. newDep := dep.DeepCopy()
  193. newDep.Task = templater.ReplaceWithExtra(dep.Task, cache, extra)
  194. newDep.Vars = templater.ReplaceVarsWithExtra(dep.Vars, cache, extra)
  195. new.Deps = append(new.Deps, newDep)
  196. }
  197. continue
  198. }
  199. newDep := dep.DeepCopy()
  200. newDep.Task = templater.Replace(dep.Task, cache)
  201. newDep.Vars = templater.ReplaceVars(dep.Vars, cache)
  202. new.Deps = append(new.Deps, newDep)
  203. }
  204. }
  205. if len(origTask.Preconditions) > 0 {
  206. new.Preconditions = make([]*ast.Precondition, 0, len(origTask.Preconditions))
  207. for _, precondition := range origTask.Preconditions {
  208. if precondition == nil {
  209. continue
  210. }
  211. newPrecondition := precondition.DeepCopy()
  212. newPrecondition.Sh = templater.Replace(precondition.Sh, cache)
  213. newPrecondition.Msg = templater.Replace(precondition.Msg, cache)
  214. new.Preconditions = append(new.Preconditions, newPrecondition)
  215. }
  216. }
  217. if len(origTask.Status) > 0 {
  218. timestampChecker := fingerprint.NewTimestampChecker(e.TempDir.Fingerprint, e.Dry)
  219. checksumChecker := fingerprint.NewChecksumChecker(e.TempDir.Fingerprint, e.Dry)
  220. for _, checker := range []fingerprint.SourcesCheckable{timestampChecker, checksumChecker} {
  221. value, err := checker.Value(&new)
  222. if err != nil {
  223. return nil, err
  224. }
  225. vars.Set(strings.ToUpper(checker.Kind()), ast.Var{Live: value})
  226. }
  227. // Adding new variables, requires us to refresh the templaters
  228. // cache of the the values manually
  229. cache.ResetCache()
  230. new.Status = templater.Replace(origTask.Status, cache)
  231. }
  232. // We only care about templater errors if we are evaluating shell variables
  233. if evaluateShVars && cache.Err() != nil {
  234. return &new, cache.Err()
  235. }
  236. return &new, nil
  237. }
  238. func asAnySlice[T any](slice []T) []any {
  239. ret := make([]any, len(slice))
  240. for i, v := range slice {
  241. ret[i] = v
  242. }
  243. return ret
  244. }
  245. func itemsFromFor(
  246. f *ast.For,
  247. dir string,
  248. sources []*ast.Glob,
  249. vars *ast.Vars,
  250. location *ast.Location,
  251. ) ([]any, []string, error) {
  252. var keys []string // The list of keys to loop over (only if looping over a map)
  253. var values []any // The list of values to loop over
  254. // Get the list from a matrix
  255. if f.Matrix.Len() != 0 {
  256. return asAnySlice(product(f.Matrix)), nil, nil
  257. }
  258. // Get the list from the explicit for list
  259. if len(f.List) > 0 {
  260. return f.List, nil, nil
  261. }
  262. // Get the list from the task sources
  263. if f.From == "sources" {
  264. glist, err := fingerprint.Globs(dir, sources)
  265. if err != nil {
  266. return nil, nil, err
  267. }
  268. // Make the paths relative to the task dir
  269. for i, v := range glist {
  270. if glist[i], err = filepath.Rel(dir, v); err != nil {
  271. return nil, nil, err
  272. }
  273. }
  274. values = asAnySlice(glist)
  275. }
  276. // Get the list from a variable and split it up
  277. if f.Var != "" {
  278. if vars != nil {
  279. v := vars.Get(f.Var)
  280. // If the variable is dynamic, then it hasn't been resolved yet
  281. // and we can't use it as a list. This happens when fast compiling a task
  282. // for use in --list or --list-all etc.
  283. if v.Value != nil && v.Sh == "" {
  284. switch value := v.Value.(type) {
  285. case string:
  286. if f.Split != "" {
  287. values = asAnySlice(strings.Split(value, f.Split))
  288. } else {
  289. values = asAnySlice(strings.Fields(value))
  290. }
  291. case []any:
  292. values = value
  293. case map[string]any:
  294. for k, v := range value {
  295. keys = append(keys, k)
  296. values = append(values, v)
  297. }
  298. default:
  299. return nil, nil, errors.TaskfileInvalidError{
  300. URI: location.Taskfile,
  301. Err: errors.New("loop var must be a delimiter-separated string, list or a map"),
  302. }
  303. }
  304. }
  305. }
  306. }
  307. return values, keys, nil
  308. }
  309. // product generates the cartesian product of the input map of slices.
  310. func product(inputMap omap.OrderedMap[string, []any]) []map[string]any {
  311. if inputMap.Len() == 0 {
  312. return nil
  313. }
  314. // Start with an empty product result
  315. result := []map[string]any{{}}
  316. // Iterate over each slice in the slices
  317. _ = inputMap.Range(func(key string, slice []any) error {
  318. var newResult []map[string]any
  319. // For each combination in the current result
  320. for _, combination := range result {
  321. // Append each element from the current slice to the combinations
  322. for _, item := range slice {
  323. newComb := make(map[string]any, len(combination))
  324. // Copy the existing combination
  325. for k, v := range combination {
  326. newComb[k] = v
  327. }
  328. // Add the current item with the corresponding key
  329. newComb[key] = item
  330. newResult = append(newResult, newComb)
  331. }
  332. }
  333. // Update result with the new combinations
  334. result = newResult
  335. return nil
  336. })
  337. return result
  338. }