Minimal CLI
Not every tool needs services, injection or applet dispatch. This demo is the framework at its smallest: one applet, one config struct, one file — used purely for best-in-class argument, environment and config-file handling. If that is all you need, that is all you pay for.
The whole tool#
A static file server, srv, in a single main.go:
package main import ( "log/slog" "net/http" fw "sxcli.dev/fw" _ "sxcli.dev/fw/configfmt/yaml" _ "sxcli.dev/fw/sink/console" ) type Config struct { Addr string `json:"addr" arg:"addr,a" usage:"listen address"` Dir string `json:"dir" arg:"dir,d" usage:"directory to serve"` } 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) if err := http.ListenAndServe(s.cfg.Addr, http.FileServer(http.Dir(s.cfg.Dir))); err != nil { slog.Error("server failed", "err", err) return 1 } return 0 } func main() { s := &Srv{cfg: Config{Addr: ":8080", Dir: "."}} // field values are the defaults fw.Register("srv", s, fw.WithConfig(&s.cfg)) fw.Main() }
No services beyond the applet itself, no inject tags, no lifecycle code —
Run blocks until the process is done, and that is fine.
What the tags bought you#
Every field with an arg tag is now settable four ways, merged with fixed
precedence — defaults < config files < environment < arguments:
$ srv --addr :9090 -d /var/www # arguments $ SRV_ADDR=:9090 srv # environment — name derived from the arg name $ echo 'srv: {addr: ":9090"}' > srv-config.yaml # in the binary's directory $ srv --help # full schema with current effective values
The environment names (SRV_ADDR, SRV_DIR) derive from the applet id and
the argument names; the config file keys nest under the applet id. Standard
config locations are searched automatically: next to the real binary,
/etc/srv/, and the XDG user config directory.
JSON is the native config format — always enabled, nothing to import.
A srv-config.json with {"srv": {"addr": ":9090"}} works even if the
binary links no format provider at all. The configfmt/yaml import in the
listing is purely additive: it teaches the binary .yaml/.yml on top of
the built-in JSON; drop the import and JSON keeps working.
Single-applet mode#
With exactly one applet registered, dispatch is off: the entire argument
vector after the binary name belongs to srv — no subcommand is consumed,
argv[0] is ignored, and there is no selector ambiguity to think about.
Binary name, symlinks, install location: all irrelevant.
Bootstrap a config file#
--write-config writes the merged effective configuration — a quick way to
generate a starting config or normalize an existing one:
$ srv --addr :9090 --config srv.yaml --write-config $ cat srv.yaml srv: addr: :9090 dir: .
Takeaway#
A flag package with config-file and environment support, --help, and
strict validation — for the cost of one struct and two tags per field. The
advanced machinery (metadata, services, injection, sinks, applet
dispatch) stays out of the way until the day you want it — the
advanced CLI demo is the first step up, the
busybox-style demo the next.