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
35 changes: 26 additions & 9 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,38 @@ permissions:

jobs:
tests:
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
zig-url: https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz
zig-dir: zig-x86_64-linux-0.16.0
- os: macos-latest
zig-url: https://ziglang.org/download/0.16.0/zig-aarch64-macos-0.16.0.tar.xz
zig-dir: zig-aarch64-macos-0.16.0
- os: windows-latest
zig-url: https://ziglang.org/download/0.16.0/zig-x86_64-windows-0.16.0.zip
zig-dir: zig-x86_64-windows-0.16.0

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Zig 0.16.0
- name: Install Zig 0.16.0 (Unix)
if: runner.os != 'Windows'
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"
curl -sSfL ${{ matrix.zig-url }} | tar -xJ
echo "$PWD/${{ matrix.zig-dir }}" >> "$GITHUB_PATH"

- name: Install dependencies
- name: Install Zig 0.16.0 (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
sudo apt-get update
sudo apt-get install -y make
Invoke-WebRequest -Uri "${{ matrix.zig-url }}" -OutFile zig.zip
Expand-Archive zig.zip -DestinationPath .
echo "$PWD\${{ matrix.zig-dir }}" | Out-File -Append -FilePath $env:GITHUB_PATH

- name: Run the tests
run: make test
- name: Run tests
run: zig build test --summary all
156 changes: 156 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# AGENTS.md

This file provides guidance to coding agents collaborating on this repository.

## Mission

Chilli is a command-line interface (CLI) microframework for Zig.
It turns a declarative description of commands, flags, and positional arguments into a parser, help generator, and dispatcher, with zero external
dependencies.
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).

## Core Rules

- Use English for code, comments, docs, and tests.
- Prefer small, focused changes over large refactoring.
- Add comments only when they clarify non-obvious behavior.
- Do not add features, error handling, or abstractions beyond what is needed for the current task.
- Keep the project dependency-free: no external Zig packages or C libraries unless explicitly agreed.

## Writing Style

- Use Oxford commas in inline lists: "a, b, and c" not "a, b, c".
- Do not use em dashes. Restructure the sentence, or use a colon or semicolon instead.
- Avoid colorful adjectives and adverbs. Write "TCP proxy" not "lightweight TCP proxy", "scoring components" not "transparent scoring components".
- Use noun phrases for checklist items, not imperative verbs. Write "redundant index detection" not "detect redundant indexes".
- Headings in Markdown files must be in the title case: "Build from Source" not "Build from source". Minor words (a, an, the, and, but, or, for, in,
on, at, to, by, of, is, are, was, were, be) stay lowercase unless they are the first word.

## Repository Layout

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

## Architecture

### Command Tree

A Chilli application is a tree of `Command` nodes. Each node owns its `flags`, `positional_args`, optional `exec`
function, and a list of subcommands. `Command.init` allocates a node; `Command.deinit` recursively frees the subtree, so
downstream users call `deinit` only on the root.

### Parsing Pipeline

Arguments flow through: `ArgIterator` over `[][]const u8` (`parser.zig`) -> per-node flag and positional resolution
(`parser.zig` + `command.zig`) -> `CommandContext` population (`context.zig`) -> `exec` dispatch on the resolved leaf
command.

### Flag and Positional Types

`FlagType` (`types.zig`) enumerates the supported value kinds (`Bool`, `Int`, `Float`, `String`). `FlagValue` is the matching-tagged union.
`types.parseValue` is the single conversion point from raw strings into a `FlagValue`; every new type or coercion belongs here.

### Help and Version Output

Help and version text are generated automatically from the command tree at runtime by `command.zig`, using the metadata in `CommandOptions` (name,
description, version, sections) and the registered flags and positional args. Grouping into
named sections is supported; custom help formatting beyond that should be added sparingly.

### Public API Surface

Everything re-exported from `src/lib.zig` is part of the public API. Changes to names or signatures there are breaking.
The rest of `src/chilli/` is internal and may be refactored freely as long as the public surface and its behavior are
preserved.

### Dependencies

Chilli has **no external Zig or C dependencies**.
The only `build.zig.zon` entries should be Chilli itself.
Please do not add dependencies without prior discussion.

## Zig Conventions

