Other CLI Libraries: urfave/cli, pflag, and Friends

Updated

September 8, 2026

Other CLI Libraries: urfave/cli, pflag, and Friends

Overview

Cobra is not the only option. This chapter surveys common libraries so you can choose deliberately—and recognize them in the wild.

Comparison matrix

Library Style Best for
stdlib flag minimal small tools, zero deps
spf13/pflag POSIX/GNU flags drop-in richer flags without full Cobra
spf13/cobra command tree large public CLIs
urfave/cli declarative apps compact multi-command tools
alecthomas/kong struct-tagged strongly typed CLIs
charmbracelet/ TUI interactive terminals (not pure pipes)

pflag alone

Cobra uses pflag. You can use pflag without Cobra:

import flag "github.com/spf13/pflag"

var (
    name = flag.StringP("name", "n", "world", "name")
    verbose = flag.BoolP("verbose", "v", false, "verbose")
)
flag.Parse()

Differences from stdlib flag:

  • Short + long flags (-n / --name)
  • Combined shorts (-abc) for bools
  • Slightly different parsing rules (more GNU-like)
go get github.com/spf13/pflag@latest

urfave/cli v2/v3 sketch

package main

import (
    "fmt"
    "os"

    "github.com/urfave/cli/v2"
)

func main() {
    app := &cli.App{
        Name:  "greet",
        Usage: "fight the loneliness!",
        Commands: []*cli.Command{
            {
                Name:  "hello",
                Usage: "say hello",
                Flags: []cli.Flag{
                    &cli.IntFlag{Name: "times", Aliases: []string{"n"}, Value: 1},
                },
                Action: func(c *cli.Context) error {
                    name := "world"
                    if c.NArg() > 0 {
                        name = c.Args().Get(0)
                    }
                    for i := 0; i < c.Int("times"); i++ {
                        fmt.Printf("hello, %s\n", name)
                    }
                    return nil
                },
            },
        },
    }
    if err := app.Run(os.Args); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

Feel: more “app struct” oriented; less boilerplate for simple trees.

Kong (struct-first)

var cli struct {
    Debug bool `help:"Enable debug mode"`
    Hello struct {
        Name string `arg:"" default:"world"`
        Times int `help:"Repeat" short:"n" default:"1"`
    } `cmd:"" help:"Say hello"`
}

// ctx := kong.Parse(&cli)
// switch ctx.Command() { case "hello": ... }

Excellent when you want flags as typed fields; different aesthetics from Cobra.

Charm ecosystem (when CLI becomes TUI)

Package Role
bubbletea Elm-style TUI framework
lipgloss styling
huh forms
bubbletea apps interactive selectors, dashboards

Rule: keep pipe-friendly commands as non-TUI; offer a separate mytool tui if needed. Don’t break jq pipelines with alternate screens.

mow.cli, kingpin, ff

Older or niche libs still appear in codebases:

  • jawher/mow.cli — fluent API
  • alecthomas/kingpin — predecessor ideas to kong
  • peterbourgon/ff — flags + env + file in a small package

Read their READMEs before adopting; Cobra/urfave cover most new work.

Choosing

zero deps, few flags     → stdlib flag
need -v/--verbose only   → pflag
nested commands, public  → cobra
compact declarative      → urfave/cli
struct tags / typed      → kong
full screen UI           → charm/bubbletea

Hybrid pattern

Many teams:

  1. Domain in pure Go packages
  2. cmd layer uses Cobra or urfave
  3. Stdlib tests against domain + thin command factories

Switching frameworks then costs little.

Rules of thumb

Do Don’t
Pick one framework per binary Mix Cobra + urfave in one tree
Match ecosystem (k8s → Cobra familiarity) Add Cobra “because tutorials do”
Keep TUI optional Force interactive UI for CI tools

Try next

  1. Reimplement greet hello -n with pflag only.
  2. Skim kubectl or gh command layout on GitHub (Cobra patterns).
  3. Port one cookbook tool (ch 316) to urfave/cli and compare LOC.