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
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
-
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
-
Save the code above as
main.go
-
Build the application:
go build -o taskman
-
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]
-
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
-
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
-
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 runsPersistentPostRun
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:
- Add due dates: Extend tasks with deadline support
- Add categories: Organize tasks by project or context
- Add search: Find tasks by keyword or priority
- Add statistics: Show productivity metrics and summaries
- Add import/export: Support CSV or other formats
Next Steps
- Learn about advanced output: Check out the API Inspector CLI example
- Explore configuration: Read the 12-Factor App tutorial
- Master subcommands: See Working with Commands
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!