diff --git a/internal/env/env.go b/internal/env/env.go index 4b64e504b5..f207667971 100644 --- a/internal/env/env.go +++ b/internal/env/env.go @@ -3,7 +3,9 @@ package env import ( "fmt" "os" + "strconv" "strings" + "time" "github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/taskfile/ast" @@ -61,3 +63,63 @@ func isTypeAllowed(v any) bool { func GetTaskEnv(key string) string { return os.Getenv(taskVarPrefix + key) } + +// GetTaskEnvBool returns the boolean value of a TASK_ prefixed env var. +// Returns the value and true if set and valid, or false and false if not set or invalid. +func GetTaskEnvBool(key string) (bool, bool) { + v := GetTaskEnv(key) + if v == "" { + return false, false + } + b, err := strconv.ParseBool(v) + return b, err == nil +} + +// GetTaskEnvInt returns the integer value of a TASK_ prefixed env var. +// Returns the value and true if set and valid, or 0 and false if not set or invalid. +func GetTaskEnvInt(key string) (int, bool) { + v := GetTaskEnv(key) + if v == "" { + return 0, false + } + i, err := strconv.Atoi(v) + return i, err == nil +} + +// GetTaskEnvDuration returns the duration value of a TASK_ prefixed env var. +// Returns the value and true if set and valid, or 0 and false if not set or invalid. +func GetTaskEnvDuration(key string) (time.Duration, bool) { + v := GetTaskEnv(key) + if v == "" { + return 0, false + } + d, err := time.ParseDuration(v) + return d, err == nil +} + +// GetTaskEnvString returns the string value of a TASK_ prefixed env var. +// Returns the value and true if set (non-empty), or empty string and false if not set. +func GetTaskEnvString(key string) (string, bool) { + v := GetTaskEnv(key) + return v, v != "" +} + +// GetTaskEnvStringSlice returns a comma-separated list from a TASK_ prefixed env var. +// Returns the slice and true if set (non-empty), or nil and false if not set. +func GetTaskEnvStringSlice(key string) ([]string, bool) { + v := GetTaskEnv(key) + if v == "" { + return nil, false + } + parts := strings.Split(v, ",") + result := make([]string, 0, len(parts)) + for _, p := range parts { + if trimmed := strings.TrimSpace(p); trimmed != "" { + result = append(result, trimmed) + } + } + if len(result) == 0 { + return nil, false + } + return result, true +} diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 6019e51d5f..7e3cacb4dc 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -126,11 +126,11 @@ func init() { pflag.BoolVar(&Status, "status", false, "Exits with non-zero exit code if any of the given tasks is not up-to-date.") pflag.BoolVar(&NoStatus, "no-status", false, "Ignore status when listing tasks as JSON") pflag.BoolVar(&Nested, "nested", false, "Nest namespaces when listing tasks as JSON") - pflag.BoolVar(&Insecure, "insecure", getConfig(config, func() *bool { return config.Remote.Insecure }, false), "Forces Task to download Taskfiles over insecure connections.") + pflag.BoolVar(&Insecure, "insecure", getConfig(config, "REMOTE_INSECURE", func() *bool { return config.Remote.Insecure }, false), "Forces Task to download Taskfiles over insecure connections.") pflag.BoolVarP(&Watch, "watch", "w", false, "Enables watch of the given task.") - pflag.BoolVarP(&Verbose, "verbose", "v", getConfig(config, func() *bool { return config.Verbose }, false), "Enables verbose mode.") + pflag.BoolVarP(&Verbose, "verbose", "v", getConfig(config, "VERBOSE", func() *bool { return config.Verbose }, false), "Enables verbose mode.") pflag.BoolVarP(&Silent, "silent", "s", false, "Disables echoing.") - pflag.BoolVar(&DisableFuzzy, "disable-fuzzy", getConfig(config, func() *bool { return config.DisableFuzzy }, false), "Disables fuzzy matching for task names.") + pflag.BoolVar(&DisableFuzzy, "disable-fuzzy", getConfig(config, "DISABLE_FUZZY", func() *bool { return config.DisableFuzzy }, false), "Disables fuzzy matching for task names.") pflag.BoolVarP(&AssumeYes, "yes", "y", false, "Assume \"yes\" as answer to all prompts.") pflag.BoolVarP(&Parallel, "parallel", "p", false, "Executes tasks provided on command line in parallel.") pflag.BoolVarP(&Dry, "dry", "n", false, "Compiles and prints tasks in the order that they would be run, without executing them.") @@ -142,10 +142,10 @@ func init() { pflag.StringVar(&Output.Group.Begin, "output-group-begin", "", "Message template to print before a task's grouped output.") pflag.StringVar(&Output.Group.End, "output-group-end", "", "Message template to print after a task's grouped output.") pflag.BoolVar(&Output.Group.ErrorOnly, "output-group-error-only", false, "Swallow output from successful tasks.") - pflag.BoolVarP(&Color, "color", "c", getConfig(config, func() *bool { return config.Color }, true), "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.") - pflag.IntVarP(&Concurrency, "concurrency", "C", getConfig(config, func() *int { return config.Concurrency }, 0), "Limit number of tasks to run concurrently.") + pflag.BoolVarP(&Color, "color", "c", getConfig(config, "COLOR", func() *bool { return config.Color }, true), "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.") + pflag.IntVarP(&Concurrency, "concurrency", "C", getConfig(config, "CONCURRENCY", func() *int { return config.Concurrency }, 0), "Limit number of tasks to run concurrently.") pflag.DurationVarP(&Interval, "interval", "I", 0, "Interval to watch for changes.") - pflag.BoolVarP(&Failfast, "failfast", "F", getConfig(config, func() *bool { return &config.Failfast }, false), "When running tasks in parallel, stop all tasks if one fails.") + pflag.BoolVarP(&Failfast, "failfast", "F", getConfig(config, "FAILFAST", func() *bool { return &config.Failfast }, false), "When running tasks in parallel, stop all tasks if one fails.") pflag.BoolVarP(&Global, "global", "g", false, "Runs global Taskfile, from $HOME/{T,t}askfile.{yml,yaml}.") pflag.BoolVar(&Experiments, "experiments", false, "Lists all the available experiments and whether or not they are enabled.") @@ -160,18 +160,18 @@ func init() { // Remote Taskfiles experiment will adds the "download" and "offline" flags if experiments.RemoteTaskfiles.Enabled() { pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.") - pflag.BoolVar(&Offline, "offline", getConfig(config, func() *bool { return config.Remote.Offline }, false), "Forces Task to only use local or cached Taskfiles.") - pflag.StringSliceVar(&TrustedHosts, "trusted-hosts", getConfig(config, func() *[]string { return &config.Remote.TrustedHosts }, nil), "List of trusted hosts for remote Taskfiles (comma-separated).") - pflag.DurationVar(&Timeout, "timeout", getConfig(config, func() *time.Duration { return config.Remote.Timeout }, time.Second*10), "Timeout for downloading remote Taskfiles.") + pflag.BoolVar(&Offline, "offline", getConfig(config, "REMOTE_OFFLINE", func() *bool { return config.Remote.Offline }, false), "Forces Task to only use local or cached Taskfiles.") + pflag.StringSliceVar(&TrustedHosts, "trusted-hosts", getConfig(config, "REMOTE_TRUSTED_HOSTS", func() *[]string { return &config.Remote.TrustedHosts }, nil), "List of trusted hosts for remote Taskfiles (comma-separated).") + pflag.DurationVar(&Timeout, "timeout", getConfig(config, "REMOTE_TIMEOUT", func() *time.Duration { return config.Remote.Timeout }, time.Second*10), "Timeout for downloading remote Taskfiles.") pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.") - pflag.DurationVar(&CacheExpiryDuration, "expiry", getConfig(config, func() *time.Duration { return config.Remote.CacheExpiry }, 0), "Expiry duration for cached remote Taskfiles.") - pflag.StringVar(&RemoteCacheDir, "remote-cache-dir", getConfig(config, func() *string { return config.Remote.CacheDir }, env.GetTaskEnv("REMOTE_DIR")), "Directory to cache remote Taskfiles.") + pflag.DurationVar(&CacheExpiryDuration, "expiry", getConfig(config, "REMOTE_CACHE_EXPIRY", func() *time.Duration { return config.Remote.CacheExpiry }, 0), "Expiry duration for cached remote Taskfiles.") + pflag.StringVar(&RemoteCacheDir, "remote-cache-dir", getConfig(config, "REMOTE_CACHE_DIR", func() *string { return config.Remote.CacheDir }, env.GetTaskEnv("REMOTE_DIR")), "Directory to cache remote Taskfiles.") } pflag.Parse() // Auto-detect color based on environment when not explicitly configured - // Priority: CLI flag > taskrc config > NO_COLOR > FORCE_COLOR/CI > default - colorExplicitlySet := pflag.Lookup("color").Changed || (config != nil && config.Color != nil) + // Priority: CLI flag > TASK_COLOR env > taskrc config > NO_COLOR > FORCE_COLOR/CI > default + colorExplicitlySet := pflag.Lookup("color").Changed || env.GetTaskEnv("COLOR") != "" || (config != nil && config.Color != nil) if !colorExplicitlySet { if os.Getenv("NO_COLOR") != "" { Color = false @@ -294,15 +294,45 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) { ) } -// getConfig extracts a config value directly from a pointer field with a fallback default -func getConfig[T any](config *taskrcast.TaskRC, fieldFunc func() *T, fallback T) T { - if config == nil { - return fallback +// getConfig extracts a config value with priority: env var > taskrc config > fallback +func getConfig[T any](config *taskrcast.TaskRC, envKey string, fieldFunc func() *T, fallback T) T { + if envKey != "" { + if val, ok := getEnvAs[T](envKey); ok { + return val + } } - - field := fieldFunc() - if field != nil { - return *field + if config != nil { + if field := fieldFunc(); field != nil { + return *field + } } return fallback } + +// getEnvAs parses a TASK_ prefixed env var as type T +func getEnvAs[T any](envKey string) (T, bool) { + var zero T + switch any(zero).(type) { + case bool: + if val, ok := env.GetTaskEnvBool(envKey); ok { + return any(val).(T), true + } + case int: + if val, ok := env.GetTaskEnvInt(envKey); ok { + return any(val).(T), true + } + case time.Duration: + if val, ok := env.GetTaskEnvDuration(envKey); ok { + return any(val).(T), true + } + case string: + if val, ok := env.GetTaskEnvString(envKey); ok { + return any(val).(T), true + } + case []string: + if val, ok := env.GetTaskEnvStringSlice(envKey); ok { + return any(val).(T), true + } + } + return zero, false +} diff --git a/website/src/docs/experiments/remote-taskfiles.md b/website/src/docs/experiments/remote-taskfiles.md index 8e8a34c75d..f37bb336bf 100644 --- a/website/src/docs/experiments/remote-taskfiles.md +++ b/website/src/docs/experiments/remote-taskfiles.md @@ -320,6 +320,8 @@ remote: - **Type**: `boolean` - **Default**: `false` - **Description**: Allow insecure connections when fetching remote Taskfiles +- **CLI equivalent**: `--insecure` +- **Environment variable**: `TASK_REMOTE_INSECURE` ```yaml remote: @@ -331,6 +333,8 @@ remote: - **Type**: `boolean` - **Default**: `false` - **Description**: Work in offline mode, preventing remote Taskfile fetching +- **CLI equivalent**: `--offline` +- **Environment variable**: `TASK_REMOTE_OFFLINE` ```yaml remote: @@ -343,6 +347,8 @@ remote: - **Default**: 10s - **Pattern**: `^[0-9]+(ns|us|µs|ms|s|m|h)$` - **Description**: Timeout duration for remote operations (e.g., '30s', '5m') +- **CLI equivalent**: `--timeout` +- **Environment variable**: `TASK_REMOTE_TIMEOUT` ```yaml remote: @@ -356,6 +362,8 @@ remote: - **Pattern**: `^[0-9]+(ns|us|µs|ms|s|m|h)$` - **Description**: Cache expiry duration for remote Taskfiles (e.g., '1h', '24h') +- **CLI equivalent**: `--expiry` +- **Environment variable**: `TASK_REMOTE_CACHE_EXPIRY` ```yaml remote: @@ -369,7 +377,7 @@ remote: - **Description**: Directory where remote Taskfiles are cached. Can be an absolute path (e.g., `/var/cache/task`) or relative to the Taskfile directory. - **CLI equivalent**: `--remote-cache-dir` -- **Environment variable**: `TASK_REMOTE_DIR` (lowest priority) +- **Environment variable**: `TASK_REMOTE_CACHE_DIR` ```yaml remote: @@ -383,6 +391,7 @@ remote: - **Description**: List of trusted hosts for remote Taskfiles. Hosts in this list will not prompt for confirmation when downloading Taskfiles - **CLI equivalent**: `--trusted-hosts` +- **Environment variable**: `TASK_REMOTE_TRUSTED_HOSTS` (comma-separated) ```yaml remote: diff --git a/website/src/docs/reference/cli.md b/website/src/docs/reference/cli.md index c434e5b854..b9ac1b8e54 100644 --- a/website/src/docs/reference/cli.md +++ b/website/src/docs/reference/cli.md @@ -10,8 +10,8 @@ outline: deep Task has multiple ways of being configured. These methods are parsed, in sequence, in the following order with the highest priority last: -- [Environment variables](./environment.md) - [Configuration files](./config.md) +- [Environment variables](./environment.md) - _Command-line flags_ In this document, we will look at the last of the three options, command-line @@ -96,6 +96,9 @@ task --version Enable verbose mode for detailed output. +- **Config equivalent**: [`verbose`](./config.md#verbose) +- **Environment variable**: [`TASK_VERBOSE`](./environment.md#task-verbose) + ```bash task build --verbose ``` @@ -112,6 +115,9 @@ task deploy --silent Disable fuzzy matching for task names. When enabled, Task will not suggest similar task names when you mistype a task name. +- **Config equivalent**: [`disable-fuzzy`](./config.md#disable-fuzzy) +- **Environment variable**: [`TASK_DISABLE_FUZZY`](./environment.md#task-disable-fuzzy) + ```bash task buidl --disable-fuzzy # Output: Task "buidl" does not exist @@ -124,6 +130,9 @@ task buidl --disable-fuzzy Stop executing dependencies as soon as one of them fails. +- **Config equivalent**: [`failfast`](./config.md#failfast) +- **Environment variable**: [`TASK_FAILFAST`](./environment.md#task-failfast) + ```bash task build --failfast ``` @@ -156,6 +165,9 @@ task test lint --parallel Limit the number of concurrent tasks. Zero means unlimited. +- **Config equivalent**: [`concurrency`](./config.md#concurrency) +- **Environment variable**: [`TASK_CONCURRENCY`](./environment.md#task-concurrency) + ```bash task test --concurrency 4 ``` @@ -232,6 +244,9 @@ task test --output group --output-group-error-only Control colored output. Enabled by default. +- **Config equivalent**: [`color`](./config.md#color) +- **Environment variable**: [`TASK_COLOR`](./environment.md#task-color) + ```bash task build --color=false # or use environment variable diff --git a/website/src/docs/reference/config.md b/website/src/docs/reference/config.md index 04a33a5de6..e308e1d4c6 100644 --- a/website/src/docs/reference/config.md +++ b/website/src/docs/reference/config.md @@ -10,11 +10,11 @@ outline: deep Task has multiple ways of being configured. These methods are parsed, in sequence, in the following order with the highest priority last: -- [Environment variables](./environment.md) - _Configuration files_ +- [Environment variables](./environment.md) - [Command-line flags](./cli.md) -In this document, we will look at the second of the three options, configuration +In this document, we will look at the first of the three options, configuration files. ## File Precedence @@ -86,6 +86,7 @@ experiments: - **Default**: `false` - **Description**: Enable verbose output for all tasks - **CLI equivalent**: [`-v, --verbose`](./cli.md#-v---verbose) +- **Environment variable**: [`TASK_VERBOSE`](./environment.md#task-verbose) ```yaml verbose: true @@ -97,6 +98,7 @@ verbose: true - **Default**: `true` - **Description**: Enable colored output. Colors are automatically enabled in CI environments (`CI=true`). - **CLI equivalent**: [`-c, --color`](./cli.md#-c---color) +- **Environment variable**: [`TASK_COLOR`](./environment.md#task-color) ```yaml color: false @@ -108,6 +110,7 @@ color: false - **Default**: `false` - **Description**: Disable fuzzy matching for task names. When enabled, Task will not suggest similar task names when you mistype a task name. - **CLI equivalent**: [`--disable-fuzzy`](./cli.md#--disable-fuzzy) +- **Environment variable**: [`TASK_DISABLE_FUZZY`](./environment.md#task-disable-fuzzy) ```yaml disable-fuzzy: true @@ -119,6 +122,7 @@ disable-fuzzy: true - **Minimum**: `1` - **Description**: Number of concurrent tasks to run - **CLI equivalent**: [`-C, --concurrency`](./cli.md#-c---concurrency-number) +- **Environment variable**: [`TASK_CONCURRENCY`](./environment.md#task-concurrency) ```yaml concurrency: 4 @@ -129,7 +133,8 @@ concurrency: 4 - **Type**: `boolean` - **Default**: `false` - **Description**: Stop executing dependencies as soon as one of them fail -- **CLI equivalent**: [`-F, --failfast`](./cli.md#f-failfast) +- **CLI equivalent**: [`-F, --failfast`](./cli.md#-f---failfast) +- **Environment variable**: [`TASK_FAILFAST`](./environment.md#task-failfast) ```yaml failfast: true diff --git a/website/src/docs/reference/environment.md b/website/src/docs/reference/environment.md index 8c4d3b2d69..533bdc6c91 100644 --- a/website/src/docs/reference/environment.md +++ b/website/src/docs/reference/environment.md @@ -9,16 +9,53 @@ outline: deep Task has multiple ways of being configured. These methods are parsed, in sequence, in the following order with the highest priority last: -- _Environment variables_ - [Configuration files](./config.md) +- _Environment variables_ - [Command-line flags](./cli.md) -In this document, we will look at the first of the three options, environment +In this document, we will look at the second of the three options, environment variables. All Task-specific variables are prefixed with `TASK_` and override their configuration file equivalents. ## Variables +All [configuration file options](./config.md) can also be set via environment +variables. The priority order is: CLI flags > environment variables > config files > defaults. + +### `TASK_VERBOSE` + +- **Type**: `boolean` (`true`, `false`, `1`, `0`) +- **Default**: `false` +- **Description**: Enable verbose output for all tasks +- **Config equivalent**: [`verbose`](./config.md#verbose) + +### `TASK_COLOR` + +- **Type**: `boolean` (`true`, `false`, `1`, `0`) +- **Default**: `true` +- **Description**: Enable colored output +- **Config equivalent**: [`color`](./config.md#color) + +### `TASK_DISABLE_FUZZY` + +- **Type**: `boolean` (`true`, `false`, `1`, `0`) +- **Default**: `false` +- **Description**: Disable fuzzy matching for task names +- **Config equivalent**: [`disable-fuzzy`](./config.md#disable-fuzzy) + +### `TASK_CONCURRENCY` + +- **Type**: `integer` +- **Description**: Limit number of tasks to run concurrently +- **Config equivalent**: [`concurrency`](./config.md#concurrency) + +### `TASK_FAILFAST` + +- **Type**: `boolean` (`true`, `false`, `1`, `0`) +- **Default**: `false` +- **Description**: When running tasks in parallel, stop all tasks if one fails +- **Config equivalent**: [`failfast`](./config.md#failfast) + ### `TASK_TEMP_DIR` Defines the location of Task's temporary directory which is used for storing @@ -34,18 +71,6 @@ Valid values are `true` (`1`) or `false` (`0`). By default, this is `true` on Windows and `false` on other operating systems. We might consider making this enabled by default on all platforms in the future. -### `TASK_REMOTE_DIR` - -Defines the location of Task's remote temporary directory which is used for -caching remote files. Can be relative like `tmp/task` or absolute like -`/tmp/.task` or `~/.task`. Relative paths are relative to the root Taskfile, not -the working directory. Defaults to: `./.task`. - -### `TASK_OFFLINE` - -Set the `--offline` flag through the environment variable. Only for remote -experiment. CLI flag `--offline` takes precedence over the env variable. - ### `FORCE_COLOR` Force color output usage.