rotini

package module
v1.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 50 Imported by: 0

README

rotini

A spec-driven codegen package for building CLI programs in Go.

Latest release Documentation Go Reference MIT license

Rotini lets you describe a command-line program in a spec file (YAML, JSON, JSONC or TOML) and generates the Go program around it. You write what each command does and rotini handles the plumbing. You can adopt as much or as little as you want; bring your own command routing, parsing, input handling, help or error reporting, and rotini still generates the command tree, typed inputs, documentation and shell completion from your spec.

A split terminal. On the left, a Go module is created, rotini is added and rotini init sets up a program that builds and runs. Then rotini generate --watch starts on the right while a command is added to the spec on the left, its handler is implemented and the program is built and run

Features

Feature What you get
Commands Sub-commands to any depth, with aliases, help groups and hidden commands.
Flags and arguments Short and long flags with GNU-style parsing (-abc, --name=value, --no-x), repeatable flags, and optional and variadic arguments.
Typed values Strings, numbers, booleans, lists and maps, plus value types such as duration, date, url, ip, bytesize and existingfile.
Validation Required values, defaults, enums, patterns, bounds and lengths, plus flags that are mutually exclusive, required together, one-of or at-least-one, and flags that require others. A bad value is a usage error naming the flag the user typed.
Environment variables Any flag can fall back to an environment variable, named from a prefix or set exactly.
Config files Values from YAML, JSON, JSONC, TOML or dotenv files at a fixed path or discovered in the XDG config directory or by walking up from the working directory. The command line always wins.
Stdin A typed payload piped on stdin, as a JSON, YAML, JSONC or TOML document, as text, or as lines.
Secrets Inputs marked secret are redacted from errors and from the record of where each value came from.
Help --help and help <command> pages with usage, examples and see-also links, under headings you can rename, or a page you write yourself.
Version --version and a version command, stamped at build time.
Shell completion Scripts for bash, zsh, fish, PowerShell and Nushell, with completion hints for values such as files and directories.
Man and markdown pages Roff man pages and markdown reference pages, ready to install or publish.
Structured output A JSON Schema for each command's output and a contract document describing the whole CLI, for scripts and agents.
Errors and exit codes Consistent Error: messages and a non-zero exit code. Errors carry a usage or internal category you can map to your own exit codes, and errors can be reported as JSON for scripts.
Deprecation Deprecated commands, aliases and flags keep working and are marked in help. Each use is reported to your code, which decides whether to warn.
Interrupts and panics Ctrl+C and SIGTERM stop the program cleanly, running its teardown, and a second Ctrl+C exits at once. A panic is reported as an error rather than a stack trace.
Suggestions "Did you mean" suggestions for a mistyped command, flag or value, opt-in.
Plugins Run separate <app>-<name> programs as sub-commands, declared or discovered. A rotini program can also be a plugin for kubectl, Docker or Flux, completing and showing help the way the host does.
Wrapper commands A command that forwards everything after its name untouched to another program.
Composed CLIs Mount one CLI inside another as a sub-command, from the same module or another, while it still builds and ships on its own.

Quick start

Requires Go 1.27 or later.

Create a Go module and add the rotini tool to it. go tool rotini init <name> sets up a working program: a spec and a conf, an entrypoint, a handler for each command, and the generated code. From there, development is a loop: change the spec, run go generate ./..., implement the handler for any new command, and build.

Example

1. Create a module and add the rotini tool.

mkdir helloworld && cd helloworld
go mod init github.com/me/helloworld
go get -tool github.com/go-rotini/rotini/cmd/rotini@latest

2. Set up the program. go mod tidy records rotini as a direct dependency, since the generated code imports it.

