├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── docs.yml │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── clap.zig ├── clap ├── args.zig ├── codepoint_counting_writer.zig ├── parsers.zig └── streaming.zig └── example ├── help.zig ├── simple-ex.zig ├── simple.zig ├── streaming-clap.zig ├── subcommands.zig └── usage.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | *.zig text eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [Hejsil] 2 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: API Reference 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | # Allow manually starting the workflow. 8 | workflow_dispatch: 9 | 10 | # If two concurrent runs are started, 11 | # prefer the latest one. 12 | concurrency: 13 | group: "pages" 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | 18 | build: 19 | name: Build website 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: mlugg/setup-zig@v2 24 | with: 25 | version: master 26 | - name: Build 27 | run: zig build docs 28 | - name: Upload 29 | uses: actions/upload-pages-artifact@v3 30 | with: 31 | path: "zig-out/docs/" 32 | 33 | publish: 34 | name: Publish website 35 | runs-on: ubuntu-latest 36 | needs: build # wait for build to finish 37 | permissions: 38 | # Request sufficient permissions to publish the website. 39 | pages: write 40 | id-token: write 41 | steps: 42 | - name: Deploy to GitHub Pages 43 | id: deployment 44 | uses: actions/deploy-pages@v4 45 | environment: 46 | name: github-pages 47 | url: ${{ steps.deployment.outputs.page_url }} 48 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: '0 0 * * 0' 7 | 8 | jobs: 9 | test: 10 | strategy: 11 | matrix: 12 | optimize: [Debug, ReleaseSmall, ReleaseSafe, ReleaseFast] 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: mlugg/setup-zig@v2 17 | with: 18 | version: master 19 | cache-key: ${{ matrix.optimize }} 20 | - run: zig test clap.zig -O${{ matrix.optimize }} 21 | examples: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: mlugg/setup-zig@v2 26 | with: 27 | version: master 28 | - run: zig build examples 29 | lint: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: mlugg/setup-zig@v2 34 | with: 35 | version: master 36 | - run: zig fmt --check . 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache 2 | zig-out 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Jimmi Holst Christensen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-clap 2 | 3 | A simple and easy to use command line argument parser library for Zig. 4 | 5 | ## Installation 6 | 7 | Developers tend to either use 8 | * The latest tagged release of Zig 9 | * The latest build of Zigs master branch 10 | 11 | Depending on which developer you are, you need to run different `zig fetch` commands: 12 | 13 | ```sh 14 | # Version of zig-clap that works with a tagged release of Zig 15 | # Replace `` with the version of zig-clap that you want to use 16 | # See: https://github.com/Hejsil/zig-clap/releases 17 | zig fetch --save https://github.com/Hejsil/zig-clap/archive/refs/tags/.tar.gz 18 | 19 | # Version of zig-clap that works with latest build of Zigs master branch 20 | zig fetch --save git+https://github.com/Hejsil/zig-clap 21 | ``` 22 | 23 | Then add the following to `build.zig`: 24 | 25 | ```zig 26 | const clap = b.dependency("clap", .{}); 27 | exe.root_module.addImport("clap", clap.module("clap")); 28 | ``` 29 | 30 | ## Features 31 | 32 | * Short arguments `-a` 33 | * Chaining `-abc` where `a` and `b` does not take values. 34 | * Multiple specifications are tallied (e.g. `-v -v`). 35 | * Long arguments `--long` 36 | * Supports both passing values using spacing and `=` (`-a 100`, `-a=100`) 37 | * Short args also support passing values with no spacing or `=` (`-a100`) 38 | * This all works with chaining (`-ba 100`, `-ba=100`, `-ba100`) 39 | * Supports options that can be specified multiple times (`-e 1 -e 2 -e 3`) 40 | * Print help message from parameter specification. 41 | * Parse help message to parameter specification. 42 | 43 | ## API Reference 44 | 45 | Automatically generated API Reference for the project can be found at 46 | https://Hejsil.github.io/zig-clap. Note that Zig autodoc is in beta; the website 47 | may be broken or incomplete. 48 | 49 | ## Examples 50 | 51 | ### `clap.parse` 52 | 53 | The simplest way to use this library is to just call the `clap.parse` function. 54 | 55 | ```zig 56 | pub fn main() !void { 57 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 58 | defer _ = gpa.deinit(); 59 | 60 | // First we specify what parameters our program can take. 61 | // We can use `parseParamsComptime` to parse a string into an array of `Param(Help)`. 62 | const params = comptime clap.parseParamsComptime( 63 | \\-h, --help Display this help and exit. 64 | \\-n, --number An option parameter, which takes a value. 65 | \\-s, --string ... An option parameter which can be specified multiple times. 66 | \\... 67 | \\ 68 | ); 69 | 70 | // Initialize our diagnostics, which can be used for reporting useful errors. 71 | // This is optional. You can also pass `.{}` to `clap.parse` if you don't 72 | // care about the extra information `Diagnostics` provides. 73 | var diag = clap.Diagnostic{}; 74 | var res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ 75 | .diagnostic = &diag, 76 | .allocator = gpa.allocator(), 77 | }) catch |err| { 78 | // Report useful error and exit. 79 | diag.report(std.io.getStdErr().writer(), err) catch {}; 80 | return err; 81 | }; 82 | defer res.deinit(); 83 | 84 | if (res.args.help != 0) 85 | std.debug.print("--help\n", .{}); 86 | if (res.args.number) |n| 87 | std.debug.print("--number = {}\n", .{n}); 88 | for (res.args.string) |s| 89 | std.debug.print("--string = {s}\n", .{s}); 90 | for (res.positionals[0]) |pos| 91 | std.debug.print("{s}\n", .{pos}); 92 | } 93 | 94 | const clap = @import("clap"); 95 | const std = @import("std"); 96 | 97 | ``` 98 | 99 | The result will contain an `args` field and a `positionals` field. `args` will have one field for 100 | each non-positional parameter of your program. The name of the field will be the longest name of the 101 | parameter. `positionals` will be a tuple with one field for each positional parameter. 102 | 103 | The fields in `args` and `postionals` are typed. The type is based on the name of the value the 104 | parameter takes. Since `--number` takes a `usize` the field `res.args.number` has the type `usize`. 105 | 106 | Note that this is only the case because `clap.parsers.default` has a field called `usize` which 107 | contains a parser that returns `usize`. You can pass in something other than `clap.parsers.default` 108 | if you want some other mapping. 109 | 110 | ```zig 111 | pub fn main() !void { 112 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 113 | defer _ = gpa.deinit(); 114 | 115 | // First we specify what parameters our program can take. 116 | // We can use `parseParamsComptime` to parse a string into an array of `Param(Help)`. 117 | const params = comptime clap.parseParamsComptime( 118 | \\-h, --help Display this help and exit. 119 | \\-n, --number An option parameter, which takes a value. 120 | \\-a, --answer An option parameter which takes an enum. 121 | \\-s, --string ... An option parameter which can be specified multiple times. 122 | \\... 123 | \\ 124 | ); 125 | 126 | // Declare our own parsers which are used to map the argument strings to other 127 | // types. 128 | const YesNo = enum { yes, no }; 129 | const parsers = comptime .{ 130 | .STR = clap.parsers.string, 131 | .FILE = clap.parsers.string, 132 | .INT = clap.parsers.int(usize, 10), 133 | .ANSWER = clap.parsers.enumeration(YesNo), 134 | }; 135 | 136 | var diag = clap.Diagnostic{}; 137 | var res = clap.parse(clap.Help, ¶ms, parsers, .{ 138 | .diagnostic = &diag, 139 | .allocator = gpa.allocator(), 140 | // The assignment separator can be configured. `--number=1` and `--number:1` is now 141 | // allowed. 142 | .assignment_separators = "=:", 143 | }) catch |err| { 144 | diag.report(std.io.getStdErr().writer(), err) catch {}; 145 | return err; 146 | }; 147 | defer res.deinit(); 148 | 149 | if (res.args.help != 0) 150 | std.debug.print("--help\n", .{}); 151 | if (res.args.number) |n| 152 | std.debug.print("--number = {}\n", .{n}); 153 | if (res.args.answer) |a| 154 | std.debug.print("--answer = {s}\n", .{@tagName(a)}); 155 | for (res.args.string) |s| 156 | std.debug.print("--string = {s}\n", .{s}); 157 | for (res.positionals[0]) |pos| 158 | std.debug.print("{s}\n", .{pos}); 159 | } 160 | 161 | const clap = @import("clap"); 162 | const std = @import("std"); 163 | 164 | ``` 165 | 166 | ### Subcommands 167 | 168 | There is an option for `clap.parse` and `clap.parseEx` called `terminating_positional`. It allows 169 | for users of `clap` to implement subcommands in their cli application: 170 | 171 | ```zig 172 | // These are our subcommands. 173 | const SubCommands = enum { 174 | help, 175 | math, 176 | }; 177 | 178 | const main_parsers = .{ 179 | .command = clap.parsers.enumeration(SubCommands), 180 | }; 181 | 182 | // The parameters for `main`. Parameters for the subcommands are specified further down. 183 | const main_params = clap.parseParamsComptime( 184 | \\-h, --help Display this help and exit. 185 | \\ 186 | \\ 187 | ); 188 | 189 | // To pass around arguments returned by clap, `clap.Result` and `clap.ResultEx` can be used to 190 | // get the return type of `clap.parse` and `clap.parseEx`. 191 | const MainArgs = clap.ResultEx(clap.Help, &main_params, main_parsers); 192 | 193 | pub fn main() !void { 194 | var gpa_state = std.heap.GeneralPurposeAllocator(.{}){}; 195 | const gpa = gpa_state.allocator(); 196 | defer _ = gpa_state.deinit(); 197 | 198 | var iter = try std.process.ArgIterator.initWithAllocator(gpa); 199 | defer iter.deinit(); 200 | 201 | _ = iter.next(); 202 | 203 | var diag = clap.Diagnostic{}; 204 | var res = clap.parseEx(clap.Help, &main_params, main_parsers, &iter, .{ 205 | .diagnostic = &diag, 206 | .allocator = gpa, 207 | 208 | // Terminate the parsing of arguments after parsing the first positional (0 is passed 209 | // here because parsed positionals are, like slices and arrays, indexed starting at 0). 210 | // 211 | // This will terminate the parsing after parsing the subcommand enum and leave `iter` 212 | // not fully consumed. It can then be reused to parse the arguments for subcommands. 213 | .terminating_positional = 0, 214 | }) catch |err| { 215 | diag.report(std.io.getStdErr().writer(), err) catch {}; 216 | return err; 217 | }; 218 | defer res.deinit(); 219 | 220 | if (res.args.help != 0) 221 | std.debug.print("--help\n", .{}); 222 | 223 | const command = res.positionals[0] orelse return error.MissingCommand; 224 | switch (command) { 225 | .help => std.debug.print("--help\n", .{}), 226 | .math => try mathMain(gpa, &iter, res), 227 | } 228 | } 229 | 230 | fn mathMain(gpa: std.mem.Allocator, iter: *std.process.ArgIterator, main_args: MainArgs) !void { 231 | // The parent arguments are not used here, but there are cases where it might be useful, so 232 | // this example shows how to pass the arguments around. 233 | _ = main_args; 234 | 235 | // The parameters for the subcommand. 236 | const params = comptime clap.parseParamsComptime( 237 | \\-h, --help Display this help and exit. 238 | \\-a, --add Add the two numbers 239 | \\-s, --sub Subtract the two numbers 240 | \\ 241 | \\ 242 | \\ 243 | ); 244 | 245 | // Here we pass the partially parsed argument iterator. 246 | var diag = clap.Diagnostic{}; 247 | var res = clap.parseEx(clap.Help, ¶ms, clap.parsers.default, iter, .{ 248 | .diagnostic = &diag, 249 | .allocator = gpa, 250 | }) catch |err| { 251 | diag.report(std.io.getStdErr().writer(), err) catch {}; 252 | return err; 253 | }; 254 | defer res.deinit(); 255 | 256 | const a = res.positionals[0] orelse return error.MissingArg1; 257 | const b = res.positionals[1] orelse return error.MissingArg1; 258 | if (res.args.help != 0) 259 | std.debug.print("--help\n", .{}); 260 | if (res.args.add != 0) 261 | std.debug.print("added: {}\n", .{a + b}); 262 | if (res.args.sub != 0) 263 | std.debug.print("subtracted: {}\n", .{a - b}); 264 | } 265 | 266 | const clap = @import("clap"); 267 | const std = @import("std"); 268 | 269 | ``` 270 | 271 | ### `streaming.Clap` 272 | 273 | The `streaming.Clap` is the base of all the other parsers. It's a streaming parser that uses an 274 | `args.Iterator` to provide it with arguments lazily. 275 | 276 | ```zig 277 | pub fn main() !void { 278 | const allocator = std.heap.page_allocator; 279 | 280 | // First we specify what parameters our program can take. 281 | const params = [_]clap.Param(u8){ 282 | .{ 283 | .id = 'h', 284 | .names = .{ .short = 'h', .long = "help" }, 285 | }, 286 | .{ 287 | .id = 'n', 288 | .names = .{ .short = 'n', .long = "number" }, 289 | .takes_value = .one, 290 | }, 291 | .{ .id = 'f', .takes_value = .one }, 292 | }; 293 | 294 | var iter = try std.process.ArgIterator.initWithAllocator(allocator); 295 | defer iter.deinit(); 296 | 297 | // Skip exe argument. 298 | _ = iter.next(); 299 | 300 | // Initialize our diagnostics, which can be used for reporting useful errors. 301 | // This is optional. You can also leave the `diagnostic` field unset if you 302 | // don't care about the extra information `Diagnostic` provides. 303 | var diag = clap.Diagnostic{}; 304 | var parser = clap.streaming.Clap(u8, std.process.ArgIterator){ 305 | .params = ¶ms, 306 | .iter = &iter, 307 | .diagnostic = &diag, 308 | }; 309 | 310 | // Because we use a streaming parser, we have to consume each argument parsed individually. 311 | while (parser.next() catch |err| { 312 | // Report useful error and exit. 313 | diag.report(std.io.getStdErr().writer(), err) catch {}; 314 | return err; 315 | }) |arg| { 316 | // arg.param will point to the parameter which matched the argument. 317 | switch (arg.param.id) { 318 | 'h' => std.debug.print("Help!\n", .{}), 319 | 'n' => std.debug.print("--number = {s}\n", .{arg.value.?}), 320 | 321 | // arg.value == null, if arg.param.takes_value == .none. 322 | // Otherwise, arg.value is the value passed with the argument, such as "-a=10" 323 | // or "-a 10". 324 | 'f' => std.debug.print("{s}\n", .{arg.value.?}), 325 | else => unreachable, 326 | } 327 | } 328 | } 329 | 330 | const clap = @import("clap"); 331 | const std = @import("std"); 332 | 333 | ``` 334 | 335 | Currently, this parser is the only parser that allows an array of `Param` that is generated at runtime. 336 | 337 | ### `help` 338 | 339 | `help` prints a simple list of all parameters the program can take. It expects the `Id` to have a 340 | `description` method and an `value` method so that it can provide that in the output. `HelpOptions` 341 | is passed to `help` to control how the help message is printed. 342 | 343 | ```zig 344 | pub fn main() !void { 345 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 346 | defer _ = gpa.deinit(); 347 | 348 | const params = comptime clap.parseParamsComptime( 349 | \\-h, --help Display this help and exit. 350 | \\-v, --version Output version information and exit. 351 | \\ 352 | ); 353 | 354 | var res = try clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ 355 | .allocator = gpa.allocator(), 356 | }); 357 | defer res.deinit(); 358 | 359 | // `clap.help` is a function that can print a simple help message. It can print any `Param` 360 | // where `Id` has a `description` and `value` method (`Param(Help)` is one such parameter). 361 | // The last argument contains options as to how `help` should print those parameters. Using 362 | // `.{}` means the default options. 363 | if (res.args.help != 0) 364 | return clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{}); 365 | } 366 | 367 | const clap = @import("clap"); 368 | const std = @import("std"); 369 | 370 | ``` 371 | 372 | ``` 373 | $ zig-out/bin/help --help 374 | -h, --help 375 | Display this help and exit. 376 | 377 | -v, --version 378 | Output version information and exit. 379 | ``` 380 | 381 | ### `usage` 382 | 383 | `usage` prints a small abbreviated version of the help message. It expects the `Id` to have a 384 | `value` method so it can provide that in the output. 385 | 386 | ```zig 387 | pub fn main() !void { 388 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 389 | defer _ = gpa.deinit(); 390 | 391 | const params = comptime clap.parseParamsComptime( 392 | \\-h, --help Display this help and exit. 393 | \\-v, --version Output version information and exit. 394 | \\ --value An option parameter, which takes a value. 395 | \\ 396 | ); 397 | 398 | var res = try clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ 399 | .allocator = gpa.allocator(), 400 | }); 401 | defer res.deinit(); 402 | 403 | // `clap.usage` is a function that can print a simple help message. It can print any `Param` 404 | // where `Id` has a `value` method (`Param(Help)` is one such parameter). 405 | if (res.args.help != 0) 406 | return clap.usage(std.io.getStdErr().writer(), clap.Help, ¶ms); 407 | } 408 | 409 | const clap = @import("clap"); 410 | const std = @import("std"); 411 | 412 | ``` 413 | 414 | ``` 415 | $ zig-out/bin/usage --help 416 | [-hv] [--value ] 417 | ``` 418 | 419 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | pub fn build(b: *std.Build) void { 2 | const optimize = b.standardOptimizeOption(.{}); 3 | const target = b.standardTargetOptions(.{}); 4 | 5 | const clap_mod = b.addModule("clap", .{ 6 | .root_source_file = b.path("clap.zig"), 7 | .target = target, 8 | .optimize = optimize, 9 | }); 10 | 11 | const test_step = b.step("test", "Run all tests in all modes."); 12 | const tests = b.addTest(.{ .root_module = clap_mod }); 13 | const run_tests = b.addRunArtifact(tests); 14 | test_step.dependOn(&run_tests.step); 15 | 16 | const example_step = b.step("examples", "Build examples"); 17 | for ([_][]const u8{ 18 | "help", 19 | "simple", 20 | "simple-ex", 21 | "streaming-clap", 22 | "subcommands", 23 | "usage", 24 | }) |example_name| { 25 | const example = b.addExecutable(.{ 26 | .name = example_name, 27 | .root_source_file = b.path(b.fmt("example/{s}.zig", .{example_name})), 28 | .target = target, 29 | .optimize = optimize, 30 | }); 31 | const install_example = b.addInstallArtifact(example, .{}); 32 | example.root_module.addImport("clap", clap_mod); 33 | example_step.dependOn(&example.step); 34 | example_step.dependOn(&install_example.step); 35 | } 36 | 37 | const docs_step = b.step("docs", "Generate docs."); 38 | const install_docs = b.addInstallDirectory(.{ 39 | .source_dir = tests.getEmittedDocs(), 40 | .install_dir = .prefix, 41 | .install_subdir = "docs", 42 | }); 43 | docs_step.dependOn(&install_docs.step); 44 | 45 | const all_step = b.step("all", "Build everything and runs all tests"); 46 | all_step.dependOn(test_step); 47 | all_step.dependOn(example_step); 48 | 49 | b.default_step.dependOn(all_step); 50 | } 51 | 52 | const std = @import("std"); 53 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .clap, 3 | .version = "0.10.0", 4 | .minimum_zig_version = "0.14.0", 5 | .fingerprint = 0x65f99e6f07a316a0, 6 | .paths = .{ 7 | "clap", 8 | "example", 9 | "LICENSE", 10 | "README.md", 11 | "build.zig", 12 | "build.zig.zon", 13 | "clap.zig", 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /clap.zig: -------------------------------------------------------------------------------- 1 | pub const default_assignment_separators = "="; 2 | 3 | /// The names a `Param` can have. 4 | pub const Names = struct { 5 | /// '-' prefix 6 | short: ?u8 = null, 7 | 8 | /// '--' prefix 9 | long: ?[]const u8 = null, 10 | 11 | /// The longest of the possible names this `Names` struct can represent. 12 | pub fn longest(names: *const Names) Longest { 13 | if (names.long) |long| 14 | return .{ .kind = .long, .name = long }; 15 | if (names.short) |*short| 16 | return .{ .kind = .short, .name = @as(*const [1]u8, short) }; 17 | 18 | return .{ .kind = .positional, .name = "" }; 19 | } 20 | 21 | pub const Longest = struct { 22 | kind: Kind, 23 | name: []const u8, 24 | }; 25 | 26 | pub const Kind = enum { 27 | long, 28 | short, 29 | positional, 30 | 31 | pub fn prefix(kind: Kind) []const u8 { 32 | return switch (kind) { 33 | .long => "--", 34 | .short => "-", 35 | .positional => "", 36 | }; 37 | } 38 | }; 39 | }; 40 | 41 | /// Whether a param takes no value (a flag), one value, or can be specified multiple times. 42 | pub const Values = enum { 43 | none, 44 | one, 45 | many, 46 | }; 47 | 48 | /// Represents a parameter for the command line. 49 | /// Parameters come in three kinds: 50 | /// * Short ("-a"): Should be used for the most commonly used parameters in your program. 51 | /// * They can take a value three different ways. 52 | /// * "-a value" 53 | /// * "-a=value" 54 | /// * "-avalue" 55 | /// * They chain if they don't take values: "-abc". 56 | /// * The last given parameter can take a value in the same way that a single parameter can: 57 | /// * "-abc value" 58 | /// * "-abc=value" 59 | /// * "-abcvalue" 60 | /// * Long ("--long-param"): Should be used for less common parameters, or when no single 61 | /// character can describe the parameter. 62 | /// * They can take a value two different ways. 63 | /// * "--long-param value" 64 | /// * "--long-param=value" 65 | /// * Positional: Should be used as the primary parameter of the program, like a filename or 66 | /// an expression to parse. 67 | /// * Positional parameters have both names.long and names.short == null. 68 | /// * Positional parameters must take a value. 69 | pub fn Param(comptime Id: type) type { 70 | return struct { 71 | id: Id, 72 | names: Names = Names{}, 73 | takes_value: Values = .none, 74 | }; 75 | } 76 | 77 | /// Takes a string and parses it into many Param(Help). Returned is a newly allocated slice 78 | /// containing all the parsed params. The caller is responsible for freeing the slice. 79 | pub fn parseParams(allocator: std.mem.Allocator, str: []const u8) ![]Param(Help) { 80 | var end: usize = undefined; 81 | return parseParamsEx(allocator, str, &end); 82 | } 83 | 84 | /// Takes a string and parses it into many Param(Help). Returned is a newly allocated slice 85 | /// containing all the parsed params. The caller is responsible for freeing the slice. 86 | pub fn parseParamsEx(allocator: std.mem.Allocator, str: []const u8, end: *usize) ![]Param(Help) { 87 | var list = std.ArrayList(Param(Help)).init(allocator); 88 | errdefer list.deinit(); 89 | 90 | try parseParamsIntoArrayListEx(&list, str, end); 91 | return try list.toOwnedSlice(); 92 | } 93 | 94 | /// Takes a string and parses it into many Param(Help) at comptime. Returned is an array of 95 | /// exactly the number of params that was parsed from `str`. A parse error becomes a compiler 96 | /// error. 97 | pub fn parseParamsComptime(comptime str: []const u8) [countParams(str)]Param(Help) { 98 | var end: usize = undefined; 99 | var res: [countParams(str)]Param(Help) = undefined; 100 | _ = parseParamsIntoSliceEx(&res, str, &end) catch { 101 | const loc = std.zig.findLineColumn(str, end); 102 | @compileError(std.fmt.comptimePrint("error:{}:{}: Failed to parse parameter:\n{s}", .{ 103 | loc.line + 1, 104 | loc.column + 1, 105 | loc.source_line, 106 | })); 107 | }; 108 | return res; 109 | } 110 | 111 | fn countParams(str: []const u8) usize { 112 | // See parseParamEx for reasoning. I would like to remove it from parseParam, but people 113 | // depend on that function to still work conveniently at comptime, so leaving it for now. 114 | @setEvalBranchQuota(std.math.maxInt(u32)); 115 | 116 | var res: usize = 0; 117 | var it = std.mem.splitScalar(u8, str, '\n'); 118 | while (it.next()) |line| { 119 | const trimmed = std.mem.trimLeft(u8, line, " \t"); 120 | if (std.mem.startsWith(u8, trimmed, "-") or 121 | std.mem.startsWith(u8, trimmed, "<")) 122 | { 123 | res += 1; 124 | } 125 | } 126 | 127 | return res; 128 | } 129 | 130 | /// Takes a string and parses it into many Param(Help), which are written to `slice`. A subslice 131 | /// is returned, containing all the parameters parsed. This function will fail if the input slice 132 | /// is to small. 133 | pub fn parseParamsIntoSlice(slice: []Param(Help), str: []const u8) ![]Param(Help) { 134 | var null_alloc = std.heap.FixedBufferAllocator.init(""); 135 | var list = std.ArrayList(Param(Help)){ 136 | .allocator = null_alloc.allocator(), 137 | .items = slice[0..0], 138 | .capacity = slice.len, 139 | }; 140 | 141 | try parseParamsIntoArrayList(&list, str); 142 | return list.items; 143 | } 144 | 145 | /// Takes a string and parses it into many Param(Help), which are written to `slice`. A subslice 146 | /// is returned, containing all the parameters parsed. This function will fail if the input slice 147 | /// is to small. 148 | pub fn parseParamsIntoSliceEx(slice: []Param(Help), str: []const u8, end: *usize) ![]Param(Help) { 149 | var null_alloc = std.heap.FixedBufferAllocator.init(""); 150 | var list = std.ArrayList(Param(Help)){ 151 | .allocator = null_alloc.allocator(), 152 | .items = slice[0..0], 153 | .capacity = slice.len, 154 | }; 155 | 156 | try parseParamsIntoArrayListEx(&list, str, end); 157 | return list.items; 158 | } 159 | 160 | /// Takes a string and parses it into many Param(Help), which are appended onto `list`. 161 | pub fn parseParamsIntoArrayList(list: *std.ArrayList(Param(Help)), str: []const u8) !void { 162 | var end: usize = undefined; 163 | return parseParamsIntoArrayListEx(list, str, &end); 164 | } 165 | 166 | /// Takes a string and parses it into many Param(Help), which are appended onto `list`. 167 | pub fn parseParamsIntoArrayListEx(list: *std.ArrayList(Param(Help)), str: []const u8, end: *usize) !void { 168 | var i: usize = 0; 169 | while (i != str.len) { 170 | var end_of_this: usize = undefined; 171 | errdefer end.* = i + end_of_this; 172 | 173 | try list.append(try parseParamEx(str[i..], &end_of_this)); 174 | i += end_of_this; 175 | } 176 | 177 | end.* = str.len; 178 | } 179 | 180 | pub fn parseParam(str: []const u8) !Param(Help) { 181 | var end: usize = undefined; 182 | return parseParamEx(str, &end); 183 | } 184 | 185 | /// Takes a string and parses it to a Param(Help). 186 | pub fn parseParamEx(str: []const u8, end: *usize) !Param(Help) { 187 | // This function become a lot less ergonomic to use once you hit the eval branch quota. To 188 | // avoid this we pick a sane default. Sadly, the only sane default is the biggest possible 189 | // value. If we pick something a lot smaller and a user hits the quota after that, they have 190 | // no way of overriding it, since we set it here. 191 | // We can recosider this again if: 192 | // * We get parseParams: https://github.com/Hejsil/zig-clap/issues/39 193 | // * We get a larger default branch quota in the zig compiler (stage 2). 194 | // * Someone points out how this is a really bad idea. 195 | @setEvalBranchQuota(std.math.maxInt(u32)); 196 | 197 | var res = Param(Help){ .id = .{} }; 198 | var start: usize = 0; 199 | var state: enum { 200 | start, 201 | 202 | start_of_short_name, 203 | end_of_short_name, 204 | 205 | before_long_name_or_value_or_description, 206 | 207 | before_long_name, 208 | start_of_long_name, 209 | first_char_of_long_name, 210 | rest_of_long_name, 211 | 212 | before_value_or_description, 213 | 214 | first_char_of_value, 215 | rest_of_value, 216 | end_of_one_value, 217 | second_dot_of_multi_value, 218 | third_dot_of_multi_value, 219 | 220 | before_description, 221 | rest_of_description, 222 | rest_of_description_new_line, 223 | } = .start; 224 | for (str, 0..) |c, i| { 225 | errdefer end.* = i; 226 | 227 | switch (state) { 228 | .start => switch (c) { 229 | ' ', '\t', '\n' => {}, 230 | '-' => state = .start_of_short_name, 231 | '<' => state = .first_char_of_value, 232 | else => return error.InvalidParameter, 233 | }, 234 | 235 | .start_of_short_name => switch (c) { 236 | '-' => state = .first_char_of_long_name, 237 | 'a'...'z', 'A'...'Z', '0'...'9' => { 238 | res.names.short = c; 239 | state = .end_of_short_name; 240 | }, 241 | else => return error.InvalidParameter, 242 | }, 243 | .end_of_short_name => switch (c) { 244 | ' ', '\t' => state = .before_long_name_or_value_or_description, 245 | '\n' => { 246 | start = i + 1; 247 | end.* = i + 1; 248 | state = .rest_of_description_new_line; 249 | }, 250 | ',' => state = .before_long_name, 251 | else => return error.InvalidParameter, 252 | }, 253 | 254 | .before_long_name => switch (c) { 255 | ' ', '\t' => {}, 256 | '-' => state = .start_of_long_name, 257 | else => return error.InvalidParameter, 258 | }, 259 | .start_of_long_name => switch (c) { 260 | '-' => state = .first_char_of_long_name, 261 | else => return error.InvalidParameter, 262 | }, 263 | .first_char_of_long_name => switch (c) { 264 | 'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => { 265 | start = i; 266 | state = .rest_of_long_name; 267 | }, 268 | else => return error.InvalidParameter, 269 | }, 270 | .rest_of_long_name => switch (c) { 271 | 'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => {}, 272 | ' ', '\t' => { 273 | res.names.long = str[start..i]; 274 | state = .before_value_or_description; 275 | }, 276 | '\n' => { 277 | res.names.long = str[start..i]; 278 | start = i + 1; 279 | end.* = i + 1; 280 | state = .rest_of_description_new_line; 281 | }, 282 | else => return error.InvalidParameter, 283 | }, 284 | 285 | .before_long_name_or_value_or_description => switch (c) { 286 | ' ', '\t' => {}, 287 | ',' => state = .before_long_name, 288 | '<' => state = .first_char_of_value, 289 | else => { 290 | start = i; 291 | state = .rest_of_description; 292 | }, 293 | }, 294 | 295 | .before_value_or_description => switch (c) { 296 | ' ', '\t' => {}, 297 | '<' => state = .first_char_of_value, 298 | else => { 299 | start = i; 300 | state = .rest_of_description; 301 | }, 302 | }, 303 | .first_char_of_value => switch (c) { 304 | '>' => return error.InvalidParameter, 305 | else => { 306 | start = i; 307 | state = .rest_of_value; 308 | }, 309 | }, 310 | .rest_of_value => switch (c) { 311 | '>' => { 312 | res.takes_value = .one; 313 | res.id.val = str[start..i]; 314 | state = .end_of_one_value; 315 | }, 316 | else => {}, 317 | }, 318 | .end_of_one_value => switch (c) { 319 | '.' => state = .second_dot_of_multi_value, 320 | ' ', '\t' => state = .before_description, 321 | '\n' => { 322 | start = i + 1; 323 | end.* = i + 1; 324 | state = .rest_of_description_new_line; 325 | }, 326 | else => { 327 | start = i; 328 | state = .rest_of_description; 329 | }, 330 | }, 331 | .second_dot_of_multi_value => switch (c) { 332 | '.' => state = .third_dot_of_multi_value, 333 | else => return error.InvalidParameter, 334 | }, 335 | .third_dot_of_multi_value => switch (c) { 336 | '.' => { 337 | res.takes_value = .many; 338 | state = .before_description; 339 | }, 340 | else => return error.InvalidParameter, 341 | }, 342 | 343 | .before_description => switch (c) { 344 | ' ', '\t' => {}, 345 | '\n' => { 346 | start = i + 1; 347 | end.* = i + 1; 348 | state = .rest_of_description_new_line; 349 | }, 350 | else => { 351 | start = i; 352 | state = .rest_of_description; 353 | }, 354 | }, 355 | .rest_of_description => switch (c) { 356 | '\n' => { 357 | end.* = i; 358 | state = .rest_of_description_new_line; 359 | }, 360 | else => {}, 361 | }, 362 | .rest_of_description_new_line => switch (c) { 363 | ' ', '\t', '\n' => {}, 364 | '-', '<' => { 365 | res.id.desc = str[start..end.*]; 366 | end.* = i; 367 | break; 368 | }, 369 | else => state = .rest_of_description, 370 | }, 371 | } 372 | } else { 373 | defer end.* = str.len; 374 | switch (state) { 375 | .rest_of_description => res.id.desc = str[start..], 376 | .rest_of_description_new_line => res.id.desc = str[start..end.*], 377 | .rest_of_long_name => res.names.long = str[start..], 378 | .end_of_short_name, 379 | .end_of_one_value, 380 | .before_value_or_description, 381 | .before_description, 382 | => {}, 383 | else => return error.InvalidParameter, 384 | } 385 | } 386 | 387 | return res; 388 | } 389 | 390 | fn testParseParams(str: []const u8, expected_params: []const Param(Help)) !void { 391 | var end: usize = undefined; 392 | const actual_params = parseParamsEx(std.testing.allocator, str, &end) catch |err| { 393 | const loc = std.zig.findLineColumn(str, end); 394 | std.debug.print("error:{}:{}: Failed to parse parameter:\n{s}\n", .{ 395 | loc.line + 1, 396 | loc.column + 1, 397 | loc.source_line, 398 | }); 399 | return err; 400 | }; 401 | defer std.testing.allocator.free(actual_params); 402 | 403 | try std.testing.expectEqual(expected_params.len, actual_params.len); 404 | for (expected_params, 0..) |_, i| 405 | try expectParam(expected_params[i], actual_params[i]); 406 | } 407 | 408 | fn expectParam(expect: Param(Help), actual: Param(Help)) !void { 409 | try std.testing.expectEqualStrings(expect.id.desc, actual.id.desc); 410 | try std.testing.expectEqualStrings(expect.id.val, actual.id.val); 411 | try std.testing.expectEqual(expect.names.short, actual.names.short); 412 | try std.testing.expectEqual(expect.takes_value, actual.takes_value); 413 | if (expect.names.long) |long| { 414 | try std.testing.expectEqualStrings(long, actual.names.long.?); 415 | } else { 416 | try std.testing.expectEqual(@as(?[]const u8, null), actual.names.long); 417 | } 418 | } 419 | 420 | test "parseParams" { 421 | try testParseParams( 422 | \\-s 423 | \\--str 424 | \\--str-str 425 | \\--str_str 426 | \\-s, --str 427 | \\--str 428 | \\-s, --str 429 | \\-s, --long Help text 430 | \\-s, --long ... Help text 431 | \\--long Help text 432 | \\-s Help text 433 | \\-s, --long Help text 434 | \\-s Help text 435 | \\--long Help text 436 | \\--long Help text 437 | \\ Help text 438 | \\... Help text 439 | \\--aa 440 | \\ This is 441 | \\ help spanning multiple 442 | \\ lines 443 | \\--aa This msg should end and the newline cause of new param 444 | \\--bb This should be a new param 445 | \\ 446 | , &.{ 447 | .{ .id = .{}, .names = .{ .short = 's' } }, 448 | .{ .id = .{}, .names = .{ .long = "str" } }, 449 | .{ .id = .{}, .names = .{ .long = "str-str" } }, 450 | .{ .id = .{}, .names = .{ .long = "str_str" } }, 451 | .{ .id = .{}, .names = .{ .short = 's', .long = "str" } }, 452 | .{ 453 | .id = .{ .val = "str" }, 454 | .names = .{ .long = "str" }, 455 | .takes_value = .one, 456 | }, 457 | .{ 458 | .id = .{ .val = "str" }, 459 | .names = .{ .short = 's', .long = "str" }, 460 | .takes_value = .one, 461 | }, 462 | .{ 463 | .id = .{ .desc = "Help text", .val = "val" }, 464 | .names = .{ .short = 's', .long = "long" }, 465 | .takes_value = .one, 466 | }, 467 | .{ 468 | .id = .{ .desc = "Help text", .val = "val" }, 469 | .names = .{ .short = 's', .long = "long" }, 470 | .takes_value = .many, 471 | }, 472 | .{ 473 | .id = .{ .desc = "Help text", .val = "val" }, 474 | .names = .{ .long = "long" }, 475 | .takes_value = .one, 476 | }, 477 | .{ 478 | .id = .{ .desc = "Help text", .val = "val" }, 479 | .names = .{ .short = 's' }, 480 | .takes_value = .one, 481 | }, 482 | .{ 483 | .id = .{ .desc = "Help text" }, 484 | .names = .{ .short = 's', .long = "long" }, 485 | }, 486 | .{ 487 | .id = .{ .desc = "Help text" }, 488 | .names = .{ .short = 's' }, 489 | }, 490 | .{ 491 | .id = .{ .desc = "Help text" }, 492 | .names = .{ .long = "long" }, 493 | }, 494 | .{ 495 | .id = .{ .desc = "Help text", .val = "A | B" }, 496 | .names = .{ .long = "long" }, 497 | .takes_value = .one, 498 | }, 499 | .{ 500 | .id = .{ .desc = "Help text", .val = "A" }, 501 | .takes_value = .one, 502 | }, 503 | .{ 504 | .id = .{ .desc = "Help text", .val = "A" }, 505 | .names = .{}, 506 | .takes_value = .many, 507 | }, 508 | .{ 509 | .id = .{ 510 | .desc = 511 | \\ This is 512 | \\ help spanning multiple 513 | \\ lines 514 | , 515 | }, 516 | .names = .{ .long = "aa" }, 517 | .takes_value = .none, 518 | }, 519 | .{ 520 | .id = .{ .desc = "This msg should end and the newline cause of new param" }, 521 | .names = .{ .long = "aa" }, 522 | .takes_value = .none, 523 | }, 524 | .{ 525 | .id = .{ .desc = "This should be a new param" }, 526 | .names = .{ .long = "bb" }, 527 | .takes_value = .none, 528 | }, 529 | }); 530 | 531 | try std.testing.expectError(error.InvalidParameter, parseParam("--long, Help")); 532 | try std.testing.expectError(error.InvalidParameter, parseParam("-s, Help")); 533 | try std.testing.expectError(error.InvalidParameter, parseParam("-ss Help")); 534 | try std.testing.expectError(error.InvalidParameter, parseParam("-ss Help")); 535 | try std.testing.expectError(error.InvalidParameter, parseParam("- Help")); 536 | } 537 | 538 | /// Optional diagnostics used for reporting useful errors 539 | pub const Diagnostic = struct { 540 | arg: []const u8 = "", 541 | name: Names = Names{}, 542 | 543 | /// Default diagnostics reporter when all you want is English with no colors. 544 | /// Use this as a reference for implementing your own if needed. 545 | pub fn report(diag: Diagnostic, stream: anytype, err: anyerror) !void { 546 | var longest = diag.name.longest(); 547 | if (longest.kind == .positional) 548 | longest.name = diag.arg; 549 | 550 | switch (err) { 551 | streaming.Error.DoesntTakeValue => try stream.print( 552 | "The argument '{s}{s}' does not take a value\n", 553 | .{ longest.kind.prefix(), longest.name }, 554 | ), 555 | streaming.Error.MissingValue => try stream.print( 556 | "The argument '{s}{s}' requires a value but none was supplied\n", 557 | .{ longest.kind.prefix(), longest.name }, 558 | ), 559 | streaming.Error.InvalidArgument => try stream.print( 560 | "Invalid argument '{s}{s}'\n", 561 | .{ longest.kind.prefix(), longest.name }, 562 | ), 563 | else => try stream.print("Error while parsing arguments: {s}\n", .{@errorName(err)}), 564 | } 565 | } 566 | }; 567 | 568 | fn testDiag(diag: Diagnostic, err: anyerror, expected: []const u8) !void { 569 | var buf: [1024]u8 = undefined; 570 | var slice_stream = std.io.fixedBufferStream(&buf); 571 | diag.report(slice_stream.writer(), err) catch unreachable; 572 | try std.testing.expectEqualStrings(expected, slice_stream.getWritten()); 573 | } 574 | 575 | test "Diagnostic.report" { 576 | try testDiag(.{ .arg = "c" }, error.InvalidArgument, "Invalid argument 'c'\n"); 577 | try testDiag( 578 | .{ .name = .{ .long = "cc" } }, 579 | error.InvalidArgument, 580 | "Invalid argument '--cc'\n", 581 | ); 582 | try testDiag( 583 | .{ .name = .{ .short = 'c' } }, 584 | error.DoesntTakeValue, 585 | "The argument '-c' does not take a value\n", 586 | ); 587 | try testDiag( 588 | .{ .name = .{ .long = "cc" } }, 589 | error.DoesntTakeValue, 590 | "The argument '--cc' does not take a value\n", 591 | ); 592 | try testDiag( 593 | .{ .name = .{ .short = 'c' } }, 594 | error.MissingValue, 595 | "The argument '-c' requires a value but none was supplied\n", 596 | ); 597 | try testDiag( 598 | .{ .name = .{ .long = "cc" } }, 599 | error.MissingValue, 600 | "The argument '--cc' requires a value but none was supplied\n", 601 | ); 602 | try testDiag( 603 | .{ .name = .{ .short = 'c' } }, 604 | error.InvalidArgument, 605 | "Invalid argument '-c'\n", 606 | ); 607 | try testDiag( 608 | .{ .name = .{ .long = "cc" } }, 609 | error.InvalidArgument, 610 | "Invalid argument '--cc'\n", 611 | ); 612 | try testDiag( 613 | .{ .name = .{ .short = 'c' } }, 614 | error.SomethingElse, 615 | "Error while parsing arguments: SomethingElse\n", 616 | ); 617 | try testDiag( 618 | .{ .name = .{ .long = "cc" } }, 619 | error.SomethingElse, 620 | "Error while parsing arguments: SomethingElse\n", 621 | ); 622 | } 623 | 624 | /// Options that can be set to customize the behavior of parsing. 625 | pub const ParseOptions = struct { 626 | allocator: std.mem.Allocator, 627 | diagnostic: ?*Diagnostic = null, 628 | 629 | /// The assignment separators, which by default is `=`. This is the separator between the name 630 | /// of an argument and its value. For `--arg=value`, `arg` is the name and `value` is the value 631 | /// if `=` is one of the assignment separators. 632 | assignment_separators: []const u8 = default_assignment_separators, 633 | 634 | /// This option makes `clap.parse` and `clap.parseEx` stop parsing after encountering a 635 | /// certain positional index. Setting `terminating_positional` to 0 will make them stop 636 | /// parsing after the 0th positional has been added to `positionals` (aka after parsing 1 637 | /// positional) 638 | terminating_positional: usize = std.math.maxInt(usize), 639 | }; 640 | 641 | /// Same as `parseEx` but uses the `args.OsIterator` by default. 642 | pub fn parse( 643 | comptime Id: type, 644 | comptime params: []const Param(Id), 645 | comptime value_parsers: anytype, 646 | opt: ParseOptions, 647 | ) !Result(Id, params, value_parsers) { 648 | var arena = std.heap.ArenaAllocator.init(opt.allocator); 649 | errdefer arena.deinit(); 650 | 651 | var iter = try std.process.ArgIterator.initWithAllocator(arena.allocator()); 652 | const exe_arg = iter.next(); 653 | 654 | const result = try parseEx(Id, params, value_parsers, &iter, .{ 655 | // Let's reuse the arena from the `ArgIterator` since we already have it. 656 | .allocator = arena.allocator(), 657 | .diagnostic = opt.diagnostic, 658 | .assignment_separators = opt.assignment_separators, 659 | .terminating_positional = opt.terminating_positional, 660 | }); 661 | 662 | return Result(Id, params, value_parsers){ 663 | .args = result.args, 664 | .positionals = result.positionals, 665 | .exe_arg = exe_arg, 666 | .arena = arena, 667 | }; 668 | } 669 | 670 | /// The result of `parse`. Is owned by the caller and should be freed with `deinit`. 671 | pub fn Result( 672 | comptime Id: type, 673 | comptime params: []const Param(Id), 674 | comptime value_parsers: anytype, 675 | ) type { 676 | return struct { 677 | args: Arguments(Id, params, value_parsers, .slice), 678 | positionals: Positionals(Id, params, value_parsers, .slice), 679 | exe_arg: ?[]const u8, 680 | arena: std.heap.ArenaAllocator, 681 | 682 | pub fn deinit(result: @This()) void { 683 | result.arena.deinit(); 684 | } 685 | }; 686 | } 687 | 688 | /// Parses the command line arguments passed into the program based on an array of parameters. 689 | /// 690 | /// The result will contain an `args` field which contains all the non positional arguments passed 691 | /// in. There is a field in `args` for each parameter. The name of that field will be the result 692 | /// of this expression: 693 | /// ``` 694 | /// param.names.longest().name` 695 | /// ``` 696 | /// 697 | /// The fields can have types other that `[]const u8` and this is based on what `value_parsers` 698 | /// you provide. The parser to use for each parameter is determined by the following expression: 699 | /// ``` 700 | /// @field(value_parsers, param.id.value()) 701 | /// ``` 702 | /// 703 | /// Where `value` is a function that returns the name of the value this parameter takes. A parser 704 | /// is simple a function with the signature: 705 | /// ``` 706 | /// fn ([]const u8) Error!T 707 | /// ``` 708 | /// 709 | /// `T` can be any type and `Error` can be any error. You can pass `clap.parsers.default` if you 710 | /// just wonna get something up and running. 711 | /// 712 | /// The result will also contain a `positionals` field which contains all positional arguments 713 | /// passed. This field will be a tuple with one field for each positional parameter. 714 | /// 715 | /// Example: 716 | /// -h, --help 717 | /// -s, --str 718 | /// -i, --int 719 | /// -m, --many ... 720 | /// 721 | /// ... 722 | /// 723 | /// struct { 724 | /// args: struct { 725 | /// help: u8, 726 | /// str: ?[]const u8, 727 | /// int: ?usize, 728 | /// many: []const usize, 729 | /// }, 730 | /// positionals: struct { 731 | /// ?u8, 732 | /// []const []const u8, 733 | /// }, 734 | /// } 735 | /// 736 | /// Caller owns the result and should free it by calling `result.deinit()` 737 | pub fn parseEx( 738 | comptime Id: type, 739 | comptime params: []const Param(Id), 740 | comptime value_parsers: anytype, 741 | iter: anytype, 742 | opt: ParseOptions, 743 | ) !ResultEx(Id, params, value_parsers) { 744 | const allocator = opt.allocator; 745 | 746 | var positional_count: usize = 0; 747 | var positionals = initPositionals(Id, params, value_parsers, .list); 748 | errdefer deinitPositionals(&positionals, allocator); 749 | 750 | var arguments = Arguments(Id, params, value_parsers, .list){}; 751 | errdefer deinitArgs(&arguments, allocator); 752 | 753 | var stream = streaming.Clap(Id, std.meta.Child(@TypeOf(iter))){ 754 | .params = params, 755 | .iter = iter, 756 | .diagnostic = opt.diagnostic, 757 | .assignment_separators = opt.assignment_separators, 758 | }; 759 | arg_loop: while (try stream.next()) |arg| { 760 | // This loop checks if we got a short or long parameter. If so, the value is parsed and 761 | // stored in `arguments` 762 | inline for (params) |*param| continue_params_loop: { 763 | const longest = comptime param.names.longest(); 764 | if (longest.kind == .positional) 765 | continue; 766 | 767 | if (param != arg.param) 768 | // This is a trick to emulate a runtime `continue` in an `inline for`. 769 | break :continue_params_loop; 770 | 771 | const parser = comptime switch (param.takes_value) { 772 | .none => null, 773 | .one, .many => @field(value_parsers, param.id.value()), 774 | }; 775 | 776 | const name = longest.name[0..longest.name.len].*; 777 | switch (param.takes_value) { 778 | .none => @field(arguments, &name) +|= 1, 779 | .one => @field(arguments, &name) = try parser(arg.value.?), 780 | .many => { 781 | const value = try parser(arg.value.?); 782 | try @field(arguments, &name).append(allocator, value); 783 | }, 784 | } 785 | } 786 | 787 | // This loop checks if we got a positional parameter. If so, the value is parsed and 788 | // stored in `positionals` 789 | comptime var positionals_index = 0; 790 | inline for (params) |*param| continue_params_loop: { 791 | const longest = comptime param.names.longest(); 792 | if (longest.kind != .positional) 793 | continue; 794 | 795 | const i = positionals_index; 796 | positionals_index += 1; 797 | 798 | if (arg.param.names.longest().kind != .positional) 799 | // This is a trick to emulate a runtime `continue` in an `inline for`. 800 | break :continue_params_loop; 801 | 802 | const parser = comptime switch (param.takes_value) { 803 | .none => null, 804 | .one, .many => @field(value_parsers, param.id.value()), 805 | }; 806 | 807 | // We keep track of how many positionals we have received. This is used to pick which 808 | // `positional` field to store to. Once `positional_count` exceeds the number of 809 | // positional parameters, the rest are stored in the last `positional` field. 810 | const pos = &positionals[i]; 811 | const last = positionals.len == i + 1; 812 | if ((last and positional_count >= i) or positional_count == i) { 813 | switch (@typeInfo(@TypeOf(pos.*))) { 814 | .optional => pos.* = try parser(arg.value.?), 815 | else => try pos.append(allocator, try parser(arg.value.?)), 816 | } 817 | 818 | if (opt.terminating_positional <= positional_count) 819 | break :arg_loop; 820 | positional_count += 1; 821 | continue :arg_loop; 822 | } 823 | } 824 | } 825 | 826 | // We are done parsing, but our arguments are stored in lists, and not slices. Map the list 827 | // fields to slices and return that. 828 | var result_args = Arguments(Id, params, value_parsers, .slice){}; 829 | inline for (std.meta.fields(@TypeOf(arguments))) |field| { 830 | switch (@typeInfo(field.type)) { 831 | .@"struct" => { 832 | const slice = try @field(arguments, field.name).toOwnedSlice(allocator); 833 | @field(result_args, field.name) = slice; 834 | }, 835 | else => @field(result_args, field.name) = @field(arguments, field.name), 836 | } 837 | } 838 | 839 | // We are done parsing, but our positionals are stored in lists, and not slices. 840 | var result_positionals: Positionals(Id, params, value_parsers, .slice) = undefined; 841 | inline for (&result_positionals, &positionals) |*res_pos, *pos| { 842 | switch (@typeInfo(@TypeOf(pos.*))) { 843 | .@"struct" => res_pos.* = try pos.toOwnedSlice(allocator), 844 | else => res_pos.* = pos.*, 845 | } 846 | } 847 | 848 | return ResultEx(Id, params, value_parsers){ 849 | .args = result_args, 850 | .positionals = result_positionals, 851 | .allocator = allocator, 852 | }; 853 | } 854 | 855 | /// The result of `parseEx`. Is owned by the caller and should be freed with `deinit`. 856 | pub fn ResultEx( 857 | comptime Id: type, 858 | comptime params: []const Param(Id), 859 | comptime value_parsers: anytype, 860 | ) type { 861 | return struct { 862 | args: Arguments(Id, params, value_parsers, .slice), 863 | positionals: Positionals(Id, params, value_parsers, .slice), 864 | allocator: std.mem.Allocator, 865 | 866 | pub fn deinit(result: *@This()) void { 867 | deinitArgs(&result.args, result.allocator); 868 | deinitPositionals(&result.positionals, result.allocator); 869 | } 870 | }; 871 | } 872 | 873 | /// Turn a list of parameters into a tuple with one field for each positional parameter. 874 | /// The type of each parameter field is determined by `ParamType`. 875 | fn Positionals( 876 | comptime Id: type, 877 | comptime params: []const Param(Id), 878 | comptime value_parsers: anytype, 879 | comptime multi_arg_kind: MultiArgKind, 880 | ) type { 881 | var fields_len: usize = 0; 882 | for (params) |param| { 883 | const longest = param.names.longest(); 884 | if (longest.kind != .positional) 885 | continue; 886 | fields_len += 1; 887 | } 888 | 889 | var fields: [fields_len]std.builtin.Type.StructField = undefined; 890 | var i: usize = 0; 891 | for (params) |param| { 892 | const longest = param.names.longest(); 893 | if (longest.kind != .positional) 894 | continue; 895 | 896 | const T = ParamType(Id, param, value_parsers); 897 | const FieldT = switch (param.takes_value) { 898 | .none => continue, 899 | .one => ?T, 900 | .many => switch (multi_arg_kind) { 901 | .slice => []const T, 902 | .list => std.ArrayListUnmanaged(T), 903 | }, 904 | }; 905 | 906 | fields[i] = .{ 907 | .name = std.fmt.comptimePrint("{}", .{i}), 908 | .type = FieldT, 909 | .default_value_ptr = null, 910 | .is_comptime = false, 911 | .alignment = @alignOf(FieldT), 912 | }; 913 | i += 1; 914 | } 915 | 916 | return @Type(.{ .@"struct" = .{ 917 | .layout = .auto, 918 | .fields = &fields, 919 | .decls = &.{}, 920 | .is_tuple = true, 921 | } }); 922 | } 923 | 924 | fn initPositionals( 925 | comptime Id: type, 926 | comptime params: []const Param(Id), 927 | comptime value_parsers: anytype, 928 | comptime multi_arg_kind: MultiArgKind, 929 | ) Positionals(Id, params, value_parsers, multi_arg_kind) { 930 | var res: Positionals(Id, params, value_parsers, multi_arg_kind) = undefined; 931 | 932 | comptime var i: usize = 0; 933 | inline for (params) |param| { 934 | const longest = comptime param.names.longest(); 935 | if (longest.kind != .positional) 936 | continue; 937 | 938 | const T = ParamType(Id, param, value_parsers); 939 | res[i] = switch (param.takes_value) { 940 | .none => continue, 941 | .one => @as(?T, null), 942 | .many => switch (multi_arg_kind) { 943 | .slice => @as([]const T, &[_]T{}), 944 | .list => std.ArrayListUnmanaged(T){}, 945 | }, 946 | }; 947 | i += 1; 948 | } 949 | 950 | return res; 951 | } 952 | 953 | /// Deinitializes a tuple of type `Positionals`. Since the `Positionals` type is generated, and we 954 | /// cannot add the deinit declaration to it, we declare it here instead. 955 | fn deinitPositionals(positionals: anytype, allocator: std.mem.Allocator) void { 956 | inline for (positionals) |*pos| { 957 | switch (@typeInfo(@TypeOf(pos.*))) { 958 | .optional => {}, 959 | .@"struct" => pos.deinit(allocator), 960 | else => allocator.free(pos.*), 961 | } 962 | } 963 | } 964 | 965 | /// Given a parameter figure out which type that parameter is parsed into when using the correct 966 | /// parser from `value_parsers`. 967 | fn ParamType(comptime Id: type, comptime param: Param(Id), comptime value_parsers: anytype) type { 968 | const parser = switch (param.takes_value) { 969 | .none => parsers.string, 970 | .one, .many => @field(value_parsers, param.id.value()), 971 | }; 972 | return parsers.Result(@TypeOf(parser)); 973 | } 974 | 975 | /// Deinitializes a struct of type `Argument`. Since the `Argument` type is generated, and we 976 | /// cannot add the deinit declaration to it, we declare it here instead. 977 | fn deinitArgs(arguments: anytype, allocator: std.mem.Allocator) void { 978 | inline for (@typeInfo(@TypeOf(arguments.*)).@"struct".fields) |field| { 979 | switch (@typeInfo(field.type)) { 980 | .int, .optional => {}, 981 | .@"struct" => @field(arguments, field.name).deinit(allocator), 982 | else => allocator.free(@field(arguments, field.name)), 983 | } 984 | } 985 | } 986 | 987 | const MultiArgKind = enum { slice, list }; 988 | 989 | /// Turn a list of parameters into a struct with one field for each none positional parameter. 990 | /// The type of each parameter field is determined by `ParamType`. Positional arguments will not 991 | /// have a field in this struct. 992 | fn Arguments( 993 | comptime Id: type, 994 | comptime params: []const Param(Id), 995 | comptime value_parsers: anytype, 996 | comptime multi_arg_kind: MultiArgKind, 997 | ) type { 998 | var fields_len: usize = 0; 999 | for (params) |param| { 1000 | const longest = param.names.longest(); 1001 | if (longest.kind == .positional) 1002 | continue; 1003 | fields_len += 1; 1004 | } 1005 | 1006 | var fields: [fields_len]std.builtin.Type.StructField = undefined; 1007 | var i: usize = 0; 1008 | for (params) |param| { 1009 | const longest = param.names.longest(); 1010 | if (longest.kind == .positional) 1011 | continue; 1012 | 1013 | const T = ParamType(Id, param, value_parsers); 1014 | const default_value = switch (param.takes_value) { 1015 | .none => @as(u8, 0), 1016 | .one => @as(?T, null), 1017 | .many => switch (multi_arg_kind) { 1018 | .slice => @as([]const T, &[_]T{}), 1019 | .list => std.ArrayListUnmanaged(T){}, 1020 | }, 1021 | }; 1022 | 1023 | const name = longest.name[0..longest.name.len] ++ ""; // Adds null terminator 1024 | fields[i] = .{ 1025 | .name = name, 1026 | .type = @TypeOf(default_value), 1027 | .default_value_ptr = @ptrCast(&default_value), 1028 | .is_comptime = false, 1029 | .alignment = @alignOf(@TypeOf(default_value)), 1030 | }; 1031 | i += 1; 1032 | } 1033 | 1034 | return @Type(.{ .@"struct" = .{ 1035 | .layout = .auto, 1036 | .fields = &fields, 1037 | .decls = &.{}, 1038 | .is_tuple = false, 1039 | } }); 1040 | } 1041 | 1042 | test "str and u64" { 1043 | const params = comptime parseParamsComptime( 1044 | \\--str 1045 | \\--num 1046 | \\ 1047 | ); 1048 | 1049 | var iter = args.SliceIterator{ 1050 | .args = &.{ "--num", "10", "--str", "cooley_rec_inp_ptr" }, 1051 | }; 1052 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1053 | .allocator = std.testing.allocator, 1054 | }); 1055 | defer res.deinit(); 1056 | } 1057 | 1058 | test "different assignment separators" { 1059 | const params = comptime parseParamsComptime( 1060 | \\-a, --aa ... 1061 | \\ 1062 | ); 1063 | 1064 | var iter = args.SliceIterator{ 1065 | .args = &.{ "-a=0", "--aa=1", "-a:2", "--aa:3" }, 1066 | }; 1067 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1068 | .allocator = std.testing.allocator, 1069 | .assignment_separators = "=:", 1070 | }); 1071 | defer res.deinit(); 1072 | 1073 | try std.testing.expectEqualSlices(usize, &.{ 0, 1, 2, 3 }, res.args.aa); 1074 | } 1075 | 1076 | test "single positional" { 1077 | const params = comptime parseParamsComptime( 1078 | \\ 1079 | \\ 1080 | ); 1081 | 1082 | { 1083 | var iter = args.SliceIterator{ .args = &.{} }; 1084 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1085 | .allocator = std.testing.allocator, 1086 | }); 1087 | defer res.deinit(); 1088 | 1089 | try std.testing.expect(res.positionals[0] == null); 1090 | } 1091 | 1092 | { 1093 | var iter = args.SliceIterator{ .args = &.{"a"} }; 1094 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1095 | .allocator = std.testing.allocator, 1096 | }); 1097 | defer res.deinit(); 1098 | 1099 | try std.testing.expectEqualStrings("a", res.positionals[0].?); 1100 | } 1101 | 1102 | { 1103 | var iter = args.SliceIterator{ .args = &.{ "a", "b" } }; 1104 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1105 | .allocator = std.testing.allocator, 1106 | }); 1107 | defer res.deinit(); 1108 | 1109 | try std.testing.expectEqualStrings("b", res.positionals[0].?); 1110 | } 1111 | } 1112 | 1113 | test "multiple positionals" { 1114 | const params = comptime parseParamsComptime( 1115 | \\ 1116 | \\ 1117 | \\ 1118 | \\ 1119 | ); 1120 | 1121 | { 1122 | var iter = args.SliceIterator{ .args = &.{} }; 1123 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1124 | .allocator = std.testing.allocator, 1125 | }); 1126 | defer res.deinit(); 1127 | 1128 | try std.testing.expect(res.positionals[0] == null); 1129 | try std.testing.expect(res.positionals[1] == null); 1130 | try std.testing.expect(res.positionals[2] == null); 1131 | } 1132 | 1133 | { 1134 | var iter = args.SliceIterator{ .args = &.{"1"} }; 1135 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1136 | .allocator = std.testing.allocator, 1137 | }); 1138 | defer res.deinit(); 1139 | 1140 | try std.testing.expectEqual(@as(u8, 1), res.positionals[0].?); 1141 | try std.testing.expect(res.positionals[1] == null); 1142 | try std.testing.expect(res.positionals[2] == null); 1143 | } 1144 | 1145 | { 1146 | var iter = args.SliceIterator{ .args = &.{ "1", "2" } }; 1147 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1148 | .allocator = std.testing.allocator, 1149 | }); 1150 | defer res.deinit(); 1151 | 1152 | try std.testing.expectEqual(@as(u8, 1), res.positionals[0].?); 1153 | try std.testing.expectEqual(@as(u8, 2), res.positionals[1].?); 1154 | try std.testing.expect(res.positionals[2] == null); 1155 | } 1156 | 1157 | { 1158 | var iter = args.SliceIterator{ .args = &.{ "1", "2", "b" } }; 1159 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1160 | .allocator = std.testing.allocator, 1161 | }); 1162 | defer res.deinit(); 1163 | 1164 | try std.testing.expectEqual(@as(u8, 1), res.positionals[0].?); 1165 | try std.testing.expectEqual(@as(u8, 2), res.positionals[1].?); 1166 | try std.testing.expectEqualStrings("b", res.positionals[2].?); 1167 | } 1168 | } 1169 | 1170 | test "everything" { 1171 | const params = comptime parseParamsComptime( 1172 | \\-a, --aa 1173 | \\-b, --bb 1174 | \\-c, --cc 1175 | \\-d, --dd ... 1176 | \\-h 1177 | \\... 1178 | \\ 1179 | ); 1180 | 1181 | var iter = args.SliceIterator{ 1182 | .args = &.{ "-a", "--aa", "-c", "0", "something", "-d", "1", "--dd", "2", "-h" }, 1183 | }; 1184 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1185 | .allocator = std.testing.allocator, 1186 | }); 1187 | defer res.deinit(); 1188 | 1189 | try std.testing.expect(res.args.aa == 2); 1190 | try std.testing.expect(res.args.bb == 0); 1191 | try std.testing.expect(res.args.h == 1); 1192 | try std.testing.expectEqualStrings("0", res.args.cc.?); 1193 | try std.testing.expectEqual(@as(usize, 1), res.positionals.len); 1194 | try std.testing.expectEqualStrings("something", res.positionals[0][0]); 1195 | try std.testing.expectEqualSlices(usize, &.{ 1, 2 }, res.args.dd); 1196 | try std.testing.expectEqual(@as(usize, 10), iter.index); 1197 | } 1198 | 1199 | test "terminating positional" { 1200 | const params = comptime parseParamsComptime( 1201 | \\-a, --aa 1202 | \\-b, --bb 1203 | \\-c, --cc 1204 | \\-d, --dd ... 1205 | \\-h 1206 | \\... 1207 | \\ 1208 | ); 1209 | 1210 | var iter = args.SliceIterator{ 1211 | .args = &.{ "-a", "--aa", "-c", "0", "something", "-d", "1", "--dd", "2", "-h" }, 1212 | }; 1213 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1214 | .allocator = std.testing.allocator, 1215 | .terminating_positional = 0, 1216 | }); 1217 | defer res.deinit(); 1218 | 1219 | try std.testing.expect(res.args.aa == 2); 1220 | try std.testing.expect(res.args.bb == 0); 1221 | try std.testing.expect(res.args.h == 0); 1222 | try std.testing.expectEqualStrings("0", res.args.cc.?); 1223 | try std.testing.expectEqual(@as(usize, 1), res.positionals.len); 1224 | try std.testing.expectEqual(@as(usize, 1), res.positionals[0].len); 1225 | try std.testing.expectEqualStrings("something", res.positionals[0][0]); 1226 | try std.testing.expectEqualSlices(usize, &.{}, res.args.dd); 1227 | try std.testing.expectEqual(@as(usize, 5), iter.index); 1228 | } 1229 | 1230 | test "overflow-safe" { 1231 | const params = comptime parseParamsComptime( 1232 | \\-a, --aa 1233 | ); 1234 | 1235 | var iter = args.SliceIterator{ 1236 | .args = &(.{"-" ++ ("a" ** 300)}), 1237 | }; 1238 | 1239 | // This just needs to not crash 1240 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1241 | .allocator = std.testing.allocator, 1242 | }); 1243 | defer res.deinit(); 1244 | } 1245 | 1246 | test "empty" { 1247 | var iter = args.SliceIterator{ .args = &.{} }; 1248 | var res = try parseEx(u8, &[_]Param(u8){}, parsers.default, &iter, .{ 1249 | .allocator = std.testing.allocator, 1250 | }); 1251 | defer res.deinit(); 1252 | } 1253 | 1254 | fn testErr( 1255 | comptime params: []const Param(Help), 1256 | args_strings: []const []const u8, 1257 | expected: []const u8, 1258 | ) !void { 1259 | var diag = Diagnostic{}; 1260 | var iter = args.SliceIterator{ .args = args_strings }; 1261 | _ = parseEx(Help, params, parsers.default, &iter, .{ 1262 | .allocator = std.testing.allocator, 1263 | .diagnostic = &diag, 1264 | }) catch |err| { 1265 | var buf: [1024]u8 = undefined; 1266 | var fbs = std.io.fixedBufferStream(&buf); 1267 | diag.report(fbs.writer(), err) catch return error.TestFailed; 1268 | try std.testing.expectEqualStrings(expected, fbs.getWritten()); 1269 | return; 1270 | }; 1271 | 1272 | try std.testing.expect(false); 1273 | } 1274 | 1275 | test "errors" { 1276 | const params = comptime parseParamsComptime( 1277 | \\-a, --aa 1278 | \\-c, --cc 1279 | \\ 1280 | ); 1281 | 1282 | try testErr(¶ms, &.{"q"}, "Invalid argument 'q'\n"); 1283 | try testErr(¶ms, &.{"-q"}, "Invalid argument '-q'\n"); 1284 | try testErr(¶ms, &.{"--q"}, "Invalid argument '--q'\n"); 1285 | try testErr(¶ms, &.{"--q=1"}, "Invalid argument '--q'\n"); 1286 | try testErr(¶ms, &.{"-a=1"}, "The argument '-a' does not take a value\n"); 1287 | try testErr(¶ms, &.{"--aa=1"}, "The argument '--aa' does not take a value\n"); 1288 | try testErr(¶ms, &.{"-c"}, "The argument '-c' requires a value but none was supplied\n"); 1289 | try testErr( 1290 | ¶ms, 1291 | &.{"--cc"}, 1292 | "The argument '--cc' requires a value but none was supplied\n", 1293 | ); 1294 | } 1295 | 1296 | pub const Help = struct { 1297 | desc: []const u8 = "", 1298 | val: []const u8 = "", 1299 | 1300 | pub fn description(h: Help) []const u8 { 1301 | return h.desc; 1302 | } 1303 | 1304 | pub fn value(h: Help) []const u8 { 1305 | return h.val; 1306 | } 1307 | }; 1308 | 1309 | pub const HelpOptions = struct { 1310 | /// Render the description of a parameter in a simular way to how markdown would render 1311 | /// such a string. This means that single newlines won't be respected unless followed by 1312 | /// bullet points or other markdown elements. 1313 | markdown_lite: bool = true, 1314 | 1315 | /// Whether `help` should print the description of a parameter on a new line instead of after 1316 | /// the parameter names. This options works together with `description_indent` to change 1317 | /// where descriptions are printed. 1318 | /// 1319 | /// description_on_new_line=false, description_indent=4 1320 | /// 1321 | /// -a, --aa This is a description 1322 | /// that is not placed on 1323 | /// a new line. 1324 | /// 1325 | /// description_on_new_line=true, description_indent=4 1326 | /// 1327 | /// -a, --aa 1328 | /// This is a description 1329 | /// that is placed on a 1330 | /// new line. 1331 | description_on_new_line: bool = true, 1332 | 1333 | /// How much to indent descriptions. See `description_on_new_line` for examples of how this 1334 | /// changes the output. 1335 | description_indent: usize = 8, 1336 | 1337 | /// How much to indent each parameter. 1338 | /// 1339 | /// indent=0, description_on_new_line=false, description_indent=4 1340 | /// 1341 | /// -a, --aa This is a description 1342 | /// that is not placed on 1343 | /// a new line. 1344 | /// 1345 | /// indent=4, description_on_new_line=false, description_indent=4 1346 | /// 1347 | /// -a, --aa This is a description 1348 | /// that is not placed on 1349 | /// a new line. 1350 | /// 1351 | indent: usize = 4, 1352 | 1353 | /// The maximum width of the help message. `help` will try to break the description of 1354 | /// parameters into multiple lines if they exceed this maximum. Setting this to the width 1355 | /// of the terminal is a nice way of using this option. 1356 | max_width: usize = std.math.maxInt(usize), 1357 | 1358 | /// The number of empty lines between each printed parameter. 1359 | spacing_between_parameters: usize = 1, 1360 | }; 1361 | 1362 | /// Print a slice of `Param` formatted as a help string to `writer`. This function expects 1363 | /// `Id` to have the methods `description` and `value` which are used by `help` to describe 1364 | /// each parameter. Using `Help` as `Id` is good choice. 1365 | /// 1366 | /// The output can be constumized with the `opt` parameter. For default formatting `.{}` can 1367 | /// be passed. 1368 | pub fn help( 1369 | writer: anytype, 1370 | comptime Id: type, 1371 | params: []const Param(Id), 1372 | opt: HelpOptions, 1373 | ) !void { 1374 | const max_spacing = blk: { 1375 | var res: usize = 0; 1376 | for (params) |param| { 1377 | var cs = ccw.codepointCountingWriter(std.io.null_writer); 1378 | try printParam(cs.writer(), Id, param); 1379 | if (res < cs.codepoints_written) 1380 | res = @intCast(cs.codepoints_written); 1381 | } 1382 | 1383 | break :blk res; 1384 | }; 1385 | 1386 | const description_indentation = opt.indent + 1387 | opt.description_indent + 1388 | max_spacing * @intFromBool(!opt.description_on_new_line); 1389 | 1390 | var first_parameter: bool = true; 1391 | for (params) |param| { 1392 | if (!first_parameter) 1393 | try writer.writeByteNTimes('\n', opt.spacing_between_parameters); 1394 | 1395 | first_parameter = false; 1396 | try writer.writeByteNTimes(' ', opt.indent); 1397 | 1398 | var cw = ccw.codepointCountingWriter(writer); 1399 | try printParam(cw.writer(), Id, param); 1400 | 1401 | const Writer = DescriptionWriter(@TypeOf(writer)); 1402 | var description_writer = Writer{ 1403 | .underlying_writer = writer, 1404 | .indentation = description_indentation, 1405 | .printed_chars = @intCast(cw.codepoints_written), 1406 | .max_width = opt.max_width, 1407 | }; 1408 | 1409 | if (opt.description_on_new_line) 1410 | try description_writer.newline(); 1411 | 1412 | const min_description_indent = blk: { 1413 | const description = param.id.description(); 1414 | 1415 | var first_line = true; 1416 | var res: usize = std.math.maxInt(usize); 1417 | var it = std.mem.tokenizeScalar(u8, description, '\n'); 1418 | while (it.next()) |line| : (first_line = false) { 1419 | const trimmed = std.mem.trimLeft(u8, line, " "); 1420 | const indent = line.len - trimmed.len; 1421 | 1422 | // If the first line has no indentation, then we ignore the indentation of the 1423 | // first line. We do this as the parameter might have been parsed from: 1424 | // 1425 | // -a, --aa The first line 1426 | // is not indented, 1427 | // but the rest of 1428 | // the lines are. 1429 | // 1430 | // In this case, we want to pretend that the first line has the same indentation 1431 | // as the min_description_indent, even though it is not so in the string we get. 1432 | if (first_line and indent == 0) 1433 | continue; 1434 | if (indent < res) 1435 | res = indent; 1436 | } 1437 | 1438 | break :blk res; 1439 | }; 1440 | 1441 | const description = param.id.description(); 1442 | var it = std.mem.splitScalar(u8, description, '\n'); 1443 | var first_line = true; 1444 | var non_emitted_newlines: usize = 0; 1445 | var last_line_indentation: usize = 0; 1446 | while (it.next()) |raw_line| : (first_line = false) { 1447 | // First line might be special. See comment above. 1448 | const indented_line = if (first_line and !std.mem.startsWith(u8, raw_line, " ")) 1449 | raw_line 1450 | else 1451 | raw_line[@min(min_description_indent, raw_line.len)..]; 1452 | 1453 | const line = std.mem.trimLeft(u8, indented_line, " "); 1454 | if (line.len == 0) { 1455 | non_emitted_newlines += 1; 1456 | continue; 1457 | } 1458 | 1459 | const line_indentation = indented_line.len - line.len; 1460 | description_writer.indentation = description_indentation + line_indentation; 1461 | 1462 | if (opt.markdown_lite) { 1463 | const new_paragraph = non_emitted_newlines > 1; 1464 | 1465 | const does_not_have_same_indent_as_last_line = 1466 | line_indentation != last_line_indentation; 1467 | 1468 | const starts_with_control_char = std.mem.indexOfScalar(u8, "=*", line[0]) != null; 1469 | 1470 | // Either the input contains 2 or more newlines, in which case we should start 1471 | // a new paragraph. 1472 | if (new_paragraph) { 1473 | try description_writer.newline(); 1474 | try description_writer.newline(); 1475 | } 1476 | // Or this line has a special control char or different indentation which means 1477 | // we should output it on a new line as well. 1478 | else if (starts_with_control_char or does_not_have_same_indent_as_last_line) { 1479 | try description_writer.newline(); 1480 | } 1481 | } else { 1482 | // For none markdown like format, we just respect the newlines in the input 1483 | // string and output them as is. 1484 | for (0..non_emitted_newlines) |_| 1485 | try description_writer.newline(); 1486 | } 1487 | 1488 | var words = std.mem.tokenizeScalar(u8, line, ' '); 1489 | while (words.next()) |word| 1490 | try description_writer.writeWord(word); 1491 | 1492 | // We have not emitted the end of this line yet. 1493 | non_emitted_newlines = 1; 1494 | last_line_indentation = line_indentation; 1495 | } 1496 | 1497 | try writer.writeAll("\n"); 1498 | } 1499 | } 1500 | 1501 | fn DescriptionWriter(comptime UnderlyingWriter: type) type { 1502 | return struct { 1503 | pub const WriteError = UnderlyingWriter.Error; 1504 | 1505 | underlying_writer: UnderlyingWriter, 1506 | 1507 | indentation: usize, 1508 | max_width: usize, 1509 | printed_chars: usize, 1510 | 1511 | pub fn writeWord(writer: *@This(), word: []const u8) !void { 1512 | std.debug.assert(word.len != 0); 1513 | 1514 | var first_word = writer.printed_chars <= writer.indentation; 1515 | const chars_to_write = try std.unicode.utf8CountCodepoints(word) + @intFromBool(!first_word); 1516 | if (chars_to_write + writer.printed_chars > writer.max_width) { 1517 | // If the word does not fit on this line, then we insert a new line and print 1518 | // it on that line. The only exception to this is if this was the first word. 1519 | // If the first word does not fit on this line, then it will also not fit on the 1520 | // next one. In that case, all we can really do is just output the word. 1521 | if (!first_word) 1522 | try writer.newline(); 1523 | 1524 | first_word = true; 1525 | } 1526 | 1527 | if (!first_word) 1528 | try writer.underlying_writer.writeAll(" "); 1529 | 1530 | try writer.ensureIndented(); 1531 | try writer.underlying_writer.writeAll(word); 1532 | writer.printed_chars += chars_to_write; 1533 | } 1534 | 1535 | pub fn newline(writer: *@This()) !void { 1536 | try writer.underlying_writer.writeAll("\n"); 1537 | writer.printed_chars = 0; 1538 | } 1539 | 1540 | fn ensureIndented(writer: *@This()) !void { 1541 | if (writer.printed_chars < writer.indentation) { 1542 | const to_indent = writer.indentation - writer.printed_chars; 1543 | try writer.underlying_writer.writeByteNTimes(' ', to_indent); 1544 | writer.printed_chars += to_indent; 1545 | } 1546 | } 1547 | }; 1548 | } 1549 | 1550 | fn printParam( 1551 | stream: anytype, 1552 | comptime Id: type, 1553 | param: Param(Id), 1554 | ) !void { 1555 | if (param.names.short != null or param.names.long != null) { 1556 | try stream.writeAll(&[_]u8{ 1557 | if (param.names.short) |_| '-' else ' ', 1558 | param.names.short orelse ' ', 1559 | }); 1560 | 1561 | if (param.names.long) |l| { 1562 | try stream.writeByte(if (param.names.short) |_| ',' else ' '); 1563 | try stream.writeAll(" --"); 1564 | try stream.writeAll(l); 1565 | } 1566 | 1567 | if (param.takes_value != .none) 1568 | try stream.writeAll(" "); 1569 | } 1570 | 1571 | if (param.takes_value == .none) 1572 | return; 1573 | 1574 | try stream.writeAll("<"); 1575 | try stream.writeAll(param.id.value()); 1576 | try stream.writeAll(">"); 1577 | if (param.takes_value == .many) 1578 | try stream.writeAll("..."); 1579 | } 1580 | 1581 | fn testHelp(opt: HelpOptions, str: []const u8) !void { 1582 | const params = try parseParams(std.testing.allocator, str); 1583 | defer std.testing.allocator.free(params); 1584 | 1585 | var buf: [2048]u8 = undefined; 1586 | var fbs = std.io.fixedBufferStream(&buf); 1587 | try help(fbs.writer(), Help, params, opt); 1588 | try std.testing.expectEqualStrings(str, fbs.getWritten()); 1589 | } 1590 | 1591 | test "clap.help" { 1592 | try testHelp(.{}, 1593 | \\ -a 1594 | \\ Short flag. 1595 | \\ 1596 | \\ -b 1597 | \\ Short option. 1598 | \\ 1599 | \\ --aa 1600 | \\ Long flag. 1601 | \\ 1602 | \\ --bb 1603 | \\ Long option. 1604 | \\ 1605 | \\ -c, --cc 1606 | \\ Both flag. 1607 | \\ 1608 | \\ --complicate 1609 | \\ Flag with a complicated and very long description that spans multiple lines. 1610 | \\ 1611 | \\ Paragraph number 2: 1612 | \\ * Bullet point 1613 | \\ * Bullet point 1614 | \\ 1615 | \\ Example: 1616 | \\ something something something 1617 | \\ 1618 | \\ -d, --dd 1619 | \\ Both option. 1620 | \\ 1621 | \\ -d, --dd ... 1622 | \\ Both repeated option. 1623 | \\ 1624 | \\ 1625 | \\ Help text 1626 | \\ 1627 | \\ ... 1628 | \\ Another help text 1629 | \\ 1630 | ); 1631 | 1632 | try testHelp(.{ .markdown_lite = false }, 1633 | \\ -a 1634 | \\ Short flag. 1635 | \\ 1636 | \\ -b 1637 | \\ Short option. 1638 | \\ 1639 | \\ --aa 1640 | \\ Long flag. 1641 | \\ 1642 | \\ --bb 1643 | \\ Long option. 1644 | \\ 1645 | \\ -c, --cc 1646 | \\ Both flag. 1647 | \\ 1648 | \\ --complicate 1649 | \\ Flag with a complicated and 1650 | \\ very long description that 1651 | \\ spans multiple lines. 1652 | \\ 1653 | \\ Paragraph number 2: 1654 | \\ * Bullet point 1655 | \\ * Bullet point 1656 | \\ 1657 | \\ 1658 | \\ Example: 1659 | \\ something something something 1660 | \\ 1661 | \\ -d, --dd 1662 | \\ Both option. 1663 | \\ 1664 | \\ -d, --dd ... 1665 | \\ Both repeated option. 1666 | \\ 1667 | ); 1668 | 1669 | try testHelp(.{ .indent = 0 }, 1670 | \\-a 1671 | \\ Short flag. 1672 | \\ 1673 | \\-b 1674 | \\ Short option. 1675 | \\ 1676 | \\ --aa 1677 | \\ Long flag. 1678 | \\ 1679 | \\ --bb 1680 | \\ Long option. 1681 | \\ 1682 | \\-c, --cc 1683 | \\ Both flag. 1684 | \\ 1685 | \\ --complicate 1686 | \\ Flag with a complicated and very long description that spans multiple lines. 1687 | \\ 1688 | \\ Paragraph number 2: 1689 | \\ * Bullet point 1690 | \\ * Bullet point 1691 | \\ 1692 | \\ Example: 1693 | \\ something something something 1694 | \\ 1695 | \\-d, --dd 1696 | \\ Both option. 1697 | \\ 1698 | \\-d, --dd ... 1699 | \\ Both repeated option. 1700 | \\ 1701 | ); 1702 | 1703 | try testHelp(.{ .indent = 0 }, 1704 | \\-a 1705 | \\ Short flag. 1706 | \\ 1707 | \\-b 1708 | \\ Short option. 1709 | \\ 1710 | \\ --aa 1711 | \\ Long flag. 1712 | \\ 1713 | \\ --bb 1714 | \\ Long option. 1715 | \\ 1716 | \\-c, --cc 1717 | \\ Both flag. 1718 | \\ 1719 | \\ --complicate 1720 | \\ Flag with a complicated and very long description that spans multiple lines. 1721 | \\ 1722 | \\ Paragraph number 2: 1723 | \\ * Bullet point 1724 | \\ * Bullet point 1725 | \\ 1726 | \\ Example: 1727 | \\ something something something 1728 | \\ 1729 | \\-d, --dd 1730 | \\ Both option. 1731 | \\ 1732 | \\-d, --dd ... 1733 | \\ Both repeated option. 1734 | \\ 1735 | ); 1736 | 1737 | try testHelp(.{ .indent = 0, .max_width = 26 }, 1738 | \\-a 1739 | \\ Short flag. 1740 | \\ 1741 | \\-b 1742 | \\ Short option. 1743 | \\ 1744 | \\ --aa 1745 | \\ Long flag. 1746 | \\ 1747 | \\ --bb 1748 | \\ Long option. 1749 | \\ 1750 | \\-c, --cc 1751 | \\ Both flag. 1752 | \\ 1753 | \\ --complicate 1754 | \\ Flag with a 1755 | \\ complicated and 1756 | \\ very long 1757 | \\ description that 1758 | \\ spans multiple 1759 | \\ lines. 1760 | \\ 1761 | \\ Paragraph number 1762 | \\ 2: 1763 | \\ * Bullet point 1764 | \\ * Bullet point 1765 | \\ 1766 | \\ Example: 1767 | \\ something 1768 | \\ something 1769 | \\ something 1770 | \\ 1771 | \\-d, --dd 1772 | \\ Both option. 1773 | \\ 1774 | \\-d, --dd ... 1775 | \\ Both repeated 1776 | \\ option. 1777 | \\ 1778 | ); 1779 | 1780 | try testHelp(.{ 1781 | .indent = 0, 1782 | .max_width = 26, 1783 | .description_indent = 6, 1784 | }, 1785 | \\-a 1786 | \\ Short flag. 1787 | \\ 1788 | \\-b 1789 | \\ Short option. 1790 | \\ 1791 | \\ --aa 1792 | \\ Long flag. 1793 | \\ 1794 | \\ --bb 1795 | \\ Long option. 1796 | \\ 1797 | \\-c, --cc 1798 | \\ Both flag. 1799 | \\ 1800 | \\ --complicate 1801 | \\ Flag with a 1802 | \\ complicated and 1803 | \\ very long 1804 | \\ description that 1805 | \\ spans multiple 1806 | \\ lines. 1807 | \\ 1808 | \\ Paragraph number 2: 1809 | \\ * Bullet point 1810 | \\ * Bullet point 1811 | \\ 1812 | \\ Example: 1813 | \\ something 1814 | \\ something 1815 | \\ something 1816 | \\ 1817 | \\-d, --dd 1818 | \\ Both option. 1819 | \\ 1820 | \\-d, --dd ... 1821 | \\ Both repeated 1822 | \\ option. 1823 | \\ 1824 | ); 1825 | 1826 | try testHelp(.{ 1827 | .indent = 0, 1828 | .max_width = 46, 1829 | .description_on_new_line = false, 1830 | }, 1831 | \\-a Short flag. 1832 | \\ 1833 | \\-b Short option. 1834 | \\ 1835 | \\ --aa Long flag. 1836 | \\ 1837 | \\ --bb Long option. 1838 | \\ 1839 | \\-c, --cc Both flag. 1840 | \\ 1841 | \\ --complicate Flag with a 1842 | \\ complicated and very 1843 | \\ long description that 1844 | \\ spans multiple lines. 1845 | \\ 1846 | \\ Paragraph number 2: 1847 | \\ * Bullet point 1848 | \\ * Bullet point 1849 | \\ 1850 | \\ Example: 1851 | \\ something 1852 | \\ something 1853 | \\ something 1854 | \\ 1855 | \\-d, --dd Both option. 1856 | \\ 1857 | \\-d, --dd ... Both repeated option. 1858 | \\ 1859 | ); 1860 | 1861 | try testHelp(.{ 1862 | .indent = 0, 1863 | .max_width = 46, 1864 | .description_on_new_line = false, 1865 | .description_indent = 4, 1866 | }, 1867 | \\-a Short flag. 1868 | \\ 1869 | \\-b Short option. 1870 | \\ 1871 | \\ --aa Long flag. 1872 | \\ 1873 | \\ --bb Long option. 1874 | \\ 1875 | \\-c, --cc Both flag. 1876 | \\ 1877 | \\ --complicate Flag with a complicated 1878 | \\ and very long description 1879 | \\ that spans multiple 1880 | \\ lines. 1881 | \\ 1882 | \\ Paragraph number 2: 1883 | \\ * Bullet point 1884 | \\ * Bullet point 1885 | \\ 1886 | \\ Example: 1887 | \\ something something 1888 | \\ something 1889 | \\ 1890 | \\-d, --dd Both option. 1891 | \\ 1892 | \\-d, --dd ... Both repeated option. 1893 | \\ 1894 | ); 1895 | 1896 | try testHelp(.{ 1897 | .indent = 0, 1898 | .max_width = 46, 1899 | .description_on_new_line = false, 1900 | .description_indent = 4, 1901 | .spacing_between_parameters = 0, 1902 | }, 1903 | \\-a Short flag. 1904 | \\-b Short option. 1905 | \\ --aa Long flag. 1906 | \\ --bb Long option. 1907 | \\-c, --cc Both flag. 1908 | \\ --complicate Flag with a complicated 1909 | \\ and very long description 1910 | \\ that spans multiple 1911 | \\ lines. 1912 | \\ 1913 | \\ Paragraph number 2: 1914 | \\ * Bullet point 1915 | \\ * Bullet point 1916 | \\ 1917 | \\ Example: 1918 | \\ something something 1919 | \\ something 1920 | \\-d, --dd Both option. 1921 | \\-d, --dd ... Both repeated option. 1922 | \\ 1923 | ); 1924 | 1925 | try testHelp(.{ 1926 | .indent = 0, 1927 | .max_width = 46, 1928 | .description_on_new_line = false, 1929 | .description_indent = 4, 1930 | .spacing_between_parameters = 2, 1931 | }, 1932 | \\-a Short flag. 1933 | \\ 1934 | \\ 1935 | \\-b Short option. 1936 | \\ 1937 | \\ 1938 | \\ --aa Long flag. 1939 | \\ 1940 | \\ 1941 | \\ --bb Long option. 1942 | \\ 1943 | \\ 1944 | \\-c, --cc Both flag. 1945 | \\ 1946 | \\ 1947 | \\ --complicate Flag with a complicated 1948 | \\ and very long description 1949 | \\ that spans multiple 1950 | \\ lines. 1951 | \\ 1952 | \\ Paragraph number 2: 1953 | \\ * Bullet point 1954 | \\ * Bullet point 1955 | \\ 1956 | \\ Example: 1957 | \\ something something 1958 | \\ something 1959 | \\ 1960 | \\ 1961 | \\-d, --dd Both option. 1962 | \\ 1963 | \\ 1964 | \\-d, --dd ... Both repeated option. 1965 | \\ 1966 | ); 1967 | 1968 | // Test with multibyte characters. 1969 | try testHelp(.{ 1970 | .indent = 0, 1971 | .max_width = 46, 1972 | .description_on_new_line = false, 1973 | .description_indent = 4, 1974 | .spacing_between_parameters = 2, 1975 | }, 1976 | \\-a Shört flåg. 1977 | \\ 1978 | \\ 1979 | \\-b Shört öptiön. 1980 | \\ 1981 | \\ 1982 | \\ --aa Löng fläg. 1983 | \\ 1984 | \\ 1985 | \\ --bb Löng öptiön. 1986 | \\ 1987 | \\ 1988 | \\-c, --cc Bóth fläg. 1989 | \\ 1990 | \\ 1991 | \\ --complicate Fläg wíth ä cömplǐcätéd 1992 | \\ änd vërý löng dèscrıptıön 1993 | \\ thät späns mültíplë 1994 | \\ lınēs. 1995 | \\ 1996 | \\ Pärägräph number 2: 1997 | \\ * Bullet pöint 1998 | \\ * Bullet pöint 1999 | \\ 2000 | \\ Exämple: 2001 | \\ sömething sömething 2002 | \\ sömething 2003 | \\ 2004 | \\ 2005 | \\-d, --dd Böth öptiön. 2006 | \\ 2007 | \\ 2008 | \\-d, --dd ... Böth repeäted öptiön. 2009 | \\ 2010 | ); 2011 | } 2012 | 2013 | /// Will print a usage message in the following format: 2014 | /// [-abc] [--longa] [-d ] [--longb ] 2015 | /// 2016 | /// First all none value taking parameters, which have a short name are printed, then non 2017 | /// positional parameters and finally the positional. 2018 | pub fn usage(stream: anytype, comptime Id: type, params: []const Param(Id)) !void { 2019 | var cos = ccw.codepointCountingWriter(stream); 2020 | const cs = cos.writer(); 2021 | for (params) |param| { 2022 | const name = param.names.short orelse continue; 2023 | if (param.takes_value != .none) 2024 | continue; 2025 | 2026 | if (cos.codepoints_written == 0) 2027 | try stream.writeAll("[-"); 2028 | try cs.writeByte(name); 2029 | } 2030 | if (cos.codepoints_written != 0) 2031 | try cs.writeAll("]"); 2032 | 2033 | var has_positionals: bool = false; 2034 | for (params) |param| { 2035 | if (param.takes_value == .none and param.names.short != null) 2036 | continue; 2037 | 2038 | const prefix = if (param.names.short) |_| "-" else "--"; 2039 | const name = blk: { 2040 | if (param.names.short) |*s| 2041 | break :blk @as(*const [1]u8, s); 2042 | if (param.names.long) |l| 2043 | break :blk l; 2044 | 2045 | has_positionals = true; 2046 | continue; 2047 | }; 2048 | 2049 | if (cos.codepoints_written != 0) 2050 | try cs.writeAll(" "); 2051 | 2052 | try cs.writeAll("["); 2053 | try cs.writeAll(prefix); 2054 | try cs.writeAll(name); 2055 | if (param.takes_value != .none) { 2056 | try cs.writeAll(" <"); 2057 | try cs.writeAll(param.id.value()); 2058 | try cs.writeAll(">"); 2059 | if (param.takes_value == .many) 2060 | try cs.writeAll("..."); 2061 | } 2062 | 2063 | try cs.writeByte(']'); 2064 | } 2065 | 2066 | if (!has_positionals) 2067 | return; 2068 | 2069 | for (params) |param| { 2070 | if (param.names.short != null or param.names.long != null) 2071 | continue; 2072 | 2073 | if (cos.codepoints_written != 0) 2074 | try cs.writeAll(" "); 2075 | 2076 | try cs.writeAll("<"); 2077 | try cs.writeAll(param.id.value()); 2078 | try cs.writeAll(">"); 2079 | if (param.takes_value == .many) 2080 | try cs.writeAll("..."); 2081 | } 2082 | } 2083 | 2084 | fn testUsage(expected: []const u8, params: []const Param(Help)) !void { 2085 | var buf: [1024]u8 = undefined; 2086 | var fbs = std.io.fixedBufferStream(&buf); 2087 | try usage(fbs.writer(), Help, params); 2088 | try std.testing.expectEqualStrings(expected, fbs.getWritten()); 2089 | } 2090 | 2091 | test "usage" { 2092 | @setEvalBranchQuota(100000); 2093 | try testUsage("[-ab]", &comptime parseParamsComptime( 2094 | \\-a 2095 | \\-b 2096 | \\ 2097 | )); 2098 | try testUsage("[-a ] [-b ]", &comptime parseParamsComptime( 2099 | \\-a 2100 | \\-b 2101 | \\ 2102 | )); 2103 | try testUsage("[--a] [--b]", &comptime parseParamsComptime( 2104 | \\--a 2105 | \\--b 2106 | \\ 2107 | )); 2108 | try testUsage("[--a ] [--b ]", &comptime parseParamsComptime( 2109 | \\--a 2110 | \\--b 2111 | \\ 2112 | )); 2113 | try testUsage("", &comptime parseParamsComptime( 2114 | \\ 2115 | \\ 2116 | )); 2117 | try testUsage("...", &comptime parseParamsComptime( 2118 | \\... 2119 | \\ 2120 | )); 2121 | try testUsage( 2122 | "[-ab] [-c ] [-d ] [--e] [--f] [--g ] [--h ] [-i ...] ", 2123 | &comptime parseParamsComptime( 2124 | \\-a 2125 | \\-b 2126 | \\-c 2127 | \\-d 2128 | \\--e 2129 | \\--f 2130 | \\--g 2131 | \\--h 2132 | \\-i ... 2133 | \\ 2134 | \\ 2135 | ), 2136 | ); 2137 | try testUsage(" ", &comptime parseParamsComptime( 2138 | \\ 2139 | \\ 2140 | \\ 2141 | \\ 2142 | )); 2143 | try testUsage(" ...", &comptime parseParamsComptime( 2144 | \\ 2145 | \\ 2146 | \\... 2147 | \\ 2148 | )); 2149 | } 2150 | 2151 | test { 2152 | _ = args; 2153 | _ = parsers; 2154 | _ = streaming; 2155 | _ = ccw; 2156 | } 2157 | 2158 | pub const args = @import("clap/args.zig"); 2159 | pub const parsers = @import("clap/parsers.zig"); 2160 | pub const streaming = @import("clap/streaming.zig"); 2161 | pub const ccw = @import("clap/codepoint_counting_writer.zig"); 2162 | 2163 | const std = @import("std"); 2164 | -------------------------------------------------------------------------------- /clap/args.zig: -------------------------------------------------------------------------------- 1 | /// An example of what methods should be implemented on an arg iterator. 2 | pub const ExampleArgIterator = struct { 3 | pub fn next(iter: *ExampleArgIterator) ?[]const u8 { 4 | _ = iter; 5 | return "2"; 6 | } 7 | }; 8 | 9 | /// An argument iterator which iterates over a slice of arguments. 10 | /// This implementation does not allocate. 11 | pub const SliceIterator = struct { 12 | args: []const []const u8, 13 | index: usize = 0, 14 | 15 | pub fn next(iter: *SliceIterator) ?[]const u8 { 16 | if (iter.args.len <= iter.index) 17 | return null; 18 | 19 | defer iter.index += 1; 20 | return iter.args[iter.index]; 21 | } 22 | }; 23 | 24 | test "SliceIterator" { 25 | const args = [_][]const u8{ "A", "BB", "CCC" }; 26 | var iter = SliceIterator{ .args = &args }; 27 | 28 | for (args) |a| 29 | try std.testing.expectEqualStrings(a, iter.next().?); 30 | 31 | try std.testing.expectEqual(@as(?[]const u8, null), iter.next()); 32 | } 33 | 34 | const std = @import("std"); 35 | -------------------------------------------------------------------------------- /clap/codepoint_counting_writer.zig: -------------------------------------------------------------------------------- 1 | /// A Writer that counts how many codepoints has been written to it. 2 | /// Expects valid UTF-8 input, and does not validate the input. 3 | pub fn CodepointCountingWriter(comptime WriterType: type) type { 4 | return struct { 5 | codepoints_written: u64, 6 | child_stream: WriterType, 7 | 8 | pub const Error = WriterType.Error || error{Utf8InvalidStartByte}; 9 | pub const Writer = std.io.Writer(*Self, Error, write); 10 | 11 | const Self = @This(); 12 | 13 | pub fn write(self: *Self, bytes: []const u8) Error!usize { 14 | const bytes_and_codepoints = try utf8CountCodepointsAllowTruncate(bytes); 15 | // Might not be the full input, so the leftover bytes are written on the next call. 16 | const bytes_to_write = bytes[0..bytes_and_codepoints.bytes]; 17 | const amt = try self.child_stream.write(bytes_to_write); 18 | const bytes_written = bytes_to_write[0..amt]; 19 | self.codepoints_written += (try utf8CountCodepointsAllowTruncate(bytes_written)).codepoints; 20 | return amt; 21 | } 22 | 23 | pub fn writer(self: *Self) Writer { 24 | return .{ .context = self }; 25 | } 26 | }; 27 | } 28 | 29 | // Like `std.unicode.utf8CountCodepoints`, but on truncated input, it returns 30 | // the number of codepoints up to that point. 31 | // Does not validate UTF-8 beyond checking the start byte. 32 | fn utf8CountCodepointsAllowTruncate(s: []const u8) !struct { bytes: usize, codepoints: usize } { 33 | const native_endian = @import("builtin").cpu.arch.endian(); 34 | var len: usize = 0; 35 | 36 | const N = @sizeOf(usize); 37 | const MASK = 0x80 * (std.math.maxInt(usize) / 0xff); 38 | 39 | var i: usize = 0; 40 | while (i < s.len) { 41 | // Fast path for ASCII sequences 42 | while (i + N <= s.len) : (i += N) { 43 | const v = std.mem.readInt(usize, s[i..][0..N], native_endian); 44 | if (v & MASK != 0) break; 45 | len += N; 46 | } 47 | 48 | if (i < s.len) { 49 | const n = try std.unicode.utf8ByteSequenceLength(s[i]); 50 | // Truncated input; return the current counts. 51 | if (i + n > s.len) return .{ .bytes = i, .codepoints = len }; 52 | 53 | i += n; 54 | len += 1; 55 | } 56 | } 57 | 58 | return .{ .bytes = i, .codepoints = len }; 59 | } 60 | 61 | pub fn codepointCountingWriter(child_stream: anytype) CodepointCountingWriter(@TypeOf(child_stream)) { 62 | return .{ .codepoints_written = 0, .child_stream = child_stream }; 63 | } 64 | 65 | const testing = std.testing; 66 | 67 | test CodepointCountingWriter { 68 | var counting_stream = codepointCountingWriter(std.io.null_writer); 69 | const stream = counting_stream.writer(); 70 | 71 | const utf8_text = "blåhaj" ** 100; 72 | stream.writeAll(utf8_text) catch unreachable; 73 | const expected_count = try std.unicode.utf8CountCodepoints(utf8_text); 74 | try testing.expectEqual(expected_count, counting_stream.codepoints_written); 75 | } 76 | 77 | test "handles partial UTF-8 writes" { 78 | var buf: [100]u8 = undefined; 79 | var fbs = std.io.fixedBufferStream(&buf); 80 | var counting_stream = codepointCountingWriter(fbs.writer()); 81 | const stream = counting_stream.writer(); 82 | 83 | const utf8_text = "ååå"; 84 | // `å` is represented as `\xC5\xA5`, write 1.5 `å`s. 85 | var wc = try stream.write(utf8_text[0..3]); 86 | // One should have been written fully. 87 | try testing.expectEqual("å".len, wc); 88 | try testing.expectEqual(1, counting_stream.codepoints_written); 89 | 90 | // Write the rest, continuing from the reported number of bytes written. 91 | wc = try stream.write(utf8_text[wc..]); 92 | try testing.expectEqual(4, wc); 93 | try testing.expectEqual(3, counting_stream.codepoints_written); 94 | 95 | const expected_count = try std.unicode.utf8CountCodepoints(utf8_text); 96 | try testing.expectEqual(expected_count, counting_stream.codepoints_written); 97 | 98 | try testing.expectEqualSlices(u8, utf8_text, fbs.getWritten()); 99 | } 100 | 101 | const std = @import("std"); 102 | -------------------------------------------------------------------------------- /clap/parsers.zig: -------------------------------------------------------------------------------- 1 | pub const default = .{ 2 | .string = string, 3 | .str = string, 4 | .u8 = int(u8, 0), 5 | .u16 = int(u16, 0), 6 | .u32 = int(u32, 0), 7 | .u64 = int(u64, 0), 8 | .usize = int(usize, 0), 9 | .i8 = int(i8, 0), 10 | .i16 = int(i16, 0), 11 | .i32 = int(i32, 0), 12 | .i64 = int(i64, 0), 13 | .isize = int(isize, 0), 14 | .f32 = float(f32), 15 | .f64 = float(f64), 16 | }; 17 | 18 | /// A parser that does nothing. 19 | pub fn string(in: []const u8) error{}![]const u8 { 20 | return in; 21 | } 22 | 23 | test "string" { 24 | try std.testing.expectEqualStrings("aa", try string("aa")); 25 | } 26 | 27 | /// A parser that uses `std.fmt.parseInt` or `std.fmt.parseUnsigned` to parse the string into an integer value. 28 | /// See `std.fmt.parseInt` and `std.fmt.parseUnsigned` documentation for more information. 29 | pub fn int(comptime T: type, comptime base: u8) fn ([]const u8) std.fmt.ParseIntError!T { 30 | return struct { 31 | fn parse(in: []const u8) std.fmt.ParseIntError!T { 32 | return switch (@typeInfo(T).int.signedness) { 33 | .signed => std.fmt.parseInt(T, in, base), 34 | .unsigned => std.fmt.parseUnsigned(T, in, base), 35 | }; 36 | } 37 | }.parse; 38 | } 39 | 40 | test "int" { 41 | try std.testing.expectEqual(@as(i8, 0), try int(i8, 10)("0")); 42 | try std.testing.expectEqual(@as(i8, 1), try int(i8, 10)("1")); 43 | try std.testing.expectEqual(@as(i8, 10), try int(i8, 10)("10")); 44 | try std.testing.expectEqual(@as(i8, 0b10), try int(i8, 2)("10")); 45 | try std.testing.expectEqual(@as(i8, 0x10), try int(i8, 0)("0x10")); 46 | try std.testing.expectEqual(@as(i8, 0b10), try int(i8, 0)("0b10")); 47 | try std.testing.expectEqual(@as(i16, 0), try int(i16, 10)("0")); 48 | try std.testing.expectEqual(@as(i16, 1), try int(i16, 10)("1")); 49 | try std.testing.expectEqual(@as(i16, 10), try int(i16, 10)("10")); 50 | try std.testing.expectEqual(@as(i16, 0b10), try int(i16, 2)("10")); 51 | try std.testing.expectEqual(@as(i16, 0x10), try int(i16, 0)("0x10")); 52 | try std.testing.expectEqual(@as(i16, 0b10), try int(i16, 0)("0b10")); 53 | 54 | try std.testing.expectEqual(@as(i8, 0), try int(i8, 10)("-0")); 55 | try std.testing.expectEqual(@as(i8, -1), try int(i8, 10)("-1")); 56 | try std.testing.expectEqual(@as(i8, -10), try int(i8, 10)("-10")); 57 | try std.testing.expectEqual(@as(i8, -0b10), try int(i8, 2)("-10")); 58 | try std.testing.expectEqual(@as(i8, -0x10), try int(i8, 0)("-0x10")); 59 | try std.testing.expectEqual(@as(i8, -0b10), try int(i8, 0)("-0b10")); 60 | try std.testing.expectEqual(@as(i16, 0), try int(i16, 10)("-0")); 61 | try std.testing.expectEqual(@as(i16, -1), try int(i16, 10)("-1")); 62 | try std.testing.expectEqual(@as(i16, -10), try int(i16, 10)("-10")); 63 | try std.testing.expectEqual(@as(i16, -0b10), try int(i16, 2)("-10")); 64 | try std.testing.expectEqual(@as(i16, -0x10), try int(i16, 0)("-0x10")); 65 | try std.testing.expectEqual(@as(i16, -0b10), try int(i16, 0)("-0b10")); 66 | 67 | try std.testing.expectEqual(@as(u8, 0), try int(u8, 10)("0")); 68 | try std.testing.expectEqual(@as(u8, 1), try int(u8, 10)("1")); 69 | try std.testing.expectEqual(@as(u8, 10), try int(u8, 10)("10")); 70 | try std.testing.expectEqual(@as(u8, 0b10), try int(u8, 2)("10")); 71 | try std.testing.expectEqual(@as(u8, 0x10), try int(u8, 0)("0x10")); 72 | try std.testing.expectEqual(@as(u8, 0b10), try int(u8, 0)("0b10")); 73 | try std.testing.expectEqual(@as(u16, 0), try int(u16, 10)("0")); 74 | try std.testing.expectEqual(@as(u16, 1), try int(u16, 10)("1")); 75 | try std.testing.expectEqual(@as(u16, 10), try int(u16, 10)("10")); 76 | try std.testing.expectEqual(@as(u16, 0b10), try int(u16, 2)("10")); 77 | try std.testing.expectEqual(@as(u16, 0x10), try int(u16, 0)("0x10")); 78 | try std.testing.expectEqual(@as(u16, 0b10), try int(u16, 0)("0b10")); 79 | 80 | try std.testing.expectEqual(std.fmt.ParseIntError.InvalidCharacter, int(u8, 10)("-10")); 81 | } 82 | 83 | /// A parser that uses `std.fmt.parseFloat` to parse the string into an float value. 84 | /// See `std.fmt.parseFloat` documentation for more information. 85 | pub fn float(comptime T: type) fn ([]const u8) std.fmt.ParseFloatError!T { 86 | return struct { 87 | fn parse(in: []const u8) std.fmt.ParseFloatError!T { 88 | return std.fmt.parseFloat(T, in); 89 | } 90 | }.parse; 91 | } 92 | 93 | test "float" { 94 | try std.testing.expectEqual(@as(f32, 0), try float(f32)("0")); 95 | } 96 | 97 | pub const EnumError = error{ 98 | NameNotPartOfEnum, 99 | }; 100 | 101 | /// A parser that uses `std.meta.stringToEnum` to parse the string into an enum value. On `null`, 102 | /// this function returns the error `NameNotPartOfEnum`. 103 | /// See `std.meta.stringToEnum` documentation for more information. 104 | pub fn enumeration(comptime T: type) fn ([]const u8) EnumError!T { 105 | return struct { 106 | fn parse(in: []const u8) EnumError!T { 107 | return std.meta.stringToEnum(T, in) orelse error.NameNotPartOfEnum; 108 | } 109 | }.parse; 110 | } 111 | 112 | test "enumeration" { 113 | const E = enum { a, b, c }; 114 | try std.testing.expectEqual(E.a, try enumeration(E)("a")); 115 | try std.testing.expectEqual(E.b, try enumeration(E)("b")); 116 | try std.testing.expectEqual(E.c, try enumeration(E)("c")); 117 | try std.testing.expectError(EnumError.NameNotPartOfEnum, enumeration(E)("d")); 118 | } 119 | 120 | fn ReturnType(comptime P: type) type { 121 | return @typeInfo(P).@"fn".return_type.?; 122 | } 123 | 124 | pub fn Result(comptime P: type) type { 125 | return @typeInfo(ReturnType(P)).error_union.payload; 126 | } 127 | 128 | const std = @import("std"); 129 | -------------------------------------------------------------------------------- /clap/streaming.zig: -------------------------------------------------------------------------------- 1 | /// The result returned from Clap.next 2 | pub fn Arg(comptime Id: type) type { 3 | return struct { 4 | const Self = @This(); 5 | 6 | param: *const clap.Param(Id), 7 | value: ?[]const u8 = null, 8 | }; 9 | } 10 | 11 | pub const Error = error{ 12 | MissingValue, 13 | InvalidArgument, 14 | DoesntTakeValue, 15 | }; 16 | 17 | /// A command line argument parser which, given an ArgIterator, will parse arguments according 18 | /// to the params. Clap parses in an iterating manner, so you have to use a loop together with 19 | /// Clap.next to parse all the arguments of your program. 20 | /// 21 | /// This parser is the building block for all the more complicated parsers. 22 | pub fn Clap(comptime Id: type, comptime ArgIterator: type) type { 23 | return struct { 24 | const State = union(enum) { 25 | normal, 26 | chaining: Chaining, 27 | rest_are_positional, 28 | 29 | const Chaining = struct { 30 | arg: []const u8, 31 | index: usize, 32 | }; 33 | }; 34 | 35 | state: State = .normal, 36 | 37 | params: []const clap.Param(Id), 38 | iter: *ArgIterator, 39 | 40 | positional: ?*const clap.Param(Id) = null, 41 | diagnostic: ?*clap.Diagnostic = null, 42 | assignment_separators: []const u8 = clap.default_assignment_separators, 43 | 44 | /// Get the next Arg that matches a Param. 45 | pub fn next(parser: *@This()) !?Arg(Id) { 46 | switch (parser.state) { 47 | .normal => return try parser.normal(), 48 | .chaining => |state| return try parser.chaining(state), 49 | .rest_are_positional => { 50 | const param = parser.positionalParam() orelse unreachable; 51 | const value = parser.iter.next() orelse return null; 52 | return Arg(Id){ .param = param, .value = value }; 53 | }, 54 | } 55 | } 56 | 57 | fn normal(parser: *@This()) !?Arg(Id) { 58 | const arg_info = (try parser.parseNextArg()) orelse return null; 59 | const arg = arg_info.arg; 60 | switch (arg_info.kind) { 61 | .long => { 62 | const eql_index = std.mem.indexOfAny(u8, arg, parser.assignment_separators); 63 | const name = if (eql_index) |i| arg[0..i] else arg; 64 | const maybe_value = if (eql_index) |i| arg[i + 1 ..] else null; 65 | 66 | for (parser.params) |*param| { 67 | const match = param.names.long orelse continue; 68 | 69 | if (!std.mem.eql(u8, name, match)) 70 | continue; 71 | if (param.takes_value == .none) { 72 | if (maybe_value != null) 73 | return parser.err(arg, .{ .long = name }, Error.DoesntTakeValue); 74 | 75 | return Arg(Id){ .param = param }; 76 | } 77 | 78 | const value = blk: { 79 | if (maybe_value) |v| 80 | break :blk v; 81 | 82 | break :blk parser.iter.next() orelse 83 | return parser.err(arg, .{ .long = name }, Error.MissingValue); 84 | }; 85 | 86 | return Arg(Id){ .param = param, .value = value }; 87 | } 88 | 89 | return parser.err(arg, .{ .long = name }, Error.InvalidArgument); 90 | }, 91 | .short => return try parser.chaining(.{ 92 | .arg = arg, 93 | .index = 0, 94 | }), 95 | .positional => if (parser.positionalParam()) |param| { 96 | // If we find a positional with the value `--` then we 97 | // interpret the rest of the arguments as positional 98 | // arguments. 99 | if (std.mem.eql(u8, arg, "--")) { 100 | parser.state = .rest_are_positional; 101 | const value = parser.iter.next() orelse return null; 102 | return Arg(Id){ .param = param, .value = value }; 103 | } 104 | 105 | return Arg(Id){ .param = param, .value = arg }; 106 | } else { 107 | return parser.err(arg, .{}, Error.InvalidArgument); 108 | }, 109 | } 110 | } 111 | 112 | fn chaining(parser: *@This(), state: State.Chaining) !?Arg(Id) { 113 | const arg = state.arg; 114 | const index = state.index; 115 | const next_index = index + 1; 116 | 117 | for (parser.params) |*param| { 118 | const short = param.names.short orelse continue; 119 | if (short != arg[index]) 120 | continue; 121 | 122 | // Before we return, we have to set the new state of the clap 123 | defer { 124 | if (arg.len <= next_index or param.takes_value != .none) { 125 | parser.state = .normal; 126 | } else { 127 | parser.state = .{ 128 | .chaining = .{ 129 | .arg = arg, 130 | .index = next_index, 131 | }, 132 | }; 133 | } 134 | } 135 | 136 | const next_is_separator = if (next_index < arg.len) 137 | std.mem.indexOfScalar(u8, parser.assignment_separators, arg[next_index]) != null 138 | else 139 | false; 140 | 141 | if (param.takes_value == .none) { 142 | if (next_is_separator) 143 | return parser.err(arg, .{ .short = short }, Error.DoesntTakeValue); 144 | return Arg(Id){ .param = param }; 145 | } 146 | 147 | if (arg.len <= next_index) { 148 | const value = parser.iter.next() orelse 149 | return parser.err(arg, .{ .short = short }, Error.MissingValue); 150 | 151 | return Arg(Id){ .param = param, .value = value }; 152 | } 153 | 154 | if (next_is_separator) 155 | return Arg(Id){ .param = param, .value = arg[next_index + 1 ..] }; 156 | 157 | return Arg(Id){ .param = param, .value = arg[next_index..] }; 158 | } 159 | 160 | return parser.err(arg, .{ .short = arg[index] }, Error.InvalidArgument); 161 | } 162 | 163 | fn positionalParam(parser: *@This()) ?*const clap.Param(Id) { 164 | if (parser.positional) |p| 165 | return p; 166 | 167 | for (parser.params) |*param| { 168 | if (param.names.long) |_| 169 | continue; 170 | if (param.names.short) |_| 171 | continue; 172 | 173 | parser.positional = param; 174 | return param; 175 | } 176 | 177 | return null; 178 | } 179 | 180 | const ArgInfo = struct { 181 | arg: []const u8, 182 | kind: enum { 183 | long, 184 | short, 185 | positional, 186 | }, 187 | }; 188 | 189 | fn parseNextArg(parser: *@This()) !?ArgInfo { 190 | const full_arg = parser.iter.next() orelse return null; 191 | if (std.mem.eql(u8, full_arg, "--") or std.mem.eql(u8, full_arg, "-")) 192 | return ArgInfo{ .arg = full_arg, .kind = .positional }; 193 | if (std.mem.startsWith(u8, full_arg, "--")) 194 | return ArgInfo{ .arg = full_arg[2..], .kind = .long }; 195 | if (std.mem.startsWith(u8, full_arg, "-")) 196 | return ArgInfo{ .arg = full_arg[1..], .kind = .short }; 197 | 198 | return ArgInfo{ .arg = full_arg, .kind = .positional }; 199 | } 200 | 201 | fn err(parser: @This(), arg: []const u8, names: clap.Names, _err: anytype) @TypeOf(_err) { 202 | if (parser.diagnostic) |d| 203 | d.* = .{ .arg = arg, .name = names }; 204 | return _err; 205 | } 206 | }; 207 | } 208 | 209 | fn expectArgs( 210 | parser: *Clap(u8, clap.args.SliceIterator), 211 | results: []const Arg(u8), 212 | ) !void { 213 | for (results) |res| { 214 | const arg = (try parser.next()) orelse return error.TestFailed; 215 | try std.testing.expectEqual(res.param, arg.param); 216 | const expected_value = res.value orelse { 217 | try std.testing.expectEqual(@as(@TypeOf(arg.value), null), arg.value); 218 | continue; 219 | }; 220 | const actual_value = arg.value orelse return error.TestFailed; 221 | try std.testing.expectEqualSlices(u8, expected_value, actual_value); 222 | } 223 | 224 | if (try parser.next()) |_| 225 | return error.TestFailed; 226 | } 227 | 228 | fn expectError( 229 | parser: *Clap(u8, clap.args.SliceIterator), 230 | expected: []const u8, 231 | ) !void { 232 | var diag: clap.Diagnostic = .{}; 233 | parser.diagnostic = &diag; 234 | 235 | while (parser.next() catch |err| { 236 | var buf: [1024]u8 = undefined; 237 | var fbs = std.io.fixedBufferStream(&buf); 238 | diag.report(fbs.writer(), err) catch return error.TestFailed; 239 | try std.testing.expectEqualStrings(expected, fbs.getWritten()); 240 | return; 241 | }) |_| {} 242 | 243 | try std.testing.expect(false); 244 | } 245 | 246 | test "short params" { 247 | const params = [_]clap.Param(u8){ 248 | .{ .id = 0, .names = .{ .short = 'a' } }, 249 | .{ .id = 1, .names = .{ .short = 'b' } }, 250 | .{ 251 | .id = 2, 252 | .names = .{ .short = 'c' }, 253 | .takes_value = .one, 254 | }, 255 | .{ 256 | .id = 3, 257 | .names = .{ .short = 'd' }, 258 | .takes_value = .many, 259 | }, 260 | }; 261 | 262 | const a = ¶ms[0]; 263 | const b = ¶ms[1]; 264 | const c = ¶ms[2]; 265 | const d = ¶ms[3]; 266 | 267 | var iter = clap.args.SliceIterator{ .args = &.{ 268 | "-a", "-b", "-ab", "-ba", 269 | "-c", "0", "-c=0", "-ac", 270 | "0", "-ac=0", "-d=0", 271 | } }; 272 | var parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 273 | 274 | try expectArgs(&parser, &.{ 275 | .{ .param = a }, 276 | .{ .param = b }, 277 | .{ .param = a }, 278 | .{ .param = b }, 279 | .{ .param = b }, 280 | .{ .param = a }, 281 | .{ .param = c, .value = "0" }, 282 | .{ .param = c, .value = "0" }, 283 | .{ .param = a }, 284 | .{ .param = c, .value = "0" }, 285 | .{ .param = a }, 286 | .{ .param = c, .value = "0" }, 287 | .{ .param = d, .value = "0" }, 288 | }); 289 | } 290 | 291 | test "long params" { 292 | const params = [_]clap.Param(u8){ 293 | .{ .id = 0, .names = .{ .long = "aa" } }, 294 | .{ .id = 1, .names = .{ .long = "bb" } }, 295 | .{ 296 | .id = 2, 297 | .names = .{ .long = "cc" }, 298 | .takes_value = .one, 299 | }, 300 | .{ 301 | .id = 3, 302 | .names = .{ .long = "dd" }, 303 | .takes_value = .many, 304 | }, 305 | }; 306 | 307 | const aa = ¶ms[0]; 308 | const bb = ¶ms[1]; 309 | const cc = ¶ms[2]; 310 | const dd = ¶ms[3]; 311 | 312 | var iter = clap.args.SliceIterator{ .args = &.{ 313 | "--aa", "--bb", 314 | "--cc", "0", 315 | "--cc=0", "--dd=0", 316 | } }; 317 | var parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 318 | 319 | try expectArgs(&parser, &.{ 320 | .{ .param = aa }, 321 | .{ .param = bb }, 322 | .{ .param = cc, .value = "0" }, 323 | .{ .param = cc, .value = "0" }, 324 | .{ .param = dd, .value = "0" }, 325 | }); 326 | } 327 | 328 | test "positional params" { 329 | const params = [_]clap.Param(u8){.{ 330 | .id = 0, 331 | .takes_value = .one, 332 | }}; 333 | 334 | var iter = clap.args.SliceIterator{ .args = &.{ 335 | "aa", 336 | "bb", 337 | } }; 338 | var parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 339 | 340 | try expectArgs(&parser, &.{ 341 | .{ .param = ¶ms[0], .value = "aa" }, 342 | .{ .param = ¶ms[0], .value = "bb" }, 343 | }); 344 | } 345 | 346 | test "all params" { 347 | const params = [_]clap.Param(u8){ 348 | .{ 349 | .id = 0, 350 | .names = .{ .short = 'a', .long = "aa" }, 351 | }, 352 | .{ 353 | .id = 1, 354 | .names = .{ .short = 'b', .long = "bb" }, 355 | }, 356 | .{ 357 | .id = 2, 358 | .names = .{ .short = 'c', .long = "cc" }, 359 | .takes_value = .one, 360 | }, 361 | .{ .id = 3, .takes_value = .one }, 362 | }; 363 | 364 | const aa = ¶ms[0]; 365 | const bb = ¶ms[1]; 366 | const cc = ¶ms[2]; 367 | const positional = ¶ms[3]; 368 | 369 | var iter = clap.args.SliceIterator{ .args = &.{ 370 | "-a", "-b", "-ab", "-ba", 371 | "-c", "0", "-c=0", "-ac", 372 | "0", "-ac=0", "--aa", "--bb", 373 | "--cc", "0", "--cc=0", "something", 374 | "-", "--", "--cc=0", "-a", 375 | } }; 376 | var parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 377 | 378 | try expectArgs(&parser, &.{ 379 | .{ .param = aa }, 380 | .{ .param = bb }, 381 | .{ .param = aa }, 382 | .{ .param = bb }, 383 | .{ .param = bb }, 384 | .{ .param = aa }, 385 | .{ .param = cc, .value = "0" }, 386 | .{ .param = cc, .value = "0" }, 387 | .{ .param = aa }, 388 | .{ .param = cc, .value = "0" }, 389 | .{ .param = aa }, 390 | .{ .param = cc, .value = "0" }, 391 | .{ .param = aa }, 392 | .{ .param = bb }, 393 | .{ .param = cc, .value = "0" }, 394 | .{ .param = cc, .value = "0" }, 395 | .{ .param = positional, .value = "something" }, 396 | .{ .param = positional, .value = "-" }, 397 | .{ .param = positional, .value = "--cc=0" }, 398 | .{ .param = positional, .value = "-a" }, 399 | }); 400 | } 401 | 402 | test "different assignment separators" { 403 | const params = [_]clap.Param(u8){ 404 | .{ 405 | .id = 0, 406 | .names = .{ .short = 'a', .long = "aa" }, 407 | .takes_value = .one, 408 | }, 409 | }; 410 | 411 | const aa = ¶ms[0]; 412 | 413 | var iter = clap.args.SliceIterator{ .args = &.{ 414 | "-a=0", "--aa=0", 415 | "-a:0", "--aa:0", 416 | } }; 417 | var parser = Clap(u8, clap.args.SliceIterator){ 418 | .params = ¶ms, 419 | .iter = &iter, 420 | .assignment_separators = "=:", 421 | }; 422 | 423 | try expectArgs(&parser, &.{ 424 | .{ .param = aa, .value = "0" }, 425 | .{ .param = aa, .value = "0" }, 426 | .{ .param = aa, .value = "0" }, 427 | .{ .param = aa, .value = "0" }, 428 | }); 429 | } 430 | 431 | test "errors" { 432 | const params = [_]clap.Param(u8){ 433 | .{ 434 | .id = 0, 435 | .names = .{ .short = 'a', .long = "aa" }, 436 | }, 437 | .{ 438 | .id = 1, 439 | .names = .{ .short = 'c', .long = "cc" }, 440 | .takes_value = .one, 441 | }, 442 | }; 443 | 444 | var iter = clap.args.SliceIterator{ .args = &.{"q"} }; 445 | var parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 446 | try expectError(&parser, "Invalid argument 'q'\n"); 447 | 448 | iter = clap.args.SliceIterator{ .args = &.{"-q"} }; 449 | parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 450 | try expectError(&parser, "Invalid argument '-q'\n"); 451 | 452 | iter = clap.args.SliceIterator{ .args = &.{"--q"} }; 453 | parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 454 | try expectError(&parser, "Invalid argument '--q'\n"); 455 | 456 | iter = clap.args.SliceIterator{ .args = &.{"--q=1"} }; 457 | parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 458 | try expectError(&parser, "Invalid argument '--q'\n"); 459 | 460 | iter = clap.args.SliceIterator{ .args = &.{"-a=1"} }; 461 | parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 462 | try expectError(&parser, "The argument '-a' does not take a value\n"); 463 | 464 | iter = clap.args.SliceIterator{ .args = &.{"--aa=1"} }; 465 | parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 466 | try expectError(&parser, "The argument '--aa' does not take a value\n"); 467 | 468 | iter = clap.args.SliceIterator{ .args = &.{"-c"} }; 469 | parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 470 | try expectError(&parser, "The argument '-c' requires a value but none was supplied\n"); 471 | 472 | iter = clap.args.SliceIterator{ .args = &.{"--cc"} }; 473 | parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 474 | try expectError(&parser, "The argument '--cc' requires a value but none was supplied\n"); 475 | } 476 | 477 | const clap = @import("../clap.zig"); 478 | const std = @import("std"); 479 | -------------------------------------------------------------------------------- /example/help.zig: -------------------------------------------------------------------------------- 1 | pub fn main() !void { 2 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 3 | defer _ = gpa.deinit(); 4 | 5 | const params = comptime clap.parseParamsComptime( 6 | \\-h, --help Display this help and exit. 7 | \\-v, --version Output version information and exit. 8 | \\ 9 | ); 10 | 11 | var res = try clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ 12 | .allocator = gpa.allocator(), 13 | }); 14 | defer res.deinit(); 15 | 16 | // `clap.help` is a function that can print a simple help message. It can print any `Param` 17 | // where `Id` has a `description` and `value` method (`Param(Help)` is one such parameter). 18 | // The last argument contains options as to how `help` should print those parameters. Using 19 | // `.{}` means the default options. 20 | if (res.args.help != 0) 21 | return clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{}); 22 | } 23 | 24 | const clap = @import("clap"); 25 | const std = @import("std"); 26 | -------------------------------------------------------------------------------- /example/simple-ex.zig: -------------------------------------------------------------------------------- 1 | pub fn main() !void { 2 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 3 | defer _ = gpa.deinit(); 4 | 5 | // First we specify what parameters our program can take. 6 | // We can use `parseParamsComptime` to parse a string into an array of `Param(Help)`. 7 | const params = comptime clap.parseParamsComptime( 8 | \\-h, --help Display this help and exit. 9 | \\-n, --number An option parameter, which takes a value. 10 | \\-a, --answer An option parameter which takes an enum. 11 | \\-s, --string ... An option parameter which can be specified multiple times. 12 | \\... 13 | \\ 14 | ); 15 | 16 | // Declare our own parsers which are used to map the argument strings to other 17 | // types. 18 | const YesNo = enum { yes, no }; 19 | const parsers = comptime .{ 20 | .STR = clap.parsers.string, 21 | .FILE = clap.parsers.string, 22 | .INT = clap.parsers.int(usize, 10), 23 | .ANSWER = clap.parsers.enumeration(YesNo), 24 | }; 25 | 26 | var diag = clap.Diagnostic{}; 27 | var res = clap.parse(clap.Help, ¶ms, parsers, .{ 28 | .diagnostic = &diag, 29 | .allocator = gpa.allocator(), 30 | // The assignment separator can be configured. `--number=1` and `--number:1` is now 31 | // allowed. 32 | .assignment_separators = "=:", 33 | }) catch |err| { 34 | diag.report(std.io.getStdErr().writer(), err) catch {}; 35 | return err; 36 | }; 37 | defer res.deinit(); 38 | 39 | if (res.args.help != 0) 40 | std.debug.print("--help\n", .{}); 41 | if (res.args.number) |n| 42 | std.debug.print("--number = {}\n", .{n}); 43 | if (res.args.answer) |a| 44 | std.debug.print("--answer = {s}\n", .{@tagName(a)}); 45 | for (res.args.string) |s| 46 | std.debug.print("--string = {s}\n", .{s}); 47 | for (res.positionals[0]) |pos| 48 | std.debug.print("{s}\n", .{pos}); 49 | } 50 | 51 | const clap = @import("clap"); 52 | const std = @import("std"); 53 | -------------------------------------------------------------------------------- /example/simple.zig: -------------------------------------------------------------------------------- 1 | pub fn main() !void { 2 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 3 | defer _ = gpa.deinit(); 4 | 5 | // First we specify what parameters our program can take. 6 | // We can use `parseParamsComptime` to parse a string into an array of `Param(Help)`. 7 | const params = comptime clap.parseParamsComptime( 8 | \\-h, --help Display this help and exit. 9 | \\-n, --number An option parameter, which takes a value. 10 | \\-s, --string ... An option parameter which can be specified multiple times. 11 | \\... 12 | \\ 13 | ); 14 | 15 | // Initialize our diagnostics, which can be used for reporting useful errors. 16 | // This is optional. You can also pass `.{}` to `clap.parse` if you don't 17 | // care about the extra information `Diagnostics` provides. 18 | var diag = clap.Diagnostic{}; 19 | var res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ 20 | .diagnostic = &diag, 21 | .allocator = gpa.allocator(), 22 | }) catch |err| { 23 | // Report useful error and exit. 24 | diag.report(std.io.getStdErr().writer(), err) catch {}; 25 | return err; 26 | }; 27 | defer res.deinit(); 28 | 29 | if (res.args.help != 0) 30 | std.debug.print("--help\n", .{}); 31 | if (res.args.number) |n| 32 | std.debug.print("--number = {}\n", .{n}); 33 | for (res.args.string) |s| 34 | std.debug.print("--string = {s}\n", .{s}); 35 | for (res.positionals[0]) |pos| 36 | std.debug.print("{s}\n", .{pos}); 37 | } 38 | 39 | const clap = @import("clap"); 40 | const std = @import("std"); 41 | -------------------------------------------------------------------------------- /example/streaming-clap.zig: -------------------------------------------------------------------------------- 1 | pub fn main() !void { 2 | const allocator = std.heap.page_allocator; 3 | 4 | // First we specify what parameters our program can take. 5 | const params = [_]clap.Param(u8){ 6 | .{ 7 | .id = 'h', 8 | .names = .{ .short = 'h', .long = "help" }, 9 | }, 10 | .{ 11 | .id = 'n', 12 | .names = .{ .short = 'n', .long = "number" }, 13 | .takes_value = .one, 14 | }, 15 | .{ .id = 'f', .takes_value = .one }, 16 | }; 17 | 18 | var iter = try std.process.ArgIterator.initWithAllocator(allocator); 19 | defer iter.deinit(); 20 | 21 | // Skip exe argument. 22 | _ = iter.next(); 23 | 24 | // Initialize our diagnostics, which can be used for reporting useful errors. 25 | // This is optional. You can also leave the `diagnostic` field unset if you 26 | // don't care about the extra information `Diagnostic` provides. 27 | var diag = clap.Diagnostic{}; 28 | var parser = clap.streaming.Clap(u8, std.process.ArgIterator){ 29 | .params = ¶ms, 30 | .iter = &iter, 31 | .diagnostic = &diag, 32 | }; 33 | 34 | // Because we use a streaming parser, we have to consume each argument parsed individually. 35 | while (parser.next() catch |err| { 36 | // Report useful error and exit. 37 | diag.report(std.io.getStdErr().writer(), err) catch {}; 38 | return err; 39 | }) |arg| { 40 | // arg.param will point to the parameter which matched the argument. 41 | switch (arg.param.id) { 42 | 'h' => std.debug.print("Help!\n", .{}), 43 | 'n' => std.debug.print("--number = {s}\n", .{arg.value.?}), 44 | 45 | // arg.value == null, if arg.param.takes_value == .none. 46 | // Otherwise, arg.value is the value passed with the argument, such as "-a=10" 47 | // or "-a 10". 48 | 'f' => std.debug.print("{s}\n", .{arg.value.?}), 49 | else => unreachable, 50 | } 51 | } 52 | } 53 | 54 | const clap = @import("clap"); 55 | const std = @import("std"); 56 | -------------------------------------------------------------------------------- /example/subcommands.zig: -------------------------------------------------------------------------------- 1 | // These are our subcommands. 2 | const SubCommands = enum { 3 | help, 4 | math, 5 | }; 6 | 7 | const main_parsers = .{ 8 | .command = clap.parsers.enumeration(SubCommands), 9 | }; 10 | 11 | // The parameters for `main`. Parameters for the subcommands are specified further down. 12 | const main_params = clap.parseParamsComptime( 13 | \\-h, --help Display this help and exit. 14 | \\ 15 | \\ 16 | ); 17 | 18 | // To pass around arguments returned by clap, `clap.Result` and `clap.ResultEx` can be used to 19 | // get the return type of `clap.parse` and `clap.parseEx`. 20 | const MainArgs = clap.ResultEx(clap.Help, &main_params, main_parsers); 21 | 22 | pub fn main() !void { 23 | var gpa_state = std.heap.GeneralPurposeAllocator(.{}){}; 24 | const gpa = gpa_state.allocator(); 25 | defer _ = gpa_state.deinit(); 26 | 27 | var iter = try std.process.ArgIterator.initWithAllocator(gpa); 28 | defer iter.deinit(); 29 | 30 | _ = iter.next(); 31 | 32 | var diag = clap.Diagnostic{}; 33 | var res = clap.parseEx(clap.Help, &main_params, main_parsers, &iter, .{ 34 | .diagnostic = &diag, 35 | .allocator = gpa, 36 | 37 | // Terminate the parsing of arguments after parsing the first positional (0 is passed 38 | // here because parsed positionals are, like slices and arrays, indexed starting at 0). 39 | // 40 | // This will terminate the parsing after parsing the subcommand enum and leave `iter` 41 | // not fully consumed. It can then be reused to parse the arguments for subcommands. 42 | .terminating_positional = 0, 43 | }) catch |err| { 44 | diag.report(std.io.getStdErr().writer(), err) catch {}; 45 | return err; 46 | }; 47 | defer res.deinit(); 48 | 49 | if (res.args.help != 0) 50 | std.debug.print("--help\n", .{}); 51 | 52 | const command = res.positionals[0] orelse return error.MissingCommand; 53 | switch (command) { 54 | .help => std.debug.print("--help\n", .{}), 55 | .math => try mathMain(gpa, &iter, res), 56 | } 57 | } 58 | 59 | fn mathMain(gpa: std.mem.Allocator, iter: *std.process.ArgIterator, main_args: MainArgs) !void { 60 | // The parent arguments are not used here, but there are cases where it might be useful, so 61 | // this example shows how to pass the arguments around. 62 | _ = main_args; 63 | 64 | // The parameters for the subcommand. 65 | const params = comptime clap.parseParamsComptime( 66 | \\-h, --help Display this help and exit. 67 | \\-a, --add Add the two numbers 68 | \\-s, --sub Subtract the two numbers 69 | \\ 70 | \\ 71 | \\ 72 | ); 73 | 74 | // Here we pass the partially parsed argument iterator. 75 | var diag = clap.Diagnostic{}; 76 | var res = clap.parseEx(clap.Help, ¶ms, clap.parsers.default, iter, .{ 77 | .diagnostic = &diag, 78 | .allocator = gpa, 79 | }) catch |err| { 80 | diag.report(std.io.getStdErr().writer(), err) catch {}; 81 | return err; 82 | }; 83 | defer res.deinit(); 84 | 85 | const a = res.positionals[0] orelse return error.MissingArg1; 86 | const b = res.positionals[1] orelse return error.MissingArg1; 87 | if (res.args.help != 0) 88 | std.debug.print("--help\n", .{}); 89 | if (res.args.add != 0) 90 | std.debug.print("added: {}\n", .{a + b}); 91 | if (res.args.sub != 0) 92 | std.debug.print("subtracted: {}\n", .{a - b}); 93 | } 94 | 95 | const clap = @import("clap"); 96 | const std = @import("std"); 97 | -------------------------------------------------------------------------------- /example/usage.zig: -------------------------------------------------------------------------------- 1 | pub fn main() !void { 2 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 3 | defer _ = gpa.deinit(); 4 | 5 | const params = comptime clap.parseParamsComptime( 6 | \\-h, --help Display this help and exit. 7 | \\-v, --version Output version information and exit. 8 | \\ --value An option parameter, which takes a value. 9 | \\ 10 | ); 11 | 12 | var res = try clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ 13 | .allocator = gpa.allocator(), 14 | }); 15 | defer res.deinit(); 16 | 17 | // `clap.usage` is a function that can print a simple help message. It can print any `Param` 18 | // where `Id` has a `value` method (`Param(Help)` is one such parameter). 19 | if (res.args.help != 0) 20 | return clap.usage(std.io.getStdErr().writer(), clap.Help, ¶ms); 21 | } 22 | 23 | const clap = @import("clap"); 24 | const std = @import("std"); 25 | --------------------------------------------------------------------------------