sxcli.devSimple Extensible CLI
You are viewing an archived snapshot (v0.1.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.

Single-applet mode#

With exactly one applet registered, there is nothing to decide — it is always dispatched, and selector logic is fully 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.

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 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.
  2. Otherwise, basename(argv[0]) must name a registered 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 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.

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, 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() — 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.