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

Dispatch & applets

Dispatch is how the binary decides which applet runs. fw.Main() takes no parameters by design — the argument vector is platform-sourced: os.Args on POSIX, and in Windows service mode the vector the Service Control Manager hands to Execute.

Applet visibility#

Most applets are plain public commands. Two registration options change how the framework treats an applet — policy, not capability, which is why they are options rather than interfaces:

RegistrationListed in usageargv[0] dispatchExplicit binary <id>Counts for single-applet mode
plainyesyesyesyes
fw.Hidden()nonoyesyes
fw.System()nonoyesno
go
go
fw.Register("debugdump", d, fw.Hidden())          // a maintenance command
fw.Register("__complete", c, fw.System())         // machinery, never typed
  • Hidden is a command you can still type but that stays out of listings and symlink dispatch — debug and maintenance entry points.
  • System declares machinery of the binary that a human is never meant to type; a shell-completion query endpoint is the canonical case. It implies Hidden, and crucially it is ignored by single-applet counting, so a module registering one can never flip an existing binary's dispatch mode.
  • Either option on a service that is not an applet is a registration error.

In every other respect they are ordinary applets: id rules, the APPLETID_ env prefix, config files, closure resolution and the lifecycle are unchanged.

Single-applet mode#

With exactly one non-System applet registered, there is nothing to decide — it is always dispatched, and selector logic is off:

  • argv[0] is ignored; binary name, symlinks, install location are all irrelevant.
  • The entire argument vector after the binary name belongs to the applet as ordinary flags and positionals. mytool myapplet --args does not treat myapplet as a selector even if it happens to match the applet's id — it is a leading bare token under normal parsing, so there is no "data or selector?" ambiguity to reason about.

One carve-out: a first bare token equal to a registered System applet's id selects that applet. So with System applets present, not every start of the binary runs the main applet — and a genuine positional colliding with a System id needs the standard leading -- escape. That is the price of letting completion scripts call binary <systemid> … without disturbing your command line.

Only dispatch is simplified. The applet's id still anchors everything else — the MYAPPLET_ env prefix, the config file names, closure resolution, and the lifecycle all proceed as usual.

Selector rules (multi-applet)#

With two or more non-System applets registered, two rules decide, in order:

  1. If the first argument exists and does not start with -, it is always an applet selector. Look it up, dispatch with the remaining arguments. An unknown name is a dispatch failure — even if basename(argv[0]) would itself name a valid applet. No fallback. Hidden and System applets are selectable here like any other: explicit selection always works.
  2. Otherwise, basename(argv[0]) must name a registered non-Hidden applet — the busybox symlink style. On Windows the .exe suffix is stripped before matching.
sh
sh
$ tbx upper --trim < notes.txt   # rule 1: first bare argument selects
$ upper --trim < notes.txt       # rule 2: argv[0] selects (a symlink to tbx)
$ tbx frobnicate                 # unknown selector: usage + applet list,
                                 # exit non-zero — no argv[0] fallback

The consequence, worth stating bluntly: in a multi-applet binary a leading bare token is never applet data. Rule 1 runs first even when the binary was invoked through a symlink — so upper notes.txt does not hand notes.txt to the upper applet; it fails dispatch looking for an applet named notes.txt. Scripts targeting multi-applet binaries keep positionals behind a flag or after --, and nobody gets surprised.

The contract change to plan for#

Registering a second non-System applet re-enables selector logic — and that changes the binary's command-line contract: yesterday mytool report.txt was a positional, today it is an unknown-applet dispatch failure. That hard flip is deliberate. Mixing and matching applets is a core idea of the framework — but an applet written assuming it owns the whole command line must not silently survive gaining a sibling. By failing loudly instead of guessing, the framework makes sure the composition is a conscious decision: code destined for multi-applet binaries is written — and its invocations are written — expecting the selector contract from the start. The busybox demo shows that side in practice.

This is exactly why System applets are excluded from the count: a completion module you blank-import must never rewrite your binary's command line as a side effect. Adding a plain applet is a decision; adding machinery is not supposed to be one.

Dispatch failures, `--help`, and listing applets#

Every dispatch failure — unknown selector, unmatched argv[0], or a binary with zero registered applets — prints usage to stderr, including the list of registered applet ids (Hidden and System ones omitted), and exits non-zero. In single-applet mode the applet list is dropped from that output.

--help is deliberately different: it renders only the dispatched applet's argument schema — core + its closure, grouped by service id — and never an applet catalog. A core argument for enumerating applets (something like --applets) is planned but not yet built; today the list only appears in dispatch-failure usage output. Programmatically, though, the enumeration exists: the core Introspector's Applets() (public applets only) and SingleApplet() (dispatch-mode truth) — see Services & injection.

Positionals#

  • Every bare token after the last flag argument is collected as a positional.
  • A bare token followed by another flag is a strict-parse error — positionals must come last.
  • A literal -- ends flag parsing; everything after it is positional, dashes and all.

In v1 positionals are collected and exposed via fw.Positionals() — nothing more. Parsing and routing them (per-applet positional schemas) is deliberately deferred.

Related: the busybox demo for the practice, the lifecycle page for what happens after dispatch, and the design spec for the rules verbatim.