Skip to content

Subcommand Design

Each subcommand should be independently useful.

Composable Commands

Each command should work independently but compose well with others. myctl select | myctl restart - demonstrates good pipeline design.


Check Command

package cmd

import (
    "context"
    "encoding/json"
    "fmt"
    "os"
    "time"

    "github.com/spf13/cobra"
)

type CheckResult struct {
    Valid     bool   `json:"valid"`
    Reason    string `json:"reason,omitempty"`
    Timestamp string `json:"timestamp"`
}

var checkCmd = &cobra.Command{
    Use:   "check",
    Short: "Check if cache rebuild is needed",
    Long: `Check the current cache state and determine if a rebuild is required.

Exit codes:
  0 - Cache is valid
  1 - Cache needs rebuild
  2 - Error occurred`,
    RunE: runCheck,
}

var outputJSON bool

func init() {
    checkCmd.Flags().BoolVar(&outputJSON, "json", false, "Output result as JSON")
    rootCmd.AddCommand(checkCmd)
}

func runCheck(cmd *cobra.Command, args []string) error {
    ctx := cmd.Context()

    valid, reason, err := performCacheCheck(ctx)
    if err != nil {
        return err
    }

    result := CheckResult{
        Valid:     valid,
        Reason:    reason,
        Timestamp: time.Now().Format(time.RFC3339),
    }

    if outputJSON {
        enc := json.NewEncoder(os.Stdout)
        enc.SetIndent("", "  ")
        return enc.Encode(result)
    }

    if valid {
        fmt.Println("Cache is valid")
        return nil
    }

    fmt.Printf("Cache needs rebuild: %s\n", reason)
    os.Exit(1)
    return nil
}

Command Registration

Organize command registration cleanly:

// cmd/root.go
package cmd

import (
    "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
    Use:   "myctl",
    Short: "Kubernetes orchestration CLI",
}

func init() {
    // Global flags
    rootCmd.PersistentFlags().StringP("namespace", "n", "default", "Kubernetes namespace")
    rootCmd.PersistentFlags().Bool("verbose", false, "Enable verbose output")
    rootCmd.PersistentFlags().Bool("dry-run", false, "Show what would be done")

    // Register subcommands
    rootCmd.AddCommand(orchestrateCmd)
    rootCmd.AddCommand(checkCmd)
    rootCmd.AddCommand(rebuildCmd)
    rootCmd.AddCommand(selectCmd)
    rootCmd.AddCommand(restartCmd)
    rootCmd.AddCommand(versionCmd)
}

func Execute() error {
    return rootCmd.Execute()
}

Reading from Stdin

Support stdin input with - argument for piping:

package cmd

import (
    "bufio"
    "fmt"
    "os"
    "strings"

    "github.com/spf13/cobra"
)

var restartCmd = &cobra.Command{
    Use:   "restart [deployments...]",
    Short: "Restart deployments",
    Long: `Restart the specified deployments.

Deployment names can be provided as arguments or piped via stdin:
  myctl select --output names | myctl restart -`,
    RunE: runRestart,
}

func runRestart(cmd *cobra.Command, args []string) error {
    ctx := cmd.Context()

    var deployments []string

    // Check if reading from stdin
    if len(args) == 1 && args[0] == "-" {
        scanner := bufio.NewScanner(os.Stdin)
        for scanner.Scan() {
            line := strings.TrimSpace(scanner.Text())
            if line != "" {
                deployments = append(deployments, line)
            }
        }
        if err := scanner.Err(); err != nil {
            return fmt.Errorf("failed to read stdin: %w", err)
        }
    } else {
        deployments = args
    }

    if len(deployments) == 0 {
        return fmt.Errorf("no deployments specified")
    }

    for _, d := range deployments {
        fmt.Printf("Restarting %s...\n", d)
        if err := restartDeployment(ctx, d); err != nil {
            return err
        }
    }

    return nil
}

Hidden Commands

Mark debugging or internal commands as hidden:

var debugCmd = &cobra.Command{
    Use:    "debug",
    Short:  "Internal debugging commands",
    Hidden: true,  // Won't appear in help
}

var dumpCacheCmd = &cobra.Command{
    Use:   "dump-cache",
    Short: "Dump internal cache state",
    RunE: func(cmd *cobra.Command, args []string) error {
        // Debug implementation
        return nil
    },
}

func init() {
    debugCmd.AddCommand(dumpCacheCmd)
    rootCmd.AddCommand(debugCmd)
}

Each command should work independently but compose well with others.

Comments