Command with precision.
Thrawn is a composable command-tree framework for Zig 0.16. It provides the mechanics shared by serious command-line applications while leaving application state and domain logic to the application.
- nested command trees, aliases, defaults, hidden and deprecated commands
- named positional arguments and validation
--long=value,-p8080, clustered short switches, and--end-of-options- typed string/integer/float/choice option values
- required, default, and repeatable options
- option dependencies, conflicts, and mutually exclusive groups
- inherited global options
- grouped generated help with full nested command paths
- configurable
help,-h, and--helptoken handling - typed application state available to handlers and hooks through
Context.state(T) - before/after hooks inherited through the command tree
- passthrough commands for wrapping other executables
- generated Markdown and man-page documentation
- Bash, Zsh, and Fish completion generation
- context-sensitive child, local-option, and inherited-global-option completion
- application-defined dynamic completion callbacks
- first-class
th.testingCLI harness - explicit stdout/stderr writers and conventional exit codes
- command-tree validation and typo suggestions
- native CI on Linux, macOS ARM64, macOS x86_64, and Windows x86_64
- separate consumer-package builds in Debug and ReleaseSafe
For a tagged release:
zig fetch --save git+https://github.com/chrisbirster/thrawn#v0.2.0Then expose the dependency module from your build.zig and import it normally:
const th = @import("thrawn");See docs/installation.md for the complete dependency setup.
Pass application-owned state without globals:
const App = struct {
verbose: bool = false,
};
fn status(ctx: *th.Context) !void {
const app = ctx.state(App) orelse return error.MissingAppState;
if (app.verbose) try ctx.print("verbose status\n", .{});
}
var app: App = .{ .verbose = true };
const code = try th.runWithOptions(init, &root_command, .{
.state = &app,
});The state pointer is application-owned and must remain valid for the duration of the run.
By default, Thrawn recognizes help, -h, and --help. Applications that want a real help command can disable only the literal command token while retaining the option forms:
const help_command: th.Command = .{
.name = "help",
.handler = showHelpTopic,
};
const code = try th.runWithOptions(init, &root_command, .{
.resolve = .{
.help_tokens = .{ .command = null },
},
});Now app help topic resolves to the declared command while app --help and app command --help still use Thrawn's generated help.
const deploy: th.Command = .{
.name = "deploy",
.options = &.{
.{ .long = "dry-run", .short = 'n' },
.{
.long = "retries",
.short = 'r',
.kind = .value,
.value_type = .integer,
.default_value = "3",
},
.{
.long = "tag",
.short = 't',
.kind = .value,
.repeatable = true,
},
},
.handler = runDeploy,
};Thrawn accepts normal CLI forms such as --retries=3, -r3, -n, clustered boolean switches, and -- to stop option parsing.
Parent .before hooks run root-to-leaf and .after hooks run leaf-to-root. Commands marked .passthrough = true stop interpreting option-looking tokens after their first positional argument, which makes wrapper commands straightforward.
The same command tree can generate Markdown or man-page content:
try th.docs.writeMarkdown(writer, &root_command);
try th.docs.writeMan(writer, &root_command);Run the repository documentation example with:
zig build docsvar result = try th.testing.run(std.testing.allocator, &root, &.{ "deploy", "ship", "--dry-run" });
defer result.deinit();
try result.expectExit(0);
try result.expectStdoutContains("ship");
try result.expectStderr("");zig build
zig build test
zig build examples
zig build run -- --help
zig build run -- --verbose fleet status
zig build run -- fleet deploy destroyer -r3 -talpha --tag=beta
zig build run -- exec node --inspect index.js
zig build run -- completion zsh
zig build docsDevelopment follows feature/* -> dev -> main -> vX.Y.Z.
Thrawn is licensed under the MIT License. See LICENSE.