Other CLI Libraries: urfave/cli, pflag, and Friends
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@latesturfave/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:
- Domain in pure Go packages
cmdlayer uses Cobra or urfave
- 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
- Reimplement
greet hello -nwith pflag only. - Skim kubectl or gh command layout on GitHub (Cobra patterns).
- Port one cookbook tool (ch 316) to urfave/cli and compare LOC.