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