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

Coming from flag/cobra/viper

An honest comparison. sxcli-fw is simpler and has fewer features than the cobra/viper stack — deliberately. What it offers instead is one composition model: when a feature is missing, the path to it is a service, not a bigger core.

The concept map#

You knowHere it is
flag.StringVar(&v, "name", "x", "usage")a config struct field — Name string with json + arg + usage tags
cobra.Command and subcommandsapplets — with symlink dispatch as a bonus
PersistentPreRun / PostRun chainsthe lifecycle: ConfiguredStartRunStop, dependency-ordered
viper.AutomaticEnv + SetEnvPrefixderived env names (<APPLET>_<ARG>), per field, no setup
viper.AddConfigPath / SetConfigFilethe standard location search + --config
viper's flag > env > config > defaultidentical, written low-to-high: defaults < config files < environment < arguments, built in
viper's yaml/toml/ini readersJSON native; format providers for the rest
wiring components by hand in main()services & injection
cobra completion bashnot built in — see below

What you give up today#

No shell completions. No positional argument routing or validation (positionals are collected, nothing more). No custom value types beyond the supported kinds. No generated man pages. No middleware ecosystem. If any of those is a hard requirement today, cobra is the mature choice and this page won't argue.

The path back: everything is a service#

The framework's answer to a missing feature is not a bigger core — it is a service someone writes once and every binary imports:

go
go
import _ "example.com/sxcli-completions" // hypothetically, one day

Shell completions are the concrete example. In cobra they are generated from the command tree the library already holds; here the equivalent source of truth is the argument schema the framework builds from every config struct in the closure. A completion service reading that schema can emit bash/zsh/fish scripts exactly the way cobra's completion subcommand does — registered as a service, pulled in with a blank import, enabled like anything else.

And the schema API exists: the core's Introspector (injected like any service — see Services & injection) exposes Applets(), Services(), and Arguments(appletID, args) — the closure-true argument schema an applet would have for a given command line, each entry carrying the argument names, env var, usage text, type, and the declared value domain (ArgInfo.Allowed). Everything a completion generator needs; the service itself just hasn't been written yet. (Custom value parsers remain an open item.) The pattern stands: features arrive as services, compose through the closure, and the core stays small.

The philosophy difference#

flag/cobra/viper are imperative: you call APIs to build up commands, flags and bindings, and the structure lives in that code. Here the structure is declarative: one struct per service is the whole interface, the framework derives arguments, environment and file keys from it, and strictness is the default — unknown arguments, unknown config keys and misconfiguration are startup errors, never silent.

If that trade — fewer features, one model, loud failures — reads like a relief rather than a limitation, Getting started takes ten minutes.