Task Manager CLI

Build a personal task management application that demonstrates advanced Cobra features. This intermediate example showcases command hierarchies, data persistence, and configuration management.

What You’ll Build

A command-line task manager called taskman with these capabilities:

  • Add new tasks with descriptions and priorities
  • List all tasks with filtering options
  • Mark tasks as completed
  • Delete unwanted tasks
  • Persistent storage using JSON files
  • Configuration for default settings

Features Demonstrated

  • Subcommands with hierarchical structure
  • Persistent flags available to all subcommands
  • Local storage using JSON files
  • Input validation with custom validators
  • Configuration files for user preferences
  • Structured data handling with Go structs

Complete Source Code

go
package main

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

type Task struct {
	ID          int       `json:"id"`
	Description string    `json:"description"`  
	Priority    string    `json:"priority"`
	Completed   bool      `json:"completed"`
	CreatedAt   time.Time `json:"created_at"`
	CompletedAt *time.Time `json:"completed_at,omitempty"`
}

type TaskManager struct {
	Tasks    []Task `json:"tasks"`
	NextID   int    `json:"next_id"`
	FilePath string `json:"-"`
}

var (
	taskFile    string
	priority    string
	showAll     bool
	taskManager *TaskManager
)

var rootCmd = &cobra.Command{
	Use:   "taskman",
	Short: "A personal task manager",
	Long: `taskman is a CLI task manager that helps you organize your work.

Store tasks locally with priorities, mark them complete, and keep
track of your productivity over time.`,
	PersistentPreRun: loadTasks,
	PersistentPostRun: saveTasks,
}

var addCmd = &cobra.Command{
	Use:   "add [description]",
	Short: "Add a new task",
	Long:  "Add a new task with description and optional priority (high, medium, low)",
	Args:  cobra.MinimumNArgs(1),
	Run:   addTask,
}

var listCmd = &cobra.Command{
	Use:   "list",
	Short: "List all tasks",
	Long:  "List tasks with optional filtering by completion status",
	Run:   listTasks,
}

var completeCmd = &cobra.Command{
	Use:   "complete [task-id]",
	Short: "Mark a task as completed",
	Args:  cobra.ExactArgs(1),
	Run:   completeTask,
}

var deleteCmd = &cobra.Command{
	Use:   "delete [task-id]", 
	Short: "Delete a task",
	Args:  cobra.ExactArgs(1),
	Run:   deleteTask,
}

func init() {
	// Persistent flags available to all commands
	rootCmd.PersistentFlags().StringVar(&taskFile, "file", "", "task file (default is $HOME/.taskman.json)")
	
	// Local flags for specific commands  
	addCmd.Flags().StringVarP(&priority, "priority", "p", "medium", "task priority (high, medium, low)")
	listCmd.Flags().BoolVarP(&showAll, "all", "a", false, "show completed tasks too")
	
	// Add subcommands
	rootCmd.AddCommand(addCmd, listCmd, completeCmd, deleteCmd)
	
	// Setup configuration
	setupConfig()
}

func setupConfig() {
	viper.SetConfigName("taskman")
	viper.SetConfigType("yaml")
	viper.AddConfigPath("$HOME")
	viper.SetDefault("priority", "medium")
	viper.SetDefault("file", filepath.Join(os.Getenv("HOME"), ".taskman.json"))
	
	viper.ReadInConfig()
}

func getTaskFile() string {
	if taskFile != "" {
		return taskFile
	}
	return viper.GetString("file")
}

func loadTasks(cmd *cobra.Command, args []string) {
	file := getTaskFile()
	taskManager = &TaskManager{
		Tasks:    make([]Task, 0),
		NextID:   1,
		FilePath: file,
	}
	
	if data, err := os.ReadFile(file); err == nil {
		json.Unmarshal(data, taskManager)
	}
}

func saveTasks(cmd *cobra.Command, args []string) {
	data, err := json.MarshalIndent(taskManager, "", "  ")
	if err != nil {
		fmt.Printf("Error saving tasks: %v\n", err)
		return
	}
	
	os.WriteFile(taskManager.FilePath, data, 0644)
}

func addTask(cmd *cobra.Command, args []string) {
	description := strings.Join(args, " ")
	
	// Validate priority
	validPriorities := map[string]bool{"high": true, "medium": true, "low": true}
	if !validPriorities[priority] {
		fmt.Printf("Invalid priority '%s'. Use: high, medium, or low\n", priority)
		os.Exit(1)
	}
	
	task := Task{
		ID:          taskManager.NextID,
		Description: description,
		Priority:    priority,
		Completed:   false,
		CreatedAt:   time.Now(),
	}
	
	taskManager.Tasks = append(taskManager.Tasks, task)
	taskManager.NextID++
	
	fmt.Printf("Added task #%d: %s [%s]\n", task.ID, task.Description, task.Priority)
}

func listTasks(cmd *cobra.Command, args []string) {
	if len(taskManager.Tasks) == 0 {
		fmt.Println("No tasks found.")
		return
	}
	
	fmt.Printf("%-4s %-10s %-50s %-10s %s\n", "ID", "STATUS", "DESCRIPTION", "PRIORITY", "CREATED")
	fmt.Println(strings.Repeat("-", 90))
	
	for _, task := range taskManager.Tasks {
		if !showAll && task.Completed {
			continue
		}
		
		status := "PENDING"
		if task.Completed {
			status = "DONE"
		}
		
		fmt.Printf("%-4d %-10s %-50s %-10s %s\n", 
			task.ID, 
			status, 
			truncate(task.Description, 50),
			strings.ToUpper(task.Priority),
			task.CreatedAt.Format("2006-01-02"))
	}
}

