From 2d90de8beb8e2fab624fd9279cf707e44258cabf Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Sun, 16 Nov 2025 18:02:41 +0100 Subject: [PATCH] feat(summary): add vars, env, and requires display --- compiler.go | 5 +- executor_test.go | 24 +++ internal/summary/summary.go | 188 ++++++++++++++++++ .../Taskfile-with-env.yml | 21 ++ .../Taskfile-with-globals.yml | 16 ++ testdata/summary-vars-requires/Taskfile.yml | 36 ++++ ...mmaryWithVarsAndRequires-shell-vars.golden | 10 + ...thVarsAndRequires-vars-and-requires.golden | 13 ++ 8 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 testdata/summary-vars-requires/Taskfile-with-env.yml create mode 100644 testdata/summary-vars-requires/Taskfile-with-globals.yml create mode 100644 testdata/summary-vars-requires/Taskfile.yml create mode 100644 testdata/summary-vars-requires/testdata/TestSummaryWithVarsAndRequires-shell-vars.golden create mode 100644 testdata/summary-vars-requires/testdata/TestSummaryWithVarsAndRequires-vars-and-requires.golden diff --git a/compiler.go b/compiler.go index 348a0728..311fd584 100644 --- a/compiler.go +++ b/compiler.go @@ -61,13 +61,14 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (* 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: ""}) + 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}) + 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 diff --git a/executor_test.go b/executor_test.go index 7f6a3feb..087a3170 100644 --- a/executor_test.go +++ b/executor_test.go @@ -621,6 +621,30 @@ func TestAlias(t *testing.T) { ) } +func TestSummaryWithVarsAndRequires(t *testing.T) { + t.Parallel() + + // Test basic case from prompt.md - vars and requires + NewExecutorTest(t, + WithName("vars-and-requires"), + WithExecutorOptions( + task.WithDir("testdata/summary-vars-requires"), + task.WithSummary(true), + ), + WithTask("mytask"), + ) + + // Test with shell variables + NewExecutorTest(t, + WithName("shell-vars"), + WithExecutorOptions( + task.WithDir("testdata/summary-vars-requires"), + task.WithSummary(true), + ), + WithTask("with-sh-var"), + ) +} + func TestLabel(t *testing.T) { t.Parallel() diff --git a/internal/summary/summary.go b/internal/summary/summary.go index 1741e054..b84cbd7a 100644 --- a/internal/summary/summary.go +++ b/internal/summary/summary.go @@ -1,6 +1,8 @@ package summary import ( + "fmt" + "os" "strings" "github.com/go-task/task/v3/internal/logger" @@ -29,6 +31,9 @@ func PrintSpaceBetweenSummaries(l *logger.Logger, i int) { func PrintTask(l *logger.Logger, t *ast.Task) { printTaskName(l, t) printTaskDescribingText(t, l) + printTaskVars(l, t) + printTaskEnv(l, t) + printTaskRequires(l, t) printTaskDependencies(l, t) printTaskAliases(l, t) printTaskCommands(l, t) @@ -118,3 +123,186 @@ func printTaskCommands(l *logger.Logger, t *ast.Task) { } } } + +// printTaskVars prints the variables defined in a task in YAML format. +// It displays the vars section with proper indentation and formatting. +// Filters out OS environment variables, auto-generated Task variables, and Taskfile env vars. +// Returns early if the task has no variables defined. +func printTaskVars(l *logger.Logger, t *ast.Task) { + if t.Vars == nil || t.Vars.Len() == 0 { + return + } + + // Create a set of OS environment variable names to filter them out + osEnvVars := getEnvVarNames() + + // Create a set of Taskfile env variable names to avoid duplication + taskfileEnvVars := make(map[string]bool) + if t.Env != nil { + for key := range t.Env.All() { + taskfileEnvVars[key] = true + } + } + + // Check if there are any non-environment variables to display + hasNonEnvVars := false + for key := range t.Vars.All() { + if !isEnvVar(key, osEnvVars) && !taskfileEnvVars[key] { + hasNonEnvVars = true + break + } + } + + if !hasNonEnvVars { + return + } + + l.Outf(logger.Default, "\n") + l.Outf(logger.Default, "vars:\n") + + for key, value := range t.Vars.All() { + // Only display variables that are not from OS environment or Taskfile env + if !isEnvVar(key, osEnvVars) && !taskfileEnvVars[key] { + formattedValue := formatVarValue(value) + l.Outf(logger.Yellow, " %s: %s\n", key, formattedValue) + } + } +} + +// printTaskEnv prints the environment variables defined in a task in YAML format. +// It displays the env section with proper indentation and formatting. +// Filters out OS environment variables and auto-generated Task variables. +// Returns early if the task has no environment variables defined. +func printTaskEnv(l *logger.Logger, t *ast.Task) { + if t.Env == nil || t.Env.Len() == 0 { + return + } + + // Create a set of OS environment variable names to filter them out + envVars := getEnvVarNames() + + // Check if there are any non-environment variables to display + hasNonEnvVars := false + for key := range t.Env.All() { + if !isEnvVar(key, envVars) { + hasNonEnvVars = true + break + } + } + + if !hasNonEnvVars { + return + } + + l.Outf(logger.Default, "\n") + l.Outf(logger.Default, "env:\n") + + for key, value := range t.Env.All() { + // Only display variables that are not from OS environment + if !isEnvVar(key, envVars) { + formattedValue := formatVarValue(value) + l.Outf(logger.Yellow, " %s: %s\n", key, formattedValue) + } + } +} + +// formatVarValue formats a variable value based on its type. +// Handles static values, shell commands (sh:), references (ref:), and maps. +func formatVarValue(v ast.Var) string { + // Shell command - check this first before Value + // because dynamic vars may have both Sh and an empty Value + if v.Sh != nil { + return fmt.Sprintf("sh: %s", *v.Sh) + } + + // Reference + if v.Ref != "" { + return fmt.Sprintf("ref: %s", v.Ref) + } + + // Static value + if v.Value != nil { + // Check if it's a map or complex type + if m, ok := v.Value.(map[string]any); ok { + return formatMap(m, 4) + } + // Simple string value + return fmt.Sprintf(`"%v"`, v.Value) + } + + return `""` +} + +// formatMap formats a map value with proper indentation for YAML. +func formatMap(m map[string]any, indent int) string { + if len(m) == 0 { + return "{}" + } + + var result strings.Builder + result.WriteString("\n") + spaces := strings.Repeat(" ", indent) + + for k, v := range m { + result.WriteString(fmt.Sprintf("%s%s: %v\n", spaces, k, v)) + } + + return result.String() +} + +// printTaskRequires prints the required variables for a task in YAML format. +// It displays the requires section with proper indentation and formatting. +// Returns early if the task has no required variables. +func printTaskRequires(l *logger.Logger, t *ast.Task) { + if t.Requires == nil || len(t.Requires.Vars) == 0 { + return + } + + l.Outf(logger.Default, "\n") + l.Outf(logger.Default, "requires:\n") + l.Outf(logger.Default, " vars:\n") + + for _, v := range t.Requires.Vars { + // If the variable has enum constraints, format accordingly + if len(v.Enum) > 0 { + l.Outf(logger.Yellow, " - %s:\n", v.Name) + l.Outf(logger.Yellow, " enum:\n") + for _, enumValue := range v.Enum { + l.Outf(logger.Yellow, " - %s\n", enumValue) + } + } else { + // Simple required variable + l.Outf(logger.Yellow, " - %s\n", v.Name) + } + } +} + +// getEnvVarNames returns a set of all OS environment variable names. +func getEnvVarNames() map[string]bool { + envMap := make(map[string]bool) + for _, e := range os.Environ() { + parts := strings.SplitN(e, "=", 2) + if len(parts) > 0 { + envMap[parts[0]] = true + } + } + return envMap +} + +// isEnvVar checks if a variable is from OS environment or auto-generated by Task. +func isEnvVar(key string, envVars map[string]bool) bool { + // Filter out auto-generated Task variables + if strings.HasPrefix(key, "TASK_") || + strings.HasPrefix(key, "CLI_") || + strings.HasPrefix(key, "ROOT_") || + key == "TASK" || + key == "TASKFILE" || + key == "TASKFILE_DIR" || + key == "USER_WORKING_DIR" || + key == "ALIAS" || + key == "MATCH" { + return true + } + // Filter out OS environment variables + return envVars[key] +} diff --git a/testdata/summary-vars-requires/Taskfile-with-env.yml b/testdata/summary-vars-requires/Taskfile-with-env.yml new file mode 100644 index 00000000..acb42acd --- /dev/null +++ b/testdata/summary-vars-requires/Taskfile-with-env.yml @@ -0,0 +1,21 @@ +version: 3 + +vars: + GLOBAL_VAR: "I am a global var" + +env: + GLOBAL_ENV: "I am a global env" + +tasks: + test-env: + desc: Task with vars and env + vars: + LOCAL_VAR: "I am a local var" + env: + LOCAL_ENV: "I am a local env" + DATABASE_URL: "postgres://localhost/mydb" + requires: + vars: + - API_KEY + cmds: + - echo "Testing env vars" diff --git a/testdata/summary-vars-requires/Taskfile-with-globals.yml b/testdata/summary-vars-requires/Taskfile-with-globals.yml new file mode 100644 index 00000000..1994249f --- /dev/null +++ b/testdata/summary-vars-requires/Taskfile-with-globals.yml @@ -0,0 +1,16 @@ +version: 3 + +vars: + GLOBAL_VAR: "I am global" + ANOTHER_GLOBAL: "Also global" + +tasks: + test-globals: + desc: Task with global and local vars + vars: + LOCAL_VAR: "I am local" + requires: + vars: + - REQUIRED_VAR + cmds: + - echo {{ .GLOBAL_VAR }} {{ .LOCAL_VAR }} diff --git a/testdata/summary-vars-requires/Taskfile.yml b/testdata/summary-vars-requires/Taskfile.yml new file mode 100644 index 00000000..5a2c177f --- /dev/null +++ b/testdata/summary-vars-requires/Taskfile.yml @@ -0,0 +1,36 @@ +version: 3 + +tasks: + mytask: + desc: It does things + summary: | + It does things and has optional and required variables. + vars: + OPTIONAL_VAR: "hello" + requires: + vars: + - REQUIRED_VAR + cmds: + - cmd: echo {{ .OPTIONAL_VAR }} {{ .REQUIRED_VAR }} + + with-sh-var: + desc: Task with shell variable + vars: + DYNAMIC_VAR: + sh: echo "world" + STATIC_VAR: "hello" + cmds: + - echo {{ .DYNAMIC_VAR }} + + no-vars: + desc: Task without variables + cmds: + - echo "no vars here" + + only-requires: + desc: Task with only requires + requires: + vars: + - NEEDED_VAR + cmds: + - echo {{ .NEEDED_VAR }} diff --git a/testdata/summary-vars-requires/testdata/TestSummaryWithVarsAndRequires-shell-vars.golden b/testdata/summary-vars-requires/testdata/TestSummaryWithVarsAndRequires-shell-vars.golden new file mode 100644 index 00000000..ae03ac33 --- /dev/null +++ b/testdata/summary-vars-requires/testdata/TestSummaryWithVarsAndRequires-shell-vars.golden @@ -0,0 +1,10 @@ +task: with-sh-var + +Task with shell variable + +vars: + DYNAMIC_VAR: sh: echo "world" + STATIC_VAR: "hello" + +commands: + - echo diff --git a/testdata/summary-vars-requires/testdata/TestSummaryWithVarsAndRequires-vars-and-requires.golden b/testdata/summary-vars-requires/testdata/TestSummaryWithVarsAndRequires-vars-and-requires.golden new file mode 100644 index 00000000..8e6c687c --- /dev/null +++ b/testdata/summary-vars-requires/testdata/TestSummaryWithVarsAndRequires-vars-and-requires.golden @@ -0,0 +1,13 @@ +task: mytask + +It does things and has optional and required variables. + +vars: + OPTIONAL_VAR: "hello" + +requires: + vars: + - REQUIRED_VAR + +commands: + - echo hello