Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
name: Publish API documentation
name: Publish API Documentation

on:
workflow_dispatch:
push:
tags:
- 'v*'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: write

Expand All @@ -20,20 +16,20 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Zig 0.16.0
run: |
curl -sSfL https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz | tar -xJ
echo "$PWD/zig-x86_64-linux-0.16.0" >> "$GITHUB_PATH"
- name: Install Zig
uses: goto-bus-stop/setup-zig@v2
with:
version: '0.16.0'

- name: Install dependencies
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y make

- name: Generate documentation
run: make docs

- name: Deploy to GitHub pages
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/zig-master.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Zig Master Canary

on:
workflow_dispatch:
schedule: # Run every Monday at 06:00 (UTC)
- cron: '0 6 * * 1'

permissions:
contents: read

jobs:
canary:
runs-on: ubuntu-latest
continue-on-error: true

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true

- name: Install Zig (master)
uses: goto-bus-stop/setup-zig@v2
with:
version: master

- name: Build
run: make build

- name: Run tests
run: make test
15 changes: 9 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Priorities, in order:
1. Correctness of argument parsing, flag resolution, and help output.
2. Minimal public API for defining and running command trees from other Zig projects.
3. Zero non-Zig dependencies, maintainable, and well-tested code.
4. Cross-platform support (Linux, macOS, and Windows).
4. Cross-platform support (Linux, macOS, Windows, and Wasm).

## Core Rules

Expand All @@ -35,14 +35,17 @@ Priorities, in order:

- `src/lib.zig`: Public API entry point. Re-exports `Command`, `CommandOptions`, `Flag`, `FlagType`, `FlagValue`, `PositionalArg`, `CommandContext`,
`styles`, and `Error`.
- `src/chilli/command.zig`: The `Command` struct (command tree, init/deinit, `run`, subcommand and flag registration).
- `src/chilli/types.zig`: Core types (`CommandOptions`, `Flag`, `FlagType`, `FlagValue`, `PositionalArg`) and the `parseValue` helper.
- `src/chilli/command.zig`: The `Command` struct, `CommandOptions`, init/deinit, `run`, subcommand and flag registration, and the private help-output
printers.
- `src/chilli/types.zig`: Core types (`Flag`, `FlagType`, `FlagValue`, `PositionalArg`) and the `parseBool` / `parseValue` helpers.
- `src/chilli/parser.zig`: Argument-string parser (`ArgIterator`, `ParsedFlag`, long/short/grouped flag handling, positional handling).
- `src/chilli/context.zig`: The `CommandContext` passed to each command's `exec` function for typed flag and argument access.
- `src/chilli/errors.zig`: Error types produced by parsing and type coercion.
- `src/chilli/utils.zig`: Shared helpers (`styles` for ANSI colors, `parseBool`, and other small utilities).
- `src/chilli/styles.zig`: ANSI escape-code constants plus a TTY-gated `s()` wrapper used by the help and error output.
- `src/chilli/deprecation.zig`: Warning formatter and stderr emitter for deprecated commands, flags, and positional arguments, with
`CHILLI_NO_DEPRECATION_WARNINGS` suppression.
- `examples/`: Self-contained example programs (`e1_simple_cli.zig` through `e8_flags_and_args.zig`) built as executables via `build.zig`.
- `.github/workflows/`: CI workflows (`tests.yml` for unit tests on Linux and Windows, `docs.yml` for API doc deployment).
- `.github/workflows/`: CI workflows (`tests.yml` for unit tests on Linux, macOS, and Windows, `docs.yml` for API doc deployment).
- `build.zig` / `build.zig.zon`: Zig build configuration and package metadata.
- `Makefile`: GNU Make wrapper around `zig build` targets.
- `docs/`: Generated API docs land in `docs/api/` (produced by `make docs`).
Expand Down Expand Up @@ -135,7 +138,7 @@ Good first tasks:

Before coding:

1. Modules affected by the change (`command`, `parser`, `types`, `context`, `errors`, or `utils`).
1. Modules affected by the change (`command`, `parser`, `types`, `context`, `errors`, `styles`, or `deprecation`).
2. Whether the change is user-visible in `--help` output, and if so, which examples will surface it.
3. Public API impact, i.e. whether the change adds to or alters anything re-exported from `src/lib.zig`, and is therefore additive or breaking.
4. Cross-platform implications, especially for anything that touches environment variables, the filesystem, or process-args encoding.
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<h2>Chilli</h2>