go tool rotini init helloworld
go mod tidy
cmd/helloworld/.rotini.spec.yaml     the spec: what the CLI accepts
cmd/helloworld/.rotini.conf.yaml     the conf: what is generated, and where
cmd/helloworld/main.go               the entrypoint
internal/cmd/helloworld/zz_rotini.go generated on every run; do not edit
internal/cmd/helloworld/*.go         one handler per command; yours to edit

3. Declare the command. Add a hello command, with an optional argument and a flag, under commands: in cmd/helloworld/.rotini.spec.yaml:

    - name: hello
      summary: say hello
      arguments:
        - name: name
          summary: who to greet
          schema: { type: string, default: world }
      flags:
        - name: shout
          summary: greet in capitals
          identifiers: [-s, --shout]
          schema: { type: bool }

4. Generate, then implement the handler. go generate ./... writes internal/cmd/helloworld/helloworld_hello.go. Its Run method already reads the typed, validated inputs (--help is answered once, for every command, by the root handler init wrote). Replace the line that prints them with the greeting code:

package helloworld

import (
	"context"
	"fmt"
	"strings"

	"github.com/go-rotini/rotini"
)

var _ rotini.Handler = (*helloworldHelloHandler)(nil)

type helloworldHelloHandler struct {
	rotini.NoCascadingPreRun
	rotini.NoPreRun
	rotini.NoPostRun
	rotini.NoCascadingPostRun
}

func (*helloworldHelloHandler) Run(ctx context.Context, rtx *rotini.Context) {
	inputs, err := rtx.Inputs[HelloworldHelloInputs]()
	if err != nil {
		rtx.HaltWith(err)
		return
	}

	greeting := "Hello, " + inputs.HelloworldHello.Arguments.Name + "!"
	if inputs.HelloworldHello.Flags.Shout {
		greeting = strings.ToUpper(greeting)
	}
	fmt.Fprintln(rtx.Stdout, greeting)
}

5. Build and run.

$ go build ./cmd/helloworld
$ ./helloworld hello
Hello, world!
$ ./helloworld hello rotini --shout
HELLO, ROTINI!

Documentation

  • The guide and references are at rotini.dev.
  • The full API reference is on pkg.go.dev.
  • Breaking changes are listed in each release's notes; see upgrading.
  • See CONTRIBUTING.md for guidelines on how to contribute to this project.
  • This project follows a code of conduct to ensure a welcoming community. See CODE_OF_CONDUCT.md.
  • To report a vulnerability, see SECURITY.md.
  • This project is licensed under the MIT License. See LICENSE for details.

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:

  1. Standalone — the command's own spec node and generated stub. The default.
  2. 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.
  3. 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.
  4. 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.
  5. 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

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:

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.

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

Examples

Constants

This section is empty.

Variables

View Source
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.

View Source
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

func AsCommand(i int, hook func(context.Context, *Context)) func(context.Context, *Context)

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

func ConfigFilePath(rtx *Context, file string) (path string, exists bool, err error)

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

func ConfigProfiles(rtx *Context, file string) ([]string, error)

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

func DecodeOutput[T any](p *Program, data []byte, format string) (T, error)

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

func ExitCause(code int) error

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

func InternalError(err error) error

InternalError tags err as a CategoryInternal error (a bug or misconfiguration) without altering its message. It returns nil when err is nil.

func IsTerminal

func IsTerminal(stream any) bool

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

func SortBy[E any](items []E, field string, desc bool) error

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

func StripANSI(text string) string

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

func SuggestionFacts(err error) (token string, candidates []string, ok bool)

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

func UsageError(err error) error

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

type ArgValueCompleter interface {
	CompleteArgValue(rtx *Context, arg, partial string) []string
}

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

type ArgvError struct {
	Field  FieldPath
	Reason string
}

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.

func (*ArgvError) Error added in v1.4.0

func (e *ArgvError) Error() string

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

func (b ByteSize) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler, rendering the size as ByteSize.String does.

func (ByteSize) String

func (b ByteSize) String() 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

func (b *ByteSize) UnmarshalText(text []byte) error

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

func CategoryOf(err error) Category

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

func (Category) String

func (c Category) String() string

String renders the category as a short, stable label.

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

func (cmd Command) PluginBinary(name string) (string, bool)

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

func (cmd Command) PluginDiscoveryErrors() []error

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

type CompletionCandidate struct {
	Value       string
	Description string
}

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:

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

func (rtx *Context) AddCompletionMessage(msg string)

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

func (rtx *Context) CheckInputs[T any](v T, set Presence) error

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

func (rtx *Context) CheckOutput(v any) error

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

func (rtx *Context) Command() 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

func (rtx *Context) CommandChain() []Command

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

func (rtx *Context) CommandPath() string

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

func (rtx *Context) Context() context.Context

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

func (rtx *Context) DashIndex() (int, bool)

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

func (rtx *Context) Dir() string

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

func (rtx *Context) Environ() []string

Environ returns a copy of the run's environment (Program.WithEnviron) as KEY=value.

func (*Context) Exit

func (rtx *Context) Exit(code int)

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

func (rtx *Context) Failed() bool

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

func (rtx *Context) HaltWith(err error)

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

func (rtx *Context) HaltWithCode(code int)

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

func (rtx *Context) Help() string

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

func (rtx *Context) Inputs[T any]() (T, error)

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

func (rtx *Context) LookupEnv(name string) (string, bool)

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

func (rtx *Context) Now() time.Time

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

func (rtx *Context) Parser() *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

func (rtx *Context) PartialInputs[T any]() (T, Presence, error)

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

func (rtx *Context) RecordError(err error)

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

func (rtx *Context) RecordInfo(msg string)

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

func (rtx *Context) RecordSuccess(msg string)

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

func (rtx *Context) RecordWarning(warn error)

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

func (rtx *Context) SetContext(ctx context.Context)

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

func (rtx *Context) Usage() string

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

func (rtx *Context) Version() string

Version returns the version set by Program.WithVersion, or "" if none was set.

func (*Context) WithClock added in v1.4.0

func (rtx *Context) WithClock(now func() time.Time) *Context

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

func (rtx *Context) WithDir(dir string) *Context

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

func (rtx *Context) WithEnviron(env []string) *Context

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

func (rtx *Context) WithHelp(help HelpFunc) *Context

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

func (rtx *Context) WithOutputChecks(enabled bool) *Context

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

func (rtx *Context) WithParser(parser *Parser) *Context

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

func (rtx *Context) WithStderr(w io.Writer) *Context

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

func (rtx *Context) WithStdin(r io.Reader) *Context

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

func (rtx *Context) WithStdout(w io.Writer) *Context

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

func (rtx *Context) WithVersion(version string) *Context

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:

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:

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 Dotenv added in v1.4.0

func Dotenv() Feature

Dotenv wires the dotenv format, for .env files.

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 Patterns added in v1.4.0

func Patterns() Feature

Patterns wires pattern constraints.

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

func Stdin(formats ...string) Feature

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).

func YAML added in v1.4.0

func YAML() Feature

YAML wires the YAML document format, for configuration files, stdin documents and object flags.

func (Feature) String added in v1.4.0

func (f Feature) String() string

String names the feature: "plugins", "stdin(json,yaml)", "codec:yaml", "values: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

func (g Glob) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler.

func (Glob) Match added in v1.4.0

func (g Glob) Match(name string) bool

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

func (g *Glob) UnmarshalText(text []byte) error

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

type HandlerLookup func(name string) (Handler, bool)

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

type HelpFunc func(path ...string) (string, error)

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

func (h HexBytes) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler as lowercase hex without a prefix.

func (HexBytes) String

func (h HexBytes) String() string

String renders the bytes as lowercase hexadecimal.

func (*HexBytes) UnmarshalText

func (h *HexBytes) UnmarshalText(text []byte) error

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

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.

func (NoPostRun) PostRun added in v1.2.0

func (NoPostRun) PostRun(ctx context.Context, rtx *Context)

PostRun does nothing.

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.

func (NoPreRun) PreRun added in v1.2.0

func (NoPreRun) PreRun(ctx context.Context, rtx *Context)

PreRun does nothing.

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.

func (Outcome) Empty

func (o Outcome) Empty() bool

Empty reports whether the run recorded nothing. The runtime does not call the reporter for an empty Outcome.

func (Outcome) Failed

func (o Outcome) Failed() bool

Failed reports whether the run recorded an error or a panic, the condition for the default reporter's exit floor (which leaves out an error the run's own signal caused).

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.

func (*OutputFile) Write added in v1.4.0

func (f *OutputFile) Write(p []byte) (int, error)

Write writes p to the file, or to stdout for "-".

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

type PanicError struct {
	Value any
	Stack []byte
}

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.

func (ParseKind) String

func (k ParseKind) String() string

String renders the kind as a short, stable label (for logs and tests).

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 NewParser

func NewParser() *Parser

NewParser returns rotini's default Parser.

func (*Parser) Parse

func (p *Parser) Parse(rtx *Context, out any) error

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

func PresenceOf[T any](v T) Presence

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:

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

func (p *Program) Execute() error

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

func (p *Program) Run(argv []string) (int, error)

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

func (p *Program) RunContext(ctx context.Context, argv []string) (int, error)

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

func (p *Program) With(opts ...Option) *Program

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

func (p *Program) WithArgs(args []string) *Program

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

func (p *Program) WithArgv0(name string) *Program

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

func (p *Program) WithBufferedOutput(enabled bool) *Program

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

func (p *Program) WithClock(now func() time.Time) *Program

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

func (p *Program) WithCompletionDescriptions(fn func(rtx *Context) bool) *Program

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

func (p *Program) WithCompletionMessages(fn func(rtx *Context) bool) *Program

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

func (p *Program) WithContext(ctx context.Context) *Program

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

func (p *Program) WithDir(dir string) *Program

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

func (p *Program) WithEnviron(env []string) *Program

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

func (p *Program) WithExit(fn func(int)) *Program

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

func (p *Program) WithFeatures(features ...Feature) *Program

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

func (p *Program) WithHelp(help HelpFunc) *Program

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

func (p *Program) WithLifecycle(fn Lifecycle) *Program

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

func (p *Program) WithOutputChecks(enabled bool) *Program

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

func (p *Program) WithPanicRecover(enabled bool) *Program

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

func (p *Program) WithParser(parser *Parser) *Program

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

func (p *Program) WithReporter(fn Reporter) *Program

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

func (p *Program) WithResolver(fn Resolver) *Program

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

func (p *Program) WithSignals(sigs ...os.Signal) *Program

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

func (p *Program) WithStderr(w io.Writer) *Program

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

func (p *Program) WithStdin(r io.Reader) *Program

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

func (p *Program) WithStdout(w io.Writer) *Program

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

func (p *Program) WithTeardownOnPanic(enabled bool) *Program

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

func (p *Program) WithTerminationTimeout(d time.Duration) *Program

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

func (p *Program) WithVersion(version string) *Program

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

func (p *Program) WithoutSignalHandling() *Program

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

type Reporter func(ctx context.Context, rtx *Context, out Outcome)

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

func StructuredReporter(structured func(rtx *Context) bool) Reporter

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

func SelectFields(v any, at string, fields []string) (Selection, error)

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

func (s Selection) MarshalJSON() ([]byte, error)

MarshalJSON writes the selected fields.

func (Selection) MarshalTOML added in v1.4.0

func (s Selection) MarshalTOML() (any, error)

MarshalTOML hands the selected fields to a toml encoder.

func (Selection) MarshalYAML added in v1.4.0

func (s Selection) MarshalYAML() (any, error)

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

func (s *Suggestor) Closest(input string, candidates []string) (suggestion string, ok bool)

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

func (s *Suggestor) For(err error) []string

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

func (s *Suggestor) Suggest(input string, candidates []string) []string

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

func (s *Suggestor) WithMaxResults(n int) *Suggestor

WithMaxResults caps how many suggestions Suggestor.Suggest and Suggestor.For return (default 3). Zero or less means no cap.

func (*Suggestor) WithMinScore

func (s *Suggestor) WithMinScore(score float64) *Suggestor

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

type TopicDef struct {
	Name    string
	Summary string
}

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.

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}}'`.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL