# zig-clap: parse parameters at comptime so the argument types follow from the spec

> zig-clap is a command line argument parser for Zig whose central trick is that the parameter specification is written as a string literal and parsed at comptime, so the fields of the result struct are typed from the names in that string. That is why --number takes a usize and res.args.number is a usize, and why swapping in your own parsers changes the type.

**Hejsil/zig-clap** — Command line argument parsing library

- Repository: https://github.com/Hejsil/zig-clap
- Stars: 1,629 · Forks: 100
- Language: Zig
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/hejsil-zig-clap

## The mechanism: a spec string parsed at comptime

zig-clap is described as a simple and easy to use command line argument parser library for Zig. What it actually does is unusual enough to be the whole story.

The parameters are declared as a multiline string and parsed at comptime:

```zig
const params = comptime clap.parseParamsComptime(
    \\-h, --help             Display this help and exit.
    \\-n, --number <usize>   An option parameter, which takes a value.
    \\-s, --string <str>...  An option parameter which can be specified multiple times.
    \\<str>...
    \\
);
```

From that string the library derives an array of Param(Help) values at compile time, and the parse call then returns a struct whose fields are typed. The README is explicit about the consequence: since --number takes a usize, the field res.args.number has the type usize. So the declaration of a value placeholder in angle brackets is what determines the field type, and there is no separate schema anywhere.

Two rules explain the field names. The result has an args field with one field per non positional parameter, named after the longest name of that parameter, so -h, --help produces a field called help. And it has a positionals field, which is a tuple with one field per positional parameter, so the single <str>... becomes positionals[0].

The consequence for a reader is that the help text and the type system are the same artifact. You cannot have documentation that disagrees with the parser, because they are generated from the same string.

## The typed field comes from a parser table, and you can replace it

The typing mechanism has one indirection that matters. The field type is not read directly from the placeholder name; it comes from a parser table. The README states that the typing works because clap.parsers.default has a field called usize containing a parser that returns usize, and that you can pass something other than clap.parsers.default if you want a different mapping.

The second example shows that table being built locally:

```zig
const YesNo = enum { yes, no };
const parsers = comptime .{
    .STR = clap.parsers.string,
    .FILE = clap.parsers.string,
    .INT = clap.parsers.int(usize, 10),
    .ANSWER = clap.parsers.enumeration(YesNo),
};
```

With that table in place a placeholder named INT produces a usize parsed in base ten, and a placeholder named ANSWER produces a YesNo enum. The result field is then an optional enum and you unwrap it with if (res.args.answer) |a|.

So the placeholder name is a key into a parser table you control, which is what makes this extensible rather than a fixed set of types. Adding your own parser is a matter of putting a function in the table under the name you intend to write in the spec.

The same call also takes options that change parsing behaviour. assignment_separators is the notable one:

```zig
.assignment_separators = "=:",
```

which makes --number=1 and --number:1 both valid, so a value separator style is a project decision rather than a hardcoded equals sign.

## Installing: the tag or the branch depends on your Zig

Installation is a zig fetch plus a build.zig entry, and the README splits it by how you obtained Zig, because developers tend to use either the latest tagged release or the latest build of Zig's master branch.

For a tagged Zig, fetch a tagged zig-clap, replacing the placeholder with the version you want:

```sh
zig fetch --save https://github.com/Hejsil/zig-clap/archive/refs/tags/<REPLACE ME>.tar.gz
```

For a Zig built from master, fetch the git branch instead:

```sh
zig fetch --save git+https://github.com/Hejsil/zig-clap
```

Then wire the module into your build:

```zig
const clap = b.dependency("clap", .{});
exe.root_module.addImport("clap", clap.module("clap"));
```

The module name in that import is what the examples use, with const clap = @import("clap") at the bottom of each one.

The important thing about that split is that it is not a convenience, it is a compatibility statement. The library has to match the language version it was written against, so a project on a released Zig and a project on a nightly Zig use different versions of the same parser.

## Syntax coverage: chaining, tallies and repeated options

The feature list in the README is short and specific, which is a good sign for a parser.

Short arguments are -a, and they chain. Where a and b do not take values, -abc works. Multiple specifications are tallied, so -v -v is two occurrences of the same flag, which is how a verbosity count arrives as a number.

Long arguments are --long. Values can be passed with spacing or with an equals sign, as in -a 100 and -a=100, and short arguments also accept a value with no separator at all, -a100. All of this works while chaining: -ba 100, -ba=100 and -ba100 are described as equivalent. That last form is unusual enough to be worth knowing about, because it is where a mistyped flag name can silently consume digits.

Repeated options are first class, so -e 1 -e 2 -e 3 produces three values rather than an error, which is what the ... in the spec marks. A trailing ... on a placeholder means it can be specified multiple times.

Two capabilities are unusual in a Zig sized parser and worth calling out separately. Help messages can be printed from the parameter specification, and a help message can also be parsed back into a parameter specification. The second one is the more interesting: it means the same string that documents your CLI can round trip back into a parser definition, which is a plausible route to generated or shared CLI definitions.

## Subcommands through terminating_positional, and what it costs

Subcommands exist but they are built from primitives rather than provided as a feature. The README describes an option on clap.parse and clap.parseEx called terminating_positional, which allows users of clap to implement subcommands in their cli application.

The pattern it enables is visible in the example. A subcommand is an enum, help and math in this case, parsed with clap.parsers.enumeration. The main parameter spec has its own flags and a single positional:

```zig
const SubCommands = enum {
    help,
    math,
};

const main_parsers = .{
    .command = clap.parsers.enumeration(SubCommands),
};

const main_params = clap.parseParamsComptime(
    \\-h, --help  Display this help and exit.
    \\<command>
    \\
);
```

