Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions internal/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
72 changes: 51 additions & 21 deletions internal/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand All @@ -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.")

Expand All @@ -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
Expand Down Expand Up @@ -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
}
11 changes: 10 additions & 1 deletion website/src/docs/experiments/remote-taskfiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down
17 changes: 16 additions & 1 deletion website/src/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
```
Expand All @@ -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
Expand All @@ -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
```
Expand Down Expand Up @@ -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
```
Expand Down Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions website/src/docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading
Loading