diff --git a/cmd/task/task.go b/cmd/task/task.go index b81e23dd5f..a02480b45c 100644 --- a/cmd/task/task.go +++ b/cmd/task/task.go @@ -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 { diff --git a/init.go b/init.go index 807d201d1c..bfd8417c50 100644 --- a/init.go +++ b/init.go @@ -14,6 +14,15 @@ 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. @@ -21,6 +30,22 @@ var DefaultTaskfile string // // 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{} @@ -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 { diff --git a/init_test.go b/init_test.go index 41095d65e1..fb661f640f 100644 --- a/init_test.go +++ b/init_test.go @@ -2,6 +2,7 @@ package task_test import ( "os" + "strings" "testing" "github.com/go-task/task/v3" @@ -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") + } +} diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 51bec00468..a7f4cad676 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -47,6 +47,7 @@ var ( Version bool Help bool Init bool + InitTemplate string Completion string List bool ListAll bool @@ -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.") diff --git a/taskfile/templates/full.yml b/taskfile/templates/full.yml new file mode 100644 index 0000000000..2429d36c76 --- /dev/null +++ b/taskfile/templates/full.yml @@ -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]