From 39326767074c778b5d9c527b2e4a848ef7eed3e1 Mon Sep 17 00:00:00 2001 From: b13rg Date: Sun, 26 Apr 2026 11:59:23 -0700 Subject: [PATCH 1/2] Add TASK_INIT_DIR environment var to specify init taskfile --- init.go | 44 +++++++++++++++++++++++++++++++---- init_test.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 4 deletions(-) diff --git a/init.go b/init.go index 807d201d1c..1599f89cf5 100644 --- a/init.go +++ b/init.go @@ -5,7 +5,10 @@ import ( "os" "github.com/go-task/task/v3/errors" + "github.com/go-task/task/v3/internal/env" + "github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/filepathext" + "github.com/go-task/task/v3/internal/fsext" "github.com/go-task/task/v3/taskfile" ) @@ -19,6 +22,11 @@ var DefaultTaskfile string // path can be either a file path or a directory path. // If path is a directory, path/Taskfile.yml will be created. // +// If the TASK_INIT_DIR environment variable is set, the template will be +// read from that location instead of using the default embedded template. +// TASK_INIT_DIR can be a file path or a directory (searched using the same +// logic as calling task). +// // The final file path is always returned and may be different from the input path. func InitTaskfile(path string) (string, error) { info, err := os.Stat(path) @@ -28,19 +36,47 @@ func InitTaskfile(path string) (string, error) { if info != nil && info.IsDir() { // path was a directory, check if there is a Taskfile already - if hasDefaultTaskfile(path) { + if hasExistingTaskfile(path) { return path, errors.TaskfileAlreadyExistsError{} } path = filepathext.SmartJoin(path, defaultFilename) } - if err := os.WriteFile(path, []byte(DefaultTaskfile), 0o644); err != nil { + // Check for TASK_INIT_DIR environment variable + initDir := env.GetTaskEnv("INIT_DIR") + if initDir == "" { + // No override specified, use the default embedded template + if err := os.WriteFile(path, []byte(DefaultTaskfile), 0o644); err != nil { //nolint:gosec + return path, err + } + return path, nil + } + + // Expand shell symbols like ~ and environment variables + initDir, err = execext.ExpandLiteral(initDir) + if err != nil { return path, err } - return path, nil + + // Use the same search logic as calling task: + // - If initDir is a file, use it directly + // - If initDir is a directory, search for a Taskfile in it + templatePath, err := fsext.SearchPath(initDir, taskfile.DefaultTaskfiles) + if err != nil { + return path, err + } + + // Read the template file + templateContent, err := os.ReadFile(templatePath) + if err != nil { + return path, err + } + + // Write the template to the destination + return path, os.WriteFile(path, templateContent, 0o644) //nolint:gosec } -func hasDefaultTaskfile(dir string) bool { +func hasExistingTaskfile(dir string) bool { for _, name := range taskfile.DefaultTaskfiles { if _, err := os.Stat(filepathext.SmartJoin(dir, name)); err == nil { return true diff --git a/init_test.go b/init_test.go index 41095d65e1..1b2d0b00fe 100644 --- a/init_test.go +++ b/init_test.go @@ -2,8 +2,12 @@ package task_test import ( "os" + "path/filepath" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/go-task/task/v3" "github.com/go-task/task/v3/internal/filepathext" ) @@ -50,3 +54,64 @@ func TestInitFile(t *testing.T) { } _ = os.Remove(file) } + +func TestInitWithEnvDir(t *testing.T) { + // Cannot use t.Parallel() with t.Setenv() + // Create a temporary directory for the test + tmpDir := t.TempDir() + outputFile := filepath.Join(tmpDir, "Taskfile.yml") + + // Create a template directory with a custom Taskfile + templateDir := filepath.Join(tmpDir, "templates") + require.NoError(t, os.MkdirAll(templateDir, 0o755)) + customTemplate := `# Custom template +version: '3' +tasks: + custom: + cmds: + - echo "custom" +` + require.NoError(t, os.WriteFile(filepath.Join(templateDir, "Taskfile.yml"), []byte(customTemplate), 0o644)) + + // Set TASK_INIT_DIR to the template directory + t.Setenv("TASK_INIT_DIR", templateDir) + + // Initialize the Taskfile + _, err := task.InitTaskfile(tmpDir) + require.NoError(t, err) + + // Read the created file and verify it matches the custom template + content, err := os.ReadFile(outputFile) + require.NoError(t, err) + assert.Equal(t, customTemplate, string(content)) +} + +func TestInitWithEnvDirFile(t *testing.T) { + // Cannot use t.Parallel() with t.Setenv() + // Create a temporary directory for the test + tmpDir := t.TempDir() + outputFile := filepath.Join(tmpDir, "Taskfile.yml") + + // Create a template file directly + templateFile := filepath.Join(tmpDir, "custom-template.yml") + customTemplate := `# Direct file template +version: '3' +tasks: + direct: + cmds: + - echo "direct" +` + require.NoError(t, os.WriteFile(templateFile, []byte(customTemplate), 0o644)) + + // Set TASK_INIT_DIR to the template file directly + t.Setenv("TASK_INIT_DIR", templateFile) + + // Initialize the Taskfile + _, err := task.InitTaskfile(tmpDir) + require.NoError(t, err) + + // Read the created file and verify it matches the custom template + content, err := os.ReadFile(outputFile) + require.NoError(t, err) + assert.Equal(t, customTemplate, string(content)) +} From a50c48f4ed21c09a7e911b2922810b55b065ea87 Mon Sep 17 00:00:00 2001 From: b13rg Date: Sun, 26 Apr 2026 12:14:26 -0700 Subject: [PATCH 2/2] Document INIT_DIR env variable --- internal/flags/flags.go | 2 +- website/src/docs/reference/cli.md | 25 +++++++++++++++-------- website/src/docs/reference/environment.md | 25 +++++++++++++++++++++++ 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 51bec00468..a4216dc5de 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -121,7 +121,7 @@ func init() { pflag.BoolVar(&Version, "version", false, "Show Task version.") pflag.BoolVarP(&Help, "help", "h", false, "Shows Task usage.") - pflag.BoolVarP(&Init, "init", "i", false, "Creates a new Taskfile.yml in the current folder.") + pflag.BoolVarP(&Init, "init", "i", false, "Creates a new Taskfile.yml in the current folder. Use TASK_INIT_DIR to specify a custom template.") pflag.StringVar(&Completion, "completion", "", "Generates shell completion script.") pflag.BoolVarP(&List, "list", "l", false, "Lists tasks with description of current Taskfile.") pflag.BoolVarP(&ListAll, "list-all", "a", false, "Lists tasks with or without a description.") diff --git a/website/src/docs/reference/cli.md b/website/src/docs/reference/cli.md index aeb417f1f4..da4d675180 100644 --- a/website/src/docs/reference/cli.md +++ b/website/src/docs/reference/cli.md @@ -63,15 +63,6 @@ task --list-all task -a ``` -### `task --init` - -Create a new Taskfile.yml in the current directory. - -```bash -task --init -task -i -``` - ::: tip Combine `--list` or `--list-all` with `--silent` (`-ls` or `-as` for shortants) @@ -80,6 +71,22 @@ similar. ::: +### `task --init` + +Create a new Taskfile.yml in the current directory. By default, Task uses a +built-in template, but you can specify a custom template location using the +`TASK_INIT_DIR` environment variable. + +- **Environment variable**: [`TASK_INIT_DIR`](./environment.md#task-init-dir) + +```bash +task --init +task -i + +# Use a custom template directory or file +TASK_INIT_DIR=~/.config/task task --init +``` + ## Options ### General diff --git a/website/src/docs/reference/environment.md b/website/src/docs/reference/environment.md index 45937e9238..5873b0f9d1 100644 --- a/website/src/docs/reference/environment.md +++ b/website/src/docs/reference/environment.md @@ -81,6 +81,31 @@ variables. The priority order is: CLI flags > environment variables > config fil - **Default**: `false` - **Description**: Prompt for missing required variables +### `TASK_INIT_DIR` + +- **Type**: `string` (file or directory path) +- **Default**: (none - uses built-in template) +- **Description**: Specifies a custom template location for `task --init`. When + set, Task will copy the Taskfile from this location instead of using the + default embedded template. + +The value can be: + +- A **file path**: Task will use the file directly as the template +- A **directory path**: Task will search the directory for a Taskfile using the + same search logic as running `task` (looks for `Taskfile.yml`, `taskfile.yml`, + `Taskfile.yaml`, etc.) + +Shell expansion is supported (e.g., `~` for home directory). + +```bash +# Use a specific template file +TASK_INIT_DIR=~/templates/Taskfile.yml task --init + +# Use a directory (Task will search for Taskfile.yml, etc.) +TASK_INIT_DIR=~/templates/my-project task --init +``` + ### `TASK_TEMP_DIR` Defines the location of Task's temporary directory which is used for storing