Go Framework
Experimental — draft docs
The Go framework is experimental. Its parsing behavior is verified against the same conformance corpus as the Rust implementation, but APIs may still change between releases. These docs are a draft: some of what they document is still in open pull requests, and details may change before release.
The Go framework builds your CLI from a usage spec — but unlike most Go CLI libraries, your shipped binary never parses the spec. usage generate go lowers the KDL into plain Go tables, typed structs, and a Parse function at build time. The result:
- Zero dependencies. The module is
github.com/jdx/usage/goand imports nothing but the standard library. - Zero-allocation parsing. A parse allocates nothing, on success and failure paths alike — roughly 57–110ns per parse on mise's real 211-command spec.
- Linker-friendly. Parse tables, validation metadata, and help text are three separate tables; the linker drops the ones you don't reference. No
initfunctions. - One source of truth. The same KDL spec generates your completions, docs, and manpages.
Quick start
Write a spec:
name "ex"
bin "ex"
version "1.0.0"
flag "-v --verbose" global=#true help="be loud"
flag "-j --jobs <n>" help="how many jobs"
arg "<file>" help="the file to process"
cmd "install" help="install a tool" {
alias "i"
flag "-f --force"
arg "<pkg>"
}Generate the Go code:
//go:generate usage generate go -f ex.usage.kdl -o tables.go -p exParse:
package main
import (
"fmt"
"os"
"github.com/jdx/usage/go/argv"
)
func main() {
cli, err := ex.Parse(os.Args[1:])
if err != nil {
exit(err.(*argv.Error))
}
if cli.Install != nil {
install(cli.Install.Pkg, cli.Install.Force, cli.Verbose)
return
}
process(cli.File)
}Parse returns a typed struct per command — cli.Install is nil unless install (or its alias i) was invoked — with flags bound, env/default fallbacks applied, and required, choices, var_min/var_max, and flag relations enforced.
Handling help, version, and failures
Unlike the Rust framework's parse(), the generated Go Parse never prints or exits — help and version requests come back as errors with Code set, and rendering is yours to invoke. The standard exit function looks like this:
func exit(e *argv.Error) {
pos := argv.Walk(ex.Root, os.Args[1:])
path := []string{"ex"}
for _, c := range pos.Chain[1:] {
path = append(path, c.Name)
}
switch e.Code {
case argv.CodeHelp:
if e.Long {
fmt.Print(argv.LongHelp(ex.HelpMeta, path, pos.Chain, ex.HelpText))
} else {
fmt.Print(argv.ShortHelp(ex.HelpMeta, path, pos.Chain, ex.HelpText))
}
os.Exit(0)
case argv.CodeVersion:
fmt.Println("ex " + ex.Version)
os.Exit(0)
default:
fmt.Fprint(os.Stderr, argv.Render(e, path, pos.Chain, ex.HelpText))
os.Exit(2)
}
}The rendered pages match usage-lib's byte for byte, and the failure messages are clap-shaped — see Help and errors.
Why generation instead of a runtime spec?
The Go module has no KDL parser, on purpose. Lowering a spec is usage-cli's job, done once at build time; the shipped binary carries tables the linker can lay out as data. Building tables at runtime is not supported — generation is the only path, and the point.
Everything is verified against the reference implementation: a shared JSON conformance corpus (all vectors passing) covers the parsing grammar, and all 211 of mise's usage lines, -h pages, and --help pages are compared byte-for-byte against usage-lib's rendering in CI.
Where to go next
- Generated code — what
usage generate goemits and whatParsedoes - The parser — the low-level zero-allocation event API
- Binding and values — env/default resolution, validation, typed conversions
- Help and errors — rendering
-h/--helppages and failures - Completions — answering shell completion requests
Current limitations
Worth knowing before you commit:
overridesis not enforced by generatedParse.conflicts,required_if, andrequired_unlessare; a spec relying on last-one-winsoverridessemantics needs to callargv.ApplyOverridesitself.- Fields are
string,bool,[]string, orint(for counts). A spec says what a value is called, never what type it is — convert withargv.Int,argv.Duration, etc. completescripts,confignodes,group,value_hint, andmountare not carried into the generated tables. Completions knowchoices; config resolution is not implemented.- Completion shell scripts come from the Rust side. The Go runtime answers completion requests over the same protocol, but you wire up the hidden subcommand yourself — see Completions.
- Command trees deeper than 16 levels are rejected (
CodeTooDeep); short flags must be ASCII.