func completeTask(cmd *cobra.Command, args []string) {
	id, err := strconv.Atoi(args[0])
	if err != nil {
		fmt.Printf("Invalid task ID: %s\n", args[0])
		os.Exit(1)
	}
	
	for i := range taskManager.Tasks {
		if taskManager.Tasks[i].ID == id {
			if taskManager.Tasks[i].Completed {
				fmt.Printf("Task #%d is already completed\n", id)
				return
			}
			
			now := time.Now()
			taskManager.Tasks[i].Completed = true
			taskManager.Tasks[i].CompletedAt = &now
			
			fmt.Printf("Completed task #%d: %s\n", id, taskManager.Tasks[i].Description)
			return
		}
	}
	
	fmt.Printf("Task #%d not found\n", id)
	os.Exit(1)
}

func deleteTask(cmd *cobra.Command, args []string) {
	id, err := strconv.Atoi(args[0])
	if err != nil {
		fmt.Printf("Invalid task ID: %s\n", args[0])
		os.Exit(1)
	}
	
	for i, task := range taskManager.Tasks {
		if task.ID == id {
			// Remove task from slice
			taskManager.Tasks = append(taskManager.Tasks[:i], taskManager.Tasks[i+1:]...)
			fmt.Printf("Deleted task #%d: %s\n", id, task.Description)
			return
		}
	}
	
	fmt.Printf("Task #%d not found\n", id)
	os.Exit(1)
}

func truncate(s string, max int) string {
	if len(s) <= max {
		return s
	}
	return s[:max-3] + "..."
}

func main() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

Build and Test Instructions

  1. Create the project:

    mkdir taskman
    cd taskman
    go mod init taskman
    go get github.com/spf13/cobra@latest
    go get github.com/spf13/viper@latest

  2. Save the code above as main.go

  3. Build the application:

    go build -o taskman

  4. Test basic functionality:

    ./taskman add "Write project documentation" --priority high
    Added task #1: Write project documentation [high]
    ./taskman add "Review pull requests" -p medium
    Added task #2: Review pull requests [medium]
    ./taskman add "Update dependencies"
    Added task #3: Update dependencies [medium]

  5. List tasks:

    ./taskman list
    ID   STATUS     DESCRIPTION                                        PRIORITY   CREATED
    ------------------------------------------------------------------------------------------
    1    PENDING    Write project documentation                        HIGH       2024-01-15
    2    PENDING    Review pull requests                               MEDIUM     2024-01-15
    3    PENDING    Update dependencies                                MEDIUM     2024-01-15

  6. Complete and delete tasks:

    ./taskman complete 1
    Completed task #1: Write project documentation
    ./taskman list --all
    ID   STATUS     DESCRIPTION                                        PRIORITY   CREATED
    ------------------------------------------------------------------------------------------
    1    DONE       Write project documentation                        HIGH       2024-01-15
    2    PENDING    Review pull requests                               MEDIUM     2024-01-15
    3    PENDING    Update dependencies                                MEDIUM     2024-01-15
    ./taskman delete 3
    Deleted task #3: Update dependencies

  7. Check help for subcommands:

    ./taskman --help
    taskman is a CLI task manager that helps you organize your work.
    Store tasks locally with priorities, mark them complete, and keep
    track of your productivity over time.
    Usage:
    taskman [command]
    Available Commands:
    add         Add a new task
    complete    Mark a task as completed
    delete      Delete a task
    help        Help about any command
    list        List all tasks
    Flags:
    --file string   task file (default is $HOME/.taskman.json)
    -h, --help          help for taskman
    ./taskman add --help
    Add a new task with description and optional priority (high, medium, low)
    Usage:
    taskman add [description] [flags]
    Flags:
    -h, --help               help for add
    -p, --priority string    task priority (high, medium, low) (default "medium")

Key Learning Points

1. Subcommand Architecture

Cobra supports hierarchical commands:

  • Each subcommand is a separate cobra.Command struct
  • Use rootCmd.AddCommand() to register subcommands
  • Each subcommand can have its own flags, validation, and logic

2. Persistent vs Local Flags

  • Persistent flags (PersistentFlags()) are available to the command and all subcommands
  • Local flags (Flags()) are only available to the specific command
  • Useful for shared options like --file that apply globally

3. Pre and Post Run Hooks

  • PersistentPreRun executes before any command runs
  • PersistentPostRun executes after any command completes
  • Perfect for setup/teardown operations like loading/saving data

4. Configuration Management

Viper integration provides:

  • Configuration file support (YAML, JSON, TOML)
  • Environment variable binding
  • Default value management
  • Hierarchical configuration resolution

5. Data Persistence

The example demonstrates:

  • JSON serialization for simple data storage
  • File-based persistence for local applications
  • Error handling for file operations

6. Input Validation

Custom validation includes:

  • Argument count validation with cobra.ExactArgs()
  • Custom business logic validation (priority values)
  • Type conversion with error handling

Extending the Example

Try these enhancements:

  1. Add due dates: Extend tasks with deadline support
  2. Add categories: Organize tasks by project or context
  3. Add search: Find tasks by keyword or priority
  4. Add statistics: Show productivity metrics and summaries
  5. Add import/export: Support CSV or other formats

Next Steps

This example shows how Cobra scales from simple tools to complex applications. With subcommands, persistence, and configuration, you can build sophisticated CLI applications that users love!