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:
| Registration | Listed in usage | argv[0] dispatch | Explicit binary <id> | Counts for single-applet mode |
|---|---|---|---|---|
| plain | yes | yes | yes | yes |
fw.Hidden() | no | no | yes | yes |
fw.System() | no | no | yes | no |
fw.Register("debugdump", d, fw.Hidden()) // a maintenance command fw.Register("__complete", c, fw.System()) // 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 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 --argsdoes not treatmyappletas 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:
- 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: 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.