[![Tests](https://img.shields.io/github/actions/workflow/status/CogitatorTech/chilli/tests.yml?label=tests&style=flat&labelColor=282c34&logo=github)](https://github.com/CogitatorTech/chilli/actions/workflows/tests.yml)
[![Zig Version](https://img.shields.io/badge/Zig-0.16.0-orange?logo=zig&labelColor=282c34)](https://ziglang.org/download)
[![Zig](https://img.shields.io/badge/zig-0.16.0-F7A41D?style=flat&labelColor=282c34&logo=zig)](https://ziglang.org/download/)
[![Docs](https://img.shields.io/badge/docs-read-blue?style=flat&labelColor=282c34&logo=read-the-docs)](https://CogitatorTech.github.io/chilli)
[![Examples](https://img.shields.io/badge/examples-view-green?style=flat&labelColor=282c34&logo=zig)](https://github.com/CogitatorTech/chilli/tree/main/examples)
[![Release](https://img.shields.io/github/release/CogitatorTech/chilli.svg?label=release&style=flat&labelColor=282c34&logo=github)](https://github.com/CogitatorTech/chilli/releases/latest)
Expand Down Expand Up @@ -56,14 +56,16 @@ Replace `<branch_or_tag>` with the desired branch or tag, like `main` (for the d
(for the specified release version).
This command will download Chilli and add it to Zig's global cache and update your project's `build.zig.zon` file.

##### Zig Version Support

Zig version supported by the main releases of Chilli:

| Zig | Chilli Tags |
|----------|-------------|
| `0.16.0` | `v0.3.x` |
| `0.15.x` | `v0.2.x` |

The `main` branch normally tracks the latest (non-developmental) Zig release.
The `main` branch normally is developed and build using the latest (non-developmental) Zig release.

#### Adding to Build Script

Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ It outlines features to be implemented and their current status.
- [x] Simple, declarative API for building commands
- [x] Named access for all flags and arguments
- [x] Shared context data for passing application state
- [ ] Deprecation notices for commands or flags
- [x] Deprecation notices for commands, flags, and positional arguments
- [ ] Built-in TUI components (like spinners and progress bars)
- [ ] Automatic command history and completion
2 changes: 1 addition & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.{
.name = .chilli,
.version = "0.3.1",
.version = "0.3.2",
.fingerprint = 0x6c259741ae4f5f73, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.paths = .{
Expand Down
132 changes: 126 additions & 6 deletions src/chilli/command.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const context = @import("context.zig");
const styles = @import("styles.zig");
const types = @import("types.zig");
const errors = @import("errors.zig");
const deprecation = @import("deprecation.zig");

/// Defines the configuration for a `Command`.
///
Expand All @@ -27,6 +28,11 @@ pub const CommandOptions = struct {
version: ?[]const u8 = null,
/// The name of the section under which this command should be grouped in a parent's help message.
section: []const u8 = "Commands",
/// If set, marks the command as deprecated. The value is a free-form
/// reason or replacement suggestion. The command is still dispatched,
/// and its `exec` is still run, but a warning is written to stderr when
/// this command resolves as the leaf of the invocation chain.
deprecated: ?[]const u8 = null,
};

/// Represents a single command in a CLI application.
Expand Down Expand Up @@ -273,6 +279,20 @@ pub const Command = struct {

try parser.validateArgs(current_cmd);

// Deprecation warnings for the resolved command and for any
// deprecated positional slots the user actually filled. Flag-level
// warnings fire inside the parser at parse time.
if (current_cmd.options.deprecated) |reason| {
deprecation.emit(current_cmd.allocator, "command", current_cmd.options.name, reason);
}
for (current_cmd.positional_args.items, 0..) |arg, i| {
if (arg.deprecated) |reason| {
if (i < current_cmd.parsed_positionals.items.len) {
deprecation.emit(current_cmd.allocator, "positional argument", arg.name, reason);
}
}
}

// Success, clear the out_failed_cmd
out_failed_cmd.* = null;

Expand Down Expand Up @@ -352,10 +372,12 @@ pub const Command = struct {
// Note: the automatic --version flag is added in `init` so `run` is
// safe to call more than once on the same command.

// Collect process arguments via iterator
// Collect process arguments via iterator. `initAllocator` is the
// cross-platform form: plain `init` is a compile error on Windows
// and WASI, where parsing the command line requires allocation.
var args_list: std.ArrayList([]const u8) = .empty;
defer args_list.deinit(self.allocator);
var args_iter = std.process.Args.Iterator.init(args);
var args_iter = try std.process.Args.Iterator.initAllocator(args, self.allocator);
defer args_iter.deinit();
while (args_iter.next()) |arg| {
try args_list.append(self.allocator, arg);
Expand Down Expand Up @@ -526,7 +548,9 @@ fn printAlignedCommands(commands: []*Command, writer: anytype) !void {
}

for (0..max_width - current_width + 2) |_| try writer.writeByte(' ');
try writer.print("{s}\n", .{cmd.options.description});
try writer.print("{s}", .{cmd.options.description});
if (cmd.options.deprecated != null) try writer.print(" (deprecated)", .{});
try writer.print("\n", .{});
}
}

Expand Down Expand Up @@ -564,6 +588,7 @@ fn printAlignedFlags(cmd: *const Command, writer: anytype) !void {
.Float => |v| try writer.print(" (default: {})", .{v}),
.String => |v| try writer.print(" (default: \"{s}\")", .{v}),
}
if (flag.deprecated != null) try writer.print(" (deprecated)", .{});
try writer.print("\n", .{});
}
}
Expand All @@ -580,12 +605,14 @@ fn printAlignedPositionalArgs(cmd: *const Command, writer: anytype) !void {
try writer.print("{s}", .{arg.description});

if (arg.variadic) {
try writer.print(" (variadic)\n", .{});
try writer.print(" (variadic)", .{});
} else if (arg.is_required) {
try writer.print(" (required)\n", .{});
try writer.print(" (required)", .{});
} else {
try writer.print(" (optional)\n", .{});
try writer.print(" (optional)", .{});
}
if (arg.deprecated != null) try writer.print(" (deprecated)", .{});
try writer.print("\n", .{});
}
}

Expand Down Expand Up @@ -1251,6 +1278,99 @@ test "help: printAlignedPositionalArgs produces correct padding" {
try std.testing.expect(std.mem.indexOf(u8, output, "(optional)") != null);
}

test "deprecation: printAlignedFlags marks deprecated flag" {
const allocator = std.testing.allocator;
styles.setEnabled(false);
defer styles.setEnabled(false);

var cmd = try Command.init(allocator, .{ .name = "app", .description = "", .exec = dummyExec });
defer cmd.deinit();
try cmd.addFlag(.{
.name = "old",
.type = .Bool,
.default_value = .{ .Bool = false },
.description = "Old flag",
.deprecated = "use --new",
});

var buf: [2048]u8 = undefined;
var writer = TestBufWriter{ .buf = &buf };
try printAlignedFlags(cmd, &writer);
const out = writer.getWritten();
try std.testing.expect(std.mem.indexOf(u8, out, "--old") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "(deprecated)") != null);
}

test "deprecation: printAlignedPositionalArgs marks deprecated arg" {
const allocator = std.testing.allocator;
styles.setEnabled(false);
defer styles.setEnabled(false);

var cmd = try Command.init(allocator, .{ .name = "app", .description = "", .exec = dummyExec });
defer cmd.deinit();
try cmd.addPositional(.{
.name = "path",
.description = "Path to file",
.is_required = true,
.deprecated = "use --input flag",
});

var buf: [2048]u8 = undefined;
var writer = TestBufWriter{ .buf = &buf };
try printAlignedPositionalArgs(cmd, &writer);
const out = writer.getWritten();
try std.testing.expect(std.mem.indexOf(u8, out, "path") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "(required)") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "(deprecated)") != null);
}

test "deprecation: printAlignedCommands marks deprecated subcommand" {
const allocator = std.testing.allocator;
styles.setEnabled(false);
defer styles.setEnabled(false);

var root = try Command.init(allocator, .{ .name = "root", .description = "", .exec = dummyExec });
defer root.deinit();
const old_sub = try Command.init(allocator, .{
.name = "old-sub",
.description = "The old way",
.exec = dummyExec,
.deprecated = "use 'new-sub' instead",
});
try root.addSubcommand(old_sub);

var buf: [2048]u8 = undefined;
var writer = TestBufWriter{ .buf = &buf };
try printAlignedCommands(root.subcommands.items, &writer);
const out = writer.getWritten();
try std.testing.expect(std.mem.indexOf(u8, out, "old-sub") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "(deprecated)") != null);
}

test "deprecation: flag is still parsed and honored when deprecated" {
// Contract: deprecation never breaks existing invocations. The flag
// must still be available via getFlagValue after parsing.
const allocator = std.testing.allocator;
deprecation.setSuppressedForTests(true); // silence the warning for this test
defer deprecation.setSuppressedForTests(false);

var cmd = try Command.init(allocator, .{ .name = "app", .description = "", .exec = dummyExec });
defer cmd.deinit();
try cmd.addFlag(.{
.name = "legacy-mode",
.type = .Bool,
.default_value = .{ .Bool = false },
.description = "",
.deprecated = "removed in v0.5",
});

var failed_cmd: ?*const Command = null;
try cmd.execute(&[_][]const u8{"--legacy-mode"}, null, &failed_cmd);
try std.testing.expect(failed_cmd == null);
const v = cmd.getFlagValue("legacy-mode").?;
try std.testing.expect(v.Bool);
}

test "help: printUsageLine produces correct output" {
const allocator = std.testing.allocator;
var cmd = try Command.init(allocator, .{ .name = "app", .description = "", .exec = dummyExec });
Expand Down
Loading
Loading