Skip to content

Commit 4ab1958

Browse files
authored
feat(summary): add vars, env, and requires display (#2524)
1 parent 54ca217 commit 4ab1958

File tree

9 files changed

+297
-2
lines changed

9 files changed

+297
-2
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
ensure unique filenames (#2507 by @vmaerten).
77
- Fix `run: when_changed` to work properly for Taskfiles included multiple times
88
(#2508, #2511 by @trulede).
9+
- The `--summary` flag now displays `vars:` (both global and task-level),
10+
`env:`, and `requires:` sections. Dynamic variables show their shell command
11+
(e.g., `sh: echo "hello"`) instead of the evaluated value (#2486 ,#2524 by
12+
@vmaerten).
913

1014
## v3.45.5 - 2025-11-11
1115

compiler.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,14 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
6161
newVar := templater.ReplaceVar(v, cache)
6262
// If the variable should not be evaluated, but is nil, set it to an empty string
6363
// This stops empty interface errors when using the templater to replace values later
64+
// Preserve the Sh field so it can be displayed in summary
6465
if !evaluateShVars && newVar.Value == nil {
65-
result.Set(k, ast.Var{Value: ""})
66+
result.Set(k, ast.Var{Value: "", Sh: newVar.Sh})
6667
return nil
6768
}
6869
// If the variable should not be evaluated and it is set, we can set it and return
6970
if !evaluateShVars {
70-
result.Set(k, ast.Var{Value: newVar.Value})
71+
result.Set(k, ast.Var{Value: newVar.Value, Sh: newVar.Sh})
7172
return nil
7273
}
7374
// Now we can check for errors since we've handled all the cases when we don't want to evaluate

executor_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,30 @@ func TestAlias(t *testing.T) {
621621
)
622622
}
623623

624+
func TestSummaryWithVarsAndRequires(t *testing.T) {
625+
t.Parallel()
626+
627+
// Test basic case from prompt.md - vars and requires
628+
NewExecutorTest(t,
629+
WithName("vars-and-requires"),
630+
WithExecutorOptions(
631+
task.WithDir("testdata/summary-vars-requires"),
632+
task.WithSummary(true),
633+
),
634+
WithTask("mytask"),
635+
)
636+
637+
// Test with shell variables
638+
NewExecutorTest(t,
639+
WithName("shell-vars"),
640+
WithExecutorOptions(
641+
task.WithDir("testdata/summary-vars-requires"),
642+
task.WithSummary(true),
643+
),
644+
WithTask("with-sh-var"),
645+
)
646+
}
647+
624648
func TestLabel(t *testing.T) {
625649
t.Parallel()
626650

internal/summary/summary.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package summary
22

33
import (
4+
"fmt"
5+
"os"
46
"strings"
57

68
"github.com/go-task/task/v3/internal/logger"
@@ -29,6 +31,9 @@ func PrintSpaceBetweenSummaries(l *logger.Logger, i int) {
2931
func PrintTask(l *logger.Logger, t *ast.Task) {
3032
printTaskName(l, t)
3133
printTaskDescribingText(t, l)
34+
printTaskVars(l, t)
35+
printTaskEnv(l, t)
36+
printTaskRequires(l, t)
3237
printTaskDependencies(l, t)
3338
printTaskAliases(l, t)
3439
printTaskCommands(l, t)
@@ -118,3 +123,168 @@ func printTaskCommands(l *logger.Logger, t *ast.Task) {
118123
}
119124
}
120125
}
126+
127+
func printTaskVars(l *logger.Logger, t *ast.Task) {
128+
if t.Vars == nil || t.Vars.Len() == 0 {
129+
return
130+
}
131+
132+
osEnvVars := getEnvVarNames()
133+
134+
taskfileEnvVars := make(map[string]bool)
135+
if t.Env != nil {
136+
for key := range t.Env.All() {
137+
taskfileEnvVars[key] = true
138+
}
139+
}
140+
141+
hasNonEnvVars := false
142+
for key := range t.Vars.All() {
143+
if !isEnvVar(key, osEnvVars) && !taskfileEnvVars[key] {
144+
hasNonEnvVars = true
145+
break
146+
}
147+
}
148+
149+
if !hasNonEnvVars {
150+
return
151+
}
152+
153+
l.Outf(logger.Default, "\n")
154+
l.Outf(logger.Default, "vars:\n")
155+
156+
for key, value := range t.Vars.All() {
157+
// Only display variables that are not from OS environment or Taskfile env
158+
if !isEnvVar(key, osEnvVars) && !taskfileEnvVars[key] {
159+
formattedValue := formatVarValue(value)
160+
l.Outf(logger.Yellow, " %s: %s\n", key, formattedValue)
161+
}
162+
}
163+
}
164+
165+
func printTaskEnv(l *logger.Logger, t *ast.Task) {
166+
if t.Env == nil || t.Env.Len() == 0 {
167+
return
168+
}
169+
170+
envVars := getEnvVarNames()
171+
172+
hasNonEnvVars := false
173+
for key := range t.Env.All() {
174+
if !isEnvVar(key, envVars) {
175+
hasNonEnvVars = true
176+
break
177+
}
178+
}
179+
180+
if !hasNonEnvVars {
181+
return
182+
}
183+
184+
l.Outf(logger.Default, "\n")
185+
l.Outf(logger.Default, "env:\n")
186+
187+
for key, value := range t.Env.All() {
188+
// Only display variables that are not from OS environment
189+
if !isEnvVar(key, envVars) {
190+
formattedValue := formatVarValue(value)
191+
l.Outf(logger.Yellow, " %s: %s\n", key, formattedValue)
192+
}
193+
}
194+
}
195+
196+
// formatVarValue formats a variable value based on its type.
197+
// Handles static values, shell commands (sh:), references (ref:), and maps.
198+
func formatVarValue(v ast.Var) string {
199+
// Shell command - check this first before Value
200+
// because dynamic vars may have both Sh and an empty Value
201+
if v.Sh != nil {
202+
return fmt.Sprintf("sh: %s", *v.Sh)
203+
}
204+
205+
// Reference
206+
if v.Ref != "" {
207+
return fmt.Sprintf("ref: %s", v.Ref)
208+
}
209+
210+
// Static value
211+
if v.Value != nil {
212+
// Check if it's a map or complex type
213+
if m, ok := v.Value.(map[string]any); ok {
214+
return formatMap(m, 4)
215+
}
216+
// Simple string value
217+
return fmt.Sprintf(`"%v"`, v.Value)
218+
}
219+
220+
return `""`
221+
}
222+
223+
// formatMap formats a map value with proper indentation for YAML.
224+
func formatMap(m map[string]any, indent int) string {
225+
if len(m) == 0 {
226+
return "{}"
227+
}
228+
229+
var result strings.Builder
230+
result.WriteString("\n")
231+
spaces := strings.Repeat(" ", indent)
232+
233+
for k, v := range m {
234+
result.WriteString(fmt.Sprintf("%s%s: %v\n", spaces, k, v))
235+
}
236+
237+
return result.String()
238+
}
239+
240+
func printTaskRequires(l *logger.Logger, t *ast.Task) {
241+
if t.Requires == nil || len(t.Requires.Vars) == 0 {
242+
return
243+
}
244+
245+
l.Outf(logger.Default, "\n")
246+
l.Outf(logger.Default, "requires:\n")
247+
l.Outf(logger.Default, " vars:\n")
248+
249+
for _, v := range t.Requires.Vars {
250+
// If the variable has enum constraints, format accordingly
251+
if len(v.Enum) > 0 {
252+
l.Outf(logger.Yellow, " - %s:\n", v.Name)
253+
l.Outf(logger.Yellow, " enum:\n")
254+
for _, enumValue := range v.Enum {
255+
l.Outf(logger.Yellow, " - %s\n", enumValue)
256+
}
257+
} else {
258+
// Simple required variable
259+
l.Outf(logger.Yellow, " - %s\n", v.Name)
260+
}
261+
}
262+
}
263+
264+
func getEnvVarNames() map[string]bool {
265+
envMap := make(map[string]bool)
266+
for _, e := range os.Environ() {
267+
parts := strings.SplitN(e, "=", 2)
268+
if len(parts) > 0 {
269+
envMap[parts[0]] = true
270+
}
271+
}
272+
return envMap
273+
}
274+
275+
// isEnvVar checks if a variable is from OS environment or auto-generated by Task.
276+
func isEnvVar(key string, envVars map[string]bool) bool {
277+
// Filter out auto-generated Task variables
278+
if strings.HasPrefix(key, "TASK_") ||
279+
strings.HasPrefix(key, "CLI_") ||
280+
strings.HasPrefix(key, "ROOT_") ||
281+
key == "TASK" ||
282+
key == "TASKFILE" ||
283+
key == "TASKFILE_DIR" ||
284+
key == "USER_WORKING_DIR" ||
285+
key == "ALIAS" ||
286+
key == "MATCH" {
287+
return true
288+
}
289+
return envVars[key]
290+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
version: 3
2+
3+
vars:
4+
GLOBAL_VAR: "I am a global var"
5+
6+
env:
7+
GLOBAL_ENV: "I am a global env"
8+
9+
tasks:
10+
test-env:
11+
desc: Task with vars and env
12+
vars:
13+
LOCAL_VAR: "I am a local var"
14+
env:
15+
LOCAL_ENV: "I am a local env"
16+
DATABASE_URL: "postgres://localhost/mydb"
17+
requires:
18+
vars:
19+
- API_KEY
20+
cmds:
21+
- echo "Testing env vars"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
version: 3
2+
3+
vars:
4+
GLOBAL_VAR: "I am global"
5+
ANOTHER_GLOBAL: "Also global"
6+
7+
tasks:
8+
test-globals:
9+
desc: Task with global and local vars
10+
vars:
11+
LOCAL_VAR: "I am local"
12+
requires:
13+
vars:
14+
- REQUIRED_VAR
15+
cmds:
16+
- echo {{ .GLOBAL_VAR }} {{ .LOCAL_VAR }}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
version: 3
2+
3+
tasks:
4+
mytask:
5+
desc: It does things
6+
summary: |
7+
It does things and has optional and required variables.
8+
vars:
9+
OPTIONAL_VAR: "hello"
10+
requires:
11+
vars:
12+
- REQUIRED_VAR
13+
cmds:
14+
- cmd: echo {{ .OPTIONAL_VAR }} {{ .REQUIRED_VAR }}
15+
16+
with-sh-var:
17+
desc: Task with shell variable
18+
vars:
19+
DYNAMIC_VAR:
20+
sh: echo "world"
21+
STATIC_VAR: "hello"
22+
cmds:
23+
- echo {{ .DYNAMIC_VAR }}
24+
25+
no-vars:
26+
desc: Task without variables
27+
cmds:
28+
- echo "no vars here"
29+
30+
only-requires:
31+
desc: Task with only requires
32+
requires:
33+
vars:
34+
- NEEDED_VAR
35+
cmds:
36+
- echo {{ .NEEDED_VAR }}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
task: with-sh-var
2+
3+
Task with shell variable
4+
5+
vars:
6+
DYNAMIC_VAR: sh: echo "world"
7+
STATIC_VAR: "hello"
8+
9+
commands:
10+
- echo
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
task: mytask
2+
3+
It does things and has optional and required variables.
4+
5+
vars:
6+
OPTIONAL_VAR: "hello"
7+
8+
requires:
9+
vars:
10+
- REQUIRED_VAR
11+
12+
commands:
13+
- echo hello

0 commit comments

Comments
 (0)