- Zig version: 0.16.0 (as declared in `build.zig.zon` and the Makefile's `ZIG_LOCAL` path).
- Formatting is enforced by `zig fmt`. Run `make format` before committing.
- Naming follows Zig standard-library conventions: `camelCase` for functions (e.g. `addFlag`, `getFlag`, `parseBool`), `snake_case` for local
variables and struct fields, `PascalCase` for types and structs, and `SCREAMING_SNAKE_CASE` for top-level compile-time constants.

## Required Validation

Run the relevant targets for any change:

| Target | Command | What It Runs |
|----------------|----------------------------------|------------------------------------------------------------------|
| Unit tests | `make test` | Inline `test` blocks across `src/lib.zig` and `src/chilli/*.zig` |
| Lint | `make lint` | Checks Zig formatting with `zig fmt --check src examples` |
| Single example | `make run EXAMPLE=e1_simple_cli` | Builds and runs one example program |
| All examples | `make run` | Builds and runs every example under `examples/` |
| Docs | `make docs` | Generates API docs into `docs/api` |
| Everything | `make all` | Runs `build`, `test`, `lint`, and `docs` |

## First Contribution Flow

1. Read the relevant module under `src/chilli/` (often `command.zig`, `parser.zig`, or `types.zig`).
2. Implement the smallest change that covers the requirement.
3. Add or update inline `test` blocks in the changed Zig module to cover the new behavior.
4. Run `make test` and `make lint`.
5. If parser or help-output behavior changed, also exercise the examples with `make run` and confirm the `--help` output is still correct.

Good first tasks:

- New example under `examples/` that demonstrates an API pattern, listed in `examples/README.md`.
- New flag-coercion case in `src/chilli/types.zig` `parseValue` (with an inline `test` block).
- Error message refinement in `src/chilli/errors.zig`, paired with a test that asserts the exact message.
- Help-formatting refinement in `src/chilli/command.zig`.

## Testing Expectations

- Unit and regression tests live as inline `test` blocks in the module they cover (`src/lib.zig` and `src/chilli/*.zig`). There is no separate
`tests/` directory.
- Tests are discovered automatically via `std.testing.refAllDecls(@This())` in `src/lib.zig`, so new `test` blocks only need to live in a module that
is reachable from `lib.zig`.
- Every new public function, flag type, or parser branch must ship with at least one `test` block that exercises it, including the error paths where
applicable.
- Tests that touch argument parsing should build their input as `[][]const u8` explicitly, not read from `std.process.args()`, so they work under
`zig test` without a real process-args environment.
- No public API change is complete without a test covering the new or changed behavior.

## Change Design Checklist

Before coding:

1. Modules affected by the change (`command`, `parser`, `types`, `context`, `errors`, or `utils`).
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.

Before submitting:

1. `make test` passes.
2. `make lint` passes.
3. `make run` succeeds for all examples when touching the parser, command dispatch, or help-output code.
4. Docs updated (`make docs`) if the public API surface changed, and `ROADMAP.md` ticked or updated if a listed item was implemented.

## Commit and PR Hygiene

- Keep commits scoped to one logical change.
- PR descriptions should include:
1. Behavioral change summary.
2. Tests added or updated.
3. Whether examples were run locally (yes/no), and on which OS.
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ would like to work on or if it has already been resolved.

### Development Workflow

> [!IMPORTANT]
> If you're using an AI-assisted coding tool like Claude Code or Codex, make sure the AI follows the instructions in the [AGENTS.md](AGENTS.md) file.

#### Prerequisites

Install GNU Make on your system if it's not already installed.
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ docs: ## Generate API documentation
@echo "Generating API documentation..."
@$(ZIG) build docs

serve-docs: ## Serve the generated documentation on a local server
serve-docs: docs ## Serve the generated documentation on a local server
@echo "Serving documentation at http://localhost:8000..."
@cd docs/api && python3 -m http.server 8000

Expand Down
35 changes: 22 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ A microframework for creating command-line applications in Zig

---

Chilli is a lightweight command-line interface (CLI) framework for the Zig programming language.
Its goal is to make it easy to create structured, maintainable, and user-friendly CLIs with minimal boilerplate,
while being small and fast, and not getting in the way of your application logic.
Chilli is a command-line interface (CLI) framework for Zig.
It turns a declarative description of commands, flags, and positional arguments into a parser, help generator, and
dispatcher, with zero external dependencies and minimal boilerplate.

### Features

- Provides a simple, low-overhead, declarative API for building CLI applications
- Provides a declarative API for building CLI applications
- Supports nested commands, subcommands, and aliases
- Provides type-safe parsing for flags, positional arguments, and environment variables
- Supports generating automatic `--help` and `--version` output with custom sections
Expand All @@ -52,10 +52,19 @@ Run the following command in the root directory of your project to download Chil
zig fetch --save=chilli "https://github.com/CogitatorTech/chilli/archive/<branch_or_tag>.tar.gz"
```

Replace `<branch_or_tag>` with the desired branch or tag, like `main` (for the development version) or `v0.2.3`
Replace `<branch_or_tag>` with the desired branch or tag, like `main` (for the development version) or `v0.3.0`
(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 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.

#### Adding to Build Script

Next, modify your `build.zig` file to make Chilli available to your build target as a module.
Expand Down Expand Up @@ -150,17 +159,17 @@ You can now run your CLI application with the `--help` flag to see the output be

```bash
$ ./your-cli-app --help
your-cli-app v0.3.0
A new CLI built with Chilli
Version: v0.1.0

USAGE:
your-cli-app [FLAGS]
Usage:
your-cli-app [flags]

FLAGS:
-n, --name <string> The name to greet [default: World]
--excitement <int> How excited to be [default: 1]
-h, --help Prints help information
-V, --version Prints version information
Flags:
-h, --help Shows help information for this command [Bool] (default: false)
--version Print version information and exit [Bool] (default: false)
-n, --name The name to greet [String] (default: "World")
--excitement How excited to be [Int] (default: 1)
```

---
Expand Down
8 changes: 4 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@ It outlines features to be implemented and their current status.
> [!IMPORTANT]
> This roadmap is a work in progress and is subject to change.

- **Command Structure**
- **Command Structure**
- [x] Nested commands and subcommands
- [x] Command aliases and single-character shortcuts
- [x] Persistent flags (flags on parent commands are available to children)

- **Argument & Flag Parsing**
- **Argument & Flag Parsing**
- [x] Long flags (`--verbose`), short flags (`-v`), and grouped boolean flags (`-vf`)
- [x] Positional Arguments (supports required, optional, and variadic)
- [x] Type-safe access for flags and arguments (e.g., `ctx.getFlag("count", i64)`)
- [x] Reading flag values from environment variables

- **Help & Usage Output**
- **Help & Usage Output**
- [x] Automatic and context-aware `--help` flag
- [x] Automatic `--version` flag
- [x] Clean, aligned help output for commands, flags, and arguments
- [x] Grouping subcommands into custom sections

- **Developer Experience**
- **Developer Experience**
- [x] Simple, declarative API for building commands
- [x] Named access for all flags and arguments
- [x] Shared context data for passing application state
Expand Down
40 changes: 35 additions & 5 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,18 @@ pub fn build(b: *std.Build) void {
const docs_step = b.step("docs", "Generate API documentation");
const doc_install_path = "docs/api";

// Zig's `-femit-docs=<path>` writes the leaf dir but does not create
// intermediate parents, and git does not track empty directories, so a
// fresh checkout may have no `docs/` at all. Create it portably here
// (idempotent: createDirPath is a no-op when the directory already exists).
const ensure_docs_dir = EnsureDirStep.create(b, "docs");
const gen_docs_cmd = b.addSystemCommand(&[_][]const u8{
b.graph.zig_exe, // Use the same zig that is running the build
"build-lib",
"src/lib.zig",
"-femit-docs=" ++ doc_install_path,
});

const mkdir_cmd = b.addSystemCommand(&[_][]const u8{
"mkdir", "-p", doc_install_path,
});
gen_docs_cmd.step.dependOn(&mkdir_cmd.step);
gen_docs_cmd.step.dependOn(&ensure_docs_dir.step);

docs_step.dependOn(&gen_docs_cmd.step);

Expand Down Expand Up @@ -100,3 +101,32 @@ pub fn build(b: *std.Build) void {
}
}
}

/// Build step that ensures a directory (relative to the build root) exists.
/// Runs `std.fs.Dir.createDirPath` at make-time, so it only fires when a
/// step that depends on it is actually being built. Portable across Linux,
/// macOS, and Windows.
const EnsureDirStep = struct {
step: std.Build.Step,
sub_path: []const u8,

fn create(b: *std.Build, sub_path: []const u8) *EnsureDirStep {
const self = b.allocator.create(EnsureDirStep) catch @panic("OOM");
self.* = .{
.step = std.Build.Step.init(.{
.id = .custom,
.name = b.fmt("ensure {s}/", .{sub_path}),
.owner = b,
.makeFn = make,
}),
.sub_path = sub_path,
};
return self;
}

fn make(step: *std.Build.Step, options: std.Build.Step.MakeOptions) anyerror!void {
_ = options;
const self: *EnsureDirStep = @fieldParentPtr("step", step);
try step.owner.build_root.handle.createDirPath(step.owner.graph.io, self.sub_path);
}
};
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.0",
.version = "0.3.1",
.fingerprint = 0x6c259741ae4f5f73, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.paths = .{
Expand Down
11 changes: 0 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,3 @@ dependencies = [
"python-dotenv (>=1.1.0,<2.0.0)",
"pre-commit (>=4.2.0,<5.0.0)"
]

[project.optional-dependencies]
dev = [
"pytest (>=8.0.1,<9.0.0)",
"pytest-cov (>=6.0.0,<7.0.0)",
"pytest-mock (>=3.14.0,<4.0.0)",
"pytest-asyncio (>=0.26.0,<0.27.0)",
"mypy (>=1.11.1,<2.0.0)",
"ruff (>=0.9.3,<1.0.0)",
"icecream (>=2.1.4,<3.0.0)"
]
Loading
Loading