Go CLI Tools

Cobra Basics

Root command structure
// cmd/root.go
package cmd

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

var rootCmd = &cobra.Command{
    Use:   "netcheck",
    Short: "Network diagnostic tool",
    Long:  "A CLI tool for network diagnostics and host scanning.",
}

func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

Cobra structures CLI apps as a tree of commands. The root command is the entry point. Subcommands attach to it.

Subcommands

Adding a subcommand with flags
// cmd/scan.go
var scanCmd = &cobra.Command{
    Use:   "scan [host]",
    Short: "Scan a host for open ports",
    Args:  cobra.ExactArgs(1),
    RunE: func(cmd *cobra.Command, args []string) error {
        host := args[0]
        ports, _ := cmd.Flags().GetIntSlice("ports")
        timeout, _ := cmd.Flags().GetDuration("timeout")

        fmt.Printf("Scanning %s (ports: %v, timeout: %s)\n",
            host, ports, timeout)
        return nil
    },
}

func init() {
    rootCmd.AddCommand(scanCmd)
    scanCmd.Flags().IntSlice("ports", []int{22, 80, 443}, "ports to scan")
    scanCmd.Flags().Duration("timeout", 5*time.Second, "connection timeout")
}

RunE returns an error (preferred over Run). init() registers the subcommand and its flags. Cobra auto-generates help and usage.

Persistent Flags

Flags inherited by all subcommands
func init() {
    rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output")
    rootCmd.PersistentFlags().String("config", "", "config file path")
}

Persistent flags are available to the command and all its children. Local flags (.Flags()) apply only to the command itself.

os.Args Alternative — flag Package

Standard library flag — no dependencies
package main

import (
    "flag"
    "fmt"
)

func main() {
    host := flag.String("host", "localhost", "target host")
    port := flag.Int("port", 8080, "target port")
    verbose := flag.Bool("v", false, "verbose output")
    flag.Parse()

    fmt.Printf("Connecting to %s:%d (verbose=%t)\n", *host, *port, *verbose)

    // Remaining positional args
    args := flag.Args()
    fmt.Println("Extra args:", args)
}
// Usage: ./tool -host 10.50.1.1 -port 443 -v

The flag package is the zero-dependency option. Use Cobra for complex CLIs with subcommands; flag for simple tools.

JSON Output

Structured CLI output
type ScanResult struct {
    Host  string `json:"host"`
    Port  int    `json:"port"`
    Open  bool   `json:"open"`
    Latency string `json:"latency,omitempty"`
}

func outputResults(results []ScanResult, format string) {
    switch format {
    case "json":
        enc := json.NewEncoder(os.Stdout)
        enc.SetIndent("", "  ")
        enc.Encode(results)
    case "table":
        w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
        fmt.Fprintln(w, "HOST\tPORT\tSTATUS")
        for _, r := range results {
            status := "closed"
            if r.Open {
                status = "open"
            }
            fmt.Fprintf(w, "%s\t%d\t%s\n", r.Host, r.Port, status)
        }
        w.Flush()
    }
}

CLI tools should support machine-readable output (JSON) for pipeline integration alongside human-readable output (table).