Skip to content
Open
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
44 changes: 40 additions & 4 deletions init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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)
Expand All @@ -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
Expand Down
65 changes: 65 additions & 0 deletions init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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))
}
2 changes: 1 addition & 1 deletion internal/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
25 changes: 16 additions & 9 deletions website/src/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
25 changes: 25 additions & 0 deletions website/src/docs/reference/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down