The return type can be obtained as a named type through clap.ResultEx, which takes the help style, the params and the parsers and gives you the return type of the corresponding parseEx call. Parsing then runs over an iterator rather than a slice, so the subcommand's own arguments are still in the stream when the top level parse returns.

That is the trade to understand. terminating_positional stops parsing at the subcommand boundary and hands the rest of the argument list to you, which means the subcommand layer is your code: you decide whether to parse it with clap again, how to dispatch it, and what the help for each subcommand looks like. You get the mechanism without the policy, which keeps the library small at the cost of writing the dispatch yourself. Streaming and usage examples in the repository's example/ directory suggest both styles are supported.

## Limitations, versioning and the state of the docs

The first limitation is the one the README states about its own documentation: Zig autodoc is in beta, so the automatically generated API reference may be broken or incomplete. The README is upfront about it, which is better than a polished site that is quietly wrong, but it does mean the examples and the source are the reference.

The second is version coupling. zig-clap releases are 0.10.0 from 2025-03-05, 0.11.0 from 2025-08-21 and 0.12.0 from 2026-04-20. Those numbers track Zig's own versions rather than the library's feature set, so a library upgrade is triggered by a compiler upgrade and not by a need in your program. The repository is not archived and the last push was on 2026-08-31, so it is being maintained against a moving language.

The third is that the parser table is a comptime struct, which is what makes it fast and what makes it slightly rigid: the mapping between placeholder names and types has to exist at compile time, and the ergonomics depend on Zig comptime being comfortable. Anyone arriving from a runtime-reflection argument parser should expect a different shape of code, not a different amount of code.

The fourth is the subcommand situation described above, which is honest but requires work. If your CLI has four subcommands with their own help and validation, you are writing a small amount of framework yourself.

Licensing is MIT, with a LICENSE file at the repository root. The repository is a small set of files: build.zig, build.zig.zon, clap.zig, a clap/ directory and an example/ directory containing help.zig, simple.zig, simple-ex.zig, streaming-clap.zig, subcommands.zig and usage.zig.

## How writing the parsing by hand differs

The alternative for a small CLI is to walk init.minimal.args yourself, which in Zig is not much code and has two advantages: no dependency, and no version to track when the compiler moves.

The difference in approach is where the error reporting goes. Handled by hand, a malformed flag is a value you check and print yourself, which is fine until you have a hundred flags, at which point every program reinvents the same table of names, types and help strings. clap turns that table into one string and derives types, help and parsing from it, which is the entire value proposition and is real once a CLI has options.

The second alternative is a parser that uses runtime reflection, which would let you define arguments as data structures built at runtime instead of at comptime. A comptime string can produce a fully typed struct without runtime type inspection, whereas types created only at runtime must be inspected at runtime. In a language without stable reflection, comptime is the natural substitute, and this library is a clean example of using it for exactly the job reflection would otherwise do.

So: a handful of flags and no dependencies argues for parsing by hand; a dozen flags with types, help and subcommands argues for this.

## Conclusion

Adopt zig-clap if you write Zig command line tools and want typed arguments without a reflection layer, since defining the CLI as a comptime string and getting a typed result struct back is the feature no other Zig parser in this style offers. Do not adopt it expecting a stable API across Zig releases: the library is versioned against Zig itself, with 0.10.0, 0.11.0 and 0.12.0 tracking Zig milestones, and installation requires choosing either a tagged tarball or the master branch depending on whether you build a released Zig or a nightly. Verify first which of those two you are on before running zig fetch, and note that the generated API reference is described as beta and possibly incomplete.

## FAQ

### How do I install zig-clap?

Run zig fetch --save with the tagged tarball URL if you use a released Zig, replacing <REPLACE ME> with the zig-clap version, or zig fetch --save git+https://github.com/Hejsil/zig-clap if you use a build of Zig's master branch. Then add const clap = b.dependency("clap", .{}); and exe.root_module.addImport("clap", clap.module("clap")) to build.zig.

### How do I get typed arguments in zig-clap?

Write the parameters with clap.parseParamsComptime, using a placeholder name in angle brackets, and pass a parser table where that name is a key. With clap.parsers.default a placeholder named usize yields a usize field; supply your own table to map names to other types, for example clap.parsers.int(usize, 10) or clap.parsers.enumeration(YesNo).

### How do I define subcommands with zig-clap?

Declare the subcommands as an enum parsed with clap.parsers.enumeration, give the top level params a positional named command, and pass the terminating_positional option to clap.parse or clap.parseEx so parsing stops at the subcommand and the remaining arguments are yours to dispatch. clap.ResultEx gives you the return type of parseEx.

### Which argument syntaxes does zig-clap accept?

Short arguments chain, as in -abc where a and b take no values, and repeated flags are tallied so -v -v counts two. Values can be passed as -a 100, -a=100 or -a100, all of which also work while chaining. The assignment separator is configurable through assignment_separators, and options marked with ... may be repeated.

### How is zig-clap versioned against Zig?

Its releases track Zig versions rather than independent feature sets: 0.10.0 on 2025-03-05, 0.11.0 on 2025-08-21 and 0.12.0 on 2026-04-20. The README also warns that the generated API reference may be broken or incomplete because Zig autodoc is in beta.

## Sources

- [Hejsil/zig-clap on GitHub](https://github.com/Hejsil/zig-clap)
- [Issues](https://github.com/Hejsil/zig-clap/issues)
- [License: MIT](https://github.com/Hejsil/zig-clap/blob/master/LICENSE)
- [README](https://github.com/Hejsil/zig-clap/blob/master/README.md)
- [Releases](https://github.com/Hejsil/zig-clap/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/hejsil-zig-clap
