Documentation
¶
Overview ¶
Package rotini is the runtime for rotini-built command-line programs. A CLI is declared in a spec file, generated into Go with the rotini tool, and run on this runtime, which does nothing the spec did not declare; everything beyond dispatch is an explicit, opt-in service.
One module serves two roles at one version:
- Tool: `go get -tool github.com/go-rotini/rotini/cmd/rotini@latest` installs the codegen binary (`go tool rotini init` / `generate` / `validate`), which compiles a spec into a per-CLI framework file plus one editable handler file per command.
- Library: `go get github.com/go-rotini/rotini` provides this package, which generated code imports and handlers are written against.
The companion CLI under cmd/rotini is built with rotini; docs/assets/examples/ holds a spec and a conf that use every key.
Declare ¶
A CLI is a .rotini.spec.yaml (or json, jsonc, toml) document declaring the command tree and every input channel — argv flags and positional arguments, the value sentinels (from: file's @path, from: stdin's -), environment variables and flags' env fallbacks, configuration files, a typed stdin payload, declared defaults — plus documentation, shell completion and plugin dispatch.
`rotini validate` checks the spec: the JSON Schema rejects what it can express and lint rules reject the rest, each problem reported at file:line:col.
Generate ¶
`rotini generate` compiles the spec into a framework file — the Definition literal, typed per-command input structs, embedded help, man and markdown pages, and completion scripts — plus one handler stub per command, created once and then owned by the program. `rotini init` scaffolds a working CLI: a spec declaring -h/--help, -v/--version and the help and version commands, a conf, an entrypoint, and a stub per command wired to the generated help pages and the program's version. All of it may be edited or deleted.
Composition ¶
A command can be composed at any node, so a CLI is assembled from specs as its tree is assembled from commands. Five modes determine where a command's spec and handler code come from:
- Standalone — the command's own spec node and generated stub. The default.
- Inline with handler passthrough — the command's own spec node, with typed inputs generated locally and handler code from a package named by handler: { import: …, convention: … }. Codegen emits the delegating call and seeds no stub. It does not cascade: an inline sub-command without its own handler gets a stub.
- Local composition — a "$ref" to a sibling spec in the same module. The child's tree merges in, the parent winning on an overlapping key, and each composed command delegates to the child's generated package.
- Module composition — a "$ref" to mod://<module>@<version>/<path>, resolved through the Go module cache and pinned by go.sum, delegating to that module's generated package.
- Declared plugin — a sibling binary <program>-<name>, dispatched at run time rather than composed at codegen; a dispatch failure is a *PluginError. Plugin discovery dispatches an unmatched token to <prefix><token> the same way.
A composed child is generated on its own, so its typed inputs start at its own root and do not include a parent's cascading flags. The parent passes those as a Dependency: the child's package declares it, and the parent, which imports the child, collects its own inputs in CascadingPreRun and sets it. Collecting there validates only the parent's own inputs, so a descendant's --help and required inputs are unaffected:
// package child
var Kubeconfig = rotini.NewDependency[string]("child.kubeconfig")
// package parent, in its CascadingPreRun
in, err := rtx.Inputs[ParentInputs]()
if err != nil { rtx.HaltWith(err); return }
rtx.SetDependency(child.Kubeconfig, in.Parent.Flags.Kubeconfig)
`rotini validate` follows refs, validates each locally composed spec as its own document and checks the assembled tree for collisions, so a duplicate name, a cycle, a missing ref or an error inside a child is caught before codegen. Generation is hermetic: a local ref reads the filesystem, a mod:// ref reads the module cache, and a git:: or raw https:// ref is refused.
The runtime ¶
The generated entrypoint builds a Program with NewWiredProgram, passing the runtime features its spec uses, and calls Program.Execute, which resolves the invoked command from argv, runs its Handler hooks, and exits. Each invocation carries a Context: the argv, the resolved chain, the program's streams and its dependencies. A handler stops with Context.HaltWith, Context.Halt, Context.HaltWithCode or Context.Exit; recorded errors, recovered panics and detected faults are reported once, after teardown, by the outcome reporter.
The runtime resolves only which command was invoked. Flags and arguments are parsed and validated when a handler calls Context.Inputs (or the per-channel Context.ArgvInputs, Context.EnvInputs, Context.FileInputs and Context.StdinInputs), which the generated stubs do first. A handler that never calls it reads the raw Context.Argv and gets no validation; Context.CheckInputs checks inputs it collects another way. A flag marked short_circuit in the spec (FlagDef.ShortCircuit), such as --help, waives every declared requirement when it is set on the command line, so the handler can act on it.
A run reads the process environment and working directory unless the program gives it its own with Program.WithEnviron and Program.WithDir. Env inputs and fallbacks, config file paths and discovery, `@file` and existingfile paths, and plugins all read through them, and a handler reads them with Context.LookupEnv, Context.Environ and Context.Dir.
The runtime's only built-in behaviors are a default SIGINT/SIGTERM trap (see Program.WithoutSignalHandling and Program.WithSignals), the hidden __complete entry the generated shell scripts call, and os.Exit as the default exit action (see Program.WithExit).
Input values ¶
Input types are declared in the spec and parsed the same way on every channel an input reads: argv, a flag's environment and configuration fallbacks, and env and config inputs.
- Scalars parse into the generated field's type: string; bool (also boolean) as true/false, yes/no, on/off, y/n, t/f or 1/0 in any case; the integers int, int8, int16, int32, int64 and rune (also integer); the unsigned integers uint, uint8, uint16, uint32, uint64 and byte; the floats float32 and float64 (also number); and any, which holds the raw text. complex64, complex128 and uintptr have no parser and are refused.
- Value types: duration with Go's units plus d and w (7d, 2w3d); time and datetime (RFC 3339) and date (2026-09-29), each taking `layout:` for another format, or a list of formats tried in order, and `relative:` for a time measured from the run's clock (2h ago, today; see Program.WithClock); url, email, timezone, mac, ip, cidr and hostport; bytesize (ByteSize: 512Mi, 10MB), hexbytes (HexBytes) and base64bytes (Base64Bytes); regexp (a compiled *regexp.Regexp) and glob (Glob, a checked path.Match pattern).
- Path checks: existingfile and existingdir are strings checked at parse time to exist and to be that kind of entry. inputfile is an existingfile where "-" means stdin, at most once per run; outputfile is a file to write, where "-" means stdout. OpenInput and CreateOutput open them.
- Shapes: count (flags only) counts occurrences (-vvv is 3); a list ([]T, or array with `items:`) and a map (map[string]T, or map and object) repeat.
- Any other type parses through its own encoding.TextUnmarshaler, with `import:` naming its package.
- A list or map flag repeats (--tag a --tag b, --label k=v); with `separator:` one value also splits (--tag a,b), CSV-style, before validation sees the items.
- `implicit_value:` makes a flag's value optional: bare --color takes it, --color=never sets one, and the next word is never consumed.
- An `enum` matches exactly, or regardless of case with `ignore_case:`, binding the declared spelling; a value's `aliases` bind as the value itself, and hidden and deprecated values are accepted but not offered.
- An argument with `variable:` or `key:` falls back to the environment and configuration files, as a flag does, for a position the command line left out.
- A flag whose schema is a named object ($ref: '#/schemas/DB') takes a structured value: JSON (--db '{"host":"h","port":5}'), key=value pairs (--db host=h,port=5, dotted keys nesting, quotes keeping a comma), a JSON or YAML file with `from: [file]` (--db @db.yaml), or one field per flag (--db.host=h). Occurrences merge in order, a later key winning; a list of objects takes one element per occurrence. Every form is validated against the named schema. Where the schema does not type a value (inside a free-form map, or in a `dotted_keys:` map), key=value text is read as JSON would read it: true, false, null and JSON numbers are typed, anything else is text, so `-p spec.replicas=5` and `-p '{"spec":{"replicas":5}}'` store the same number.
Slices at the boundary ¶
- A slice rotini returns is a copy. Context.CommandChain and every Outcome channel return their own, so modifying one does not affect the run. Context.Argv is the exception: it is the live argv, for a handler's own parser.
- A slice passed in is retained, not copied. Program.WithArgs, Program.WithSignals and the slices inside an InputSettings are held by reference, so modifying the caller's slice afterwards changes the program.
Outcomes ¶
A handler does not print its outcome; it records it, and one reporter (Program.WithReporter) receives all five channels as an Outcome once the lifecycle finishes:
- Context.RecordInfo — neutral informational output.
- Context.RecordSuccess — what succeeded.
- Context.RecordWarning — non-fatal: a deprecation, a fallback. Never changes the code.
- Context.RecordError — the end user's failures: a bad input, a domain error.
- Recovered panics and rotini-detected faults, captured by the runtime; there is no record call for them.
Recording does not stop the run. The reporter is called only when some channel is non-empty, so a run that records nothing exits silently with code 0 unless a handler set one.
A handler stops in one of four ways:
- Context.HaltWith records an error and stops forward progress, setting no code. It is the usual way for a hook to fail.
- Context.Halt stops forward progress without recording anything or setting a code.
- Context.HaltWithCode stops and sets a code, when the code carries meaning: a filter reporting "no match" as 1, a wrapper passing through a child's status.
- Context.Exit stops and sets a code, skipping pending teardown.
A hook that records a failure without stopping lets the next hook run, often repeating the same failure.
The default reporter prints infos, warnings, errors, panics, then successes, all to stderr so stdout carries only what handlers write, then applies the exit floor: a recorded error or fault exits 1 unless a handler already set a non-zero code. It leaves out an error the run's own signal trap caused, since that run exits 128+n anyway. A custom reporter receives every record and owns the exit code entirely.
Every failure class is errors.Is-able against the ErrUsage or ErrInternal sentinel, so CategoryOf classifies it (except a plugin timeout, which is CategoryNone), and errors.As-able to a typed value with structured fields. Rotini's own messages expose no decoder or OS internals and no secret values:
- *ParseError — the argv channel. ParseError.Kind identifies the failure; Token and Candidates are the facts a "did you mean" needs.
- *InputError — the env, config, stdin and flag-fallback channels, carrying the channel, the input and a message, with the recon cause reachable via errors.As. An env or config value outside its enum also carries Token and Candidates.
- *PluginError — a plugin dispatch, recorded as an error. A missing discovered plugin is a usage error; a missing declared plugin, or a plugin that cannot start, is internal; a timeout is neither. A missing discovered plugin carries Candidates: the names the command knows, to rank its Name against.
- *DependencyError and *PanicError arrive as panics, as does a *WiringError from the program's wiring. Context.Inputs returns one *WiringError as an error instead: config inputs requested on a program built without an InputSettings.
Rotini prints no "did you mean" suggestions and no help on error. A program that wants either branches on the error in its handler (errors.As to the type, then its Kind or CategoryOf) or writes its own reporter. A value a flag's environment or config fallback supplied is named with its source in a value error, so the user can find it.
Sharing dependencies between handlers ¶
A value every handler needs (a store, a client, a logger) is a dependency, named by a typed Dependency handle so its name and type cannot drift apart:
// declared once, beside the thing it names
var Store = rotini.NewDependency[*store.Store]("tasks.store")
// main.go — the value's type is checked where it is supplied
cmd.NewProgram().WithDependency(tasks.Store, store.New()).Execute()
// or several at once ([WithDependency] and [Program.With])
cmd.NewProgram().
With(
rotini.WithDependency(tasks.Store, store.New()),
rotini.WithDependency(tasks.Client, client.New()),
).
WithVersion(version).
Execute()
// any handler
s := rtx.MustGetDependency(tasks.Store)
Context.GetDependency reports a miss instead of routing it to the reporter, and Context.SetDependency sets one for the rest of this run only. Context.Command is the command whose hook is running, Context.CommandPath its canonical path ("tasks add"), and Context.CommandChain the full chain with the tokens the user typed.
Opt-in services ¶
Everything else is a function or type a handler calls when it needs it, with nothing to register. Rotini's own settings are typed options, not dependencies, so a dependency can never shadow one: Program.WithInputSettings, Program.WithInputReader, Program.WithParser, Program.WithVersion and Program.WithHelp set them; Context.Parser, Context.Version and Context.Help read them.
Context.Inputs reads every declared channel, reconciled and validated, into the command's generated inputs type:
inputs, err := rtx.Inputs[DeployInputs]()
Context.InputsWithReport adds the provenance InputReport, whose InputReport.Format prints where each value came from (InputSource.Origin). Both use the InputSettings the generated NewProgram supplies via Program.WithInputSettings.
Parser parses and validates the argv channel alone (GNU/POSIX grammar, typed coercion, enum and constraint checks), failing with a *ParseError. InputReader is the engine behind Context.Inputs, for callers who hold the settings explicitly. Neither needs to be supplied; Program.WithParser replaces only the parser Context.Parser returns, which has no options.
Deprecations reports the deprecated aliases and identifiers this invocation used.
Context.WriteOutput writes a command's output in the format the handler passes: json, yaml and toml by rotini, any other by the handler's renderer. Context.WriteOutputItem writes one item of a stream. Context.CheckOutput, Program.WithOutputChecks and DecodeOutput check values against the declared shape, and StructuredReporter reports a run's outcome as JSON lines on stderr when the program's rule marks the run structured. Context.WriteOutputTo and Context.WriteOutputItemTo write to another writer. SelectFields keeps the output fields a `--json id,title` flag names, as a Selection these write in place of the output, and SortBy sorts items by one field.
OpenInput and CreateOutput open an inputfile or outputfile value, where "-" is stdin or stdout; CreateOutput writes a file atomically. Program.WithBufferedOutput buffers stdout and checks the final flush.
The per-channel methods (Context.ArgvInputs, Context.EnvInputs, Context.FileInputs, Context.StdinInputs, Context.DefaultInputs, merged by MergeInputs or MergeInputsWithReport) read channels one at a time, for programs with custom precedence.
Context.CheckInputs checks inputs the program collected itself (a prompt, a secrets service, a test) against the spec, with PresenceOf marking which fields were supplied; a hand-built InputLayer merged with rotini's is checked the same way by InputReport.Validate.
ArgvOf turns an inputs value back into the argv and env that supply it, for tests and tools that drive a CLI. The rotinitest package builds on it to run a command from its inputs type in a test.
SuggestionFacts reads the rejected token and its candidates from any error that carries them, and Suggestor ranks them into "did you mean" suggestions; Suggestor.For does both in one call.
Program.WithResolver and Program.WithLifecycle replace the resolve and run phases. FlagValueCompleter and ArgValueCompleter supply dynamic completion, and Program.Complete answers it in a host's CompletionFormat: PluginCompletion for a plugin host such as kubectl completing a rotini plugin. Program.WithCompletion makes __complete answer in that format, for hosts that call it (Docker, Flux). When the conf turns completion messages on, a completer adds its own with Context.AddCompletionMessage, and Program.WithCompletionMessages decides when they show. A completer reads what has been typed so far with Context.PartialInputs and the program's context with Context.Context, and asks for no trailing space with Context.SetCompletionOptions; Program.WithCompletionDescriptions decides whether candidates carry descriptions.
Batteries ¶
Rotini does nothing on import, starts no background goroutine and touches no terminal. It ships no styler, table, spinner, prompt, pager or process runner; use golang.org/x/term, os/exec and similar libraries. Its two small helpers:
- StripANSI removes ANSI escape sequences, making a styled string safe for a man page, a markdown page or a completion description.
- IsTerminal reports whether a stream is a terminal, to choose a default output format or skip a prompt.
Program shapes ¶
An interactive loop, a daemon or a server answering a peer runs the same program by calling Program.Run repeatedly. Each dispatch gets a fresh Context, so nothing carries over between invocations, while dependencies registered up front reach all of them. Run is safe for concurrent use once configuration is complete; the handlers value and the program's streams remain shared, so a concurrent host synchronizes those. See Program.Run.
Rotini ships no loop. A host calls Program.RunContext once per line or request; supplying the context leaves signal handling to the host.
What rotini does not ship ¶
Some functionality lives in sibling modules, imported directly:
- watching files — go-rotini/fs, fs.NewWatcher
- a single-instance lock — go-rotini/fs, fs.PIDLock
- caching in a long-running program — go-rotini/memcache
Example (BranchingOnErrors) ¶
Example_branchingOnErrors branches on the typed error a missing input produces: the handler chooses the message and the exit code, and rotini's reporter is left with nothing to add.
def := Definition{
Name: "todo", Handler: "Todo",
Commands: []CommandDef{{
Name: "add", Handler: "TodoAdd",
Arguments: []ArgDef{{Name: "title", Type: "string", Required: true}},
}},
}
for _, argv := range [][]string{{"add", "buy milk"}, {"add"}} {
code, _ := NewProgram(def, branchingApp{}).
WithStdout(os.Stdout).
WithoutSignalHandling().
Run(argv)
fmt.Println("exit:", code)
}
Output: added "buy milk" exit: 0 missing required input: <title> Run 'todo add --help' for usage. exit: 2
Example (Unopinionated) ¶
Example_unopinionated runs a program without the input helpers: WithArgs supplies argv, WithExit captures the code, and WithoutSignalHandling installs no signal trap.
def := Definition{
Name: "greet", Handler: "Main",
Flags: []FlagDef{{Name: "name", Identifiers: []string{"--name"}, Type: "string"}},
}
os.Setenv("GREETING", "hi")
defer os.Unsetenv("GREETING")
code := -1
NewProgram(def, unopinionatedApp{}).
WithArgs([]string{"--name", "ada"}).
WithStdout(os.Stdout).
WithoutSignalHandling().
WithExit(func(c int) { code = c }).
Execute()
fmt.Println("exit:", code)
Output: hi, ada! (command "greet") exit: 0
Index ¶
- Variables
- func ArgvOf[T any](def Definition, v T, set Presence, opts ...ArgvOption) (argv, env []string, err error)
- func AsCommand(i int, hook func(context.Context, *Context)) func(context.Context, *Context)
- func ConfigFilePath(rtx *Context, file string) (path string, exists bool, err error)
- func ConfigProfiles(rtx *Context, file string) ([]string, error)
- func DecodeOutput[T any](p *Program, data []byte, format string) (T, error)
- func ExitCause(code int) error
- func InternalError(err error) error
- func IsTerminal(stream any) bool
- func MergeInputs[T any](layers ...InputLayer[T]) T
- func OpenInput(rtx *Context, path string) (io.ReadCloser, error)
- func PluginCompletion(w io.Writer, result CompletionResult) error
- func Ptr[T any](v T) *T
- func SetConfigValue(rtx *Context, file, key, raw string, opts ...ConfigWriteOption) error
- func SortBy[E any](items []E, field string, desc bool) error
- func StripANSI(text string) string
- func SuggestionFacts(err error) (token string, candidates []string, ok bool)
- func UnsetConfigValue(rtx *Context, file, key string, opts ...ConfigWriteOption) error
- func UsageError(err error) error
- type AppDirectories
- type ArgDef
- type ArgValueCompleter
- type ArgvError
- type ArgvOption
- type Base64Bytes
- type ByteSize
- type Category
- type Command
- type CommandDef
- type Completion
- type CompletionCandidate
- type CompletionDescriptionsDef
- type CompletionFormat
- type CompletionMessagesDef
- type CompletionOptions
- type CompletionResult
- type ConfigFile
- type ConfigKey
- type ConfigWriteOption
- type Constraints
- type Context
- func (rtx *Context) AddCompletionMessage(msg string)
- func (rtx *Context) ArgvInputs[T any]() (InputLayer[T], error)
- func (rtx *Context) CheckInputs[T any](v T, set Presence) error
- func (rtx *Context) CheckOutput(v any) error
- func (rtx *Context) Command() Command
- func (rtx *Context) CommandChain() []Command
- func (rtx *Context) CommandPath() string
- func (rtx *Context) Context() context.Context
- func (rtx *Context) DashIndex() (int, bool)
- func (rtx *Context) DefaultInputs[T any]() (InputLayer[T], error)
- func (rtx *Context) Dir() string
- func (rtx *Context) EnvInputs[T any]() (InputLayer[T], error)
- func (rtx *Context) Environ() []string
- func (rtx *Context) Exit(code int)
- func (rtx *Context) Failed() bool
- func (rtx *Context) FileInputs[T any]() (InputLayer[T], error)
- func (rtx *Context) GetDependency[T any](dep Dependency[T]) (T, bool)
- func (rtx *Context) Halt()
- func (rtx *Context) HaltWith(err error)
- func (rtx *Context) HaltWithCode(code int)
- func (rtx *Context) Help() string
- func (rtx *Context) Inputs[T any]() (T, error)
- func (rtx *Context) InputsWithReport[T any]() (T, InputReport, error)
- func (rtx *Context) LookupEnv(name string) (string, bool)
- func (rtx *Context) MustGetDependency[T any](dep Dependency[T]) T
- func (rtx *Context) Now() time.Time
- func (rtx *Context) Parser() *Parser
- func (rtx *Context) PartialInputs[T any]() (T, Presence, error)
- func (rtx *Context) RecordError(err error)
- func (rtx *Context) RecordInfo(msg string)
- func (rtx *Context) RecordSuccess(msg string)
- func (rtx *Context) RecordWarning(warn error)
- func (rtx *Context) SetCompletionOptions(opts CompletionOptions)
- func (rtx *Context) SetContext(ctx context.Context)
- func (rtx *Context) SetDependency[T any](dep Dependency[T], value T)
- func (rtx *Context) SetDependencyIfAbsent[T any](dep Dependency[T], value T)
- func (rtx *Context) StdinInputs[T any]() (InputLayer[T], error)
- func (rtx *Context) Usage() string
- func (rtx *Context) Version() string
- func (rtx *Context) WithClock(now func() time.Time) *Context
- func (rtx *Context) WithDir(dir string) *Context
- func (rtx *Context) WithEnviron(env []string) *Context
- func (rtx *Context) WithHelp(help HelpFunc) *Context
- func (rtx *Context) WithInputReader(fn func(InputSettings) *InputReader) *Context
- func (rtx *Context) WithInputSettings(meta InputSettings) *Context
- func (rtx *Context) WithOutputChecks(enabled bool) *Context
- func (rtx *Context) WithParser(parser *Parser) *Context
- func (rtx *Context) WithStderr(w io.Writer) *Context
- func (rtx *Context) WithStdin(r io.Reader) *Context
- func (rtx *Context) WithStdout(w io.Writer) *Context
- func (rtx *Context) WithVersion(version string) *Context
- func (rtx *Context) WriteOutput[T any](v T, format string, render func(io.Writer, string, T) error) error
- func (rtx *Context) WriteOutputItem[T any](item T, format string, render func(io.Writer, string, T) error) error
- func (rtx *Context) WriteOutputItemTo[T any](w io.Writer, item T, format string, render func(io.Writer, string, T) error) error
- func (rtx *Context) WriteOutputTo[T any](w io.Writer, v T, format string, render func(io.Writer, string, T) error) error
- type Definition
- type Dependency
- type DependencyError
- type Deprecation
- type DiscoverDef
- type DiscoveredPlugin
- type Effects
- type EnumValue
- type ExitStatusDef
- type Feature
- func Bounds() Feature
- func Chdir() Feature
- func ConfigFiles() Feature
- func Dotenv() Feature
- func EmailValues() Feature
- func Env() Feature
- func EnvFiles() Feature
- func Fallbacks() Feature
- func FlagDependencies() Feature
- func FlagGroups() Feature
- func IPValues() Feature
- func JSON() Feature
- func JSONC() Feature
- func MACValues() Feature
- func Multicall() Feature
- func ObjectFlags() Feature
- func PathExpansion() Feature
- func Patterns() Feature
- func Plugins() Feature
- func Profiles() Feature
- func RegexpValues() Feature
- func ResponseFiles() Feature
- func Schemas() Feature
- func Stdin(formats ...string) Feature
- func StreamPaths() Feature
- func TOML() Feature
- func TimeZoneValues() Feature
- func URLValues() Feature
- func YAML() Feature
- type FieldPath
- type FlagDef
- type FlagDependency
- type FlagGroup
- type FlagGroupKind
- type FlagValueCompleter
- type Glob
- type Handler
- type HandlerLookup
- type HelpFunc
- type HexBytes
- type InputError
- type InputLayer
- type InputReader
- type InputReport
- type InputSettings
- type InputSource
- type Lifecycle
- type LifecycleStep
- type MulticallDef
- type NoCascadingPostRun
- type NoCascadingPreRun
- type NoHooks
- type NoPostRun
- type NoPreRun
- type Option
- type Outcome
- type OutputDef
- type OutputFile
- type OutputOption
- type Page
- type PanicError
- type ParseError
- type ParseKind
- type Parser
- type PathFromDef
- type PluginDef
- type PluginDiscoveryDef
- type PluginDispatch
- type PluginError
- type PluginErrorKind
- type Presence
- type ProfilesDef
- type Program
- func (p *Program) Complete(words []string, format CompletionFormat) (int, error)
- func (p *Program) Definition() Definition
- func (p *Program) Execute() error
- func (p *Program) Run(argv []string) (int, error)
- func (p *Program) RunContext(ctx context.Context, argv []string) (int, error)
- func (p *Program) With(opts ...Option) *Program
- func (p *Program) WithArgs(args []string) *Program
- func (p *Program) WithArgv0(name string) *Program
- func (p *Program) WithBufferedOutput(enabled bool) *Program
- func (p *Program) WithClock(now func() time.Time) *Program
- func (p *Program) WithCompletion(format CompletionFormat) *Program
- func (p *Program) WithCompletionDescriptions(fn func(rtx *Context) bool) *Program
- func (p *Program) WithCompletionMessages(fn func(rtx *Context) bool) *Program
- func (p *Program) WithContext(ctx context.Context) *Program
- func (p *Program) WithDependency[T any](dep Dependency[T], value T) *Program
- func (p *Program) WithDir(dir string) *Program
- func (p *Program) WithEnviron(env []string) *Program
- func (p *Program) WithExit(fn func(int)) *Program
- func (p *Program) WithFeatures(features ...Feature) *Program
- func (p *Program) WithHelp(help HelpFunc) *Program
- func (p *Program) WithInputReader(fn func(InputSettings) *InputReader) *Program
- func (p *Program) WithInputSettings(meta InputSettings) *Program
- func (p *Program) WithLifecycle(fn Lifecycle) *Program
- func (p *Program) WithOutputChecks(enabled bool) *Program
- func (p *Program) WithPanicRecover(enabled bool) *Program
- func (p *Program) WithParser(parser *Parser) *Program
- func (p *Program) WithReporter(fn Reporter) *Program
- func (p *Program) WithResolver(fn Resolver) *Program
- func (p *Program) WithSignals(sigs ...os.Signal) *Program
- func (p *Program) WithStderr(w io.Writer) *Program
- func (p *Program) WithStdin(r io.Reader) *Program
- func (p *Program) WithStdout(w io.Writer) *Program
- func (p *Program) WithTeardownOnPanic(enabled bool) *Program
- func (p *Program) WithTerminationTimeout(d time.Duration) *Program
- func (p *Program) WithVersion(version string) *Program
- func (p *Program) WithoutSignalHandling() *Program
- type Reporter
- type Resolution
- type Resolver
- type ResponseFilesDef
- type Selection
- type Suggestor
- func (s *Suggestor) Closest(input string, candidates []string) (suggestion string, ok bool)
- func (s *Suggestor) For(err error) []string
- func (s *Suggestor) Suggest(input string, candidates []string) []string
- func (s *Suggestor) WithMaxResults(n int) *Suggestor
- func (s *Suggestor) WithMinScore(score float64) *Suggestor
- type TopicDef
- type WiringError
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( ErrUsage = errors.New("usage error") ErrInternal = errors.New("internal error") )
ErrUsage and ErrInternal are the sentinels the categories match on, so a reporter may branch either way:
if errors.Is(err, rotini.ErrUsage) { /* usage */ }
switch rotini.CategoryOf(err) { case rotini.CategoryUsage: /* usage */ }
Prefer the UsageError and InternalError constructors to wrapping these with fmt.Errorf: they tag the category without prepending the sentinel's text to the message.
var ErrDependencyNotFound = errors.New("rotini: dependency not found")
ErrDependencyNotFound is the sentinel for a dependency that is not registered, or is registered as a different type. Context.MustGetDependency panics with a *DependencyError wrapping it, which the runtime recovers and routes to the reporter.
Functions ¶
func ArgvOf ¶ added in v1.4.0
func ArgvOf[T any](def Definition, v T, set Presence, opts ...ArgvOption) (argv, env []string, err error)
ArgvOf turns a typed inputs value back into the command line that supplies it: argv (without the program name, as Program.Run takes it) and env, the KEY=value variables for its env inputs, ready for Program.WithEnviron. It is the inverse of parsing, for tests and for tools that drive a CLI:
in := cmd.TaskrAddInputs{}
in.TaskrAdd.Flags.Priority = 2
in.TaskrAdd.Arguments.Title = "write docs"
argv, env, err := rotini.ArgvOf(p.Definition(), in, rotini.PresenceOf(in))
// argv: add --priority=2 -- "write docs"
Only the fields set names are written, each even when it holds its default. PresenceOf marks the non-zero fields; build the Presence by hand to write a zero value, such as --count=0 or --color=false over a true default.
The command comes from the generated Definition, which records each command's inputs type, or from ArgvPath. Each command's flags follow its name, so a flag two commands both declare lands on the right one. Values are written attached (--name=value). A list or map repeats the flag, and a separator-split one quotes items that hold the separator. An object is JSON, a count repeats the flag, and false on a negatable flag is its negated form (--no-name, or the declared one). A "--" always comes before the invoked command's positionals, so a positional that looks like a flag or a command stays a positional; it shows in Context.DashIndex. A passthrough command's words follow its name as they are, except that with response files on, a word before any "--" that starts with the prefix has it doubled. On a flag or argument reading `@file` values, a value starting with @ is written with the @ doubled. On Windows, where rotini expands a glob argument's words itself, each `*`, `?` and `[` in one is written as `[*]`, `[?]` and `[[]`, which read back as the literal character.
A value no command line can supply is an *ArgvError, never silently dropped:
- a secret flag or argument, unless ArgvSecrets is passed;
- a secret flag or argument that refuses a literal value (its from: leaves out value), even with ArgvSecrets: pass it through a file or stdin;
- a config or stdin input (write the file, or supply stdin, yourself);
- an argument of a command other than the invoked one, or one after an unset argument;
- "-" on a flag or argument that reads stdin from it;
- an explicit empty list or map on a flag without a separator, or a nil pointer in set;
- text a free-form map value would read back as another type ("true", "3", "null");
- a time its layout can't show exactly.
Any other error is the program's mistake: T isn't an inputs type of def, or doesn't match the command it names.
Example ¶
ArgvOf writes a typed inputs value back as the command line that supplies it. A generated Definition records each command's inputs type, so a test of a generated CLI passes p.Definition() and needs no ArgvPath.
type deployFlags struct {
Env string `rotini:"env"`
Replica int `rotini:"replicas"`
Wait bool `rotini:"wait"`
}
type deployCommand struct {
Flags deployFlags
Arguments struct {
Service string `rotini:"service"`
}
}
type deployInputs struct {
App struct{ Flags, Arguments struct{} }
Deploy deployCommand
}
def := Definition{
Name: "app", Handler: "App",
Commands: []CommandDef{{
Name: "deploy", Handler: "AppDeploy", Inputs: reflect.TypeFor[deployInputs](),
Arguments: []ArgDef{{Name: "service", Type: "string"}},
Flags: []FlagDef{
{Name: "env", Identifiers: []string{"--env", "-e"}, Type: "string"},
{Name: "replicas", Identifiers: []string{"--replicas"}, Type: "int", Default: "2"},
{Name: "wait", Identifiers: []string{"--wait"}, Type: "bool", Negatable: true, Default: "true"},
},
}},
}
var in deployInputs
in.Deploy.Flags.Env = "prod"
in.Deploy.Arguments.Service = "api"
argv, _, err := ArgvOf(def, in, PresenceOf(in))
fmt.Println(argv, err)
// PresenceOf leaves zero values out; name them in set to write them.
set := PresenceOf(in)
set["Deploy.Flags.Replica"] = InputSource{}
set["Deploy.Flags.Wait"] = InputSource{}
argv, _, err = ArgvOf(def, in, set)
fmt.Println(argv, err)
Output: [deploy --env=prod -- api] <nil> [deploy --env=prod --replicas=0 --no-wait -- api] <nil>
func AsCommand ¶ added in v1.2.0
AsCommand labels a hook with the chain index of the command it belongs to, so that Context.Command and Context.Inputs inside it refer to that command. The previous label is restored when the hook returns. DefaultLifecycle wraps every hook it plans. An unlabeled hook reports the invoked command.
A nil hook returns nil, which the engine skips.
func ConfigFilePath ¶ added in v1.4.0
ConfigFilePath reports the file the configuration file declared as file resolves to for this run, the one SetConfigValue writes, and whether it exists. It suits a `config path` command. A file found in the system configuration directories is an error, as it is for writing.
func ConfigProfiles ¶ added in v1.4.0
ConfigProfiles returns the names of the profiles the named config_files entry defines, sorted. It reads the file the running command would read: the path its config_source input gives, or its discover search, from the run's environment and directory. A file that does not exist defines none. It suits a completer for the profile selector:
names, err := rotini.ConfigProfiles(rtx, "app")
func DecodeOutput ¶ added in v1.2.0
DecodeOutput decodes a command's output, as captured from its stdout, into T, checking it against the command's declared schema. T is the generated <Prefix>Output type, which names the command; for output written item by item with Context.WriteOutputItem, T is a slice of it and every item is decoded. format is json, yaml or toml. It is for tests:
var stdout bytes.Buffer
p := cmd.NewProgram().WithStdout(&stdout)
p.Run([]string{"list", "-o", "json"})
list, err := rotini.DecodeOutput[cmd.TaskrListOutput](p, stdout.Bytes(), "json")
Output written from a Selection leaves fields out, so it fails the schema's `required` here; DecodeOutput is for whole outputs.
func ExitCause ¶ added in v1.2.0
ExitCause returns a context-cancellation cause that sets the exit code of the run the cancellation halts:
ctx, cancel := context.WithCancelCause(parent) prog.WithContext(ctx) cancel(rotini.ExitCause(3)) // exits 3
A cancellation without an ExitCause also halts the run, and the exit code is resolved as usual. Cancellation never preempts a running hook, and teardown always runs. See Program.WithContext.
func InternalError ¶
InternalError tags err as a CategoryInternal error (a bug or misconfiguration) without altering its message. It returns nil when err is nil.
func IsTerminal ¶
IsTerminal reports whether stream is attached to a terminal: an *os.File, or any value with an Fd() uintptr method, such as Context.Stdout under Program.WithBufferedOutput. Anything else, a nil value included, is not a terminal. A program uses it to pick a default output format or to skip a prompt when its output is piped:
format := in.Flags.Format
if format == "" && !rotini.IsTerminal(rtx.Stdout) {
format = "json"
}
It checks the device itself, so /dev/null, a character device that is not a terminal, reports false. On Windows only a console counts: a mintty or Cygwin pseudo-terminal is a pipe to the program, so it reports false. This is the one terminal check rotini offers; it still never styles output or prompts.
func MergeInputs ¶ added in v1.2.0
func MergeInputs[T any](layers ...InputLayer[T]) T
MergeInputs merges layers into one inputs value. Slice order is precedence, low → high: a field set by a later layer wins, a field no layer set stays the zero value, and an unset layer field never clobbers a lower layer's. Overlaying an empty layer is the identity.
func OpenInput ¶ added in v1.4.0
func OpenInput(rtx *Context, path string) (io.ReadCloser, error)
OpenInput opens an input-file value (a spec's `type: inputfile`) for reading:
- "-" is the run's stdin, read byte for byte with no line-ending or byte-order-mark changes. Every "-" in a run reads the same single stream, and closing it leaves stdin open. When stdin is a terminal, OpenInput returns a usage error rather than wait for typing. A read that the run's signal interrupts returns an *InputError and stops the run with the signal's exit code.
- Any other path is opened relative to the run's directory (Context.Dir); a file that cannot be opened is a usage error naming the path as given.
A handler for `cat [FILE...]` reads each value in turn, treating no files as "-":
files := in.Arguments.Files
if len(files) == 0 {
files = []string{"-"}
}
for _, name := range files {
f, err := rotini.OpenInput(rtx, name)
if err != nil {
return err
}
_, err = io.Copy(rtx.Stdout, f)
f.Close()
if err != nil {
return err
}
}
Use "-" rather than /dev/stdin: a path is opened on its own and would not share the stream another stdin consumer reads.
func PluginCompletion ¶ added in v1.2.0
func PluginCompletion(w io.Writer, result CompletionResult) error
PluginCompletion is the CompletionFormat the plugin hosts kubectl, Docker and Flux read: one candidate per line ("value\tdescription" allowed), then a final ":<directive>" line, a number telling the shell what to do next. kubectl reads it from kubectl_complete-<plugin>, and the Docker and Flux CLIs from the plugin's own __complete (see Program.WithCompletion).
The hint maps onto the directives: kind none to 4, offer no file names; kind directory to 16, directory names only; kind file with extensions to 8, file names with those extensions, which the format carries as the candidates; and kind file or no hint at all to 0, whose fallback is file completion. The hosts read the candidates of the filtering directives as their arguments, so a file or directory hint applies only when there are no candidates. Kinds the hosts have no completer for (command, executable, user, group and host) map to 4: the hosts offer nothing for them beyond the candidates. NoSpace adds 2 and KeepOrder adds 32. The directive line is always written, since the hosts read the last line as the directive.
Each message is written after the regular candidates as a candidate line carrying the hosts' message marker, which the hosts' completion scripts show as a message where the shell can.
func Ptr ¶
func Ptr[T any](v T) *T
Ptr returns a pointer to v, for the pointer Constraints bounds: Constraints{Minimum: rotini.Ptr(0.0)} declares an enforced >= 0. Prefer the built-in new(v); go fix inlines Ptr to it.
func SetConfigValue ¶ added in v1.4.0
func SetConfigValue(rtx *Context, file, key, raw string, opts ...ConfigWriteOption) error
SetConfigValue writes raw as key's value in the configuration file declared as file (its config_files name), for a command such as `config set <key> <value>`. It writes nothing on any failure.
The value is checked the way the command line checks a flag's: the key must be one an input reads from the file, raw is split on the key's separator for a list (map entries are key=value) and must parse as the key's type, and the enum, bounds, pattern and item counts apply. An enum value is written in its declared spelling, numbers and booleans as such, and everything else as a string. A key declared secret is refused without AllowSecretWrite. When the file declares a schema, the edited file must still match it.
The file is the one the command reads: its config_source path, else its fixed path or its discover search, from the run's environment and directory. A file that doesn't exist is created there, with any missing directories; a walk-up search creates it in the run's directory. A file found in the system configuration directories (xdg-system) is refused. A symbolic link is followed and the file it points to is written.
The edit keeps the file's comments, key order and layout: only the value's text changes, or the key's lines are added at the end of its parent mapping. A layout that can't be edited safely, such as a key reached through a YAML alias, is refused. YAML, JSON, JSONC, TOML and dotenv files are written. The new file replaces the old one atomically and keeps its permissions; a new file gets 0644, or 0600 when any of its keys is secret. Two commands writing the same file at once can lose one of the changes.
In a file that declares profiles, the value goes into the profile the run selects, unless ToProfile or ToSharedKeys says otherwise; with no profile selected it goes into the shared keys. A value written to the shared keys that the selected profile overrides is recorded as a warning.
Errors are *InputError values: a usage error for what the user can fix (an unknown key, an invalid value, a file that can't be parsed or written), an internal one for the author's mistakes (an unknown file name, a key of a custom type without ParseConfigValue).
func SortBy ¶ added in v1.4.0
SortBy sorts items in place, stably, by the field named by its JSON property name (a dotted name reaches into nested objects), descending when desc. Numbers compare as numbers, strings that are all RFC 3339 times as times, other strings byte by byte, and false before true. Items lacking the field, or holding null, sort last in both directions. It returns an error, leaving items as they were, when an item is not an object, when the field holds an object or a list, or when items hold values of different kinds in it.
It reads items through JSON, which is fine for the lists a command line prints.
func StripANSI ¶ added in v1.2.0
StripANSI removes every ANSI escape sequence from text, SGR styling and OSC alike, leaving the characters a terminal would display. Rotini applies it to man pages, markdown pages and completion descriptions, whose consumers would print escapes literally.
A CSI sequence is ESC "[", parameter bytes (digits, ";", ":", "?"), intermediate bytes (0x20–0x2f) and one final byte (0x40–0x7e); an OSC sequence is ESC "]" and a body ended by BEL or by ESC "\". An ESC that starts neither is kept.
func SuggestionFacts ¶ added in v1.3.0
SuggestionFacts returns the token err rejected and the candidates it was checked against: a mistyped flag, command or enum value (*ParseError), an env or config value outside its enum (*InputError), or a mistyped sub-command at a command with plugin discovery (*PluginError, whose Name is the token). It searches err's tree in order, through wrapped and joined errors, and returns the first error that carries both. ok is false when none does; an error whose token was redacted, because the input is secret, never counts. It ranks nothing: pass the result to a Suggestor or to a ranking of your own.
if token, candidates, ok := rotini.SuggestionFacts(err); ok {
if best := closest(token, candidates); best != "" {
fmt.Fprintf(rtx.Stderr, "Did you mean %q?\n", best)
}
}
func UnsetConfigValue ¶ added in v1.4.0
func UnsetConfigValue(rtx *Context, file, key string, opts ...ConfigWriteOption) error
UnsetConfigValue removes key from the configuration file declared as file, as SetConfigValue would write it: from the selected profile of a file with profiles, unless ToProfile or ToSharedKeys says otherwise. Comment lines above the key stay, and mappings the removal leaves empty are removed, except a profile and the key holding the profiles. Removing a key the file doesn't hold, or from a file that doesn't exist, is not an error.
func UsageError ¶
UsageError tags err as bad input the end-user can correct, without altering its message: the result reads exactly like err but matches ErrUsage, and errors.Is/As still reach err. It returns nil when err is nil.
if id == "" {
return rotini.UsageError(fmt.Errorf("a widget id is required"))
}
Types ¶
type AppDirectories ¶ added in v1.4.0
type AppDirectories struct {
Config string // settings the user edits
Data string // files the program keeps and the user would back up
Cache string // files that can be deleted at any time
State string // history and logs: kept across runs, not worth backing up
}
AppDirectories are where a program keeps its files. AppDirs computes them; it creates nothing.
func AppDirs ¶ added in v1.4.0
func AppDirs(rtx *Context, app, strategy string) (AppDirectories, error)
AppDirs returns app's directories, read through the run's environment (Context.LookupEnv and the home directory it implies), so a test that injects an environment gets its own. strategy uses the words of a config file's `discover.strategy`:
- "xdg": the XDG base-directory layout on every OS: $XDG_CONFIG_HOME (default ~/.config), $XDG_DATA_HOME (~/.local/share), $XDG_CACHE_HOME (~/.cache) and $XDG_STATE_HOME (~/.local/state), each joined with app.
- "native": the platform's own. On macOS, ~/Library/Application Support/<app> for config and data, ~/Library/Caches/<app>, and ~/Library/Application Support/<app>/state, with XDG variables ignored. On Windows, %AppData%\<app> (roaming) for config, %LocalAppData%\<app> for data, and %LocalAppData%\<app>\cache and \state. Elsewhere, the same as "xdg".
An XDG variable holding a relative path is ignored, as the XDG spec requires. app must be a single directory name. A missing home directory is an error. A nil rtx reads the process environment. Create a data or state directory with 0o700, as the XDG spec asks.
type ArgDef ¶
type ArgDef struct {
Name string
Type string
Required bool
Variadic bool
Default string
Enum []string
// EnumValues describes the Enum values; see [FlagDef.EnumValues].
EnumValues []EnumValue
// IgnoreCase matches a value against Enum without regard to case and binds the declared
// spelling; see [FlagDef.IgnoreCase].
IgnoreCase bool
// Separator splits each value of a variadic argument into several; see [FlagDef.Separator].
Separator string
// Layout is how a time argument's value is written; see [FlagDef.Layout].
Layout string
// Layouts and Relative are the argument's other time forms; see [FlagDef.Layouts] and
// [FlagDef.Relative].
Layouts []string
Relative string
// Deprecated is the argument's deprecation message: supplying it reports a [Deprecation]
// carrying it.
Deprecated string
// DeprecatedSince and RemovedIn are the argument's planned lifecycle; see
// [CommandDef.DeprecatedSince].
DeprecatedSince string
RemovedIn string
// Complete is the declarative shell-completion hint for this argument's value.
Complete Completion
Secret bool // when true, the value is redacted in usage/validation error output
Hidden bool // omitted from completion candidates (it still parses); help omission happens at codegen
// Passthrough marks the last argument as where raw words start: flags before it parse as
// usual, and it and every word after it are taken as typed, flag-shaped words and "--"
// included. It is a variadic []string. See [Context.DashIndex].
Passthrough bool
// From lists extra acquisition modes, as for a flag ([FlagDef.From]): "file" resolves an
// @path value and "stdin" a bare "-". Only an argument before any variadic one takes it.
From []string
// Glob expands each word of this variadic argument that contains `*`, `?` or `[` into the
// paths it matches, on Windows only, where the shell passes patterns through. A word
// that names an existing path, or matches nothing, is kept as written.
Glob bool
Constraints
}
ArgDef describes a single positional argument of a command. Variadic is true for a trailing slice argument that absorbs the remaining positionals.
type ArgValueCompleter ¶
ArgValueCompleter is the positional-argument counterpart of FlagValueCompleter: completion calls CompleteArgValue on the invoked command's handler with the argument's logical name and the word being typed. The same contract applies: a nil return falls back to the static enum, candidates keep their order, and a candidate may carry a "value\tdescription" suffix.
type ArgvError ¶ added in v1.4.0
ArgvError is a value ArgvOf can't write as a command line: a secret, a config or stdin input, or a value no argv spelling reads back, such as an explicit empty list on a flag without a separator. Field names the input.
type ArgvOption ¶ added in v1.4.0
type ArgvOption func(*argvConfig)
ArgvOption configures ArgvOf.
func ArgvPath ¶ added in v1.4.0
func ArgvPath(names ...string) ArgvOption
ArgvPath names the command the inputs value belongs to, by its path below the root (no names for the root itself). ArgvOf otherwise finds it by the inputs type the generated Definition records, so ArgvPath is for a hand-built Definition.
func ArgvSecrets ¶ added in v1.4.0
func ArgvSecrets() ArgvOption
ArgvSecrets lets ArgvOf write secret flags and arguments into argv. Without it they are an *ArgvError, since a command line is visible to other users of the machine (ps). Secret env inputs are always written, into env.
type Base64Bytes ¶
type Base64Bytes []byte
Base64Bytes is binary data written as base64. Standard and URL-safe alphabets are both accepted, padded or not. A spec declares one with `type: base64bytes`.
func (Base64Bytes) MarshalText ¶
func (b Base64Bytes) MarshalText() ([]byte, error)
MarshalText implements encoding.TextMarshaler with the standard, padded alphabet.
func (Base64Bytes) String ¶
func (b Base64Bytes) String() string
String renders the bytes as standard, padded base64.
func (*Base64Bytes) UnmarshalText ¶
func (b *Base64Bytes) UnmarshalText(text []byte) error
UnmarshalText implements encoding.TextUnmarshaler.
type ByteSize ¶
type ByteSize int64
ByteSize is a count of bytes that parses human sizes: `512Mi`, `10MB`, `1.5GiB`, `4096`.
Suffixes follow the SI and IEC standards; the `i` makes a unit binary:
B 1 K KB M MB … 1000, 1000², … (decimal) Ki KiB Mi MiB … 1024, 1024², … (binary)
Letters are case-insensitive and fractions are allowed (`1.5Gi`); the result is rounded to a whole number of bytes. A bare `m` is decimal (megabytes), unlike some tools that read it as binary.
A spec declares one with `type: bytesize`.
func (ByteSize) MarshalText ¶
MarshalText implements encoding.TextMarshaler, rendering the size as ByteSize.String does.
func (ByteSize) String ¶
String renders the size in the largest binary unit it is an exact multiple of — `512Mi`, `2Gi` — and in plain bytes otherwise, so it always parses back to the same value.
func (*ByteSize) UnmarshalText ¶
UnmarshalText implements encoding.TextUnmarshaler.
type Category ¶
type Category int
Category classifies an error by whose fault it is, so a reporter can choose the exit code and message style from one call to CategoryOf. Rotini tags its own errors (a missing dependency is CategoryInternal, a parse failure CategoryUsage); user code tags domain errors with UsageError or InternalError.
A category is a label, not an exit code. The default reporter exits 1 for any recorded error or fault; a program that wants distinct codes maps categories in its own reporter.
The constants are ordered by increasing severity (none < usage < internal), so a reporter can keep the worst of several with a plain comparison. The ordering is part of the contract; the numeric values are not.
const ( // CategoryNone is an unclassified error that rotini cannot attribute. CategoryNone Category = iota // CategoryUsage is bad input from the end-user: an unknown flag, a missing required // argument, a value that fails validation. The user fixes it by changing the command. CategoryUsage // CategoryInternal is a bug or misconfiguration in the program, such as a missing // dependency or a wiring mistake. Only the author can fix it. CategoryInternal )
func CategoryOf ¶
CategoryOf returns the Category an error carries, or CategoryNone when it matches neither sentinel:
cmd.NewProgram().WithReporter(func(ctx context.Context, rtx *rotini.Context, out rotini.Outcome) {
worst := rotini.CategoryNone
for _, err := range out.Errors {
fmt.Fprintln(rtx.Stderr, err)
if c := rotini.CategoryOf(err); c > worst {
worst = c
}
}
switch worst {
case rotini.CategoryInternal:
rtx.Exit(70)
case rotini.CategoryUsage:
rtx.Exit(2)
}
})
Inside a reporter the lifecycle has already settled, so Context.HaltWithCode is a no-op and Context.Exit is the only way to set a code.
CategoryOf classifies a single error and tests ErrUsage first, so an error carrying both sentinels (such as the errors.Join that Program.Run returns for a run that recorded both) reports CategoryUsage. To classify a whole run, walk out.Errors and keep the most severe category, as above.
Example ¶
Errors are tagged with a category at the source and mapped to exit codes in one switch, typically inside a reporter. Here usage errors map to 2.
classify := func(err error) int {
switch CategoryOf(err) {
case CategoryUsage:
return 2
case CategoryInternal:
return 70
default:
return 1
}
}
fmt.Println(classify(UsageError(errors.New("unknown flag"))))
fmt.Println(classify(InternalError(errors.New("wiring mismatch"))))
fmt.Println(classify(errors.New("untagged")))
Output: 2 70 1
type Command ¶ added in v1.2.0
type Command struct {
Name string
Handler string // ProgramHandlers method for this command; see [CommandDef.Handler]
Matched string // the argv token that resolved this command (name or an alias); "" for the root
DeprecatedIdentifiers []string // aliases of this command that are deprecated
Deprecated string // the command's deprecation message, when it is deprecated as a whole
DeprecatedSince string // the release that deprecated it; see [CommandDef.DeprecatedSince]
RemovedIn string // the release that will remove it
ReplacedBy string // the command to use instead; see [CommandDef.ReplacedBy]
Flags []FlagDef
Arguments []ArgDef
FlagGroups []FlagGroup
FlagDependencies []FlagDependency
Commands []CommandDef // sub-commands; empty for a leaf
Plugins []PluginDef // co-located plugin binaries dispatched as sub-commands
PluginDiscovery *PluginDiscoveryDef // plugin auto-discovery (nil = off)
// PluginPath is an extra directory searched for this command's declared and discovered
// plugin binaries, with a leading ~ and $VAR references already expanded from the run's
// environment ([Program.WithEnviron]). Empty means only the host binary's directory and PATH
// are searched.
PluginPath string
Passthrough bool // every token after this command is a raw positional (no flag parsing)
Output *OutputDef // what the command writes to stdout (nil = not declared); see [Context.WriteOutput]
// OptionsFirst stops flags at this command's first argument when it is the invoked one; see
// [CommandDef.OptionsFirst].
OptionsFirst bool
// DeprecatedIdentifiersRemovedIn is the release removing each deprecated alias; see
// [CommandDef.DeprecatedIdentifiersRemovedIn].
DeprecatedIdentifiersRemovedIn map[string]string
// Invoked reports whether this is the command the user invoked: the last command in the
// chain. Exactly one entry of [Context.CommandChain] has it set; in a cascading hook,
// rtx.Command().Invoked distinguishes the invoked command from its ancestors.
Invoked bool
// Usage is the command's usage line; see [CommandDef.Usage] and [Context.Usage].
Usage string
// contains filtered or unexported fields
}
Command is one node on the invoked command path, root → leaf, as resolved for this invocation and exposed via Context.CommandChain. For a statically composed child, the chain is its full path under the parent.
Fields copy the matching Definition (root) or CommandDef fields; an empty slice means the command declares none of that kind.
func (Command) DiscoveredPlugins ¶ added in v1.2.0
func (cmd Command) DiscoveredPlugins() []DiscoveredPlugin
DiscoveredPlugins returns each plugin discovered for cmd — an executable "<prefix>foo" found next to the binary, in the plugin path, or on PATH — deduped and sorted by name, with any name colliding with a declared sub-command, declared plugin or alias removed. It returns nil when cmd has no discovery or discovery is hidden. Rotini renders nothing; a handler lists the result itself:
chain := rtx.CommandChain()
for _, p := range chain[len(chain)-1].DiscoveredPlugins() {
fmt.Fprintf(out, " %s\t%s\n", p.Name, p.Path)
}
It reads the filesystem on every call and is best-effort: an unreadable directory contributes nothing. Command.PluginDiscoveryErrors reports failures of the configured plugin path.
func (Command) PluginBinary ¶ added in v1.2.0
PluginBinary reports the executable the named plugin of cmd would run, and whether it resolves. It searches where dispatch searches, in the same order: next to the host binary, then the command's plugin path, then PATH. Use it, not exec.LookPath, to check which plugins are installed.
name may be a declared plugin's name or alias, or a discovered plugin's token. It returns "", false when cmd declares no such plugin and has no discovery, or when the binary is not found. It reads the filesystem on every call.
func (Command) PluginDiscoveryErrors ¶ added in v1.2.0
PluginDiscoveryErrors returns the problems encountered scanning cmd's configured plugin path — typically that it is unreadable or not a directory — and nil when there is no discovery, no plugin path, discovery is hidden, or the path scanned cleanly. A path that does not exist is not a problem. The directory of the binary and the $PATH entries are not reported. Rotini prints no warning itself, since that would corrupt completion output. Each error carries the path and cause, so errors.Is(err, fs.ErrPermission) classifies it.
type CommandDef ¶
type CommandDef struct {
Name string
Aliases []string
Summary string // one-line description (completion candidates carry it as "name\tsummary")
Handler string // ProgramHandlers method, e.g. "RotiniGenerate"
Hidden bool // omitted from completion candidates (it still dispatches); help omission happens at codegen
DeprecatedIdentifiers []string // aliases (subset of Aliases) that [Deprecations] reports when used to invoke
// Deprecated is the command's deprecation message: invoking it by any name reports a
// [Deprecation] carrying it. Empty means the command is not deprecated as a whole.
Deprecated string
// DeprecatedSince and RemovedIn are the releases (X.Y.Z) the spec says deprecated the
// command and will remove it; DeprecatedIdentifiersRemovedIn is the release removing each
// deprecated alias. [Deprecations] reports them as [Deprecation.Since] and
// [Deprecation.RemovedIn].
DeprecatedSince string
RemovedIn string
DeprecatedIdentifiersRemovedIn map[string]string
// ReplacedBy is the command to use instead of this deprecated one, as the user types it
// ("taskr purge"); [Deprecations] reports it. "" when the spec names none.
ReplacedBy string
// HiddenAliases are more names that invoke the command but are never listed: completion
// and suggestions leave them out.
HiddenAliases []string
Flags []FlagDef
Arguments []ArgDef
FlagGroups []FlagGroup // cross-flag presence rules validated at parse time
FlagDependencies []FlagDependency // conditional cross-flag requirements validated at parse time
Commands []CommandDef
Plugins []PluginDef // plugin binaries dispatched as sub-commands of this command
PluginDiscovery *PluginDiscoveryDef // plugin auto-discovery on this command (nil = off)
PluginPath string // extra directory searched for BOTH this command's declared plugins and its discovered plugins
Passthrough bool // every token after this command is a raw positional (no flag parsing)
Output *OutputDef // what the command writes to stdout (nil = not declared)
// OptionsFirst stops flag parsing at this command's first argument when it is the invoked
// command: every later word is an argument, flag-shaped words and "--" included. Words
// before that argument parse as usual. Sub-commands don't inherit it.
OptionsFirst bool
// Inputs is the command's generated inputs type, which [ArgvOf] finds the command by; nil
// when not recorded.
Inputs reflect.Type
// ExitStatus lists the exit codes the command documents (the spec's exit_status). Each
// command lists its own; a sub-command doesn't inherit its parent's.
ExitStatus []ExitStatusDef
// Usage is the command's usage line, with its full invocation ("taskr add <title>
// [flags]"); see [Definition.Usage].
Usage string
// Effects is what running the command does (spec effects:); nil when undeclared. Not
// inherited by sub-commands.
Effects *Effects
}
CommandDef describes one command node within a Definition. Handler is the ProgramHandlers method name the runtime invokes to obtain this command's Handler.
type Completion ¶
type Completion struct {
// Kind is what the value is. Empty means no hint: the shell applies its own default, which
// for bash and zsh is file completion.
// - "file" and "directory" complete paths; "none" completes nothing, suppressing that
// default, for opaque values such as resource IDs.
// - "command" completes command paths below the root: the words already given to the
// argument are the path, and the candidates are that command's visible sub-commands.
// Rotini answers it itself, and tells the shell "none".
// - "executable", "user", "group" and "host" are completed by the shell's own completer:
// program names (bash offers every command name it knows), local accounts and groups,
// and the host names the shell knows. PowerShell has no host completer and offers
// users and groups on Windows only.
Kind string
// Extensions narrows Kind "file" to these suffixes, written without a dot
// ("yaml", "json"). Empty offers every file.
Extensions []string
// Message is a line shown when the input's value is being completed and there is nothing
// to offer, written by the author in the spec (`complete.message`) or derived from its
// summary. It is set only when completion messages are on.
Message string
}
Completion is a declarative hint about what an input's value is, for the shell to complete. It reaches the shell as a directive on the last line of the hidden __complete output, and each generated script translates it into that shell's own path completion. A dynamic completer (FlagValueCompleter, ArgValueCompleter) wins when it answers; the hint is the fallback.
type CompletionCandidate ¶ added in v1.1.1
CompletionCandidate is one offered value and its optional one-line description.
type CompletionDescriptionsDef ¶ added in v1.4.0
type CompletionDescriptionsDef struct {
// Env is the environment variable the conf's `descriptions_env` names. Set to 0, false or
// off, in any case, it hides every candidate's description in every shell.
// [Program.WithCompletionDescriptions] replaces this check.
Env string
}
CompletionDescriptionsDef is how completion descriptions are switched at run time, from the conf's completion feature.
type CompletionFormat ¶ added in v1.1.1
type CompletionFormat func(w io.Writer, result CompletionResult) error
CompletionFormat writes a CompletionResult to w in one completion protocol, the wire shape a completing host expects. Rotini computes the answer; the format only encodes it. PluginCompletion is the built-in for the plugin hosts kubectl, Docker and Flux.
A format is called once per request and must write only the answer, since the host parses all of it.
type CompletionMessagesDef ¶ added in v1.3.0
type CompletionMessagesDef struct {
// Env is the environment variable the conf's `messages_env` names, "" for none. Set to
// 0, false or off, in any case, it hides every message. [Program.WithCompletionMessages]
// replaces this check.
Env string
}
CompletionMessagesDef is how completion messages are switched at run time, from the conf's completion feature.
type CompletionOptions ¶ added in v1.4.0
type CompletionOptions struct {
// NoSpace: the shell adds no space after the inserted candidate, so the user goes on typing
// the same word, such as a path segment. macOS's bash 3.2 always adds the space, and fish
// adds none only after a candidate ending in one of @ = / : . ,
NoSpace bool
// KeepOrder: the whole answer is shown in the order given, sub-command names included. A
// completer's own candidates keep their order without it. bash before 4.4 sorts anyway.
KeepOrder bool
}
CompletionOptions are what a completer can ask of the shell for its answer, with Context.SetCompletionOptions.
type CompletionResult ¶ added in v1.1.1
type CompletionResult struct {
// Candidates are the offered values, in order — sub-commands, flags, enum values, or what a
// handler's [FlagValueCompleter] or [ArgValueCompleter] returned — already filtered by the
// typed prefix, with hidden inputs left out. Descriptions are empty when they are switched
// off ([Program.WithCompletionDescriptions]).
Candidates []CompletionCandidate
// Hint is the spec's declared `complete:` hint for the input being completed, or the zero
// value when it declares none. A completer that answers still wins over it: the hint is the
// fallback for when Candidates is empty, except kind "none", which also means "never offer
// files" when there are candidates. Kind "command" is answered in Candidates, and means
// "none" to the shell.
Hint Completion
// Messages are lines for the shell to show, not offer: what completers added with
// [Context.AddCompletionMessage], in order, or else, when there are no candidates, the
// input's static message ([Completion.Message]). Empty when completion messages are off or
// switched off at run time.
Messages []string
// NoSpace asks the shell not to add a space after the inserted candidate: every candidate
// ends in "=" (a map key, whose value comes next), or a completer asked for it with
// [Context.SetCompletionOptions].
NoSpace bool
// KeepOrder asks the shell to show the candidates in the order given, not sorted: a
// completer's own order, an enum's declared order, or unset required flags first.
KeepOrder bool
}
CompletionResult is one completion answer, before any wire format: what a program offers for the word being completed. It is what a CompletionFormat renders.
type ConfigFile ¶
type ConfigFile struct {
Name string // logical name
// Scope is the command path this source is declared on. Sources cascade: one is in
// scope for the invoked chain when its Scope is one of the chain's commands. "" is
// unscoped, in scope for every command; generated descriptors always set it.
Scope string
Path string // fixed file path (may contain ~)
Format string // "json" | "yaml" | "toml"; "" lets the input reader infer from the extension
Discover *DiscoverDef // run-time location strategy, instead of a fixed Path
PathFrom *PathFromDef // runtime inputs that supply/override the path (spec config_source)
// Schema is the self-contained JSON Schema the input reader validates the loaded document
// against at bind time; "" is none. The file that actually resolved is the one
// validated, and an absent optional file passes vacuously.
Schema string
// As is "env" for a dotenv file read as environment variables (the `.env` shape): its
// KEY=value lines feed env inputs and flag environment fallbacks under the real
// environment, which wins variable by variable, and never configuration inputs. "" (or
// "config") is a configuration file.
As string
// Profiles selects one named section of the file per run; nil when the file has none.
Profiles *ProfilesDef
// Keys is every key an input reads from this file, sorted by key, one entry per declaring
// input: what [SetConfigValue] checks a value against, and what a key completer offers.
Keys []ConfigKey
}
ConfigFile is one configuration-file source the input reader reads (reconciled by recon). Exactly one of Path and Discover locates the file (the spec enforces this).
type ConfigKey ¶ added in v1.4.0
type ConfigKey struct {
Constraints // the input's bounds, pattern and item counts
Key string // the dotted path as written in the file; a dotenv file's variable name
Type string // the input's type, as [FlagDef.Type] spells it
Enum []string // the allowed values; nil allows any
EnumValues []EnumValue // each Enum value's aliases and notes; nil when none declares more
IgnoreCase bool // Enum matches without regard to case
Layout string // a time input's layout; see [FlagDef.Layout]
Layouts []string // every layout when there are several; see [FlagDef.Layouts]
Separator string // what splits a list or map value given as one string; "" is ","
Secret bool // the value is a secret
Object bool // the value is an object, which is set by editing the file
}
ConfigKey is one key a configuration file may hold, as the input that reads it declares it. Generated code lists them in ConfigFile.Keys.
type ConfigWriteOption ¶ added in v1.4.0
type ConfigWriteOption func(*configWrite)
ConfigWriteOption adjusts SetConfigValue and UnsetConfigValue.
func AllowSecretWrite ¶ added in v1.4.0
func AllowSecretWrite() ConfigWriteOption
AllowSecretWrite lets SetConfigValue write a key declared secret. The file must not be readable by other users: a new file is created with mode 0600, and an existing file readable by its group or by others is refused, not changed.
func ParseConfigValue ¶ added in v1.4.0
func ParseConfigValue(fn func(raw string) error) ConfigWriteOption
ParseConfigValue checks the raw value of a key whose type rotini can't parse itself, one a spec takes from `import:`; the value is then written as the string given. For any other key fn runs after rotini's own checks. An error from fn is reported as an invalid value.
func ToProfile ¶ added in v1.4.0
func ToProfile(name string) ConfigWriteOption
ToProfile writes into the named profile of a file that declares profiles, creating the profile when the file doesn't define it yet, instead of the profile the run selects.
func ToSharedKeys ¶ added in v1.4.0
func ToSharedKeys() ConfigWriteOption
ToSharedKeys writes the file's top-level keys, which every profile shares, instead of the profile the run selects.
type Constraints ¶
type Constraints struct {
Minimum *float64 // inclusive numeric lower bound; nil = unset
Maximum *float64 // inclusive numeric upper bound; nil = unset
ExclusiveMinimum *float64 // strict numeric lower bound (value must be >); nil = unset
ExclusiveMaximum *float64 // strict numeric upper bound (value must be <); nil = unset
MultipleOf *float64 // the value must be an integer multiple (strictly positive); nil = unset
MinLength int // minimum string length in runes; 0 = unset
MaxLength *int // maximum string length in runes; nil = unset
MinItems int // minimum item count (repeatable flag / variadic argument); 0 = unset
MaxItems *int // maximum item count; nil = unset
Pattern string // regular expression the value must contain (string types); "" = unset
PatternMessage string // what a Pattern failure tells the user, in place of the regex; "" = show the regex
// UniqueItems rejects a list that holds the same value twice (repeatable flag / variadic
// argument). Values compare as their type reads them, so `01` and `1` are the same int.
UniqueItems bool
}
Constraints carries the validation bounds a spec may declare on a flag or argument. The parser enforces them after reconciliation, so a value supplied via env or config is checked too. The numeric bounds and the two maximums are pointers, nil meaning unset, so `minimum: 0` and `maxItems: 0` are enforced bounds. For the two minimums 0 means unset.
type Context ¶
type Context struct {
// Stdin, Stdout and Stderr are the program's streams ([Program.WithStdin] and siblings).
// Handlers use them instead of os.Std* so tests can substitute streams. They are set before
// dispatch and never nil.
//
// Set them with [Context.WithStdin] and its siblings on a standalone Context, and with
// [Program.WithStdin] and its siblings for a run. The default reporter writes to the
// Program's streams.
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
// Argv is the raw argument vector for this invocation, command names and flags included,
// for a handler that runs its own parser. It is the live slice, not a copy: mutating it
// changes what later parses see. A command's declared positionals are the Arguments field
// of its generated inputs.
Argv []string
// contains filtered or unexported fields
}
Context is rotini's per-invocation context: the program's dependencies and streams, the raw argument vector (Context.Argv) and the resolved command chain (Context.CommandChain). One is built per Program.Run and passed to every hook of that run, so hooks share its dependencies, records and exit state, and nothing carries over between runs.
The surface groups into eight jobs:
- invocation — the Context.Argv, Context.Stdin, Context.Stdout and Context.Stderr fields, and the run's environment and working directory: Context.LookupEnv, Context.Environ and Context.Dir, and the run's clock reading, Context.Now; Context.DashIndex says where a "--" was typed; Context.Context is the running hook's context, and Context.SetContext hands a derived one to the hooks after it
- command — Context.Command is the command whose hook is running (its Invoked field reports whether the user ran it), Context.CommandPath names it, and Context.CommandChain lists every command from the root to the invoked one, and Context.Usage is its usage line
- inputs — Context.Inputs returns the command's validated inputs, Context.InputsWithReport adds where each value came from, and the per-channel Context.ArgvInputs, Context.EnvInputs, Context.FileInputs, Context.StdinInputs and Context.DefaultInputs read one channel each; Context.CheckInputs checks inputs the program collected itself against the spec
- output — Context.WriteOutput writes the command's declared output to stdout, Context.WriteOutputItem one item of a stream, Context.WriteOutputTo and Context.WriteOutputItemTo write them to another writer, and Context.CheckOutput checks a value against the declared shape without writing it
- dependencies — Context.GetDependency and Context.MustGetDependency read one; Context.SetDependency and Context.SetDependencyIfAbsent set one for this run
- records — Context.RecordInfo, Context.RecordSuccess, Context.RecordWarning, Context.RecordError, and Context.Failed; during completion, Context.AddCompletionMessage and Context.SetCompletionOptions, Context.PartialInputs reads what has been typed so far, and Context.Context is the program's base context
- stopping — Context.HaltWith to fail, Context.Halt to stop, Context.HaltWithCode to stop with a code, Context.Exit to stop and skip pending teardown
- rotini's own settings — Context.Version, Context.Help and Context.Parser read them; Context.WithVersion, Context.WithHelp, Context.WithParser, Context.WithInputSettings, Context.WithInputReader, Context.WithEnviron, Context.WithDir, Context.WithClock, Context.WithStdin, Context.WithStdout, Context.WithStderr and Context.WithOutputChecks set them on a standalone Context
Nothing is parsed or validated until a handler calls an inputs method; a handler with its own parser reads Context.Argv instead.
A Context is safe for concurrent use by the goroutines a hook starts. Context.Command reflects the step running when it is called (see its doc). A Context must not be copied.
Methods do not check for a nil receiver, with these exceptions: the With setters do nothing and return nil, the inputs methods return an error, and Deprecations returns none.
A nil argument to a With setter restores that setting's default.
func NewContextFor ¶
func NewContextFor(def Definition, argv []string) *Context
NewContextFor builds a Context with argv resolved against def, as the runtime does before dispatch, using the os streams until Context.WithStdout and its siblings replace them, and no program settings. It has every Feature. It serves tests of the Parser, the Context.Inputs family, or a single hook:
def := rotini.Definition{Name: "app", Handler: "App", Commands: []rotini.CommandDef{ … }}
rtx := rotini.NewContextFor(def, []string{"build", "x.yaml"})
var in appInputs
err := rtx.Parser().Parse(rtx, &in)
A single hook runs against captured streams:
var out bytes.Buffer
rtx := rotini.NewContextFor(def, []string{"list", "-o", "json"}).
WithStdin(strings.NewReader("")).
WithStdout(&out)
(&listHandler{}).Run(t.Context(), rtx)
To test a generated program end to end, build it with the generated NewProgram and run it with Program.WithExit and captured streams.
A plugin token resolves to the chain that precedes it; no plugin is executed.
func (*Context) AddCompletionMessage ¶ added in v1.3.0
AddCompletionMessage adds a line for the shell to show while it completes, from a FlagValueCompleter or ArgValueCompleter: why there is nothing to offer, or how to narrow a long list. Messages show in the order added, alongside any candidates, and in place of the input's static message. Each is reduced to one plain line.
services, err := loadServices()
if err != nil {
rtx.AddCompletionMessage("could not read deploy.yaml: " + err.Error())
return nil
}
It does nothing outside a completion request, or when the conf's completion feature doesn't turn messages on. zsh and bash 4.4 or later show messages; other shells skip them.
func (*Context) ArgvInputs ¶ added in v1.2.0
func (rtx *Context) ArgvInputs[T any]() (InputLayer[T], error)
ArgvInputs parses the command line only — flags and positionals across the resolved chain, exactly as supplied: no defaults, no fallback, and no required or enum validation. Validate the overlaid result with InputReport.Validate, so a required flag satisfied by another layer passes. Parse failures are *ParseError values.
func (*Context) CheckInputs ¶ added in v1.3.0
CheckInputs reports whether v satisfies the spec for the running command: required inputs, enums, bounds, lengths, patterns, item counts, object schemas, flag groups and flag dependencies. It is the input-side partner of Context.CheckOutput, for inputs the program collected itself: from a prompt, a secrets service, a test, or handler code.
set says which fields were supplied. Presence rules (required inputs, flag groups and flag dependencies) read it, and value rules apply only to the fields it names, so a zero value left unset is not checked against its enum or bounds. As on the command line, a list flag or variadic argument left out has zero items, so its minItems applies unless it has a default. PresenceOf builds a set from v's non-zero fields; build one by hand when a zero value must count as supplied. A declared default satisfies a required input, as it does for Context.Inputs.
A streamed stdin field (an iterator) counts as supplied when it is non-nil; its items are checked as they are read, not here.
It reads no channel, applies no defaults and never changes v. T must describe the running command, as for Context.Inputs. Each input is named by its canonical spelling: a flag's first long identifier, an argument's <name>, an environment variable's name, a config key. A short-circuit flag (FlagDef.ShortCircuit) set true in v and named in set waives every check.
Failures are the same values Context.Inputs returns: a *ParseError for flags, arguments and the cross-flag rules, an *InputError for environment, config and stdin inputs, both in CategoryUsage. A T that does not describe the running command is a ParseKindInternal error.
in := askForMissing(rtx, argv.Values)
if err := rtx.CheckInputs(in, rotini.PresenceOf(in)); err != nil {
rtx.HaltWith(err)
return
}
func (*Context) CheckOutput ¶ added in v1.2.0
CheckOutput checks v against the invoked command's declared output schema, returning an internal error that names each field that does not match:
taskr list: output does not match its contract: output.tasks[2].status: value is not in enum
It writes nothing. Program.WithOutputChecks makes every WriteOutput call check. A Selection is checked as the type it was selected from, against the schema with every `required` removed.
func (*Context) Command ¶
Command returns the command whose hook is running.
In PreRun, Run and PostRun that is the command the user invoked. A cascading hook runs for every command in the chain, and there it is the command the hook belongs to:
$ mig db status hook Command() Command().Invoked ──── ───────── ───────────────── mig's CascadingPreRun mig false db's CascadingPreRun db false the leaf's PreRun / Run / PostRun status true db's CascadingPostRun db false mig's CascadingPostRun mig false
Invoked distinguishes the command the user ran from its ancestors:
func (*songsHandler) CascadingPreRun(ctx context.Context, rtx *rotini.Context) {
if rtx.Command().Invoked {
// `musak songs`: this command is the invocation.
return
}
// `musak songs list`: a sub-command is running.
}
Context.Inputs, Context.CommandPath and Context.Help all describe the command Command returns. Outside a lifecycle step (a Context from NewContextFor, or in the reporter), Command returns the invoked command.
Goroutines ¶
Command reports the step running when it is called, not the hook that started the caller. A goroutine that outlives its hook may therefore see a later command, and inputs it reads anchor there too. Capture what it needs before starting it:
cmd := rtx.Command()
go func() { log.Println(cmd.Name) }()
func (*Context) CommandChain ¶ added in v1.2.0
CommandChain returns every command of this invocation from the root (index 0) to the invoked command (the last entry, the only one with Invoked set).
The slice is a copy; changing it does not affect the run. The copy is shallow: each command's Flags, Arguments and Commands are the Definition's own slices, shared by every run, and must be treated as read-only.
func (*Context) CommandPath ¶
CommandPath returns the path of Context.Command, space-joined from the root: "tasks add" for a sub-command, "tasks" for the root. In the root's cascading hook during `tasks add` it is "tasks".
The names are canonical, not aliases; each command's Matched field in Context.CommandChain holds the token the user typed.
func (*Context) Context ¶ added in v1.4.0
Context returns the context.Context the running hook received: the run's context, or one an earlier hook passed to Context.SetContext. It is never nil. Read it in the hook; from a goroutine that outlives the hook it returns whatever is current.
func (*Context) DashIndex ¶ added in v1.4.0
DashIndex reports how many of the invoked command's positional words came before a "--" that ended flags, and whether one was typed, so a handler forwarding its arguments can tell the words typed before the "--" from those after it:
app exec -- ls -la → 0, true app exec a -- b → 1, true app exec ls -la → 0, false
It counts words as typed, before a separator splits them. A "--" that is itself an argument isn't counted: one after a passthrough argument has started (`app exec ls -- x`), after an options_first command's first argument, or anywhere after a passthrough command's name. It reads only the command line, never a file or stdin, and reports (0, false) when the command line doesn't parse.
func (*Context) DefaultInputs ¶ added in v1.2.0
func (rtx *Context) DefaultInputs[T any]() (InputLayer[T], error)
DefaultInputs synthesizes the spec's declared defaults as an explicit layer, conventionally the lowest. Flag and argument defaults come from the resolved chain; env and config defaults from their recon tags.
func (*Context) Dir ¶ added in v1.4.0
Dir returns the run's absolute working directory (Program.WithDir): the process working directory unless one was injected, or "" when it can't be read.
func (*Context) EnvInputs ¶ added in v1.2.0
func (rtx *Context) EnvInputs[T any]() (InputLayer[T], error)
EnvInputs acquires the environment channel: every Env input, by its explicit variable or the SNAKE_UPPER projection of its key, plus the env fallback of any flag that declares a recon key. A missing required input or a constraint violation is this call's error.
func (*Context) Environ ¶ added in v1.4.0
Environ returns a copy of the run's environment (Program.WithEnviron) as KEY=value.
func (*Context) Exit ¶
Exit sets the exit code and stops the lifecycle immediately, skipping every pending teardown hook, as os.Exit skips deferred calls. Prefer Context.HaltWithCode for an orderly stop. It records no error. A panic recovered before Exit still reaches the reporter.
During the lifecycle the first non-zero code wins; inside the reporter, Exit overrides any code already set.
func (*Context) Failed ¶
Failed reports whether this run has so far recorded an error or captured a fault. A teardown hook uses it to decide between committing and rolling back:
func (*migrateHandlers) CascadingPostRun(ctx context.Context, rtx *rotini.Context) {
if rtx.Failed() {
tx.Rollback()
return
}
tx.Commit()
}
The errors themselves reach only the reporter, through Outcome. Once true, Failed stays true for the rest of the run.
func (*Context) FileInputs ¶ added in v1.2.0
func (rtx *Context) FileInputs[T any]() (InputLayer[T], error)
FileInputs acquires the configuration-files channel: every Config input from the InputSettings sources, declared order being precedence, plus the config fallback of any flag that declares a recon key. Required and constraint failures are this call's errors.
func (*Context) GetDependency ¶ added in v1.2.0
func (rtx *Context) GetDependency[T any](dep Dependency[T]) (T, bool)
GetDependency returns the value registered under dep and whether one is registered as type T. It never panics.
client, ok := rtx.GetDependency(tasks.Client)
func (*Context) Halt ¶
func (rtx *Context) Halt()
Halt stops the lifecycle's forward progress without setting an exit code or recording anything. Teardown is unaffected: every PostRun and CascadingPostRun whose setup hook began still runs, in reverse. Under the default plan:
Hook Effect of Halt ──── ────────────── CascadingPreRun no further setup, no PreRun, no Run PreRun Run is skipped Run none: Run is the last forward step PostRun none: teardown runs to completion CascadingPostRun none
Use Halt to stop when nothing failed:
if !inputs.Force && !confirmed {
rtx.Halt()
return
}
Use Context.HaltWith to fail, Context.HaltWithCode to stop with a specific code, and Context.Exit to stop without teardown. Halt is a no-op inside the reporter.
func (*Context) HaltWith ¶
HaltWith records err and stops the lifecycle's forward progress: Context.RecordError and Context.Halt in one call. It sets no exit code; the reporter decides it.
if err := store.Save(task); err != nil {
rtx.HaltWith(err)
return
}
It is the standard way for any hook to fail. Where Halt has no effect (Run and the teardown hooks), HaltWith only records. A nil err records nothing and still halts. To record and continue, use RecordError alone.
Inside the reporter it has no effect: the halt is a no-op and the record is dropped.
func (*Context) HaltWithCode ¶
HaltWithCode sets the exit code and stops the lifecycle's forward progress, for a code that carries meaning: a filter reporting "no match" as 1, a wrapper passing through a child's status. It records no error.
Teardown is unaffected, as with Context.Halt. The first non-zero code wins, so a later HaltWithCode or Context.Exit does not replace an earlier code.
Halt() stop HaltWith(err) stop and record err HaltWithCode(n) stop and set exit code n Exit(n) stop, set exit code n, and skip pending teardown
It is a no-op inside the reporter; the reporter sets the code with Context.Exit.
func (*Context) Help ¶
Help returns the help page of Context.Command from Program.WithHelp, as a generated --help prints it, or "" when there is none. A cascading hook gets its own command's page. The page is the running program's, so a composed command shows its full path and inherited flags.
func (*Context) Inputs ¶ added in v1.2.0
Inputs acquires every declared channel (argv, environment, configuration files, the stdin payload, defaults), reconciles them in the standard precedence defaults < files < env < argv, validates the result, and returns it. When a short-circuit flag (FlagDef.ShortCircuit) is set on the command line, the declared requirements are waived, so the handler gets the values read so far and can act on the flag. A command line or environment value that can't be read is still an error; a configuration file that can't be read is skipped for that call.
inputs, err := rtx.Inputs[MycliDeployInputs]()
Stdin is outside that order because it never competes: it fills only the leaf command's payload field, which no other channel writes. It is read once per run and held in memory, so a later call binds the same payload; a streamed stdin is an iterator over the run's one stream instead. It is not read at all, and the field stays nil, under a short-circuit flag or when the command's `unless_argument` file was given. A read of piped stdin ends when the run is canceled (by a trapped signal, for one) or when the hook's Context.Context ends (a deadline set with Context.SetContext, for one), with an *InputError whose cause is the cancellation's.
T must be the inputs type generated for the command whose hook is running (Context.Command), in any hook. Its last field describes that command and the preceding fields its ancestors, so the struct is anchored on the running command regardless of how deep the invocation went. A type that cannot be anchored there is an error, not a silent zero value.
Inputs delegates to InputReader.Read; errors are *ParseError and *InputError values, or a *WiringError when a command declares config inputs and the program has no InputSettings, or when an input needs a Feature the program does not wire. On error the returned T is partially filled and must not be used. A command line that doesn't parse is reported before any other channel is read. Use Context.InputsWithReport for a merged value and per-field provenance alongside the error.
func (*Context) InputsWithReport ¶ added in v1.2.0
func (rtx *Context) InputsWithReport[T any]() (T, InputReport, error)
InputsWithReport is Context.Inputs with provenance: the same reconciled, validated inputs plus an InputReport giving each field's Winner and History. It acquires each channel separately and overlays the layers in the standard precedence. An acquisition error returns the zero T; a validation error returns the merged inputs and report alongside it, so the caller can see which layer supplied the offending value.
func (*Context) LookupEnv ¶ added in v1.4.0
LookupEnv reads one variable of the run's environment (Program.WithEnviron): the process environment unless one was injected.
func (*Context) MustGetDependency ¶ added in v1.2.0
func (rtx *Context) MustGetDependency[T any](dep Dependency[T]) T
MustGetDependency returns the value registered under dep, or panics with a *DependencyError when it is missing or not a T. The runtime recovers the panic during dispatch and routes it to the reporter.
s := rtx.MustGetDependency(tasks.Store)
Example ¶
A typed handle names a dependency once, and every read is typed. GetDependency reports absence; MustGetDependency panics, and in a hook that panic reaches the reporter as a *PanicError after teardown.
type apiClient struct{ baseURL string }
api := NewDependency[*apiClient]("api")
rtx := NewContextFor(Definition{Name: "app", Handler: "App"}, nil)
rtx.SetDependency(api, &apiClient{baseURL: "https://api.example"})
client := rtx.MustGetDependency(api)
fmt.Println(client.baseURL)
if _, ok := rtx.GetDependency(NewDependency[*apiClient]("other")); !ok {
fmt.Println("nothing registered as \"other\"")
}
Output: https://api.example nothing registered as "other"
func (*Context) Now ¶ added in v1.4.0
Now returns the run's clock reading (Program.WithClock): the instant relative time values are measured from. The first call, or the first relative value, reads the clock; later calls return the same instant.
func (*Context) Parser ¶
Parser returns the Parser set by Program.WithParser, or a new default parser. It never returns nil.
func (*Context) PartialInputs ¶ added in v1.4.0
PartialInputs reads what has been typed so far, for a completer: the command line up to the word being completed, over the environment, over the declared defaults, in that precedence. T is the inputs type of the running command, anchored as Context.Inputs anchors it; a FlagValueCompleter runs as the command that declares the flag.
in, set, _ := rtx.PartialInputs[DeployInputs]()
if _, ok := set["Deploy.Flags.Region"]; ok {
return servicesIn(in.Deploy.Flags.Region)
}
It is lenient: nothing is validated or required, an unknown or half-typed flag is skipped, and a value that doesn't convert leaves its field unset. It never reads stdin or a file: a `-` or `@file` value is kept as typed, and configuration files are not read. The Presence names every field a layer supplied, defaults included.
Outside a completion request it reads the whole of Context.Argv. The error is a wiring mistake: no resolved command, or a T that doesn't describe the running command.
func (*Context) RecordError ¶
RecordError records err as one of this run's errors. It neither prints nor stops the lifecycle, so a handler can record several errors before stopping, or leave the decision to a later hook that checks Context.Failed. A nil err is ignored.
for _, path := range inputs.Check.Arguments.Paths {
if err := validate(path); err != nil {
rtx.RecordError(err)
}
}
To record and stop in one call, use Context.HaltWith.
func (*Context) RecordInfo ¶
RecordInfo records msg as an informational message for the reporter. Like every record method it neither prints nor stops the lifecycle. An empty msg is ignored.
func (*Context) RecordSuccess ¶
RecordSuccess records msg as a success message for the reporter. An empty msg is ignored. The reporter runs after the lifecycle, so records appear after anything a handler wrote directly to Context.Stdout.
func (*Context) RecordWarning ¶
RecordWarning records warn as a non-fatal warning: a deprecation, a fallback, a skipped item. It is an error so a reporter can inspect it with errors.As or CategoryOf; it never affects the exit code. A nil warn is ignored.
func (*Context) SetCompletionOptions ¶ added in v1.4.0
func (rtx *Context) SetCompletionOptions(opts CompletionOptions)
SetCompletionOptions asks the shell for opts, from a FlagValueCompleter or ArgValueCompleter, for this request's whole answer. Options add up across calls: one a call asks for stays on.
rtx.SetCompletionOptions(rotini.CompletionOptions{NoSpace: true})
return []string{"s3://", "gs://"}
It does nothing outside a completion request.
func (*Context) SetContext ¶ added in v1.4.0
SetContext hands a derived context to the rest of the run: the hooks after the calling one, Handler.Run included, receive ctx, as do the teardowns of those later hooks. A nil ctx is ignored.
type appHandler struct{ cancel context.CancelFunc }
func (h *appHandler) CascadingPreRun(ctx context.Context, rtx *rotini.Context) {
ctx, h.cancel = context.WithTimeout(ctx, 30*time.Second)
rtx.SetContext(ctx)
}
func (h *appHandler) CascadingPostRun(ctx context.Context, rtx *rotini.Context) {
h.cancel()
}
The rules:
- a teardown receives the context its own forward hook received, so the root's CascadingPostRun above gets the run's context, not the one its pre-run set, and never a deadline that has already passed;
- a call from a teardown or the reporter changes nothing, and the reporter always receives the run's context;
- the caller owns the cancel function: keep it in a handler field and call it in the matching teardown (handlers are created once per run);
- canceling the run's context still reaches every derived one, but a derived context ending does not halt the run, and an ExitCause on it sets no exit code: a hook that sees the derived context end stops with Context.HaltWith or Context.HaltWithCode.
func (*Context) SetDependency ¶ added in v1.2.0
func (rtx *Context) SetDependency[T any](dep Dependency[T], value T)
SetDependency registers value under dep for this run, replacing any existing value, so hooks that run later see it. Use it for per-run values such as a client built from the inputs; program-wide values belong in Program.WithDependency. It is safe for concurrent use.
func (*Context) SetDependencyIfAbsent ¶ added in v1.2.0
func (rtx *Context) SetDependencyIfAbsent[T any](dep Dependency[T], value T)
SetDependencyIfAbsent registers value under dep for this run only if nothing is registered there yet, atomically. A handler can register its real implementation this way while a test that registered a double under the same name keeps the double.
rtx.SetDependencyIfAbsent(Clock, time.Now) now := rtx.MustGetDependency(Clock)
func (*Context) StdinInputs ¶ added in v1.2.0
func (rtx *Context) StdinInputs[T any]() (InputLayer[T], error)
StdinInputs acquires the stdin channel: the leaf command's typed payload, decoded per its declared format, schema-validated, and honoring required.
func (*Context) Usage ¶ added in v1.4.0
Usage returns the usage line of Context.Command ("taskr add <title> [flags]"): the spec's `usage` when set, else the line rotini derives from the command's shape, which is the line help prints under its Usage heading. A cascading hook gets its own command's line. It returns "" for a Context with no command, or a hand-built Definition that sets none.
Nothing prints it by default. A reporter can print it after a usage error:
if len(out.Errors) > 0 && rotini.CategoryOf(out.Errors[0]) == rotini.CategoryUsage {
if u := rtx.Usage(); u != "" {
fmt.Fprintf(rtx.Stderr, "Usage: %s\n", u)
}
}
Handlers use it rather than the generated Usage function, so a composed command reports the line of the program it runs in.
func (*Context) Version ¶
Version returns the version set by Program.WithVersion, or "" if none was set.
func (*Context) WithClock ¶ added in v1.4.0
WithClock sets the clock relative time values read. See Program.WithClock. It is for a Context built with NewContextFor; during a run, it replaces the run's reading. nil restores time.Now.
func (*Context) WithDir ¶ added in v1.4.0
WithDir sets the directory relative paths resolve against. See Program.WithDir. It is for a Context built with NewContextFor; during a run, the change lasts for the rest of that run. A relative dir is made absolute now.
func (*Context) WithEnviron ¶ added in v1.4.0
WithEnviron sets the environment Context.Inputs and its siblings read. See Program.WithEnviron. It is for a Context built with NewContextFor; during a run, the change lasts for the rest of that run.
func (*Context) WithHelp ¶
WithHelp sets where Context.Help finds pages. See Program.WithHelp. It is for a Context built with NewContextFor; during a run, the change lasts for the rest of that run. A nil help restores the default, no pages.
func (*Context) WithInputReader ¶ added in v1.2.0
func (rtx *Context) WithInputReader(fn func(InputSettings) *InputReader) *Context
WithInputReader replaces the input reader Context.Inputs uses. See Program.WithInputReader. It is for a Context built with NewContextFor; during a run, the change lasts for the rest of that run. A nil fn restores the default, NewInputReader.
func (*Context) WithInputSettings ¶ added in v1.2.0
func (rtx *Context) WithInputSettings(meta InputSettings) *Context
WithInputSettings supplies the generated descriptor Context.Inputs reads from. See Program.WithInputSettings. It is for a Context built with NewContextFor; during a run, the change lasts for the rest of that run.
func (*Context) WithOutputChecks ¶ added in v1.4.0
WithOutputChecks makes Context.WriteOutput and Context.WriteOutputItem check each value against the command's declared output schema before writing it, as Program.WithOutputChecks does for a run. It is for a Context built with NewContextFor; during a run, the change lasts for the rest of that run.
func (*Context) WithParser ¶
WithParser sets the parser Context.Parser returns. Parser has no options, so this changes nothing today; see Program.WithParser. It is for a Context built with NewContextFor; during a run, the change lasts for the rest of that run. A nil parser restores the default.
func (*Context) WithStderr ¶ added in v1.4.0
WithStderr sets Context.Stderr for a Context built with NewContextFor. A nil writer restores os.Stderr. During a run it changes only this run's later handler writes and what a custom reporter writes to; the default reporter keeps the Program's stream. Redirect a whole run with Program.WithStderr. Configure it before use.
func (*Context) WithStdin ¶ added in v1.4.0
WithStdin sets the stream Context.Stdin reads and stdin inputs decode, for a Context built with NewContextFor. A nil reader restores os.Stdin. During a run it changes only what this run's hooks and a custom reporter read; redirect a whole run with Program.WithStdin. Configure it before anything reads stdin.
func (*Context) WithStdout ¶ added in v1.4.0
WithStdout sets Context.Stdout, where handlers and Context.WriteOutput write, for a Context built with NewContextFor. A nil writer restores os.Stdout. During a run it changes only this run's later handler writes and what a custom reporter writes to; redirect a whole run with Program.WithStdout. Configure it before use.
func (*Context) WithVersion ¶
WithVersion sets what Context.Version reports. See Program.WithVersion. It is for a Context built with NewContextFor; during a run, the change lasts for the rest of that run.
func (*Context) WriteOutput ¶ added in v1.2.0
func (rtx *Context) WriteOutput[T any](v T, format string, render func(io.Writer, string, T) error) error
WriteOutput writes v, the invoked command's output, to rtx.Stdout in format: indented json, yaml or toml by rotini, any other format by render, which may be nil when the handler only ever passes those three. An empty format means json.
It returns an internal error, and writes nothing, when the command declares a stream (output_stream), when v is not the type the command declares as its output, when no renderer is passed for a format rotini does not write, or, with Program.WithOutputChecks, when v does not match the declared shape. A renderer's error is returned unwrapped. A command that declares no output may still use it; nothing is checked.
v may be a Selection made from a value of the declared type by SelectFields: it is checked as that type, and against the declared shape with every `required` removed.
func (*Context) WriteOutputItem ¶ added in v1.2.0
func (rtx *Context) WriteOutputItem[T any](item T, format string, render func(io.Writer, string, T) error) error
WriteOutputItem writes one item of a stream to rtx.Stdout, for a command that writes its output item by item as each is ready: compact json, one value per line; a yaml document starting "---"; any other format by render. toml cannot be streamed. Its checks are WriteOutput's, with the item checked against the declared shape, which for a command that declares output_stream is one item. On a command that doesn't, the item is checked as WriteOutput checks a value.
func (*Context) WriteOutputItemTo ¶ added in v1.4.0
func (rtx *Context) WriteOutputItemTo[T any](w io.Writer, item T, format string, render func(io.Writer, string, T) error) error
WriteOutputItemTo is Context.WriteOutputItem to w instead of rtx.Stdout.
func (*Context) WriteOutputTo ¶ added in v1.4.0
func (rtx *Context) WriteOutputTo[T any](w io.Writer, v T, format string, render func(io.Writer, string, T) error) error
WriteOutputTo is Context.WriteOutput to w instead of rtx.Stdout: an output file from CreateOutput, a buffer, or several writers at once. Its checks and errors are WriteOutput's. To write the output to a file and to stdout (a tee):
out, err := rotini.CreateOutput(rtx, in.Flags.Output)
if err != nil {
return err
}
defer out.Abort()
if err := rtx.WriteOutputTo(io.MultiWriter(rtx.Stdout, out), report, format, render); err != nil {
return err
}
return out.Close()
io.MultiWriter stops at the first writer that fails, so a closed stdout also ends the file.
type Definition ¶
type Definition struct {
Name string
Handler string // ProgramHandlers method for the root command, e.g. "Rotini"
Flags []FlagDef
Arguments []ArgDef
FlagGroups []FlagGroup // cross-flag presence rules validated at parse time
FlagDependencies []FlagDependency // conditional cross-flag requirements validated at parse time
Commands []CommandDef
Plugins []PluginDef // co-located plugin binaries dispatched as sub-commands of the root
PluginDiscovery *PluginDiscoveryDef // plugin auto-discovery on the root command (nil = off)
PluginPath string // extra directory searched for BOTH declared and discovered plugins
Passthrough bool // every token after the program name is a raw positional (no flag parsing)
OptionsFirst bool // flags stop at the root's first argument; see [CommandDef.OptionsFirst]
Output *OutputDef // what the root command writes to stdout (nil = not declared)
// CompletionMessages turns on completion messages, when the conf's completion feature
// declares `messages`; nil leaves them off. See [Context.AddCompletionMessage].
CompletionMessages *CompletionMessagesDef
// CompletionDescriptions is how completion descriptions are switched off at run time, when
// the conf's completion feature declares `descriptions_env`; nil leaves them always on. See
// [Program.WithCompletionDescriptions].
CompletionDescriptions *CompletionDescriptionsDef
// ResponseFiles turns on response files: a word starting with the prefix names a file whose
// lines are read as more words. nil leaves them off.
ResponseFiles *ResponseFilesDef
// Inputs is the root command's generated inputs type, which [ArgvOf] finds a command by;
// nil when not recorded.
Inputs reflect.Type
// ExitStatus lists the exit codes the root command documents (the spec's exit_status).
ExitStatus []ExitStatusDef
// Usage is the root command's usage line: the spec's `usage` when set, else the line
// rotini derives from its shape ("taskr [flags]"). It is the line help prints under its
// Usage heading. See [Context.Usage].
Usage string
// Multicall dispatches on the name the binary was invoked as (the spec's root
// `multicall`); nil leaves it off. See [Program.WithArgv0].
Multicall *MulticallDef
// Topics lists the root's help topics (the spec's `topics:`), which completion offers
// with the root's sub-commands for a command path.
Topics []TopicDef
// Effects is what running the root command does (spec effects:); nil when undeclared.
Effects *Effects
}
Definition is the compiled command tree of a generated rotini program. Codegen emits it as a Go literal; the runtime parses argv, dispatches and completes against it. Help pages are rendered at codegen and supplied through Program.WithHelp. The Definition types are data only, with no behavior.
A Definition is the runtime form of the command tree `rotini generate` produces from your spec. Building one by hand is supported for tests (NewContextFor) and for tooling; it is not a way to define a CLI, which the spec is. Fields are added as the spec gains keys, including in minor releases, so construct values with field names (rotini.FlagDef{Name: "verbose"}), not positionally.
type Dependency ¶ added in v1.2.0
type Dependency[T any] struct { // contains filtered or unexported fields }
Dependency is a typed handle for one of the program's dependencies, pairing the name it is stored under with the type it is stored as, so a handler needs neither a string key nor a type assertion:
// declared once, beside the thing it names
var Store = rotini.NewDependency[*store.Store]("taskr.store")
// main.go: the value's type is checked at compile time here
cmd.NewProgram().WithDependency(tasks.Store, store.New()).Execute()
// any handler
s := rtx.MustGetDependency(tasks.Store)
Registration happens at run time, so a missing registration is not a compile error; it is reported when a handler requests the dependency.
Dependencies have two scopes:
- program-wide: Program.WithDependency or the WithDependency option, seeded into every run
- one run: Context.SetDependency or Context.SetDependencyIfAbsent from a hook, visible to the hooks that run after it
A handle whose name is known only at run time can be built as rotini.NewDependency[any](name).
The zero Dependency is named "" and is shared by every zero Dependency of any type; build handles with NewDependency.
func NewDependency ¶ added in v1.2.0
func NewDependency[T any](name string) Dependency[T]
NewDependency returns a typed handle for a dependency stored under name. The name must be unique within a program; an empty name is accepted and refers to the same entry as the zero Dependency.
func (Dependency[T]) Name ¶ added in v1.2.0
func (d Dependency[T]) Name() string
Name returns the name the dependency is stored under.
func (Dependency[T]) String ¶ added in v1.2.0
func (d Dependency[T]) String() string
String implements fmt.Stringer, returning the dependency's name.
type DependencyError ¶ added in v1.2.0
type DependencyError struct {
Name string // the name the dependency was requested under
// contains filtered or unexported fields
}
DependencyError reports a dependency that was requested but not registered, or registered as a different type. It unwraps to ErrDependencyNotFound; recover the name with errors.As.
func (*DependencyError) Error ¶ added in v1.2.0
func (e *DependencyError) Error() string
Error returns a one-line message stating whether the dependency is missing or registered as a different type.
func (*DependencyError) Unwrap ¶ added in v1.2.0
func (e *DependencyError) Unwrap() []error
Unwrap returns ErrDependencyNotFound and ErrInternal, so CategoryOf classifies the error as CategoryInternal.
type Deprecation ¶
type Deprecation struct {
Kind string // "flag", "argument" or "command"
Name string // the input's logical name (the flag/argument/command name)
Identifier string // the token actually used on argv (e.g. "--conf", "build"); an argument's <name>
Message string // the spec's `deprecated:` message, when the input is deprecated as a whole
Since string // the spec's `deprecated_since:` release (X.Y.Z), or ""
RemovedIn string // the release that removes this token: its own planned removal, else the input's `removed_in:`, or ""
// Value is the deprecated enum value as typed, when the deprecation is about the value given
// rather than the input; Message is then the value's own message. Empty otherwise.
Value string
// ReplacedBy is what to use instead, as the user types it: a command ("taskr purge"), a flag
// identifier ("--output") or an enum value. Empty when the spec names none.
ReplacedBy string
}
Deprecation is a deprecated token found in this invocation's argv: the identifier used, the kind of input, its logical name, the spec's `deprecated:` message, and the releases the spec plans. It implements error so it can be recorded or printed directly.
func Deprecations ¶
func Deprecations(rtx *Context) []Deprecation
Deprecations returns each deprecated token this invocation used: a command invoked via a deprecated alias or deprecated as a whole, a flag set via a deprecated identifier, a deprecated argument given a value, or a deprecated enum value given to a flag or argument (with Deprecation.Value set). Rotini prints nothing; the handler decides what to do:
for _, d := range rotini.Deprecations(rtx) {
if d.ReplacedBy != "" {
rtx.RecordWarning(fmt.Errorf("%w; use %s instead", d, d.ReplacedBy))
continue
}
rtx.RecordWarning(d)
}
It needs no Parser: it reads the resolved chain and argv from rtx, and never reads a file or stdin. It returns nil for a nil rtx. When argv does not parse, only command deprecations are reported.
func (Deprecation) Error ¶
func (d Deprecation) Error() string
Error renders the deprecation notice as a single line, with the author's message when there is one: `flag "--conf" is deprecated: use --config`.
type DiscoverDef ¶
type DiscoverDef struct {
// Strategy is "walk-up" (the working directory up to the filesystem root), "xdg"
// ($XDG_CONFIG_HOME/<app> when absolute, else ~/.config/<app>), "native" (the platform's
// config directory: %AppData% on Windows, ~/Library/Application Support on macOS, else as
// xdg) or "xdg-system" (each absolute directory of $XDG_CONFIG_DIRS, else /etc/xdg).
Strategy string
File string // the file name looked for in each searched directory
App string // the application directory under the config root (xdg, native and xdg-system)
}
DiscoverDef locates a configuration file at run time. The strategy orders the directories searched for File; the first containing it wins, and a file found nowhere is absent.
type DiscoveredPlugin ¶
type DiscoveredPlugin struct {
// Name is the token a user types — "foo" for an executable "<prefix>foo".
Name string
// Path is the executable dispatch would run for Name right now: the first "<prefix>foo"
// in search order (next to the binary, then the plugin path, then PATH), so a copy
// shadowed by an earlier one is not the one listed.
Path string
}
DiscoveredPlugin is one plugin discovery found: the token it is invoked by and the binary that token runs.
type Effects ¶ added in v1.4.0
type Effects struct {
// Kind is "read" (changes nothing), "write" (creates or changes things) or "destructive"
// (deletes or overwrites what can't be got back).
Kind string
// Idempotent says whether running it again with the same inputs changes nothing more;
// nil when the spec doesn't say.
Idempotent *bool
// OpenWorld says whether it reaches the network or other systems outside the machine;
// nil when the spec doesn't say.
OpenWorld *bool
}
Effects is what a command does when it runs, or what one of its flags adds to that: the spec's `effects`. Rotini doesn't act on it; it is data for a reporter, a wrapper or a tool that runs the CLI for someone and decides what needs asking first.
type EnumValue ¶ added in v1.4.0
type EnumValue struct {
Value string
Summary string // one line saying what the value means; "" when none
// Aliases are other spellings accepted for the value; each binds as Value.
Aliases []string
// Hidden values are accepted but not offered: completion, help and error candidates leave
// them out.
Hidden bool
// Deprecated is the value's deprecation message: using the value on the command line, or one
// of DeprecatedAliases, reports a [Deprecation] carrying it. A deprecated value is not
// offered either.
Deprecated string
// DeprecatedAliases are the Aliases whose use reports a [Deprecation], for retiring one
// spelling while the value stays.
DeprecatedAliases []string
// DeprecatedSince, RemovedIn and ReplacedBy plan the value's removal, as for an input.
DeprecatedSince, RemovedIn, ReplacedBy string
}
EnumValue describes one declared value of an enum input beyond its spelling: the spec's object form of an enum item.
type ExitStatusDef ¶ added in v1.4.0
type ExitStatusDef struct {
Code int
Name string // the code's snake_case name (`not_found`); "" when none
Summary string // what the code means; "" when none
Retryable bool // running the same command again may succeed
DocsURL string // a link to more about the code (the spec's docs_url); "" when none
}
ExitStatusDef is one exit code a command documents: the spec's `exit_status` entry.
type Feature ¶ added in v1.4.0
type Feature struct {
// contains filtered or unexported fields
}
Feature is one optional part of the runtime. A program built with NewWiredProgram links and runs only the features it is given; one built with NewProgram or NewProgramFunc, and a Context from NewContextFor, has every feature. The generated NewProgram passes the features its spec uses.
The constructors group into:
- program — Plugins, ResponseFiles, Chdir, Multicall
- input channels — Stdin, Env, ConfigFiles, Profiles, EnvFiles, Fallbacks, PathExpansion, ObjectFlags, Schemas
- document formats — JSON, YAML, TOML, JSONC, Dotenv
- validation — Bounds, Patterns, FlagGroups, FlagDependencies, StreamPaths
- value types — URLValues, EmailValues, TimeZoneValues, MACValues, IPValues, RegexpValues
A run that needs a feature its program was not given fails with a *WiringError whose Feature names it.
func Bounds ¶ added in v1.4.0
func Bounds() Feature
Bounds wires numeric, length and item-count bounds and unique items.
func Chdir ¶ added in v1.4.0
func Chdir() Feature
Chdir wires a root flag with the chdir role: changing the working directory, in runs and in completion.
func ConfigFiles ¶ added in v1.4.0
func ConfigFiles() Feature
ConfigFiles wires configuration files: config inputs (the Config sub-structs), per-input file pins, discovery, custom InputSettings.Sources, ConfigFilePath and writing values back. Each file's format also needs its codec.
func EmailValues ¶ added in v1.4.0
func EmailValues() Feature
EmailValues wires mail.Address inputs (type email).
func Env ¶ added in v1.4.0
func Env() Feature
Env wires env inputs: the Env sub-structs of an inputs struct.
func EnvFiles ¶ added in v1.4.0
func EnvFiles() Feature
EnvFiles wires .env files (a configuration file read as environment variables) and variable_file inputs.
func Fallbacks ¶ added in v1.4.0
func Fallbacks() Feature
Fallbacks wires flag and argument fallbacks to the environment and configuration files.
func FlagDependencies ¶ added in v1.4.0
func FlagDependencies() Feature
FlagDependencies wires CommandDef.FlagDependencies validation.
func FlagGroups ¶ added in v1.4.0
func FlagGroups() Feature
FlagGroups wires CommandDef.FlagGroups validation.
func IPValues ¶ added in v1.4.0
func IPValues() Feature
IPValues wires netip.Addr, netip.Prefix and netip.AddrPort inputs (types ip, cidr and hostport).
func JSON ¶ added in v1.4.0
func JSON() Feature
JSON wires the JSON document format, for configuration files, stdin documents and records, and object flags.
func JSONC ¶ added in v1.4.0
func JSONC() Feature
JSONC wires the JSON-with-comments document format, for configuration files and stdin documents.
func MACValues ¶ added in v1.4.0
func MACValues() Feature
MACValues wires net.HardwareAddr inputs (type mac).
func Multicall ¶ added in v1.4.0
func Multicall() Feature
Multicall wires Definition.Multicall: dispatching on the name the program was invoked as.
func ObjectFlags ¶ added in v1.4.0
func ObjectFlags() Feature
ObjectFlags wires flags whose value is an object (FlagDef.ObjectSchema). It also needs JSON, YAML and Schemas.
func PathExpansion ¶ added in v1.4.0
func PathExpansion() Feature
PathExpansion wires declared path expansion ("~", "~user", $VAR, relative to a configuration file) and "~user" in configuration file and plugin paths.
func Plugins ¶ added in v1.4.0
func Plugins() Feature
Plugins wires declared plugins and plugin discovery: running a plugin, finding it on the PATH, discovering plugins and completing their names.
func Profiles ¶ added in v1.4.0
func Profiles() Feature
Profiles wires configuration file profiles (ConfigFile.Profiles): selecting one named section of a file per run.
func RegexpValues ¶ added in v1.4.0
func RegexpValues() Feature
RegexpValues wires *regexp.Regexp inputs (type regexp).
func ResponseFiles ¶ added in v1.4.0
func ResponseFiles() Feature
ResponseFiles wires Definition.ResponseFiles: expanding a response file named on the command line, in runs and in completion.
func Schemas ¶ added in v1.4.0
func Schemas() Feature
Schemas wires JSON Schema checks of stdin documents and records, configuration files and object flags.
func Stdin ¶ added in v1.4.0
Stdin wires reading the leaf command's Stdin field in the listed formats, among text, lines, bytes, json, jsonl, yaml, jsonc and toml; with none listed, every one. A document format also needs its codec (JSON, YAML, JSONC, TOML; jsonl needs JSON).
func StreamPaths ¶ added in v1.4.0
func StreamPaths() Feature
StreamPaths wires the inputfile and outputfile types.
func TOML ¶ added in v1.4.0
func TOML() Feature
TOML wires the TOML document format, for configuration files and stdin documents.
func TimeZoneValues ¶ added in v1.4.0
func TimeZoneValues() Feature
TimeZoneValues wires *time.Location inputs (type timezone).
func URLValues ¶ added in v1.4.0
func URLValues() Feature
URLValues wires *url.URL inputs (type url).
type FieldPath ¶
type FieldPath string
FieldPath identifies one leaf field of a generated inputs struct by its dot-joined Go field path, e.g. "RotiniGenerate.Flags.ConfFilePath".
type FlagDef ¶
type FlagDef struct {
Name string
Identifiers []string // CLI forms, e.g. {"--loud", "-l"}
Summary string // one-line description (completion candidates carry it as "identifier\tsummary")
Type string // resolved Go type, e.g. "bool", "string", "[]string", "int", "time.Duration"
Required bool
Default string
// Defaults is the multi-value default for a REPEATABLE input (a `[]…` or map type):
// each element is seeded as one occurrence, exactly as if the user had repeated the
// flag. It is used only when Default is empty, and only when the input is unset from
// every channel — a default never merges with a supplied value.
Defaults []string
Enum []string
// EnumValues lists every Enum value, in order, with what it declares beyond its spelling (a
// summary); nil when no value declares more than its spelling.
EnumValues []EnumValue
// IgnoreCase matches a value against Enum without regard to case (`--mode FAST` against
// fast/slow) and binds the declared spelling, so a handler compares against one form.
IgnoreCase bool
// Separator splits each value of a list or map flag into several (`--tags a,b` is two
// tags), CSV-style: a quoted item keeps the separator (`--tags '"a,b",c'`). Empty means
// one value per occurrence.
Separator string
// ImplicitValue is the value a flag takes when given without one (`--color` means
// "always"), making its value optional: a value must then be attached (`--color=never`),
// since the next argument is never consumed. Empty means the flag requires a value.
ImplicitValue string
// Layout is how a time input's value is written: a Go reference-time layout
// ("2006-01-02", "Jan 2 2006 15:04"), or "unix" / "unixmilli" for a timestamp. Empty means
// RFC 3339. `type: date` gets "2006-01-02".
Layout string
// Layouts lists every layout when there are several, tried in order: the first that parses
// wins, and an error names them all. Layout is then the first. nil means Layout alone.
Layouts []string
// Relative also accepts times measured from the run's clock ([Program.WithClock]): "past"
// (2h or -2h ago, yesterday), "future" (2h or +2h from now, tomorrow) or "both" (a sign is
// required), plus now and today. Absolute values are tried first. Empty means absolute only.
Relative string
// ObjectSchema is the JSON Schema of an object-valued flag's value — set when the spec's
// schema is a named object (`$ref: '#/schemas/DB'`), or a list of them. The flag then
// takes JSON, key=value pairs, a YAML @file, or one field per flag (--db.host=…); see
// "Input values" in the package documentation.
ObjectSchema string
Secret bool // when true, the value is redacted in usage/validation error output
Hidden bool // omitted from completion candidates (it still parses); help omission happens at codegen
DeprecatedIdentifiers []string // identifiers (subset of Identifiers) that [Deprecations] reports when used
// Deprecated is the flag's deprecation message: setting it by any identifier reports a
// [Deprecation] carrying it. Empty means the flag is not deprecated as a whole.
Deprecated string
// DeprecatedSince, RemovedIn and DeprecatedIdentifiersRemovedIn are the flag's planned
// lifecycle; see [CommandDef.DeprecatedSince].
DeprecatedSince string
RemovedIn string
DeprecatedIdentifiersRemovedIn map[string]string
// ReplacedBy is the identifier of the flag to use instead of this deprecated one
// ("--output"); [Deprecations] reports it. "" when the spec names none.
ReplacedBy string
// HiddenIdentifiers are more identifiers the command line accepts for the flag but that are
// never listed: completion and suggestions leave them out. A negatable flag's hidden long
// identifiers get their unlisted "--no-" forms too.
HiddenIdentifiers []string
// Negatable adds a "--no-<x>" form for every long identifier of a bool flag, which sets
// it false, overriding a true default, config value or environment variable.
Negatable bool
// Negation, with Negatable, is the one negated identifier ("--plain" for --color) in place of
// the derived "--no-<x>" forms.
Negation string
// ShortCircuit marks a flag that replaces the command's normal run (--help, --version):
// when it is set on the command line, every declared requirement of the chain is waived,
// so [Context.Inputs] succeeds and the handler decides what to do. Errors reading the
// command line are still reported.
ShortCircuit bool
// NoRepeat makes a single-value flag given more than once on the command line an error
// (spec `repeatable: false`) instead of the last value winning.
NoRepeat bool
DottedKeys bool // map flag whose key=value keys are '.'-separated paths into nested maps (spec dotted_keys)
KeyPaths []string // a map flag's declared key paths (from its schema's properties), completed up to the '='
From []string // extra acquisition modes (spec from:): "file" resolves @path values, "stdin" resolves a bare "-"
// Complete is the declarative shell-completion hint for this flag's value (spec
// complete:). The zero value means no hint.
Complete Completion
Constraints
// Role is what the flag means to a program driving the CLI (spec role:), "" for none.
// One role changes what the runtime does: "chdir" names the root flag whose directory the
// run resolves relative paths and config discovery against (see [Context.Dir]).
Role string
// Effects is what giving the flag adds to its command's effects (spec effects:); nil when
// undeclared. A run's effect is the highest of its command's and every given flag's.
Effects *Effects
}
FlagDef describes a single flag of a command. Name is the logical name and matches the `rotini:"<name>"` tag on the corresponding generated input field.
type FlagDependency ¶
type FlagDependency struct {
When string // the flag whose presence triggers the rule; "" when only Unless decides
Requires []string // flags that must also be set when the rule triggers
// Equals narrows When to these values, compared as the flag's value is stored: an enum's
// main value, "true" or "false" for a bool. nil means any value.
Equals []string
Unless []string // flags any one of which, set on argv, turns the rule off
Forbids []string // flags that can't be set when the rule triggers
}
FlagDependency is a conditional cross-flag rule: when the When flag is set on argv (with one of the Equals values, when there are any) and none of the Unless flags is, every flag in Requires must be set too and none in Forbids may be. With no When, the rule applies unless an Unless flag is set. "Set" follows the same convention as FlagGroup.
type FlagGroup ¶
type FlagGroup struct {
Kind FlagGroupKind
Flags []string // logical flag names that make up the group
}
FlagGroup constrains which of a command's flags may, or must, appear together. Flags are referenced by their logical Name, and "set" means explicitly provided on argv — a default or fallback does not count. The Parser enforces it; a violation is a usage error.
type FlagGroupKind ¶
type FlagGroupKind string
FlagGroupKind names a cross-flag presence rule. The value is the spec's `kind`.
const ( // FlagGroupMutuallyExclusive: at most one of the group's flags may be set. FlagGroupMutuallyExclusive FlagGroupKind = "mutually_exclusive" // FlagGroupRequiredTogether: set all of the group's flags, or none. FlagGroupRequiredTogether FlagGroupKind = "required_together" // FlagGroupOneOf: exactly one of the group's flags must be set. FlagGroupOneOf FlagGroupKind = "one_of" // FlagGroupAtLeastOne: at least one of the group's flags must be set. FlagGroupAtLeastOne FlagGroupKind = "at_least_one" )
type FlagValueCompleter ¶
type FlagValueCompleter interface {
CompleteFlagValue(rtx *Context, flag, partial string) []string
}
FlagValueCompleter is an optional interface a command's handler may implement to supply dynamic completion candidates for one of its flags' values. Completion resolves the handler of the command that declares the flag and, if it implements this interface, calls CompleteFlagValue with the flag's logical name and the word being typed. A nil return falls back to the flag's static enum; a non-nil return, empty included, is authoritative.
rtx carries the resolved chain, the completion words in Context.Argv (the last is the word being typed), the program's base context in Context.Context, and the dependencies registered with Program.WithDependency. The completer runs as the command that declares the flag, so Context.Command and the inputs methods anchor there, an ancestor's included.
The line is half-typed, so Context.Inputs would fail validation. Context.PartialInputs reads what has been typed so far, leniently, over the environment and the defaults:
in, _, _ := rtx.PartialInputs[DeployInputs]() return servicesIn(rtx.Context(), in.Deploy.Flags.Region)
A panic in a completer is not recovered. It may run on every keystroke, so it must be read-only and fast; a completer that calls the network derives a deadline from Context.Context.
Candidates are offered in the order returned. A candidate may carry a one-line description after a tab, "value\tdescription", which every shell shows beside the value (bash on the second TAB) unless the conf's `descriptions_env` switches descriptions off. Context.SetCompletionOptions asks the shell not to add a space after the inserted value.
type Glob ¶ added in v1.4.0
type Glob string
Glob is a validated path.Match pattern: `*` and `?` match within one path segment, `[a-z]` a class, and `\` escapes the next character. It matches slash-separated names, and `**` is not recursive: it means two `*`. A spec declares one with `type: glob`.
func (Glob) MarshalText ¶ added in v1.4.0
MarshalText implements encoding.TextMarshaler.
func (Glob) Match ¶ added in v1.4.0
Match reports whether name matches the pattern. name's separators are read as slashes, so a Windows path matches the same pattern a Unix one does. A malformed pattern, which Glob.UnmarshalText rejects, matches nothing.
func (*Glob) UnmarshalText ¶ added in v1.4.0
UnmarshalText implements encoding.TextUnmarshaler, rejecting a malformed pattern.
type Handler ¶ added in v1.2.0
type Handler interface {
CascadingPreRun(ctx context.Context, rtx *Context)
PreRun(ctx context.Context, rtx *Context)
Run(ctx context.Context, rtx *Context)
PostRun(ctx context.Context, rtx *Context)
CascadingPostRun(ctx context.Context, rtx *Context)
}
Handler is the lifecycle interface every command's handler implements. The runtime invokes the hooks in order, sharing one Context across the chain. Embed the No* types below to declare only the hooks a command uses.
Instance lifetime ¶
Per run, the runtime asks the program's handlers (the generated ProgramHandlers, through NewProgramFunc or NewProgram) once for each command in the chain, and that value serves the command's hooks for the run:
- A field carries state between one command's own hooks. The leaf's PreRun, Run and PostRun share one value, as do a command's CascadingPreRun and CascadingPostRun.
- A field cannot cross commands. State that travels down the chain is a dependency.
Where the state flows decides the tool:
state flows… use ──────────── ─── between one command's own hooks a field on the handler between different commands in the chain rtx.SetDependency(dep, v) across every run of the program p.WithDependency(dep, v)
Hand-written handler sets ¶
Generated wiring methods return a new handler per call, so fields are per-run state. A hand-written wiring method that returns a shared value makes that handler's fields shared across runs, and a data race under concurrent Program.Run. Handlers that keep state in fields must be returned fresh on every call.
type HandlerLookup ¶ added in v1.4.0
HandlerLookup returns the Handler for one command, given the handler name its CommandDef.Handler (or Definition.Handler) holds. It reports false for a name it does not know. It is called once per command in the chain on every run, so it should return a new handler per call: a handler's fields are per-run state.
type HelpFunc ¶
HelpFunc returns the help page of the command named by path (canonical names below the root; none for the root), or an error when there is no such command. Codegen generates a Help function of this type.
type HexBytes ¶
type HexBytes []byte
HexBytes is binary data written as hexadecimal, with or without a `0x` prefix: `deadbeef`, `0xDEADBEEF`. A spec declares one with `type: hexbytes`.
func (HexBytes) MarshalText ¶
MarshalText implements encoding.TextMarshaler as lowercase hex without a prefix.
func (*HexBytes) UnmarshalText ¶
UnmarshalText implements encoding.TextUnmarshaler.
type InputError ¶ added in v1.2.0
type InputError struct {
Channel string // one of "env", "config", "stdin", "flag", "argument"
Input string // the offending input key/path, when a single one is known (else "")
Msg string // the message Error returns; it never includes Cause's text
Cause error // the underlying recon/decode/OS error, reachable via errors.As (may be nil)
// Token and Candidates are set when a value is not one of an env or config input's enum:
// the rejected value ("[redacted]" for a secret input) and the allowed values. They are
// empty for any other failure. [SuggestionFacts] reads them.
Token string
Candidates []string
// contains filtered or unexported fields
}
InputError reports a failure acquiring or decoding one of a command's non-argv input channels: environment variables, configuration files, a typed stdin payload, or a flag's env/config fallback. It is the counterpart of the argv channel's *ParseError.
Error names the channel, the input, and what went wrong. The underlying recon, decode or OS Cause is reachable via errors.As but kept out of the message, and the value of an input marked secret is redacted.
A bad value, a missing required input, or a malformed document the user supplied is CategoryUsage; a registry build, schema compile, IO read, or codegen mismatch is CategoryInternal.
var be *rotini.InputError
if errors.As(err, &be) {
fmt.Fprintf(os.Stderr, "bad %s input %q: %s\n", be.Channel, be.Input, be.Error())
}
func (*InputError) Error ¶ added in v1.2.0
func (e *InputError) Error() string
Error returns Msg: the channel, the input, and what went wrong. A secret input's value is redacted.
func (*InputError) Unwrap ¶ added in v1.2.0
func (e *InputError) Unwrap() []error
Unwrap exposes the Cause and the category sentinel, so errors.Is/As reach both the original recon error and ErrUsage/ErrInternal.
type InputLayer ¶ added in v1.2.0
type InputLayer[T any] struct { Name string Values T Set Presence // contains filtered or unexported fields }
InputLayer is one input channel's view of the inputs type T: the values it supplied (all other fields are zero) and which fields those are. Layers from rotini's channel parsers also carry unexported data that InputReport.Validate uses. A hand-built InputLayer (one the program builds from its own source, such as a prompt or a secrets service) participates in overlay, provenance and validation: the fields its Set names count as supplied, and their values are checked the way Context.CheckInputs checks them. A nil or empty Set means the layer supplied nothing: overlaying it leaves every field as it was.
In a merged InputReport, a hand-built layer's field whose InputSource leaves Layer empty (or "custom", as PresenceOf writes it) is attributed to the layer's Name, and an empty Raw is filled with the supplied value's text. A field the spec marks secret shows Raw as "[redacted]" whatever the layer gave. Both need a rotini layer in the merge, which says what the spec declares.
type InputReader ¶ added in v1.2.0
type InputReader struct {
// contains filtered or unexported fields
}
InputReader is the default multi-source input reader. It fills a command's typed inputs from argv (via a default Parser) and from the non-argv channels — environment variables, configuration files, and the leaf command's typed stdin payload — reconciled and decoded by recon. It is the engine behind Context.Inputs; Context.ArgvInputs and its siblings expose the channels individually.
reader := rotini.NewInputReader(settings)
var in WidgetCreateInputs
if err := reader.Read(rtx, &in); err != nil { /* handler owns it */ }
Env values fill the generated <Prefix>Env struct and config-file values <Prefix>Config. The two channels use independent registries, so an env var never leaks into a config field or the reverse. Flags may additionally fall back to env and config.
func NewInputReader ¶ added in v1.2.0
func NewInputReader(meta InputSettings) *InputReader
NewInputReader returns the default input reader, configured from the generated InputSettings. It carries no features of its own: a read uses the features of the Context it reads, so a reader set with Program.WithInputReader reads with the program's.
func (*InputReader) Read ¶ added in v1.2.0
func (b *InputReader) Read(rtx *Context, out any) error
Read fills out, a non-nil pointer to the generated inputs struct, from every channel. Flag and argument validation runs once over the reconciled values, so a required flag is satisfiable from env or config and an env- or config-supplied value is still enum-checked.
It returns the first error: a *ParseError from the argv channel or an *InputError from the others, with the recon cause reachable via errors.As, or a *WiringError when a command declares config inputs but the program has no InputSettings, or when reading needs a Feature the program built with NewWiredProgram does not wire (its Feature names it).
Example ¶
InputReader.Read reconciles every declared channel in one call; here a flag unset in argv is read from its environment fallback.
def := Definition{
Name: "app", Handler: "App",
Flags: []FlagDef{
// The recon key gives the flag fallbacks: $SERVER_PORT, then any
// declared config files, then the default.
{Name: "port", Identifiers: []string{"--port"}, Type: "int", Default: "8080"},
},
}
var inputs struct {
App struct {
Flags struct {
Port int `rotini:"port" recon:"server.port"`
}
Arguments struct{}
}
}
os.Setenv("SERVER_PORT", "9090")
defer os.Unsetenv("SERVER_PORT")
rtx := NewContextFor(def, nil) // --port absent from argv
if err := NewInputReader(InputSettings{}).Read(rtx, &inputs); err != nil {
fmt.Println("bind:", err)
return
}
fmt.Println("port:", inputs.App.Flags.Port)
Output: port: 9090
type InputReport ¶ added in v1.2.0
type InputReport struct {
// contains filtered or unexported fields
}
InputReport is the merged provenance of one overlay: which layer won each field, every layer that set it (low → high), and validation over the merged values. The zero InputReport reports no fields, and its Validate returns nil.
func MergeInputsWithReport ¶ added in v1.2.0
func MergeInputsWithReport[T any](layers ...InputLayer[T]) (T, InputReport)
MergeInputsWithReport is MergeInputs plus the merged InputReport: which layer won each field, the full per-field history, and Validate over the merged result. A field a hand-built InputLayer won is validated by the value it supplied, as Context.CheckInputs checks it.
Example ¶
ExampleMergeInputsWithReport shows the merge and provenance contract with two hand-built layers: order is precedence, and the InputReport names the winner.
type inputs struct {
App struct {
Flags struct {
Color string `rotini:"color"`
}
}
}
var defaults, env inputs
defaults.App.Flags.Color = "blue"
env.App.Flags.Color = "teal"
merged, report := MergeInputsWithReport(
InputLayer[inputs]{Name: "defaults", Values: defaults, Set: Presence{"App.Flags.Color": {Layer: "defaults", Raw: "blue"}}},
InputLayer[inputs]{Name: "env", Values: env, Set: Presence{"App.Flags.Color": {Layer: "env", Raw: "teal"}}},
)
win, _ := report.Winner("App.Flags.Color")
fmt.Println(merged.App.Flags.Color, "from", win.Layer)
Output: teal from env
func (InputReport) Fields ¶ added in v1.2.0
func (r InputReport) Fields() []FieldPath
Fields returns every field any layer set, sorted.
func (InputReport) Format ¶ added in v1.4.0
func (r InputReport) Format(w io.Writer) error
Format writes one line per field a layer set, sorted by field path: the field, its value (redacted when the input is secret), where it came from (InputSource.Origin, else the layer's name), and the values it overrode, nearest first. It writes nothing for a report with no fields, and returns the first write error.
TaskrDeploy.Flags.Port = "9000" from env:TASKR_PORT; overrides config:user#deploy.port "8000", default "8080" TaskrDeploy.Flags.Token = [redacted] from argv:--token TaskrDeploy.Arguments.Target = "prod" from argv:<target> TaskrDeploy.Stdin = (document) from stdin
A field the spec marks secret prints [redacted] whatever a layer supplied, a hand-built layer's text included. A value with no text prints (document) for a decoded stdin document, (stream) for streamed stdin, which Format never reads, and (set) otherwise.
func (InputReport) History ¶ added in v1.2.0
func (r InputReport) History(path FieldPath) []InputSource
History returns every layer that set path, low → high precedence; the last element is the winner.
func (InputReport) Validate ¶ added in v1.2.0
func (r InputReport) Validate() error
Validate runs the same declarative checks Parser.Parse applies — required, enum, constraints, flag groups and dependencies — with "explicitly set" meaning set by the argv layer. Run it after the overlay so a required flag satisfied by any layer passes.
It checks what the layers supplied, not the merged struct field by field:
- Presence rules fire on absence: a required input no layer supplied is an error.
- Value rules fire only on a supplied value: an unsupplied field's zero value is not checked against its enum or bounds. Item counts are the exception, as on the command line: a list flag or variadic argument nobody supplied has zero items, so its minItems applies unless it has a default. Environment and config lists are counted only when supplied.
A merge that omits Context.DefaultInputs can therefore yield an enum-constrained flag as "" without error.
A hand-built layer's fields count as supplied (a required input it supplies passes, and a flag it supplies counts as set for flag groups and dependencies), and the values it won are checked as Context.CheckInputs checks them, naming each input by its canonical spelling.
A merge of hand-built layers alone has no command to check against, so Validate reports a ParseKindInternal error pointing at Context.CheckInputs, which takes the command from the running context.
A streamed stdin field is carried, not checked: its items are checked as they are read.
func (InputReport) Winner ¶ added in v1.2.0
func (r InputReport) Winner(path FieldPath) (InputSource, bool)
Winner returns the provenance of the layer that supplied path's final value.
type InputSettings ¶ added in v1.2.0
type InputSettings struct {
ConfigFiles []ConfigFile // per-command config_files sources, each tagged with its Scope; the input reader scopes them to the invoked chain (cascade, nearest-wins)
// EnvPrefix scopes every derived env-var name under "<EnvPrefix>_". Explicit variable
// names are exempt, and with a prefix set the unprefixed names no longer bind.
EnvPrefix string
// Sources are custom recon sources — a secrets manager, a remote config service —
// joined into the config precedence after the declared config_files, so explicit
// files beat ambient services. Codegen never emits one; the program appends its own.
// A per-input `file:` pin stays a config_files anchor and cannot name a custom
// source, and a source name colliding with a declared file is rejected.
Sources []recon.Source
// StdinSchemas maps a command's stdin payload type name ("<Prefix>Stdin") to a
// self-contained JSON Schema the input reader validates the decoded payload against.
StdinSchemas map[string]string
}
InputSettings is the generated descriptor the InputReader consumes to fill the non-argv input channels. It carries the document-level concerns the dispatch-time Definition omits.
type InputSource ¶ added in v1.2.0
type InputSource struct {
Layer string // the supplying layer's name: "defaults", "files", "env", "argv", "stdin", or custom
Raw string // the supplied text, a list's values joined with ", " whichever layer supplied it ("" when non-textual, e.g. a decoded stdin document); "[redacted]" for secrets
// Origin names where in the layer the value came from: "argv:--port" (the identifier as
// typed), "argv:<target>" (an argument), "env:TASKR_PORT", "env:TASKR_TOKEN_FILE" (read
// from the file a variable_file variable names), "env:TASKR_HTTP__*" (a nested env family),
// "config:user#deploy.port" (a configuration file's name and key), "default"
// or "stdin". It is "" for a hand-built layer, which names itself by Layer.
Origin string
}
InputSource records which layer supplied a field's value and the raw text it supplied. Raw is pre-redacted for inputs the spec marks secret.
type Lifecycle ¶
type Lifecycle func(chain []Command, handlers []Handler) []LifecycleStep
Lifecycle is the run phase's planner: given the resolved chain and each command's Handler, index-aligned, it returns the ordered steps the engine executes. Handlers are resolved before it is called. See DefaultLifecycle.
A plan built from scratch should wrap each hook in AsCommand so Context.Command and Context.Inputs anchor on the hook's own command; an unwrapped hook reports the invoked command, which is wrong for a cascading hook.
type LifecycleStep ¶
type LifecycleStep struct {
Name string // diagnostic label, e.g. "cascading:app", "prerun:build", "run:build"
Do func(ctx context.Context, rtx *Context) // the forward hook
Undo func(ctx context.Context, rtx *Context) // the paired teardown; nil for none
}
LifecycleStep pairs one forward hook with its teardown; a plan is a slice of them.
Either half may be nil. A nil Undo is a step with no teardown, like the default plan's Run step; a nil Do is a teardown-only step. A step counts as begun when the engine reaches it, so a teardown-only step still unwinds.
func DefaultLifecycle ¶
func DefaultLifecycle(chain []Command, handlers []Handler) []LifecycleStep
DefaultLifecycle is rotini's run-phase plan, exported for a custom Lifecycle to wrap: one CascadingPreRun/CascadingPostRun pair per command, root → leaf, then the leaf's PreRun/PostRun pair, then the leaf's Run with no teardown. Every hook is wrapped in AsCommand.
type MulticallDef ¶ added in v1.4.0
type MulticallDef struct {
// Prefix is stripped from the invoked name before it is matched against the root's
// commands: with "acme-", acme-ls runs `ls`. A name without the prefix runs the root.
Prefix string
// Complete answers shell completion for the root when the invoked name starts with it
// (kubectl's "kubectl_complete-"): the arguments are the words to complete, answered in the
// format [Program.WithCompletion] sets, else [PluginCompletion].
Complete string
}
MulticallDef is how a program dispatches on the name its binary was invoked as (the spec's root `multicall`). The zero value is busybox style: a binary linked as `ls` runs the `ls` command, as if the user had typed `<root> ls`. The name is the base name of argv[0], never a resolved symlink, and on Windows a trailing .exe is dropped and the match ignores case. A name that matches no command, the root's own name included, runs the root as usual.
type NoCascadingPostRun ¶ added in v1.2.0
type NoCascadingPostRun struct{}
NoCascadingPostRun is an embeddable no-op Handler.CascadingPostRun. NoHooks embeds all four no-ops at once.
func (NoCascadingPostRun) CascadingPostRun ¶ added in v1.2.0
func (NoCascadingPostRun) CascadingPostRun(ctx context.Context, rtx *Context)
CascadingPostRun does nothing.
type NoCascadingPreRun ¶ added in v1.2.0
type NoCascadingPreRun struct{}
NoCascadingPreRun is an embeddable no-op Handler.CascadingPreRun. NoHooks embeds all four no-ops at once.
func (NoCascadingPreRun) CascadingPreRun ¶ added in v1.2.0
func (NoCascadingPreRun) CascadingPreRun(ctx context.Context, rtx *Context)
CascadingPreRun does nothing.
type NoHooks ¶ added in v1.2.0
type NoHooks struct {
NoCascadingPreRun
NoPreRun
NoPostRun
NoCascadingPostRun
}
NoHooks embeds the four no-op hooks, for a hand-written handler:
type handlers struct{ rotini.NoHooks }
func (*handlers) Run(ctx context.Context, rtx *rotini.Context) { … }
A method declared on the outer type takes precedence over the promoted no-op.
NoHooks does not supply Run, so a handler with a missing or misspelled Run fails the `var _ rotini.Handler` assertion at compile time. Generated stubs embed the No* types they need individually.
type NoPostRun ¶ added in v1.2.0
type NoPostRun struct{}
NoPostRun is an embeddable no-op Handler.PostRun. NoHooks embeds all four no-ops at once.
type NoPreRun ¶ added in v1.2.0
type NoPreRun struct{}
NoPreRun is an embeddable no-op Handler.PreRun. NoHooks embeds all four no-ops at once.
type Option ¶
type Option func(*Program)
Option is one configuration step as a value, applied by Program.With. WithDependency returns one; a program can define its own:
func devDefaults() rotini.Option {
return func(p *rotini.Program) { p.WithStdout(os.Stderr).WithoutSignalHandling() }
}
func WithDependency ¶ added in v1.2.0
func WithDependency[T any](dep Dependency[T], value T) Option
WithDependency returns an Option that registers value under dep for the whole program; it is the composable form of Program.WithDependency, for use with Program.With:
cmd.NewProgram(). With( rotini.WithDependency(tasks.Store, store), rotini.WithDependency(tasks.Client, client), ). WithVersion(version). Execute()
type Outcome ¶
type Outcome struct {
// Infos are [Context.RecordInfo] messages: neutral output, no bearing on the exit code.
Infos []string
// Successes are [Context.RecordSuccess] messages.
Successes []string
// Warnings are [Context.RecordWarning] values: non-fatal, never raising the exit code.
Warnings []error
// Errors are [Context.RecordError] values.
Errors []error
// Panics are recovered panics and rotini-detected faults, captured by the runtime; there
// is no record call for them.
Panics []*PanicError
}
Outcome is everything a run recorded, handed to the Reporter. Each slice is in recording order and is a copy. Context exposes no other way to read the records.
type OutputDef ¶ added in v1.2.0
type OutputDef struct {
// Type is the generated <Prefix>Output type: the whole output, or one item when the command
// writes a stream of them.
Type reflect.Type
// Schema is the shape as a self-contained JSON Schema, which [Program.WithOutputChecks],
// [Context.CheckOutput] and [DecodeOutput] check values against. "" when there is none.
Schema string
// Stream is the spec's `output_stream`: the command writes a stream of Type items with
// [Context.WriteOutputItem], rather than one value with [Context.WriteOutput].
Stream bool
}
OutputDef is what a command declares it writes to stdout when it succeeds: the spec's `output:`, recorded by `rotini generate`. A nil OutputDef means the command declares none.
type OutputFile ¶ added in v1.4.0
type OutputFile struct {
// contains filtered or unexported fields
}
OutputFile is a file being written by CreateOutput. It is an io.WriteCloser: Close commits what was written and Abort discards it. Both are idempotent, and Close after Abort does nothing. The idiom commits only on success:
out, err := rotini.CreateOutput(rtx, in.Flags.Output, rotini.Overwrite(in.Flags.Force))
if err != nil {
return err
}
defer out.Abort()
if err := rtx.WriteOutputTo(out, report, "json", nil); err != nil {
return err
}
return out.Close()
func CreateOutput ¶ added in v1.4.0
func CreateOutput(rtx *Context, path string, opts ...OutputOption) (*OutputFile, error)
CreateOutput opens an output-file value for writing (a spec's `type: outputfile`):
- "-" is the run's stdout, Context.Stdout; Close and Abort do nothing.
- A path is written atomically: to a temporary file beside it, renamed into place by Close. Abort, a failed write or a failed Close removes the temporary file and leaves an existing file untouched, and so does a run that ends with the file still open, by a return, a halt or a panic. A relative path is resolved against the run's directory (Context.Dir).
- An existing file that is not a regular file, such as /dev/null, a named pipe or NUL on Windows, is written directly, not atomically.
An existing regular file is replaced only with Overwrite(true); otherwise CreateOutput returns a usage error that matches fs.ErrExist. The check is repeated before the rename, but a file created in between is still replaced. A symbolic link is followed, so the file it points to is replaced and the link is kept. Replacing a file gives it a new identity: hard links to the old one, its owner and its extended attributes are not carried over. On Windows, the rename is retried briefly while another program holds the file open, and a read-only file cannot be replaced.
The file is not synced to disk: Close makes it visible whole, as most tools that write files do, not durable across a power loss.
func (*OutputFile) Abort ¶ added in v1.4.0
func (f *OutputFile) Abort() error
Abort discards the file: the temporary file is closed and removed, and an existing file is left as it was. A file written directly (a device or a named pipe) is only closed. Abort on stdout, or after Close or Abort, does nothing.
func (*OutputFile) Close ¶ added in v1.4.0
func (f *OutputFile) Close() error
Close commits the file: the temporary file is closed and renamed into place. If a write failed, or closing or renaming fails (a full disk often shows only here), the temporary file is removed, an existing file is left as it was, and the error is returned. Close on stdout, or after Close or Abort, does nothing.
func (*OutputFile) Name ¶ added in v1.4.0
func (f *OutputFile) Name() string
Name is the path as given to CreateOutput, "-" for stdout.
type OutputOption ¶ added in v1.4.0
type OutputOption func(*outputConfig)
OutputOption configures CreateOutput.
func FileMode ¶ added in v1.4.0
func FileMode(perm fs.FileMode) OutputOption
FileMode sets the permissions of a file CreateOutput creates or replaces, before the umask. The default is 0644 for a new file, and the replaced file's own permissions when overwriting; FileMode(0o600) keeps secret output private.
func Overwrite ¶ added in v1.4.0
func Overwrite(enabled bool) OutputOption
Overwrite lets CreateOutput replace an existing file. Without it, or with Overwrite(false), an existing file is a usage error. A command usually passes the value of a --force flag it declares:
rotini.CreateOutput(rtx, in.Flags.Output, rotini.Overwrite(in.Flags.Force))
type Page ¶ added in v1.2.0
type Page struct {
// Name is the command path joined with "-" and lowercased ("taskr-add"): the name `man`
// looks the page up by, and the man page's file name without its extension.
Name string
// Path is the command path below the root, ["add"]; nil for the root command.
Path []string
// Content is the page itself: roff for a man page, markdown for a markdown page.
Content string
// Topic is true for a help topic's page (the spec's `topics:`), listed after the commands'
// pages; its Path is the topic's name.
Topic bool
}
Page is one generated documentation page for one command: a man page or a markdown reference page. With the man feature on, codegen emits a ManPages function returning every command's man page; with the markdown feature on, MarkdownPages. Both list the visible commands in tree order, root first.
for _, p := range cmd.ManPages() {
path := filepath.Join(dir, p.Name+"."+cmd.ManSection)
if err := os.WriteFile(path, []byte(p.Content), 0o644); err != nil {
return err
}
}
type PanicError ¶
PanicError carries a panic recovered from a lifecycle hook to the reporter: Value is the value passed to panic, Stack the goroutine stack captured at recovery. Error renders Value alone.
It also carries faults rotini detects without a panic, such as a *WiringError or a resolver failure, with the error as Value and a nil Stack.
func (*PanicError) Error ¶
func (e *PanicError) Error() string
Error renders the panic value without the stack.
func (*PanicError) Unwrap ¶
func (e *PanicError) Unwrap() []error
Unwrap returns the panic value when it is an error, followed by ErrInternal, so a panic is CategoryInternal unless its error value classifies otherwise (CategoryOf tests ErrUsage first).
type ParseError ¶
type ParseError struct {
Kind ParseKind // what went wrong, for branching without matching Msg (zero = ParseKindUnspecified)
Msg string // the human-readable failure
Command string // the command in whose scope parsing failed ("" when not command-scoped)
Flag string // the flag involved, by the identifier or label used ("" when not flag-related)
Token string // the offending argv token or value ("" when none)
Candidates []string // the vocabulary Token failed against — sibling commands, declared flags, enum members (nil when none applies)
}
ParseError is a parse-time failure caused by bad input. Its message carries no suggestions or usage text; the structured fields let a handler compose its own response: switch on Kind, pass Token and Candidates to a Suggestor, or render help for Command. It unwraps to ErrUsage (ErrInternal for ParseKindInternal), so CategoryOf can classify it. The default reporter exits 1; a program wanting exit code 2 for usage errors maps it in its own reporter (see Category).
var pe *rotini.ParseError
if errors.As(err, &pe) {
switch pe.Kind {
case rotini.ParseKindUnknownFlag, rotini.ParseKindUnknownCommand:
// pe.Token + pe.Candidates feed a Suggestor's "did you mean"
case rotini.ParseKindMissingRequired:
// prompt, or point at the help for pe.Command
}
}
func (*ParseError) Error ¶
func (e *ParseError) Error() string
Error renders the parse failure as a single, user-facing line.
func (*ParseError) Unwrap ¶
func (e *ParseError) Unwrap() error
Unwrap returns the category sentinel: ErrInternal for ParseKindInternal and ErrUsage for every other kind.
type ParseKind ¶
type ParseKind int
ParseKind classifies a *ParseError so a reporter can branch on the failure without matching the message. Every kind is the end-user's to fix except ParseKindInternal, a misuse of the parser API by the author.
const ( // ParseKindUnspecified is the zero value: a [*ParseError] whose construction // site did not classify it (a hand-built error that sets no Kind). ParseKindUnspecified ParseKind = iota ParseKindUnknownFlag // an argv token looked like a flag no command on the chain declares ParseKindUnknownCommand // a stray positional on a branch-only command (a mistyped sub-command) ParseKindNeedsValue // a value-taking flag was given no value ParseKindInvalidValue // a value could not be coerced/resolved (bad type, unreadable @file, malformed map, value on a no-value flag) ParseKindEnumViolation // a value was not one of a declared enum's members ParseKindConstraintViolation // a declared bound or flag-group/dependency rule was violated ParseKindMissingRequired // a required flag or argument was absent ParseKindNoArguments // a positional was given to a command that accepts none ParseKindTooManyArguments // more positionals than the command's declared (non-variadic) arity ParseKindInternal // a parser API misuse: nil parser/context, a bad out argument, or an inputs type that does not describe the running command ParseKindMisplacedFlag // a flag was typed where it can't apply, such as before a plugin's name )
The parse failure kinds.
type Parser ¶
type Parser struct{}
Parser is rotini's argv parser: it parses and validates the command line against what the resolved chain declares (GNU/POSIX grammar, typed coercion, enum and constraint checks), failing with a *ParseError.
Parsing is opt-in: a CLI that wants raw argv reads Context.Argv instead. A handler reads the parser with Context.Parser:
parser := rtx.Parser() var in MycliInputs err := parser.Parse(rtx, &in)
func (*Parser) Parse ¶
Parse binds the running command's arguments into out — a non-nil pointer to the generated inputs struct — from the resolved chain and raw argv on rtx, in the json.Unmarshal style:
var in MycliInputs
if err := parser.Parse(rtx, &in); err != nil { /* handler owns it */ }
It applies declared defaults, fills out by reflection from the `rotini:"…"` struct tags, then validates. Built-ins and any encoding.TextUnmarshaler are coerced, and a trailing []string absorbs the remaining positionals. Parse does not consult env or config fallbacks; Context.Inputs does.
It returns a *ParseError when out is not a non-nil pointer, a flag is unknown or missing its value, a value cannot be coerced, a required input is absent, a value falls outside a declared enum, or any declared constraint, flag group or flag dependency is violated.
Unlike Context.Inputs and the per-channel layer methods, Parse does not check that out describes the running command: it binds what fits and leaves the rest zeroed, so one struct can span a whole tree. A handler reading its own inputs should use Context.Inputs.
Example ¶
Parser.Parse fills the generated input struct from argv alone, with typed coercion, defaults, and enum and constraint checks, failing with a *ParseError.
def := Definition{
Name: "app", Handler: "App",
Commands: []CommandDef{{
Name: "deploy", Handler: "AppDeploy",
Arguments: []ArgDef{{Name: "service", Type: "string", Required: true}},
Flags: []FlagDef{
{Name: "env", Identifiers: []string{"--env"}, Type: "string", Default: "dev", Enum: []string{"dev", "prod"}},
{Name: "loud", Identifiers: []string{"--loud", "-l"}, Type: "count"},
},
}},
}
// The shape codegen emits: one field per command on the resolved path.
var inputs struct {
App struct {
Flags struct{}
Arguments struct{}
}
Deploy struct {
Flags struct {
Env string `rotini:"env"`
Loud int `rotini:"loud"`
}
Arguments struct {
Service string `rotini:"service"`
}
}
}
rtx := NewContextFor(def, []string{"deploy", "api", "--env", "prod", "-ll"})
if err := NewParser().Parse(rtx, &inputs); err != nil {
fmt.Println("usage:", err)
return
}
fmt.Printf("%s → %s (verbosity %d)\n",
inputs.Deploy.Arguments.Service, inputs.Deploy.Flags.Env, inputs.Deploy.Flags.Loud)
Output: api → prod (verbosity 2)
type PathFromDef ¶
type PathFromDef struct {
Flag string // logical flag name searched across the resolved chain
Env string // environment variable read directly; comma-separated names: the first one set wins
}
PathFromDef names the runtime inputs that supply a ConfigFile's path — the declarative two-phase parse, where argv and env are read first and the file channel then opens whatever they pointed at. Precedence: the flag set on argv, then the env variable, then the flag's default, then the entry's own path or discover. A path supplied this way must exist.
type PluginDef ¶ added in v1.2.0
type PluginDef struct {
Name string
Aliases []string
Summary string // one-line description (completion candidates carry it as "name\tsummary")
Binary string // expected executable name, e.g. "kubectl-ctx"
Timeout time.Duration // 0 means no timeout
}
PluginDef describes a co-located sub-command, kubectl/git plugin style: invoking it execs the sibling Binary with the remaining arguments passed through.
type PluginDiscoveryDef ¶ added in v1.2.0
type PluginDiscoveryDef struct {
Prefix string // executable-name prefix, e.g. "acme-"
Hidden bool // dispatch discovered plugins but omit them from completion listings
}
PluginDiscoveryDef enables plugin discovery on a command: an unmatched token execs the sibling binary Prefix+<token>, and `<Prefix>*` executables are offered as completion candidates unless Hidden. A nil pointer means discovery is off for that command.
type PluginDispatch ¶ added in v1.2.0
type PluginDispatch struct {
Def PluginDef
Args []string
Dir string
// Discovered marks a plugin-discovery dispatch rather than a declared plugin, which
// decides the error category when the binary cannot be resolved.
Discovered bool
}
PluginDispatch is a resolved plugin invocation, declared or discovered: Def.Binary run with Args. Dir is the command's plugin path, searched after the host binary's own directory and before PATH; empty means none.
type PluginError ¶ added in v1.2.0
type PluginError struct {
Name string // the declared plugin name (or discovery token)
Binary string // the plugin binary that was sought or spawned
Kind PluginErrorKind // what went wrong
Timeout time.Duration // the elapsed deadline, for Kind == PluginTimeout (else 0)
Cause error // the underlying OS/exec error, reachable via errors.As (may be nil)
Msg string // the human-readable failure
// Candidates are the names the dispatching command knows (its sub-commands, their aliases
// and its declared plugins), set when a discovered token matched no binary, so a mistyped
// sub-command can be ranked against them; nil otherwise. Name is the token.
Candidates []string
// contains filtered or unexported fields
}
PluginError reports a failure by rotini to carry out a plugin dispatch. The plugin's own non-zero exit is not a PluginError; its exit code passes through unchanged.
var re *rotini.PluginError
if errors.As(err, &re) && re.Kind == rotini.PluginTimeout {
fmt.Fprintf(os.Stderr, "%s timed out after %s\n", re.Name, re.Timeout)
}
A missing binary is CategoryUsage when discovered (a mistyped token) and CategoryInternal when declared (an install problem); a spawn failure is CategoryInternal; a timeout is CategoryNone.
func (*PluginError) Error ¶ added in v1.2.0
func (e *PluginError) Error() string
Error renders the plugin-dispatch failure as a single, user-facing line.
func (*PluginError) Unwrap ¶ added in v1.2.0
func (e *PluginError) Unwrap() []error
Unwrap exposes the Cause (when present) and the category sentinel (ErrUsage/ErrInternal) so errors.Is/As reach both; a timeout adds no sentinel, so CategoryOf reports CategoryNone.
type PluginErrorKind ¶ added in v1.2.0
type PluginErrorKind int
PluginErrorKind classifies a plugin-dispatch failure: the plugin binary could not be located, it exceeded its declared timeout, or it could not be spawned.
const ( // PluginNotFound: no binary was found next to the executable, in the // plugin path, or on PATH. PluginNotFound PluginErrorKind = iota // PluginTimeout: the plugin ran past its declared timeout and was killed. PluginTimeout // PluginStartFailed: the binary was found but could not be started (a fork, // exec or pipe failure). The plugin's own non-zero exit is not an error kind. PluginStartFailed )
func (PluginErrorKind) String ¶ added in v1.2.0
func (k PluginErrorKind) String() string
String renders the kind as a short, stable label.
type Presence ¶
type Presence map[FieldPath]InputSource
Presence maps each field a layer supplied to its provenance. Overlay copies only these fields, so a layer's absent fields never overwrite a lower layer's values.
func PresenceOf ¶ added in v1.3.0
PresenceOf returns a Presence marking every non-zero input field of v as supplied, in the shape Context.CheckInputs and a hand-built InputLayer read. A field holding its zero value (false, 0, "") is not marked, so build the Presence by hand when a zero value must count as supplied. A non-nil pointer to a zero value (a nullable input) is marked. Each field's source is Layer "custom"; in a merge, the hand-built InputLayer's Name replaces it.
type ProfilesDef ¶ added in v1.4.0
type ProfilesDef struct {
Under string // the top-level key holding the named sections
Flag string // logical name of the selector flag, searched across the chain; "" when none
Env string // variables read directly, comma-separated, the first one set winning
Default string // the profile used when argv and the environment select none; "" means the shared keys only
}
ProfilesDef selects one named section of a ConfigFile per run. The section's keys are read as if they sat at the top of the file, winning over the file's other top-level keys, which every profile shares. The selection is the flag set on argv, then the first variable of Env that is set, then Default.
type Program ¶
type Program struct {
// contains filtered or unexported fields
}
Program is a rotini CLI ready to run: the compiled command tree (Definition), the handlers that implement it, and the settings around them. The generated entrypoint builds one with NewWiredProgram and calls Program.Execute. Every With method returns the receiver, so calls chain.
The surface groups into eight jobs:
- run — Program.Execute exits, Program.Run returns the code, Program.RunContext scopes one invocation, Program.Complete answers a completion request in a CompletionFormat; Program.Definition returns the command tree it runs
- streams — Program.WithStdin, Program.WithStdout, Program.WithStderr, and Program.WithBufferedOutput to buffer stdout
- process — Program.WithExit, Program.WithArgs, Program.WithContext, Program.WithSignals, Program.WithoutSignalHandling, Program.WithCompletion, Program.WithCompletionMessages, Program.WithCompletionDescriptions, and the environment and working directory a run reads, Program.WithEnviron and Program.WithDir, and the clock relative times read, Program.WithClock; Program.WithTerminationTimeout bounds the wait after a signal, and Program.WithArgv0 sets the name a multicall program dispatches on
- failure — Program.WithTeardownOnPanic, Program.WithPanicRecover, Program.WithReporter
- output — Program.WithOutputChecks checks every output written with Context.WriteOutput against the command's declared contract
- the program's dependencies — Program.WithDependency, or Program.With with the WithDependency option to register several at once
- rotini's own settings — Program.WithVersion, Program.WithParser, Program.WithInputReader, and the two the generated code sets, Program.WithInputSettings and Program.WithHelp; Program.WithFeatures adds runtime features to a program built with NewWiredProgram
- phase replacement — Program.WithResolver, Program.WithLifecycle
A setting that rotini reads (the runtime or generated code) is a typed option on the Program; a setting only the program's own code reads is a dependency. Typed options cannot be shadowed by a dependency name, and a wrong type is a compile error.
A Program is reusable: Program.Run dispatches one invocation and returns, giving each call a fresh Context.
A nil argument to a With method restores that setting's default, as each method's doc says. Program.With skips a nil Option, and Program.WithDependency registers whatever value it is given, nil included.
The With methods are not synchronized: configure before the first run. Applied between sequential runs, they take effect on the next one.
Methods do not check for a nil receiver. The zero value is not usable; start from NewProgram or NewProgramFunc.
func NewProgram ¶
func NewProgram(def Definition, handlers any) *Program
NewProgram wires a command tree and its aggregate handler set to the runtime. Dispatch calls the method of handlers named by each Command.Handler in def to obtain that command's Handler. A nil handlers value is accepted here; a run that reaches dispatch then fails with a *WiringError.
NewProgram finds the methods by reflection, which keeps every exported method of the program's types in the binary. Generated code uses NewWiredProgram, and a hand-written handler set can use NewProgramFunc, for a smaller binary.
func NewProgramFunc ¶ added in v1.4.0
func NewProgramFunc(def Definition, lookup HandlerLookup) *Program
NewProgramFunc wires a command tree to the runtime through lookup, with no reflection, and gives the program every Feature. Hand-written handler sets can use it for a smaller binary than NewProgram gives; generated code calls NewWiredProgram, which links only the features its spec uses.
A nil lookup is accepted here; a run that reaches dispatch then fails with a *WiringError, and completion offers no dynamic candidates.
func NewWiredProgram ¶ added in v1.4.0
func NewWiredProgram(def Definition, lookup HandlerLookup, features ...Feature) *Program
NewWiredProgram is NewProgramFunc with exactly the listed features: the program links and runs only those, so a small CLI's binary is smaller and starts faster. It is what the generated NewProgram calls, passing the features its spec uses; Program.WithFeatures adds more for code the spec does not describe. A run that needs a feature the program does not have fails with a *WiringError naming it.
A nil lookup behaves as it does for NewProgramFunc.
func (*Program) Complete ¶ added in v1.1.1
func (p *Program) Complete(words []string, format CompletionFormat) (int, error)
Complete answers one shell-completion request in the given format and returns the exit code, the completion counterpart of Program.Run. It serves hosts that do not call the hidden __complete entry, from the program's own code:
code, _ := cmd.NewProgram().Complete(words, rotini.PluginCompletion)
kubectl runs a separate kubectl_complete-<plugin> with only the plugin's words; a spec that declares `multicall: {complete: kubectl_complete-}` answers it without code (see MulticallDef).
words are the words after the program's own name; the last is the word being completed, empty when the cursor starts a new one, and no words at all completes a new first word. The answer is what __complete computes, written to the program's stdout by format. A nil format uses rotini's own format, the __complete default, which is private to rotini's generated scripts.
Completers receive the program's base context (Program.WithContext) through Context.Context. Rotini sets no deadline and traps no signal for a completion request.
The exit code is 0, or 1 when the format fails to write, with its error.
func (*Program) Definition ¶ added in v1.4.0
func (p *Program) Definition() Definition
Definition returns the command tree the program runs, for tooling and tests that work from it, such as ArgvOf. Its slices are the program's own and shared by every run, so treat them as read-only.
func (*Program) Execute ¶
Execute runs the arguments set by Program.WithArgs (default os.Args[1:]) through Program.Run and passes the resulting code to the exit action (os.Exit by default; see Program.WithExit).
The returned error joins every Context.RecordError value and every captured fault with errors.Join, so errors.Is and errors.As reach each one. It is returned only when the exit action returns; under os.Exit the process ends first. The reporter has already reported the outcome by then; the error lets an embedding host act on the failure.
Signals ¶
With no Program.WithContext, rotini traps os.Interrupt and syscall.SIGTERM. The first signal halts the lifecycle as Context.HaltWithCode does (forward progress stops, every begun teardown hook runs) with exit code 128+signum. A second signal calls the exit action with 130 immediately. See Program.WithoutSignalHandling and Program.WithSignals.
Under the default exit action, Execute leaves rotini's handler registered until the process exits: a signal arriving after the run settles is ignored, and the process exits with the run's code.
func (*Program) Run ¶
Run dispatches one invocation of argv and returns its exit code and error (see Program.Execute). It resolves the invoked command, executes a declared or discovered plugin if one was selected, and otherwise runs the lifecycle; inputs are parsed only when a handler calls Context.Inputs. Run never ends the process.
Each call gets a fresh Context. Dependencies registered with Program.WithDependency are seeded into every run; one set with Context.SetDependency stays local to its run.
With no supplied context, Run installs and removes the signal trap on every call (about 30µs). A host dispatching in a loop uses Program.RunContext or Program.WithoutSignalHandling.
Concurrency ¶
Run is safe for concurrent use once configuration is complete. Each run's records, exit state and run-local dependencies are its own. Shared, and synchronized by the host:
- The handlers value given to NewProgram, whose methods are called from each run's goroutine.
- The program's streams.
With no supplied context, each concurrent run installs its own trap and all of them observe a signal. A concurrent host passes its own context (Program.RunContext) or disables the trap with Program.WithoutSignalHandling.
func (*Program) RunContext ¶
RunContext is Program.Run under ctx, for this invocation only; the program is not modified. As with Program.WithContext, supplying a context leaves signal handling to the caller unless Program.WithSignals was set. A nil ctx returns exit code 1 and an ErrInternal error without running.
func (*Program) With ¶
With applies each Option in order and returns the program:
cmd.NewProgram(). With( rotini.WithDependency(tasks.Store, store), rotini.WithDependency(tasks.Client, client), ). WithVersion(version). Execute()
A later Option registering the same dependency replaces an earlier one. A nil Option is skipped.
func (*Program) WithArgs ¶
WithArgs sets the argument vector Program.Execute runs (defaults to os.Args[1:]).
Only Execute reads it. Program.Run and Program.RunContext use the argv they are passed, so `p.WithArgs(x).Run(nil)` runs with no arguments.
A nil args restores the default, reading os.Args[1:] again; pass []string{} to run with none.
func (*Program) WithArgv0 ¶ added in v1.4.0
WithArgv0 sets the name the program was invoked as (default os.Args[0]), which a spec that declares `multicall` dispatches on: a binary linked as `ls` runs the `ls` command. Only that dispatch reads it, so tests and embedding hosts set it to exercise multicall. "" restores the default.
func (*Program) WithBufferedOutput ¶ added in v1.4.0
WithBufferedOutput buffers Context.Stdout for every run, which makes a command that writes many small pieces of output much faster (two million lines: about 2.5s unbuffered, 0.06s buffered). It is off by default, and WithBufferedOutput(false) turns it off again:
cmd.NewProgram().WithBufferedOutput(true).Execute()
When the run ends, before the reporter runs, rotini flushes the buffer and checks the result: a write that fails then (a full disk, a closed pipe) is recorded as an error, so the run exits non-zero. This happens on every path that reaches the reporter, a halt, a recovered panic and a trapped signal included. A panic re-raised by Program.WithPanicRecover(false) flushes first, ignoring a failure; a second signal's forced exit flushes only when no write is in progress, so it may drop buffered output.
On a terminal, stdout is not buffered, so prompts and progress show as they are written. Under buffering, rtx.Stdout is not an *os.File: use IsTerminal to check it, and flush before waiting for input or handing the terminal to another program:
if f, ok := rtx.Stdout.(interface{ Flush() error }); ok {
if err := f.Flush(); err != nil {
return err
}
}
Buffered stdout reaches its destination after what was written to stderr in the meantime, which shows only when both go to one place (>out 2>&1). Writes made to os.Stdout directly bypass the buffer and come out of order: use rtx.Stdout. A subprocess given rtx.Stdout as its stdout writes through a pipe into the buffer, and does not see a terminal; give it os.Stdout (after a flush) if it needs one. Plugins and completion requests write to the program's stdout directly, unbuffered.
func (*Program) WithClock ¶ added in v1.4.0
WithClock sets the clock relative time values read: `2h` ago, `today`, `tomorrow`. A run reads it once, on first use, and every relative value in the run, and Context.Now, uses that reading, so `--since 1h --until now` agree however long the run takes. nil restores time.Now.
func (*Program) WithCompletion ¶ added in v1.1.1
func (p *Program) WithCompletion(format CompletionFormat) *Program
WithCompletion sets the CompletionFormat the hidden __complete entry answers in, in place of rotini's own. It serves a plugin whose host completes it by calling the plugin's __complete and reading the host's format, as the Docker CLI (`docker-<name> __complete <name> …`) and the Flux CLI (`flux-<name> __complete …`) do with the format PluginCompletion writes:
cmd.NewProgram().WithCompletion(rotini.PluginCompletion).Execute()
Rotini's generated completion scripts read rotini's own format, so a standalone CLI leaves this unset. A host that runs a separately named completer without a __complete word, such as kubectl's kubectl_complete-<name>, is served by calling Program.Complete from main.
A nil format restores rotini's own.
func (*Program) WithCompletionDescriptions ¶ added in v1.4.0
WithCompletionDescriptions sets the program's own rule for whether completion candidates carry their descriptions, replacing the check of the environment variable the conf's `descriptions_env` names. It is asked once per completion request, before anything is written, and applies to every shell and to PluginCompletion:
cmd.NewProgram().WithCompletionDescriptions(func(rtx *rotini.Context) bool {
return !settings.Plain
}).Execute()
Descriptions show by default. A nil fn restores the environment variable check.
func (*Program) WithCompletionMessages ¶ added in v1.3.0
WithCompletionMessages sets the program's own rule for whether completion messages show, replacing the check of the environment variable the conf's `messages_env` names. It is asked once per completion request, before anything is written, and applies to every shell and to PluginCompletion:
cmd.NewProgram().WithCompletionMessages(func(rtx *rotini.Context) bool {
return !settings.Quiet
}).Execute()
It has no effect unless the conf turns completion messages on. A nil fn restores the environment variable check.
func (*Program) WithContext ¶
WithContext sets the base context threaded to every lifecycle hook, the reporter, and any plugin exec, so a caller can cancel or time-bound the whole run. A nil context restores the default: rotini owns each run's context, and traps signals unless Program.WithSignals or Program.WithoutSignalHandling decided otherwise.
Cancellation is cooperative: it does not preempt a running hook, but once the context is canceled no further forward hook starts, and the teardown of every begun setup hook runs in reverse. ExitCause attaches an exit code; without one the code is resolved as usual.
Supplying a context disables rotini's signal trap by default, leaving signals to the caller (typically via signal.NotifyContext). Program.WithSignals re-enables the trap on top of a supplied context; Program.WithoutSignalHandling disables it without one.
func (*Program) WithDependency ¶ added in v1.2.0
func (p *Program) WithDependency[T any](dep Dependency[T], value T) *Program
WithDependency registers value under dep for the whole program, so every run sees it, and returns p for chaining:
cmd.NewProgram(). WithDependency(tasks.Store, store). WithVersion(version). Execute()
A later registration under the same name replaces an earlier one.
Go infers T from both arguments, so for a handle declared over an interface the value must already have that interface type. Convert the value, or name the type:
var Store = rotini.NewDependency[store.Store]("taskr.store") // an interface
p.WithDependency(Store, store.Store(store.NewMem()))
p.WithDependency[store.Store](Store, store.NewMem())
The dependency namespace belongs to the application. Rotini's own settings (input reader, parser, version, help) are typed Program options, so no dependency name can shadow them.
func (*Program) WithDir ¶ added in v1.4.0
WithDir sets the directory a run resolves relative paths against: walk-up configuration discovery, relative configuration paths, `@file` values, existingfile and existingdir checks, and the directory plugins run in. "" (the default) is the process working directory. A relative dir is made absolute when the run starts.
Rotini can't change the working directory for one run, so handlers still run in the process's. A handler that opens a relative path joins it first:
f, err := os.Open(filepath.Join(rtx.Dir(), inputs.Deploy.Flags.Manifest))
An existingfile or existingdir value is checked against the run's directory and bound as typed, so the handler joins it the same way.
func (*Program) WithEnviron ¶ added in v1.4.0
WithEnviron sets the environment a run reads, in exec.Cmd.Env's KEY=value form: env inputs, flag env fallbacks, config_source variables, $VAR and ~ in configuration paths and in inputs that declare expand, the XDG and home directories (AppDirs included), plugin lookup and the plugin's own environment, and the completion messages switch. nil (the default) reads the process environment live; a non-nil slice, even an empty one, is the whole environment. Later duplicates win; on Windows names match case-insensitively.
Each run reads it through Context.LookupEnv and Context.Environ, so tests can run in parallel and one process can run programs with different environments:
code, err := cmd.NewProgram().
WithEnviron([]string{"HOME=" + home, "APP_TOKEN=t"}).
WithDir(t.TempDir()).
Run([]string{"deploy"})
A custom source in InputSettings reads where it reads; a handler reads the run's environment with Context.LookupEnv.
func (*Program) WithExit ¶
WithExit overrides what Program.Execute does with the resolved exit code (default os.Exit). The same function receives the forced exit code when a second trapped signal arrives. A nil function restores os.Exit.
When fn returns, Execute returns the run's error to its caller; under os.Exit it never does. This makes an end-to-end test or an embedding host possible:
code := -1
err := cmd.NewProgram().
WithArgs(argv).WithStdout(&out).WithStderr(&errs).
WithExit(func(c int) { code = c }).
Execute()
func (*Program) WithFeatures ¶ added in v1.4.0
WithFeatures adds features to a program built with NewWiredProgram, for code its spec does not describe: a custom InputSettings.Sources entry needs ConfigFiles, and an inputs struct written by hand needs the features its fields use. It is a no-op on a program built with NewProgram or NewProgramFunc, which has every feature. Configure before the first run.
func (*Program) WithHelp ¶
WithHelp sets where Context.Help finds a command's help page. The generated NewProgram passes its own Help function.
Help is a program-level setting so that a command composed from another spec prints the page of the program it runs in, with that program's full command path and inherited flags.
A nil help restores the default, no pages: Context.Help then returns "", so a generated --help prints nothing.
func (*Program) WithInputReader ¶ added in v1.2.0
func (p *Program) WithInputReader(fn func(InputSettings) *InputReader) *Program
WithInputReader replaces the input reader that Context.Inputs and the per-channel methods use. fn receives the program's InputSettings, so a replacement starts from the generated descriptor and keeps the declared configuration sources:
p.WithInputReader(func(meta rotini.InputSettings) *rotini.InputReader {
meta.Sources = append(meta.Sources, mySource)
return rotini.NewInputReader(meta)
})
A nil fn restores the default, NewInputReader.
func (*Program) WithInputSettings ¶ added in v1.2.0
func (p *Program) WithInputSettings(meta InputSettings) *Program
WithInputSettings supplies the generated input descriptor: the configuration sources, the env prefix and the stdin schemas Context.Inputs reads from. The generated NewProgram calls it; a hand-built program calls it to enable those channels.
func (*Program) WithLifecycle ¶
WithLifecycle overrides the run phase's plan: which hooks run, in what pairing and order (see Lifecycle and DefaultLifecycle). Halting, the reverse teardown unwind, panic handling and exit-code resolution are unchanged. A nil lifecycle restores DefaultLifecycle.
Example ¶
A lifecycle that wraps DefaultLifecycle and swaps the cascading pairs, so CascadingPostRun unwinds root→leaf. Halting, unwind and panic handling are unchanged.
NewProgram(exampleDef(), exHandlers{}).
WithArgs([]string{"status"}).
WithExit(func(int) {}).
WithLifecycle(func(chain []Command, hs []Handler) []LifecycleStep {
steps := DefaultLifecycle(chain, hs)
for i, j := 0, len(hs)-1; i < j; i, j = i+1, j-1 {
steps[i].Undo, steps[j].Undo = steps[j].Undo, steps[i].Undo
}
return steps
}).
Execute()
Output: app.CascadingPreRun status.CascadingPreRun status.PreRun status.Run status.PostRun app.CascadingPostRun status.CascadingPostRun
func (*Program) WithOutputChecks ¶ added in v1.2.0
WithOutputChecks makes every Context.WriteOutput and Context.WriteOutputItem call check its value against the command's declared output schema before writing it. A value that does not match is an internal error naming each field at fault, and nothing is written. It is off by default; enable it in tests or debug builds, and WithOutputChecks(false) turns it off again:
p := cmd.NewProgram().WithOutputChecks(true)
Only output written through WriteOutput and WriteOutputItem is checked. To check captured stdout, use DecodeOutput.
func (*Program) WithPanicRecover ¶
WithPanicRecover controls where a hook panic goes. The default, true, recovers it and routes it to the reporter as a *PanicError. False re-raises it to the caller, for an embedding host's own recover, a crash reporter, or debugging.
Combined with Program.WithTeardownOnPanic:
- recover=true: the panic reaches the reporter; teardown runs per WithTeardownOnPanic.
- recover=false, teardown=true: teardown runs, then the panic is re-raised (its stack starts at the re-raise, not the original site).
- recover=false, teardown=false: the hook runs unguarded, so the panic propagates immediately with its original stack and no teardown.
Only panics on the hook goroutine are recovered; a panic in a goroutine a handler started crashes the process.
func (*Program) WithParser ¶
WithParser sets the Parser that Context.Parser returns. Parser has no options, so every Parser behaves like NewParser's and this setting changes nothing today; Context.Inputs and the InputReader always use the default parser. A nil parser restores the default.
func (*Program) WithReporter ¶ added in v1.2.0
WithReporter sets the program's outcome reporter. See Reporter.
The default prints infos, warnings, errors, panics, then successes, all to stderr, so stdout carries only what handlers write. It applies an exit floor: a recorded error or panic exits 1 unless a handler already set a non-zero code. It leaves out an error caused by the run's own signal trap (the trap's cancellation, or context.Canceled after it), since a signal is a graceful stop and the exit code, 128+n, already says why the run ended. A custom reporter receives every recorded error and owns the exit code entirely.
A nil fn restores the default reporter.
func (*Program) WithResolver ¶
WithResolver overrides the resolve phase: argv to invocation target, plus the argv the parsers later see. A resolver that rewrites tokens should rewrite argv and delegate to DefaultResolver, so routing and parsing agree. A resolver error is reported as a fault (CategoryInternal unless the error carries a category) and fails the run.
Completion walks the Definition, so an alias known only to the resolver is dispatchable but not completed. A nil resolver restores DefaultResolver.
Example ¶
A resolver that adds a routing alias: "st" rewrites to "status" and delegates to DefaultResolver, so routing and parsing agree on the rewritten argv. A resolver alias is not completed; declare aliases in the spec for that.
NewProgram(exampleDef(), exHandlers{}).
WithArgs([]string{"st"}).
WithExit(func(int) {}).
WithResolver(func(def Definition, argv []string) (Resolution, error) {
if len(argv) > 0 && argv[0] == "st" {
argv = append([]string{"status"}, argv[1:]...)
}
return DefaultResolver(def, argv)
}).
Execute()
Output: app.CascadingPreRun status.CascadingPreRun status.PreRun status.Run status.PostRun status.CascadingPostRun app.CascadingPostRun
func (*Program) WithSignals ¶
WithSignals enables rotini's signal trap for the given signals, whether or not a context was supplied. With a supplied context the trap cancels a derived child, never the caller's context. The first signal halts the run (teardown runs, exit 128+signum); a second exits immediately with 130 through the exit action.
An empty list restores the default: os.Interrupt and syscall.SIGTERM are trapped unless a context was supplied, undoing an earlier WithSignals or Program.WithoutSignalHandling.
func (*Program) WithStderr ¶
WithStderr overrides the program's standard error (default os.Stderr): Context.Stderr, where the default reporter writes everything the run recorded. A nil writer restores os.Stderr.
func (*Program) WithStdin ¶
WithStdin overrides the program's standard input (default os.Stdin): the reader exposed as Context.Stdin and decoded for stdin inputs. A nil reader restores os.Stdin.
func (*Program) WithStdout ¶
WithStdout overrides the program's standard output (default os.Stdout): Context.Stdout, where handlers and Context.WriteOutput write and the runtime writes completion candidates. A nil writer restores os.Stdout.
func (*Program) WithTeardownOnPanic ¶
WithTeardownOnPanic controls whether teardown runs when a hook panics. The default, true, halts forward progress and still runs the teardown of every begun setup hook, as deferred calls run during a panic. False skips the remaining teardown, as Context.Exit does.
Where the panic goes is controlled separately by Program.WithPanicRecover.
func (*Program) WithTerminationTimeout ¶ added in v1.4.0
WithTerminationTimeout bounds how long a run may take to finish after the first trapped signal halts it. When the run hasn't returned within d (teardown and the reporter included), the exit action is called with the signal's exit code (128+n), as a second signal does: buffered stdout is flushed on a best-effort basis, the remaining teardown doesn't run, and the reporter may not have printed.
d <= 0 restores the default: wait for teardown, or for a second signal. It has no effect when rotini installs no signal trap (see Program.WithSignals and Program.WithoutSignalHandling).
func (*Program) WithVersion ¶
WithVersion sets the program's version string, read with Context.Version and printed by the generated version command and --version flag.
var version = "0.0.0" // go build -ldflags "-X main.version=1.2.3" cmd.NewProgram().WithVersion(version).Execute()
func (*Program) WithoutSignalHandling ¶
WithoutSignalHandling disables rotini's signal trap; of it and Program.WithSignals, the later call wins. Rotini still owns the run context but calls no signal.Notify, so the program's own handling is the only one (signal.Notify registrations are additive).
Rotini exposes no cancel in this mode, so the program's signal handler cannot halt the run gracefully; for that, use Program.WithContext with signal.NotifyContext.
type Reporter ¶ added in v1.2.0
Reporter is the program's outcome reporter. The runtime calls it once per run, after the lifecycle and its teardown finish, when the Outcome is not empty.
The reporter decides what to print and where, and sets the final exit code: Context.Exit inside it overrides the code the lifecycle set, and Context.HaltWithCode is a no-op.
The error Program.Run returns is built before the reporter is called, so modifying the Outcome does not change it. Records made inside the reporter are dropped.
A panic inside a reporter is not recovered. A reporter that can fail handles its own failure, for example by writing to rtx.Stderr and setting a code with Context.Exit.
func StructuredReporter ¶ added in v1.2.0
StructuredReporter returns a Reporter for programs whose output scripts read. When structured reports true for the run, it writes each thing the run recorded to stderr as one JSON object per line, and leaves stdout alone, so a partial result there is never interleaved with an error:
{"schema_version":1,"error":{"category":"usage","command":"taskr add","exit_code":1,"kind":"missing-required","message":"missing required input: <title>"}}
{"schema_version":1,"error":{"candidates":["add","list","done"],"category":"usage","command":"taskr","exit_code":1,"kind":"unknown-command","message":"unknown command \"lst\" for \"taskr\"","token":"lst"}}
{"schema_version":1,"warning":{"command":"taskr list","message":"the cache is stale"}}
Infos and successes are written the same way, as {"info":{…}} and {"success":{…}}. A field is present only when rotini knows it: kind, flag and token come from a *ParseError, kind from a *PluginError too, and token from any error SuggestionFacts reads. With such a token, candidates lists the words it was checked against, as SuggestionFacts returns them, unranked: a tool can offer the nearest, while rotini itself suggests nothing. Each line's shape is described by schema-error.json in the rotini repository, and the contract document includes it.
When structured reports false, or is nil, it reports as the default reporter does. Either way the exit code follows the default reporter's rule, and an error the run's own signal caused is left out, as the default reporter leaves it out.
structured is the program's own rule, typically whether its format flag asks for json. The run may have failed in parsing, so the rule should read Context.Argv, not validated inputs:
cmd.NewProgram().WithReporter(rotini.StructuredReporter(func(rtx *rotini.Context) bool {
return slices.Contains(rtx.Argv, "--json")
})).Execute()
type Resolution ¶
type Resolution struct {
// Chain is the resolved command path the run phase dispatches (when
// Plugin is nil). It must be non-empty — the root command is always there.
Chain []Command
// Plugin, when non-nil, short-circuits local dispatch: the runtime execs this binary
// instead, stdio passed through and context honored.
Plugin *PluginDispatch
// Argv is the vector the run phase exposes as [Context.Argv]. A resolver that rewrites
// tokens returns the rewritten vector here so parsing agrees with its routing; nil keeps
// the original argv.
Argv []string
}
Resolution is the outcome of the resolve phase: the invoked command path (root → leaf), or a plugin dispatch that replaces local execution.
func DefaultResolver ¶
func DefaultResolver(def Definition, argv []string) (Resolution, error)
DefaultResolver is rotini's resolve phase, exported for a custom Resolver to wrap. It descends sub-commands by name or alias, skips flags and their values, stops at the first positional, and diverts to a plugin dispatch for declared and discovered plugins. It does not validate the invoked command's input.
A plugin receives only the words after its name, so flags typed before the name are checked strictly: a short-circuit flag set there (such as --help) cancels the dispatch, and the run answers it for the command the words before the plugin name resolve to, with Resolution.Argv cut to those words. The root's `role: chdir` flag may come there too: the plugin runs in its directory. Any other flag there, or one that does not parse, is returned as a usage *ParseError, with the commands before the plugin name as the Chain.
type Resolver ¶
type Resolver func(def Definition, argv []string) (Resolution, error)
Resolver is the resolve phase: it matches argv against the Definition to decide what this invocation targets. An error fails the run: a usage error is reported as one, anything else as a fault. With an error, Resolution.Chain may hold the commands resolved so far, which Context.CommandPath then reports to the reporter; without one it reports the root. See DefaultResolver.
The Definition is passed by value, but its slices are the program's own and shared by every run. A resolver must treat it as read-only; writing through it changes every later run of the Program.
type ResponseFilesDef ¶ added in v1.4.0
type ResponseFilesDef struct {
Prefix string // one character, such as "@"
}
ResponseFilesDef is how a program reads response files (the spec's root `response_files`). Before the first "--", a word starting with Prefix (`@args.rsp`) is replaced by the file's lines, one word per line; a doubled prefix (`@@x`) stands for the literal word with one prefix (`@x`). Expansion happens once, before command resolution, so every parse sees the expanded words, and Context.Argv holds them. NewContextFor doesn't expand.
type Selection ¶ added in v1.4.0
type Selection struct {
// contains filtered or unexported fields
}
Selection is a value reduced to some of its fields by SelectFields. Context.WriteOutput and Context.WriteOutputItem accept it in place of the type it was selected from, and check it against the declared schema with every `required` removed, since a selection leaves fields out. It writes as json, yaml or toml; a renderer receives it as is, and can marshal it to JSON. Its zero value is not a selection, and writing one is an internal error.
func SelectFields ¶ added in v1.4.0
SelectFields returns v keeping only fields, named by their JSON property names, the names `-o json` shows. A field with dots reaches into nested objects: owner.login keeps the login of each item's owner. at is the dotted path to the items the fields belong to: the spec's `values_from` without its "output" prefix, so "" for v itself and "tasks" for v's tasks. Lists on the way, v itself included, are stepped through, so each element is reduced, and everything outside at (an envelope's total) is kept. A field an item lacks is left out of that item.
Sort before selecting, since the sort field need not be selected:
if in.Flags.SortBy != "" {
if err := rotini.SortBy(list.Tasks, in.Flags.SortBy, in.Flags.Reverse); err != nil {
return err
}
}
if len(in.Flags.JSON) > 0 {
sel, err := rotini.SelectFields(list, "tasks", in.Flags.JSON)
if err != nil {
return err
}
return rtx.WriteOutput(sel, "json", nil)
}
v is converted through JSON, which is fine for the lists a command line prints.
func (Selection) MarshalJSON ¶ added in v1.4.0
MarshalJSON writes the selected fields.
func (Selection) MarshalTOML ¶ added in v1.4.0
MarshalTOML hands the selected fields to a toml encoder.
func (Selection) MarshalYAML ¶ added in v1.4.0
MarshalYAML hands the selected fields to a yaml encoder.
type Suggestor ¶
type Suggestor struct {
// contains filtered or unexported fields
}
Suggestor ranks a possibly-mistyped token against a list of candidates by optimal string alignment distance. It prints nothing and holds no state beyond its configuration. Rotini never suggests on its own; a program opts in by calling a Suggestor, typically from its reporter with Suggestor.For:
var suggestor = rotini.NewSuggestor()
func reporter(_ context.Context, rtx *rotini.Context, out rotini.Outcome) {
for _, err := range out.Errors {
fmt.Fprintf(rtx.Stderr, "Error: %s\n", err)
if hits := suggestor.For(err); len(hits) > 0 {
fmt.Fprintf(rtx.Stderr, "Did you mean %q?\n", hits[0])
}
}
}
For reads its token and candidates with SuggestionFacts; a program with its own ranking calls SuggestionFacts directly instead.
Matching is always case-insensitive. A configured Suggestor is safe for concurrent use; finish configuring it before sharing it across goroutines.
func NewSuggestor ¶
func NewSuggestor() *Suggestor
NewSuggestor returns a Suggestor with a minimum score of 0.75 and at most 3 results. The With methods return the receiver, so they chain. The zero value is not usable.
func (*Suggestor) Closest ¶
Closest returns the single best suggestion for input, or ok=false when input exactly matches a candidate or nothing clears the minimum score.
func (*Suggestor) For ¶
For returns the suggestions for the token err rejected, ranked against its candidates, nearest first. It reads them with SuggestionFacts, so it works for a *ParseError, an *InputError and a *PluginError. It returns nil when err carries no facts and when the token is near nothing.
func (*Suggestor) Suggest ¶
Suggest ranks candidates by nearness to input, nearest first, keeping those at or above the minimum score and at most the configured number of results.
An input that matches a candidate byte-for-byte yields nil; a case-only difference ("--VERBOSE" for "--verbose") is treated as a typo.
Ties prefer the candidate sharing the longer common prefix with input, then break lexicographically, so the result is deterministic. Empty candidates are skipped and duplicates collapse to the first occurrence.
func (*Suggestor) WithMaxResults ¶
WithMaxResults caps how many suggestions Suggestor.Suggest and Suggestor.For return (default 3). Zero or less means no cap.
func (*Suggestor) WithMinScore ¶
WithMinScore sets the similarity a candidate must reach to be offered, in [0,1]. A value outside that range is ignored. Below about 0.7 the ranker starts offering unrelated words; above 0.75 it no longer matches one-edit typos of four-character commands.
type TopicDef ¶ added in v1.4.0
TopicDef is one help topic the root declares (the spec's `topics:`): a page that isn't a command, shown by the generated Help function for its name. Completion offers topic names with the root's sub-commands when it completes a command path (`help <TAB>`).
type WiringError ¶
type WiringError struct {
Command string // the command whose handler wiring is broken
Handler string // the handler method name the Definition referenced
Msg string // the human-readable failure
// Feature names the missing feature, as its [Feature.String] spells it ("env",
// "codec:yaml", "values:url"); "" for the other wiring errors.
Feature string
}
WiringError reports that the generated Definition and the handler set are out of sync — a resolved command names a handler method that does not exist, or whose return value does not implement Handler — or that a command declares config inputs but the program has no InputSettings, or that a program built with NewWiredProgram needs a Feature it was not given. It is always CategoryInternal.
Command and Handler are empty for a missing InputSettings, a nil handlers value passed to NewProgram and a nil lookup passed to NewProgramFunc.
func (*WiringError) Error ¶
func (e *WiringError) Error() string
Error renders the mismatch as a single line.
func (*WiringError) Unwrap ¶
func (e *WiringError) Unwrap() error
Unwrap reports ErrInternal: a wiring mismatch is the author's bug, never the user's.
Source Files
¶
- acquired_lines.go
- appdirs.go
- argspans.go
- argv_of.go
- bufout.go
- chdir.go
- check_inputs.go
- codecs.go
- completion.go
- completion_context.go
- config_write.go
- config_write_file.go
- config_write_value.go
- context.go
- context_ctx.go
- context_usage.go
- dashindex.go
- definition.go
- dependency.go
- deprecation.go
- doc.go
- effects.go
- enum.go
- environ.go
- environ_dirs.go
- errors.go
- expand.go
- expand_inputs.go
- features.go
- features_inputs.go
- features_needs.go
- features_program.go
- features_values.go
- flag_dependencies.go
- flag_leaves.go
- glob.go
- handlers.go
- hidden_spellings.go
- input_args.go
- input_decode.go
- input_labels.go
- input_reader.go
- input_settings.go
- inputs.go
- inputs_format.go
- inputs_handbuilt.go
- lifecycle.go
- multicall.go
- object.go
- open_input.go
- output.go
- output_file.go
- output_file_other.go
- output_select.go
- pages.go
- parser.go
- path_kinds.go
- path_missing_other.go
- patterns.go
- plugin.go
- profiles.go
- program.go
- program_definition.go
- program_environ.go
- program_func.go
- program_process.go
- reporter.go
- resolve.go
- responsefile.go
- scalar.go
- secret_literal.go
- source_env.go
- source_envfile.go
- stdin.go
- stdin_bind.go
- stdin_stream.go
- suggestor.go
- tagmeta.go
- tagmeta_list.go
- terminal.go
- terminal_linux.go
- timemap_channels.go
- timevalue.go
- topics.go
- types.go
- unique.go
- validator_cache.go
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
rotini
command
|
|
|
internal
|
|
|
benchcli
command
Command benchcli measures a generated program built from this working tree: the fixture CLI in e2e/testdata/script/r11_lean_runtime.txtar.
|
Command benchcli measures a generated program built from this working tree: the fixture CLI in e2e/testdata/script/r11_lean_runtime.txtar. |
|
cfgedit
Package cfgedit changes one value in a config file's source text and leaves every other byte as it was: comments, key order, quoting, indentation, line endings and a byte-order mark.
|
Package cfgedit changes one value in a config file's source text and leaves every other byte as it was: comments, key order, quoting, indentation, line endings and a byte-order mark. |
|
cmd/rotini
Code generated by rotini; DO NOT EDIT.
|
Code generated by rotini; DO NOT EDIT. |
|
codegen
Package codegen turns a CLI definition (a .rotini.spec and .rotini.conf) into a Go program.
|
Package codegen turns a CLI definition (a .rotini.spec and .rotini.conf) into a Go program. |
|
contractdiff
Package contractdiff compares two versions of a rotini contract document and reports what changed for the people and scripts using the CLI: breaking, possibly breaking, expected (planned for removal at or below the release being prepared) or safe.
|
Package contractdiff compares two versions of a rotini contract document and reports what changed for the people and scripts using the CLI: breaking, possibly breaking, expected (planned for removal at or below the release being prepared) or safe. |
|
Package rotinitest runs a rotini program from a test with typed inputs, and checks what it wrote and how it exited.
|
Package rotinitest runs a rotini program from a test with typed inputs, and checks what it wrote and how it exited. |
|
Package shape formats a command's output with a Go template the end-user passes, as in `--format '{{.title}}'`.
|
Package shape formats a command's output with a Go template the end-user passes, as in `--format '{{.title}}'`. |