Documentation
¶
Overview ¶
Package cli is the command-line engine: it validates a whole command tree once, resolves an argument vector against it, renders the help the tree implies, and returns a typed error carrying an exit status. It never ends the process.
Package cli — range 0.3.62.* (ADR 0065 service/cli block).
Package cli — the engine's configuration: two writers, and deliberately nothing else.
Package cli — declares the sentinel *errs.Error engine outcomes. Each var's name equals its errs.Define Reason in SCREAMING_SNAKE form.
Package cli — resolution and dispatch: the loop that turns an argument vector into one corecli.InvocationValue and one corecli.Action call.
Package cli — the one seam between this domain and internal/core/config.
Package cli — the help, generated from the declarations and from nowhere else.
Index ¶
Constants ¶
const CodeCommandPanicked errs.Code = 0x00_03_3E_04 // 0.3.62.4
CodeCommandPanicked identifies an Action whose goroutine panicked; the engine recovered it, captured the originating stack, and turned it into this command's failure so main still gets a typed exit status instead of the runtime's 2.
const CodeHelpWriteFailed errs.Code = 0x00_03_3E_05 // 0.3.62.5
CodeHelpWriteFailed identifies a help request (-h) whose answer the diagnostic stream did not take: a closed pipe, a full disk, a writer that accepted only part of the page. Asking a question is not a failure, but an answer nobody received is, so this is EX_IOERR and not the 0 a delivered help earns.
const CodeInvalidFlags errs.Code = 0x00_03_3E_03 // 0.3.62.3
CodeInvalidFlags identifies a flag vector package flag refused: an unknown flag, a missing value, or a value its flag.Value could not parse. flag's own text is preserved in Private, never in Public, because it quotes the operator's value verbatim.
const CodeMissingCommand errs.Code = 0x00_03_3E_02 // 0.3.62.2
CodeMissingCommand identifies a group invoked with no sub-command at all. A group has no action of its own, so this is an incomplete command line and never a successful no-op.
const CodeUnknownCommand errs.Code = 0x00_03_3E_01 // 0.3.62.1
CodeUnknownCommand identifies an argument vector naming a sub-command the group does not declare. The group's help — which lists every name it DOES declare — has been written to the diagnostic stream by the time this is returned.
Variables ¶
var ( // UnknownCommand is returned when a token does not name any child of the // group that was reached. // // There is deliberately no "did you mean …?". The group's help has already // been written, and it lists every name that WOULD have worked — which is // the complete answer, cannot be wrong, and costs no edit-distance table // to keep correct. A suggestion is a second answer that can differ from // the first. UnknownCommand = errs.Define(CodeUnknownCommand, "UNKNOWN_COMMAND", "That sub-command does not exist", "service/cli: the token names no child of this group; the fields carry the group path and the token", errs.WithExitCode(exitUsage)) // MissingCommand is returned when a group is invoked with nothing after // its flags. // // It is a failure and not a quiet success on purpose: a group declares no // action, so `tool db` did nothing, and a script that read exit status 0 // there would treat "I forgot the verb" as "the migration ran". MissingCommand = errs.Define(CodeMissingCommand, "MISSING_COMMAND", "This command needs a sub-command", "service/cli: a group has no action of its own; the field carries the group path and its help was written", errs.WithExitCode(exitUsage)) // InvalidFlags is returned when package flag refuses the vector. // // flag's own message quotes the operator's value ("invalid value \"abc\" // for flag -n"), so it lives in a field and in Private and never in // Public, which is the wire-safe half. InvalidFlags = errs.Define(CodeInvalidFlags, "INVALID_FLAGS", "The flags for this command could not be parsed", "service/cli: flag.FlagSet.Parse refused the vector; the fields carry the command path and flag's own text", errs.WithExitCode(exitUsage)) // CommandPanicked is the failure of an Action that panicked. // // It is recovered rather than left to the runtime for one reason that is // not "hiding the bug": a panicking Go program exits with status 2, which // sysexits gives no meaning and which several supervisors read as a usage // error. Nothing is lost — the recovered value AND the stack of the // goroutine that actually failed both travel as fields — and what changes // is that main, rather than the runtime, decides what the process does // next. It keeps the default EX_SOFTWARE (70): the command line was fine. CommandPanicked = errs.Define(CodeCommandPanicked, "COMMAND_PANICKED", "The command panicked and was recovered", "service/cli: the Action panicked; the fields carry the command path, the panic value and the originating stack") // HelpWriteFailed is returned when -h asked for the help and the // diagnostic stream did not take it. // // Only the -h path returns it. After a bad command line the usage error is // the verdict and the help is the actionable half of it, so a failed write // there does not replace the status the operator's mistake earned. HelpWriteFailed = errs.Define(CodeHelpWriteFailed, "HELP_WRITE_FAILED", "The help could not be written", "service/cli: the diagnostic stream refused the help -h asked for, or took part of it; the fields carry the command path", errs.WithExitCode(exitIOErr)) )
Functions ¶
func New ¶
New validates a command tree and returns the corecli.Executor that runs it.
The WHOLE tree is validated here, not the branch an invocation happens to take. A declaration is a wiring fact, and a wiring fault discovered by the operator who typed the one command nobody had tried is a fault discovered in production: `tool db migrate` being unreachable must fail on the first run of the binary, not on the first run of the migration.
Every refusal is one of internal/core/cli's sentinels and carries EX_CONFIG (78). A corecli.Binder is CALLED here, once, on a throwaway set — that is what proves it binds no reserved name — so a Binder must be safe to call more than once. A Binder that panics is deliberately not recovered: it runs inside the caller's own main, on the line that wrote it, and a recovered panic there would replace a stack pointing at the bug with a sentence about a tree.
Types ¶
type Config ¶
type Config struct {
// Output is where a command's OWN output goes. It reaches an [Action] as
// InvocationValue.Output, so a command never has to reach for a process
// stream itself.
//
// The zero value resolves to os.Stderr, NOT os.Stdout. ADR 0030 makes
// stdout a protocol channel that no SDK default may claim: a tool whose
// output is a JSON document on a pipe sets Output: os.Stdout in main,
// which is one visible line at the one place that can honestly make the
// choice. A zero value is what somebody who has not yet learned the
// question exists chose, so it must not be the dangerous one.
Output io.Writer
// ErrOutput is where the generated help, and the usage that follows a bad
// command line, are written. The zero value resolves to os.Stderr.
//
// It is a SECOND writer rather than the same one so that setting Output to
// os.Stdout for a machine-readable tool does not put a help page in the
// middle of the document a consumer is parsing.
//
// What is deliberately NOT written here is the error itself. The engine
// owns the help — it is generated from the declarations and only the SDK
// can render it — and the caller owns how an error is presented, because a
// caller may want it as a line on stderr, as a log record, or as JSON. Two
// voices printing one failure is how a CLI ends up saying it twice.
ErrOutput io.Writer
}
Config configures one New. Its zero value is a working configuration.
It carries two writers and nothing else. There is no Signals field, no shutdown budget, no logger, no config source and no env prefix: every one of those is a domain the SDK already ships, and a CLI that grew its own copy would be a second, weaker one that the first would eventually disagree with. ADR 0065 §D6 lists what this domain composes and what it refuses to redo.
Config is a published concrete shape (pkg/v1/cli.Config aliases it), so ADR 0040 applies: it may still change while the module is v0, said out loud, and not after v1.
type FlagSourceValue ¶
type FlagSourceValue struct {
// contains filtered or unexported fields
}
FlagSourceValue is one invocation's flags, shaped as the flat key→value layer internal/core/config.Source yields. It is the whole of what this domain contributes to configuration, and everything it deliberately is not is listed in FlagSource.
FlagSourceValue is a published concrete shape (pkg/v1/cli.FlagSource returns it), so ADR 0040 applies: it may still change while the module is v0, said out loud, and not after v1.
func FlagSource ¶
func FlagSource(invocation corecli.InvocationValue) *FlagSourceValue
FlagSource snapshots the flags an operator ACTUALLY TYPED on one invocation and returns them as a configuration layer.
Only what was set ¶
It walks flag.FlagSet.Visit and never VisitAll. That one word is the whole value of this adapter: VisitAll yields every DECLARED flag, including the ones nobody passed, carrying their Go defaults — so a flag with a zero default would override the file and the environment on every single run, and a configuration file would appear to be ignored for reasons nothing reports. Visit yields only what the command line actually carried, which is exactly what "the flags are the highest-precedence layer" is supposed to mean.
The flag name IS the config key ¶
No transformation: no camelCase to snake_case, no "-" to "_", no automatic prefixing. Every such rule is a second grammar a reader has to learn and a generator has to reproduce, and its failure mode is a key nobody typed silently not matching a field. A flag that feeds `database.max_conns` declares itself as "database.max_conns" — package flag accepts a dot in a name and parses -database.max_conns=5 without help. And when the name is wrong, the config schema (ADR 0061) REFUSES an unaddressed key rather than ignoring it, so the mistake is loud at startup.
What this is not ¶
It is not a configuration loader. It reads no file, no environment variable and no default; it does no layering, no decoding and no validation. Those are the config domain, they already exist, and a CLI carrying its own copy would be a second one to disagree with. The intended shape is one line:
err := config.LoadSchema(&cfg, schema,
config.FileSource("toml", "/etc/tool.toml"),
config.EnvSource("TOOL_"),
cli.FlagSource(invocation)) // last wins
Precedence inside one invocation ¶
The sets are visited root first, so a leaf flag overrides an ancestor flag of the same name — the same "later wins" direction config.Load already uses between sources, rather than a second rule to remember.