Skip to content
Closed
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
38 changes: 38 additions & 0 deletions cmd/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,44 @@ func run() error {
return log.PrintExperiments()
}

// Handle --init-template flag (list or create with specified template)
if pflag.Lookup("init-template").Changed {
if flags.InitTemplate == "list" {
// List available templates
log.Outf(logger.Default, "Available templates:\n")
for _, name := range task.ListTemplates() {
log.Outf(logger.Default, " * %s\n", name)
}
return nil
}

// Create Taskfile with specified built-in template
wd, err := os.Getwd()
if err != nil {
return err
}
args, _, err := args.Get()
if err != nil {
return err
}
path := wd
if len(args) > 0 {
name := args[0]
if filepathext.IsExtOnly(name) {
name = filepathext.SmartJoin(filepath.Dir(name), "Taskfile"+filepath.Ext(name))
}
path = filepathext.SmartJoin(wd, name)
}
finalPath, err := task.InitTaskfileWithTemplate(path, flags.InitTemplate)
if err != nil {
return err
}
if !flags.Silent {
log.Outf(logger.Green, "Taskfile created: %s\n", filepathext.TryAbsToRel(finalPath))
}
return nil
}

if flags.Init {
wd, err := os.Getwd()
if err != nil {
Expand Down
36 changes: 35 additions & 1 deletion init.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,38 @@ const defaultFilename = "Taskfile.yml"
//go:embed taskfile/templates/default.yml
var DefaultTaskfile string

//go:embed taskfile/templates/full.yml
var FullTaskfile string

// BuiltinTemplates maps template names to their content
var BuiltinTemplates = map[string]string{
"default": DefaultTaskfile,
"full": FullTaskfile,
}

// InitTaskfile creates a new Taskfile at path.
//
// path can be either a file path or a directory path.
// If path is a directory, path/Taskfile.yml will be created.
//
// The final file path is always returned and may be different from the input path.
func InitTaskfile(path string) (string, error) {
return InitTaskfileWithTemplate(path, "default")
}

// InitTaskfileWithTemplate creates a new Taskfile at path using the specified template.
//
// template must be a built-in template name (e.g., "default", "full").
// If path is a directory, path/Taskfile.yml will be created.
//
// The final file path is always returned and may be different from the input path.
func InitTaskfileWithTemplate(path, template string) (string, error) {
// Resolve template content
content, ok := BuiltinTemplates[template]
if !ok {
return path, errors.New("unknown template: " + template)
}

info, err := os.Stat(path)
if err == nil && !info.IsDir() {
return path, errors.TaskfileAlreadyExistsError{}
Expand All @@ -34,12 +59,21 @@ func InitTaskfile(path string) (string, error) {
path = filepathext.SmartJoin(path, defaultFilename)
}

if err := os.WriteFile(path, []byte(DefaultTaskfile), 0o644); err != nil {
if err := os.WriteFile(path, []byte(content), 0o644); err != nil { //nolint:gosec
return path, err
}
return path, nil
}

// ListTemplates returns the list of built-in template names
func ListTemplates() []string {
names := make([]string, 0, len(BuiltinTemplates))
for name := range BuiltinTemplates {
names = append(names, name)
}
return names
}

func hasDefaultTaskfile(dir string) bool {
for _, name := range taskfile.DefaultTaskfiles {
if _, err := os.Stat(filepathext.SmartJoin(dir, name)); err == nil {
Expand Down
89 changes: 89 additions & 0 deletions init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package task_test

import (
"os"
"strings"
"testing"

"github.com/go-task/task/v3"
Expand Down Expand Up @@ -50,3 +51,91 @@ func TestInitFile(t *testing.T) {
}
_ = os.Remove(file)
}

func TestInitTaskfileWithFullTemplate(t *testing.T) {
t.Parallel()

const dir = "testdata/init"
file := filepathext.SmartJoin(dir, "Taskfile.full.yml")

_ = os.Remove(file)
if _, err := os.Stat(file); err == nil {
t.Errorf("Taskfile.full.yml should not exist")
}

if _, err := task.InitTaskfileWithTemplate(file, "full"); err != nil {
t.Error(err)
}

if _, err := os.Stat(file); err != nil {
t.Errorf("Taskfile.full.yml should exist")
}

// Verify content contains expected full template markers
content, err := os.ReadFile(file)
if err != nil {
t.Error(err)
}
contentStr := string(content)
if !strings.Contains(contentStr, "vars:") || !strings.Contains(contentStr, "tasks:") {
t.Error("Content should contain expected template sections")
}

_ = os.Remove(file)
}

func TestInitTaskfileWithTemplate(t *testing.T) {
t.Parallel()

const dir = "testdata/init"
file := filepathext.SmartJoin(dir, "Taskfile.default.yml")

_ = os.Remove(file)
if _, err := os.Stat(file); err == nil {
t.Errorf("Taskfile.default.yml should not exist")
}

if _, err := task.InitTaskfileWithTemplate(file, "default"); err != nil {
t.Error(err)
}

if _, err := os.Stat(file); err != nil {
t.Errorf("Taskfile.default.yml should exist")
}

_ = os.Remove(file)
}

func TestListTemplates(t *testing.T) {
t.Parallel()

templates := task.ListTemplates()
if len(templates) < 2 {
t.Errorf("Expected at least 2 templates, got %d", len(templates))
}

// Check that expected templates are present
found := make(map[string]bool)
for _, name := range templates {
found[name] = true
}

if !found["default"] {
t.Error("Expected 'default' template to be available")
}
if !found["full"] {
t.Error("Expected 'full' template to be available")
}
}

func TestInitTaskfileWithUnknownTemplate(t *testing.T) {
t.Parallel()

const dir = "testdata/init"
file := filepathext.SmartJoin(dir, "Taskfile.unknown.yml")

_, err := task.InitTaskfileWithTemplate(file, "nonexistent")
if err == nil {
t.Error("Expected error for unknown template")
}
}
2 changes: 2 additions & 0 deletions internal/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ var (
Version bool
Help bool
Init bool
InitTemplate string
Completion string
List bool
ListAll bool
Expand Down Expand Up @@ -122,6 +123,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.StringVar(&InitTemplate, "init-template", "", "Create a new Taskfile using a template. Use 'list' to show available templates.")
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
171 changes: 171 additions & 0 deletions taskfile/templates/full.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# yaml-language-server: $schema=https://taskfile.dev/schema.json
# Docs guide: https://taskfile.dev/docs/guide
version: '3'

# include other Taskfiles
includes:
docs:
taskfile: ./documentation
optional: true

# Global variables available to all tasks
vars:
GREETING: Hello, world!
# Environment variables can be referenced or dynamically generated
BUILD_DIR: build
# Dynamic variables execute shell commands
GIT_COMMIT:
sh: git rev-parse --short HEAD 2>/dev/null || echo "unknown"

# Global task output configuration
output: group

tasks:
# Default task - runs when no specific task is provided
default:
desc: Display greeting and show build information
deps:
- show-info
cmds:
- echo "{{.GREETING}}"
silent: true

# Task that runs from current user dir
up:
dir: '{{.USER_WORKING_DIR}}'
preconditions:
- test -f docker-compose.yml
cmds:
- docker-compose up -d

---

# Task with dependencies that run in parallel
build:
desc: Build the project with versioning
aliases: ['b']
deps:
- prepare
- compile
sources:
- src/**/*.go
- main.go
generates:
- '{{.BUILD_DIR}}/app'
vars:
VERSION: '{{.GIT_COMMIT}}'
env:
TOOL_ENV: 'value'
cmds:
- echo "Building version {{.VERSION}}..."
- mkdir -p {{.BUILD_DIR}}
status:
- test -f {{.BUILD_DIR}}/app

# Task that validates required variables
deploy:
desc: Deploy the application
requires:
vars:
- name: ENVIRONMENT
enum: [dev, staging, prod]
- DEPLOYMENT_KEY
prompt: Deploying to {{.ENVIRONMENT}}. Continue?
cmds:
- echo "Deploying to {{.ENVIRONMENT}} with key {{.DEPLOYMENT_KEY}}"

# Subtask for build process
prepare:
internal: true
desc: Prepare build environment
cmds:
- echo "Preparing environment..."

# Another subtask
compile:
internal: true
desc: Compile source code
cmds:
- echo "Compiling source code..."

# Task with looping
show-info:
desc: Display system information
vars:
INFO_ITEMS: [OS, Shell, Task]
cmds:
- for:
var: INFO_ITEMS
cmd: echo "{{.ITEM}} info"
silent: true

# Task with conditional execution
test:
desc: Run tests based on environment
aliases: ['t']
cmds:
- cmd: echo "Running tests in development mode"
if: '[ "{{.ENVIRONMENT}}" = "dev" ]'
- cmd: echo "Running full test suite"
if: '[ "{{.ENVIRONMENT}}" != "dev" ]'

# Task showing using if with for loops
process-items:
cmds:
- for: ['a', 'b', 'c']
cmd: echo "processing {{.ITEM}}"
if: '[ "{{.ITEM}}" != "b" ]'

# Task demonstrating different ways to call other tasks
workflow:
desc: Run a complete workflow
aliases: ['w']
cmds:
- task: default
- task: build
vars:
VERSION: 1.0.0
- defer: echo "Workflow complete!"

# Task that can skip if status check passes
install-deps:
desc: Install dependencies
cmds:
- echo "Installing dependencies..."
status:
- test -d node_modules
run: once

# Task demonstrating error handling and recovery
validate:
desc: Validate configuration with error recovery
aliases: ['val','v']
cmds:
- cmd: test -f config.json
ignore_error: true
- echo "Validation check completed"

variable-datatypes:
desc: Illustrates Taskfile datatypes
vars:
STRING: 'Hello, World!'
BOOL: true
INT: 42
FLOAT: 3.14
ARRAY: [1, 2, 3]
MAP:
map: { A: 1, B: 2, C: 3 }
JSON: '{"a": 1, "b": 2, "c": 3}' # or set via sh
JSONFOO: # json string into map
ref: 'fromJson .JSON'
cmds:
- 'echo {{.STRING}}' # Hello, World!
- 'echo {{.BOOL}}' # true
- 'echo {{.INT}}' # 42
- 'echo {{.FLOAT}}' # 3.14
- 'echo {{.ARRAY}}' # [1 2 3]
- 'echo {{index .ARRAY 0}}' # 1
- 'echo {{.MAP}}' # map[A:1 B:2 C:3]
- 'echo {{.MAP.A}}' # 1
- 'echo {{.JSON}}' # {"a": 1, "b": 2, "c": 3}
- 'echo {{.JSONFOO}}' # map[A:1 B:2 C:3]