sxcli.devSimple Extensible CLI
You are viewing an archived snapshot (v0.2.0). Switch to the current version →

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:

go
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:

sh
sh
$ 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:

sh
sh
$ 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.