sxcli.devSimple Extensible CLI

Overview

sxcli.dev/completion adds bash and zsh completion to any binary built on sxcli.dev/fw. It is an ordinary ecosystem module — it consumes the framework's public introspection API from the outside, with no privileged access, and nothing about it is or needs to be part of the core.

The whole integration#

Each shell package catalogs one service; the composition names what it takes, exactly like the framework's log sinks — a binary carries only the shells it wants to support:

go
go
import (
    "sxcli.dev/completion/bash"
    "sxcli.dev/completion/zsh"
)

fw.Builder().
    Accept(bash.ID, zsh.ID /* , your services… */).
    Main()

fw.Solo and fw.Main() accept everything catalogued, so a single-applet binary just imports and goes.

That is all. There is nothing to declare beyond what the framework already knows: completions are computed from the same config structs, usage: tags and registration metadata that drive your binary's arguments, environment variables and config files. Declare once, get everything.

Installing the completions#

Each shell package registers a System applet (completionbash, completionzsh) — machinery of the binary, never listed in usage, invoked by the generated scripts rather than by humans. Being System is what makes the module safe to add: it is excluded from single-applet counting, so accepting it can never flip your binary's dispatch mode.

The script for a command name is generated by the binary itself:

sh
sh
eval "$(mybin completionbash --script)"     # bash
eval "$(mybin completionzsh --script)"      # zsh, after compinit

In a busybox-style installation, generate through each symlink you actually created — each name gets its own registration with the target applet baked in:

sh
sh
eval "$(cat completionbash --script)"       # ./cat -> mybin
eval "$(ls completionbash --script)"

Put the eval line in your shell rc file, or write the generated script into the shell's completion directory — it is ordinary shell text.

What completes#

  • Applet names — the first word of a multi-applet binary, public applets only. Hidden and System applets are never offered: a completion must not suggest what a human should not type.
  • Argument names — long forms of the dispatched applet's whole closure, core arguments included. Already-used scalars drop out; repeatable slice arguments stay.
  • Declared value domains — a field with Allowed metadata completes exactly its legal values: the same declaration the framework already enforces at startup.
  • Files and directories — fields declaring HintFile / HintDirectory hand over to the shell's native file completion (the core's own --config does this).
  • Services — fields declaring HintServiceID complete from the binary's actual registry (--disable, --enable), in the operator's vocabulary: the aliases people type, not the path-shaped ids code uses.
  • --name=value — bools complete true/false after the =.

Zsh additionally renders each candidate's description from the usage: text and Doc metadata.

Everything is computed per keystroke against the real configuration: an --enable already typed on the line changes the closure — and the completions with it.

How it works#

The framework core exposes a read-only Introspector; this module consumes it like any other consumer could. The generated scripts are deliberately dumb transports: they forward the shell's raw completion state, and every decision happens in Go, where it is tested — unit tests against a fake introspector, integration tests that re-exec a real framework binary, and the generated scripts executed under the real shells.

Other shells#

Bash and zsh are what this module ships — and the machinery they are built on is public API. A third-party adapter (fish, PowerShell, elvish, …) is two small pieces over two packages:

  • engineComplete(src, query) answers what completes: applet names, argument names, declared domains, file/directory directives. The adapter only decodes its shell's transport into a Query and encodes the Candidates back.
  • script — the --script generation policy (which target to bake for the name the binary was invoked as), so dispatch semantics are never reimplemented per shell.

The bash and zsh packages are the reference implementations — each is one template plus ~60 lines of Go.

Status#

v0 — the API is settling and may still move. Module path sxcli.dev/completion, requires sxcli.dev/fw v0.3.0+, licensed under Apache-2.0.