cli

package module
v0.1.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 20, 2026 License: MIT Imports: 8 Imported by: 1

README

cli

cli is a tiny, idiomatic Go library for subcommand-based CLIs — modeled after the Go tool (go mod init, go mod tidy, etc.).

It provides:

  • A Command type with per-command flag.FlagSet.
  • Hierarchical subcommands (go mod tidy).
  • Built-in help command and usage printing.
  • Explicit error handling (UsageError vs runtime errors).
  • No globals, no reflection, no dependencies.

Install

go get github.com/jon-ski/cli

Quick Start

package main

import (
	"fmt"

	"github.com/jon-ski/cli"
)

func main() {
	// Root command: name left blank -> auto-filled from argv[0] (no .exe on Windows)
	root := cli.NewCommand("", "root tool", "[-v]")
	var verbose bool
	root.Flags.BoolVar(&verbose, "v", false, "verbose output")

	// "prog greet [name]"
	greet := cli.NewCommand("greet", "print a greeting", "[name]")
	greet.Run = func(ctx *cli.Context, args []string) error {
		name := "world"
		if len(args) == 1 {
			name = args[0]
		}
		if verbose {
			fmt.Fprintln(ctx.Stdout, "verbose: greeting about to print")
		}
		fmt.Fprintf(ctx.Stdout, "hello, %s!\n", name)
		return nil
	}
	root.Add(greet)

	// Run with standard os.Args, exit handling, and help.
	cli.Main(root)
}
Usage
$ prog greet
hello, world!

$ prog greet alice
hello, alice!

$ prog -v greet bob
verbose: greeting about to print
hello, bob!

$ prog help
usage: prog [-v]

root tool

subcommands:
  greet  print a greeting

flags:
  -v    verbose output

API

Command
type Command struct {
    Name  string
    Usage string
    Short string
    Long  string
    Flags *flag.FlagSet
    Run   func(ctx *Context, args []string) error
    Sub   []*Command
}
  • Name: token for the command ("mod", "tidy").
  • Usage: syntax string ("[path]").
  • Short: one-line summary.
  • Long: longer help text.
  • Flags: standard *flag.FlagSet, per command.
  • Run: invoked after flag parse. Return *UsageError for usage mistakes; any other error prints and exits 1.
  • Sub: nested subcommands.
Context
type Context struct {
    Stdin  io.Reader
    Stdout io.Writer
    Stderr io.Writer
    Env    func(key string) string
}

Injected into Run. Lets you swap in buffers for testing.

Errors
type UsageError struct { Err error }
func Usagef(format string, a ...any) *UsageError
  • Signals wrong usage → prints usage + exits 2.
  • All other errors print and exit 1.
Entrypoint
func Main(root *Command)
  • Dispatches on os.Args.
  • Provides help as a command.
  • Handles exit codes consistently.

If you leave root.Name blank, cli.Main sets it from argv[0] automatically and removes any .exe suffix on Windows.


Design Goals

  • Parallels Go’s own CLI (cmd/go).
  • No globals: you construct the tree yourself.
  • Testable: call Exec directly with a Context.
  • Minimal: zero dependencies, only stdlib.
  • Explicit errors: no silent os.Exit in library code.

Testing

Commands can be tested without os.Exit:

ctx := &cli.Context{
    Stdin:  strings.NewReader(""),
    Stdout: &bytes.Buffer{},
    Stderr: &bytes.Buffer{},
    Env:    func(string) string { return "" },
}

err := root.Exec(ctx, []string{"greet", "test"})
if err != nil {
    t.Fatalf("unexpected error: %v", err)
}
got := ctx.Stdout.(*bytes.Buffer).String()
if got != "hello, test!\n" {
    t.Errorf("got %q", got)
}

Example Hierarchy

prog
 ├─ greet [name]
 └─ mod
     ├─ init [path]
     └─ tidy

Mirrors go mod init, go mod tidy.


Exit Codes

  • 0 success
  • 1 runtime error
  • 2 usage error (bad args/flags)

Why not Cobra/urfave/kingpin?

Because this is tiny, predictable, and Go-tool-like:

  • Cobra: large, reflection-heavy, YAML-friendly.
  • urfave: global state, broad feature set.
  • cli: explicit tree, small surface, idiomatic flags.

If you want similar to how go does it, this is it.


License

MIT — do whatever you want, attribution appreciated.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Main

func Main(root *Command)

Main is a convenience entrypoint that executes root with os.Args and handles exits.

Types

type Command

type Command struct {
	// Name is the command name token (e.g., "mod", "init", "tidy")
	Name string

	// Usage is the usage line after the command path, e.g. "[-v] [path]".
	Usage string

	// Short is a one-line summary. Long is extended help (optional).
	Short string
	Long  string

	// Flags are parsed for this specific command before invoking run.
	Flags *flag.FlagSet

	// Run executes command logic. args are the remaining, non-flag args.
	// Return *UsageError for bad usage; any other error prints and returns exit code 1.
	Run func(ctx *Context, args []string) error

	// Sub holds nested subcommands (e.g., "go mod" has children "init", "tidy")
	Sub []*Command
	// contains filtered or unexported fields
}

Command is a subcommand, mirroring cmd/go's style: a FlagSet, short/long help, optional children, and a Run func. Only the fields below are needed for a minimal, testable core.

func NewCommand

func NewCommand(name, short, usage string) *Command

func (*Command) Add

func (c *Command) Add(children ...*Command)

Add registers a child subcommand and sets parent pointers.

func (*Command) Exec

func (c *Command) Exec(ctx *Context, args []string) error

Exec dispatches args to subcommands, parses flags, and calls Run. It mirrors how cmd/go walks the command tree: first arg selects a child; if none, the current command's flags parse and Run executes.

func (*Command) Path

func (c *Command) Path() []string

Path returns the full command path tokens from root to this command.

func (*Command) PrintUsage

func (c *Command) PrintUsage(ctx *Context)

PrintUsage writes a concise usage block for c.

type Context

type Context struct {
	Stdin  io.Reader
	Stdout io.Writer
	Stderr io.Writer

	// Env returns the value of an environment variable (nil-safe when not set)
	Env func(key string) string
}

Context carries I/O and environment access. Keep it tiny and swappable in tests.

type UsageError

type UsageError struct {
	Err error
}

UsageError indicates "you used the command wrong" (prints usage + exit code 2).

func Usagef

func Usagef(format string, a ...any) *UsageError

func (*UsageError) Error

func (e *UsageError) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL