Advanced CLI
The minimal CLI stopped at struct tags. This demo
adds the declarative layer on top: Metadata — long-form
descriptions for humans and tooling, value domains the framework
enforces on every source, and advisory hints about what a value
denotes. Same file server, grown up.
The applet, annotated#
package main import ( "log/slog" "net/http" "sxcli.dev/fw" ) const ID = "example.com/srv" type Config struct { Version uint32 `json:"version"` Addr string `json:"addr" conf:"addr,a" usage:"listen address"` Dir string `json:"dir" conf:"dir,d" usage:"directory to serve"` Index string `json:"index" conf:"index" usage:"directory index mode"` Codecs []string `json:"codecs" conf:"codec" usage:"compression codec, repeatable"` } type Srv struct{ cfg Config } func (s *Srv) Configured() error { return nil } func (s *Srv) Run() int { slog.Info("serving", "dir", s.cfg.Dir, "addr", s.cfg.Addr, "index", s.cfg.Index, "codecs", s.cfg.Codecs) // … same server as the minimal demo … _ = http.ListenAndServe return 0 } func main() { fw.Solo(fw.NewRegistration(ID, func() *Srv { return &Srv{cfg: Config{Version: 1, Addr: ":8080", Dir: ".", Index: "list"}} }, func(s *Srv) *Config { return &s.cfg }, ). Alias("srv"). Metadata(&fw.Metadata{ Description: "Static file server with negotiable directory " + "index behavior and response compression.", Fields: map[string]any{ "Dir": fw.FieldMetadata[string]{ Hint: fw.HintDirectory, // advisory: tooling completes paths Doc: "Document root served over HTTP.", }, "Index": fw.FieldMetadata[string]{ Allowed: []string{"list", "none", "spa"}, Doc: "How directory URLs respond: 'list' renders a " + "listing, 'none' returns 404, 'spa' serves " + "index.html for client-side routing.", }, "Codecs": fw.FieldMetadata[string]{ Allowed: []string{"gzip", "zstd", "br"}, Doc: "Compression codecs offered during content " + "negotiation, in preference order.", }, }, })) }
Three kinds of annotation, per field:
Doc— the long-form description. Theusage:tag stays the--helpone-liner;Docis what documentation generators and rich completion UIs show.Allowed— a closed value domain. Note the default ("list") is itself inside the domain: a registered default outside its own declared set is caught when the composition is built, with every other violation.Hint— advisory, never enforced:--dirnames a directory, so tooling can complete paths there. The framework will not check that the directory exists (that is the applet's job, with a better error) — a hint says what a value denotes, not what it must be.AllowedandHintare mutually exclusive on one field: a closed enum and "it's a path" contradict each other.
Enforced, not advisory#
A non-empty Allowed is honored by the machinery on every write
path — the same rule whether the value arrives by argument, environment
or config file:
$ srv --index tree # startup fails: names the source (argument), # the offending value, and the allowed set $ SRV__INDEX=tree srv # same failure, source: environment $ echo '{"srv":{"version":1,"index":"tree"}}' > srv-config.json && srv
Slice fields are checked per element:
$ srv --codec gzip --codec lzma # fails: 'lzma' not in [gzip zstd br] $ SRV__CODEC=gzip,zstd srv # fine — every element in the domain
The applet keeps whatever checks it wants as defense in depth, but by
the time Configured() runs, s.cfg.Index is guaranteed to be one of
the three declared values — invalid configurations die at startup,
loudly, before any code runs.
Where the declarations surface#
Declare once, and the same metadata feeds everything downstream:
- the framework's own validation, as above;
Describe("srv")on the introspection surface returns the long-form description;Arguments("srv", args)returns the schema withArgInfo.AllowedandArgInfo.Hintpopulated — a completion service offers exactlylist,none,spaafter--index(and can trust the set, because the machinery enforces it), and switches to directory completion after--dir.
The full rules live in Your config struct § Metadata and value domains.