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.
One applet is active#
A binary can carry many applets, but only one is ever active. Dispatch chooses it, and only that applet enters the dependency graph — the others are never resolved, never configured, never started. Their arguments are not parsed and their config sections are not read.
What the number of applets decides, then, is not whether they can coexist but whether the binary needs a selector word to tell them apart. That is the whole of the two modes below.
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:
| Registration | Listed in usage | argv[0] dispatch | Explicit binary <alias> | Counts for single-applet mode |
|---|---|---|---|---|
| plain | yes | yes | yes | yes |
.Hidden() | no | no | yes | yes |
.System() | no | no | yes | no |
fw.NewRegistration(DumpID, newDump, dumpCfg). Alias("debugdump").Hidden().Register() // a maintenance command fw.NewRegistration(CompleteID, newComplete, completeCfg). Alias("completionbash").System().Register() // machinery, never typed
Hiddenis a command you can still type but that stays out of listings and symlink dispatch — debug and maintenance entry points.Systemdeclares machinery of the binary that a human is never meant to type; a shell-completion query endpoint is the canonical case. It impliesHidden, 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 and alias rules,
the ALIAS__ env prefix, config files, closure resolution and the
lifecycle are unchanged.
Dispatch speaks aliases, never ids. Both the selector word and the
argv[0] basename are looked up among aliases, so the path-shaped id
your code uses never appears on a command line. Any of a service's
aliases selects it; listings show the primary one.
Single-applet mode#
With exactly one non-System applet in the composition, 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 --argsdoes not treatmyappletas a selector even if it happens to match the applet's alias — 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 alias 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 alias needs the standard leading
-- escape. That is the price of letting completion scripts call
binary <systemalias> … without disturbing your command line.
Only dispatch is simplified. The applet's primary alias still anchors
everything else — the MYAPPLET__ env prefix, the config file names and
section, closure resolution, and the lifecycle all proceed as usual.
Selector rules (multi-applet)#
With two or more non-System applets in the composition, two rules
decide, in order:
- 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 ifbasename(argv[0])would itself name a valid applet. No fallback.HiddenandSystemapplets are selectable here like any other: explicit selection always works. - Otherwise,
basename(argv[0])must name a registered non-Hiddenapplet — the busybox symlink style. On Windows the.exesuffix is stripped before matching.
$ 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:
accepting a completion module 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
one line per public applet alias (Hidden and System ones omitted),
and exits 2. 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, with
the positional shape — 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#
Positionals are declared, not collected. The applet's config struct binds
them with pos: tags — pos:"0" for an indexed scalar, pos:"rest" for
the tail — so they are typed, validated and rendered in --help like
every other field. fw.Positionals() is gone; see
Your config struct for the tag rules.
Parsing still works the way it did:
- 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.
Only applets may declare positionals. A plain service carrying a
pos: tag is a registration error. Flags and env vars are namespaced per
service, but there is one command line and it belongs to the applet being
dispatched — two services claiming argument 0 could only be resolved by
composition order, which is exactly what this release set out to
eliminate.
That ownership is per dispatched applet, not per binary: in a multi-applet tool each applet has its own positional contract, and the others' declarations lie dormant.
Related: the busybox demo for the practice, the lifecycle page for what happens after dispatch, and the design spec for the rules verbatim.