diff options
| author | 2022-02-25 19:40:00 +0100 | |
|---|---|---|
| committer | 2022-03-09 18:12:40 +0100 | |
| commit | 5f7b75d5523d9581eca5a72a362868ff517992e8 (patch) | |
| tree | 5e874f9c935e0d7c838ea5aadf270ce2929f8e8a /clap.zig | |
| parent | Bump actions/checkout from 2.4.0 to 3 (diff) | |
| download | zig-clap-5f7b75d5523d9581eca5a72a362868ff517992e8.tar.gz zig-clap-5f7b75d5523d9581eca5a72a362868ff517992e8.tar.xz zig-clap-5f7b75d5523d9581eca5a72a362868ff517992e8.zip | |
Allow for clap to parse argument values into types
This changes
- `.flag`, `.option`, `.options` and `.positionals` are now just fields
you access on the result of `parse` and `parseEx`.
- `clap.ComptimeClap` has been removed.
- `clap.StreamingClap` is now called `clap.streaming.Clap`
- `parse` and `parseEx` now takes a `value_parsers` argument that
provides the parsers to parse values.
- Remove `helpEx`, `helpFull`, `usageEx` and `usageFull`. They now just
expect `Id` to have methods for getting the description and value
texts.
Diffstat (limited to 'clap.zig')
| -rw-r--r-- | clap.zig | 546 |
1 files changed, 365 insertions, 181 deletions
| @@ -1,5 +1,6 @@ | |||
| 1 | const std = @import("std"); | 1 | const std = @import("std"); |
| 2 | 2 | ||
| 3 | const builtin = std.builtin; | ||
| 3 | const debug = std.debug; | 4 | const debug = std.debug; |
| 4 | const heap = std.heap; | 5 | const heap = std.heap; |
| 5 | const io = std.io; | 6 | const io = std.io; |
| @@ -8,21 +9,37 @@ const process = std.process; | |||
| 8 | const testing = std.testing; | 9 | const testing = std.testing; |
| 9 | 10 | ||
| 10 | pub const args = @import("clap/args.zig"); | 11 | pub const args = @import("clap/args.zig"); |
| 12 | pub const parsers = @import("clap/parsers.zig"); | ||
| 13 | pub const streaming = @import("clap/streaming.zig"); | ||
| 11 | 14 | ||
| 12 | test "clap" { | 15 | test "clap" { |
| 13 | testing.refAllDecls(@This()); | 16 | testing.refAllDecls(@This()); |
| 14 | } | 17 | } |
| 15 | 18 | ||
| 16 | pub const ComptimeClap = @import("clap/comptime.zig").ComptimeClap; | 19 | /// The names a `Param` can have. |
| 17 | pub const StreamingClap = @import("clap/streaming.zig").StreamingClap; | ||
| 18 | |||
| 19 | /// The names a ::Param can have. | ||
| 20 | pub const Names = struct { | 20 | pub const Names = struct { |
| 21 | /// '-' prefix | 21 | /// '-' prefix |
| 22 | short: ?u8 = null, | 22 | short: ?u8 = null, |
| 23 | 23 | ||
| 24 | /// '--' prefix | 24 | /// '--' prefix |
| 25 | long: ?[]const u8 = null, | 25 | long: ?[]const u8 = null, |
| 26 | |||
| 27 | pub fn longest(names: *const Names) Longest { | ||
| 28 | if (names.long) |long| | ||
| 29 | return .{ .kind = .long, .name = long }; | ||
| 30 | if (names.short) |*short| { | ||
| 31 | // TODO: Zig cannot figure out @as(*const [1]u8, short) in the ano literal | ||
| 32 | const casted: *const [1]u8 = short; | ||
| 33 | return .{ .kind = .short, .name = casted }; | ||
| 34 | } | ||
| 35 | |||
| 36 | return .{ .kind = .positinal, .name = "" }; | ||
| 37 | } | ||
| 38 | |||
| 39 | pub const Longest = struct { | ||
| 40 | kind: enum { long, short, positinal }, | ||
| 41 | name: []const u8, | ||
| 42 | }; | ||
| 26 | }; | 43 | }; |
| 27 | 44 | ||
| 28 | /// Whether a param takes no value (a flag), one value, or can be specified multiple times. | 45 | /// Whether a param takes no value (a flag), one value, or can be specified multiple times. |
| @@ -124,18 +141,18 @@ fn parseParamRest(line: []const u8) Param(Help) { | |||
| 124 | return .{ | 141 | return .{ |
| 125 | .takes_value = if (takes_many) .many else .one, | 142 | .takes_value = if (takes_many) .many else .one, |
| 126 | .id = .{ | 143 | .id = .{ |
| 127 | .msg = mem.trim(u8, line[help_start..], " \t"), | 144 | .desc = mem.trim(u8, line[help_start..], " \t"), |
| 128 | .value = line[1..len], | 145 | .val = line[1..len], |
| 129 | }, | 146 | }, |
| 130 | }; | 147 | }; |
| 131 | } | 148 | } |
| 132 | 149 | ||
| 133 | return .{ .id = .{ .msg = mem.trim(u8, line, " \t") } }; | 150 | return .{ .id = .{ .desc = mem.trim(u8, line, " \t") } }; |
| 134 | } | 151 | } |
| 135 | 152 | ||
| 136 | fn expectParam(expect: Param(Help), actual: Param(Help)) !void { | 153 | fn expectParam(expect: Param(Help), actual: Param(Help)) !void { |
| 137 | try testing.expectEqualStrings(expect.id.msg, actual.id.msg); | 154 | try testing.expectEqualStrings(expect.id.desc, actual.id.desc); |
| 138 | try testing.expectEqualStrings(expect.id.value, actual.id.value); | 155 | try testing.expectEqualStrings(expect.id.val, actual.id.val); |
| 139 | try testing.expectEqual(expect.names.short, actual.names.short); | 156 | try testing.expectEqual(expect.names.short, actual.names.short); |
| 140 | try testing.expectEqual(expect.takes_value, actual.takes_value); | 157 | try testing.expectEqual(expect.takes_value, actual.takes_value); |
| 141 | if (expect.names.long) |long| { | 158 | if (expect.names.long) |long| { |
| @@ -147,58 +164,58 @@ fn expectParam(expect: Param(Help), actual: Param(Help)) !void { | |||
| 147 | 164 | ||
| 148 | test "parseParam" { | 165 | test "parseParam" { |
| 149 | try expectParam(Param(Help){ | 166 | try expectParam(Param(Help){ |
| 150 | .id = .{ .msg = "Help text", .value = "value" }, | 167 | .id = .{ .desc = "Help text", .val = "val" }, |
| 151 | .names = .{ .short = 's', .long = "long" }, | 168 | .names = .{ .short = 's', .long = "long" }, |
| 152 | .takes_value = .one, | 169 | .takes_value = .one, |
| 153 | }, try parseParam("-s, --long <value> Help text")); | 170 | }, try parseParam("-s, --long <val> Help text")); |
| 154 | 171 | ||
| 155 | try expectParam(Param(Help){ | 172 | try expectParam(Param(Help){ |
| 156 | .id = .{ .msg = "Help text", .value = "value" }, | 173 | .id = .{ .desc = "Help text", .val = "val" }, |
| 157 | .names = .{ .short = 's', .long = "long" }, | 174 | .names = .{ .short = 's', .long = "long" }, |
| 158 | .takes_value = .many, | 175 | .takes_value = .many, |
| 159 | }, try parseParam("-s, --long <value>... Help text")); | 176 | }, try parseParam("-s, --long <val>... Help text")); |
| 160 | 177 | ||
| 161 | try expectParam(Param(Help){ | 178 | try expectParam(Param(Help){ |
| 162 | .id = .{ .msg = "Help text", .value = "value" }, | 179 | .id = .{ .desc = "Help text", .val = "val" }, |
| 163 | .names = .{ .long = "long" }, | 180 | .names = .{ .long = "long" }, |
| 164 | .takes_value = .one, | 181 | .takes_value = .one, |
| 165 | }, try parseParam("--long <value> Help text")); | 182 | }, try parseParam("--long <val> Help text")); |
| 166 | 183 | ||
| 167 | try expectParam(Param(Help){ | 184 | try expectParam(Param(Help){ |
| 168 | .id = .{ .msg = "Help text", .value = "value" }, | 185 | .id = .{ .desc = "Help text", .val = "val" }, |
| 169 | .names = .{ .short = 's' }, | 186 | .names = .{ .short = 's' }, |
| 170 | .takes_value = .one, | 187 | .takes_value = .one, |
| 171 | }, try parseParam("-s <value> Help text")); | 188 | }, try parseParam("-s <val> Help text")); |
| 172 | 189 | ||
| 173 | try expectParam(Param(Help){ | 190 | try expectParam(Param(Help){ |
| 174 | .id = .{ .msg = "Help text" }, | 191 | .id = .{ .desc = "Help text" }, |
| 175 | .names = .{ .short = 's', .long = "long" }, | 192 | .names = .{ .short = 's', .long = "long" }, |
| 176 | }, try parseParam("-s, --long Help text")); | 193 | }, try parseParam("-s, --long Help text")); |
| 177 | 194 | ||
| 178 | try expectParam(Param(Help){ | 195 | try expectParam(Param(Help){ |
| 179 | .id = .{ .msg = "Help text" }, | 196 | .id = .{ .desc = "Help text" }, |
| 180 | .names = .{ .short = 's' }, | 197 | .names = .{ .short = 's' }, |
| 181 | }, try parseParam("-s Help text")); | 198 | }, try parseParam("-s Help text")); |
| 182 | 199 | ||
| 183 | try expectParam(Param(Help){ | 200 | try expectParam(Param(Help){ |
| 184 | .id = .{ .msg = "Help text" }, | 201 | .id = .{ .desc = "Help text" }, |
| 185 | .names = .{ .long = "long" }, | 202 | .names = .{ .long = "long" }, |
| 186 | }, try parseParam("--long Help text")); | 203 | }, try parseParam("--long Help text")); |
| 187 | 204 | ||
| 188 | try expectParam(Param(Help){ | 205 | try expectParam(Param(Help){ |
| 189 | .id = .{ .msg = "Help text", .value = "A | B" }, | 206 | .id = .{ .desc = "Help text", .val = "A | B" }, |
| 190 | .names = .{ .long = "long" }, | 207 | .names = .{ .long = "long" }, |
| 191 | .takes_value = .one, | 208 | .takes_value = .one, |
| 192 | }, try parseParam("--long <A | B> Help text")); | 209 | }, try parseParam("--long <A | B> Help text")); |
| 193 | 210 | ||
| 194 | try expectParam(Param(Help){ | 211 | try expectParam(Param(Help){ |
| 195 | .id = .{ .msg = "Help text", .value = "A" }, | 212 | .id = .{ .desc = "Help text", .val = "A" }, |
| 196 | .names = .{}, | 213 | .names = .{}, |
| 197 | .takes_value = .one, | 214 | .takes_value = .one, |
| 198 | }, try parseParam("<A> Help text")); | 215 | }, try parseParam("<A> Help text")); |
| 199 | 216 | ||
| 200 | try expectParam(Param(Help){ | 217 | try expectParam(Param(Help){ |
| 201 | .id = .{ .msg = "Help text", .value = "A" }, | 218 | .id = .{ .desc = "Help text", .val = "A" }, |
| 202 | .names = .{}, | 219 | .names = .{}, |
| 203 | .takes_value = .many, | 220 | .takes_value = .many, |
| 204 | }, try parseParam("<A>... Help text")); | 221 | }, try parseParam("<A>... Help text")); |
| @@ -206,7 +223,7 @@ test "parseParam" { | |||
| 206 | try testing.expectError(error.TrailingComma, parseParam("--long, Help")); | 223 | try testing.expectError(error.TrailingComma, parseParam("--long, Help")); |
| 207 | try testing.expectError(error.TrailingComma, parseParam("-s, Help")); | 224 | try testing.expectError(error.TrailingComma, parseParam("-s, Help")); |
| 208 | try testing.expectError(error.InvalidShortParam, parseParam("-ss Help")); | 225 | try testing.expectError(error.InvalidShortParam, parseParam("-ss Help")); |
| 209 | try testing.expectError(error.InvalidShortParam, parseParam("-ss <value> Help")); | 226 | try testing.expectError(error.InvalidShortParam, parseParam("-ss <val> Help")); |
| 210 | try testing.expectError(error.InvalidShortParam, parseParam("- Help")); | 227 | try testing.expectError(error.InvalidShortParam, parseParam("- Help")); |
| 211 | } | 228 | } |
| 212 | 229 | ||
| @@ -230,15 +247,15 @@ pub const Diagnostic = struct { | |||
| 230 | Arg{ .prefix = "", .name = diag.arg }; | 247 | Arg{ .prefix = "", .name = diag.arg }; |
| 231 | 248 | ||
| 232 | switch (err) { | 249 | switch (err) { |
| 233 | error.DoesntTakeValue => try stream.print( | 250 | streaming.Error.DoesntTakeValue => try stream.print( |
| 234 | "The argument '{s}{s}' does not take a value\n", | 251 | "The argument '{s}{s}' does not take a value\n", |
| 235 | .{ a.prefix, a.name }, | 252 | .{ a.prefix, a.name }, |
| 236 | ), | 253 | ), |
| 237 | error.MissingValue => try stream.print( | 254 | streaming.Error.MissingValue => try stream.print( |
| 238 | "The argument '{s}{s}' requires a value but none was supplied\n", | 255 | "The argument '{s}{s}' requires a value but none was supplied\n", |
| 239 | .{ a.prefix, a.name }, | 256 | .{ a.prefix, a.name }, |
| 240 | ), | 257 | ), |
| 241 | error.InvalidArgument => try stream.print( | 258 | streaming.Error.InvalidArgument => try stream.print( |
| 242 | "Invalid argument '{s}{s}'\n", | 259 | "Invalid argument '{s}{s}'\n", |
| 243 | .{ a.prefix, a.name }, | 260 | .{ a.prefix, a.name }, |
| 244 | ), | 261 | ), |
| @@ -303,34 +320,6 @@ test "Diagnostic.report" { | |||
| 303 | ); | 320 | ); |
| 304 | } | 321 | } |
| 305 | 322 | ||
| 306 | pub fn Args(comptime Id: type, comptime params: []const Param(Id)) type { | ||
| 307 | return struct { | ||
| 308 | arena: std.heap.ArenaAllocator, | ||
| 309 | clap: ComptimeClap(Id, params), | ||
| 310 | exe_arg: ?[]const u8, | ||
| 311 | |||
| 312 | pub fn deinit(a: *@This()) void { | ||
| 313 | a.arena.deinit(); | ||
| 314 | } | ||
| 315 | |||
| 316 | pub fn flag(a: @This(), comptime name: []const u8) bool { | ||
| 317 | return a.clap.flag(name); | ||
| 318 | } | ||
| 319 | |||
| 320 | pub fn option(a: @This(), comptime name: []const u8) ?[]const u8 { | ||
| 321 | return a.clap.option(name); | ||
| 322 | } | ||
| 323 | |||
| 324 | pub fn options(a: @This(), comptime name: []const u8) []const []const u8 { | ||
| 325 | return a.clap.options(name); | ||
| 326 | } | ||
| 327 | |||
| 328 | pub fn positionals(a: @This()) []const []const u8 { | ||
| 329 | return a.clap.positionals(); | ||
| 330 | } | ||
| 331 | }; | ||
| 332 | } | ||
| 333 | |||
| 334 | /// Options that can be set to customize the behavior of parsing. | 323 | /// Options that can be set to customize the behavior of parsing. |
| 335 | pub const ParseOptions = struct { | 324 | pub const ParseOptions = struct { |
| 336 | /// The allocator used for all memory allocations. Defaults to the `heap.page_allocator`. | 325 | /// The allocator used for all memory allocations. Defaults to the `heap.page_allocator`. |
| @@ -346,59 +335,348 @@ pub const ParseOptions = struct { | |||
| 346 | pub fn parse( | 335 | pub fn parse( |
| 347 | comptime Id: type, | 336 | comptime Id: type, |
| 348 | comptime params: []const Param(Id), | 337 | comptime params: []const Param(Id), |
| 338 | comptime value_parsers: anytype, | ||
| 349 | opt: ParseOptions, | 339 | opt: ParseOptions, |
| 350 | ) !Args(Id, params) { | 340 | ) !Result(Id, params, value_parsers) { |
| 351 | var arena = heap.ArenaAllocator.init(opt.allocator); | 341 | var arena = heap.ArenaAllocator.init(opt.allocator); |
| 352 | errdefer arena.deinit(); | 342 | errdefer arena.deinit(); |
| 353 | 343 | ||
| 354 | var iter = try process.ArgIterator.initWithAllocator(arena.allocator()); | 344 | var iter = try process.ArgIterator.initWithAllocator(arena.allocator()); |
| 355 | const exe_arg = iter.next(); | 345 | const exe_arg = iter.next(); |
| 356 | 346 | ||
| 357 | const clap = try parseEx(Id, params, &iter, .{ | 347 | const result = try parseEx(Id, params, value_parsers, &iter, .{ |
| 358 | // Let's reuse the arena from the `OSIterator` since we already have it. | 348 | // Let's reuse the arena from the `OSIterator` since we already have it. |
| 359 | .allocator = arena.allocator(), | 349 | .allocator = arena.allocator(), |
| 360 | .diagnostic = opt.diagnostic, | 350 | .diagnostic = opt.diagnostic, |
| 361 | }); | 351 | }); |
| 362 | 352 | ||
| 363 | return Args(Id, params){ | 353 | return Result(Id, params, value_parsers){ |
| 354 | .args = result.args, | ||
| 355 | .positionals = result.positionals, | ||
| 364 | .exe_arg = exe_arg, | 356 | .exe_arg = exe_arg, |
| 365 | .arena = arena, | 357 | .arena = arena, |
| 366 | .clap = clap, | ||
| 367 | }; | 358 | }; |
| 368 | } | 359 | } |
| 369 | 360 | ||
| 370 | /// Parses the command line arguments passed into the program based on an | 361 | pub fn Result( |
| 371 | /// array of `Param`s. | 362 | comptime Id: type, |
| 363 | comptime params: []const Param(Id), | ||
| 364 | comptime value_parsers: anytype, | ||
| 365 | ) type { | ||
| 366 | return struct { | ||
| 367 | args: Arguments(Id, params, value_parsers, .slice), | ||
| 368 | positionals: []const FindPositionalType(Id, params, value_parsers), | ||
| 369 | exe_arg: ?[]const u8, | ||
| 370 | arena: std.heap.ArenaAllocator, | ||
| 371 | |||
| 372 | pub fn deinit(result: @This()) void { | ||
| 373 | result.arena.deinit(); | ||
| 374 | } | ||
| 375 | }; | ||
| 376 | } | ||
| 377 | |||
| 378 | /// Parses the command line arguments passed into the program based on an array of parameters. | ||
| 379 | /// | ||
| 380 | /// The result will contain an `args` field which contains all the non positional arguments passed | ||
| 381 | /// in. There is a field in `args` for each parameter. The name of that field will be the result | ||
| 382 | /// of this expression: | ||
| 383 | /// ``` | ||
| 384 | /// param.names.longest().name` | ||
| 385 | /// ``` | ||
| 386 | /// | ||
| 387 | /// The fields can have types other that `[]const u8` and this is based on what `value_parsers` | ||
| 388 | /// you provide. The parser to use for each parameter is determined by the following expression: | ||
| 389 | /// ``` | ||
| 390 | /// @field(value_parsers, param.id.value()) | ||
| 391 | /// ``` | ||
| 392 | /// | ||
| 393 | /// Where `value` is a function that returns the name of the value this parameter takes. A parser | ||
| 394 | /// is simple a function with the signature: | ||
| 395 | /// ``` | ||
| 396 | /// fn ([]const u8) Error!T | ||
| 397 | /// ``` | ||
| 398 | /// | ||
| 399 | /// `T` can be any type and `Error` can be any error. You can pass `clap.parsers.default` if you | ||
| 400 | /// just wonna get something up and running. | ||
| 401 | /// | ||
| 402 | /// Caller ownes the result and should free it by calling `result.deinit()` | ||
| 372 | pub fn parseEx( | 403 | pub fn parseEx( |
| 373 | comptime Id: type, | 404 | comptime Id: type, |
| 374 | comptime params: []const Param(Id), | 405 | comptime params: []const Param(Id), |
| 406 | comptime value_parsers: anytype, | ||
| 375 | iter: anytype, | 407 | iter: anytype, |
| 376 | opt: ParseOptions, | 408 | opt: ParseOptions, |
| 377 | ) !ComptimeClap(Id, params) { | 409 | ) !ResultEx(Id, params, value_parsers) { |
| 378 | const Clap = ComptimeClap(Id, params); | 410 | const allocator = opt.allocator; |
| 379 | return try Clap.parse(iter, opt); | 411 | var positionals = std.ArrayList( |
| 412 | FindPositionalType(Id, params, value_parsers), | ||
| 413 | ).init(allocator); | ||
| 414 | |||
| 415 | var arguments = Arguments(Id, params, value_parsers, .list){}; | ||
| 416 | errdefer deinitArgs(Id, params, value_parsers, .list, allocator, &arguments); | ||
| 417 | |||
| 418 | var stream = streaming.Clap(Id, @typeInfo(@TypeOf(iter)).Pointer.child){ | ||
| 419 | .params = params, | ||
| 420 | .iter = iter, | ||
| 421 | .diagnostic = opt.diagnostic, | ||
| 422 | }; | ||
| 423 | while (try stream.next()) |arg| { | ||
| 424 | inline for (params) |*param| { | ||
| 425 | if (param == arg.param) { | ||
| 426 | const parser = comptime switch (param.takes_value) { | ||
| 427 | .none => undefined, | ||
| 428 | .one, .many => @field(value_parsers, param.id.value()), | ||
| 429 | }; | ||
| 430 | |||
| 431 | // TODO: Update opt.diagnostics when `parser` fails. This is blocked by compiler | ||
| 432 | // bugs that causes an infinit loop. | ||
| 433 | const longest = comptime param.names.longest(); | ||
| 434 | switch (longest.kind) { | ||
| 435 | .short, .long => switch (param.takes_value) { | ||
| 436 | .none => @field(arguments, longest.name) = true, | ||
| 437 | .one => @field(arguments, longest.name) = try parser(arg.value.?), | ||
| 438 | .many => { | ||
| 439 | const value = try parser(arg.value.?); | ||
| 440 | try @field(arguments, longest.name).append(allocator, value); | ||
| 441 | }, | ||
| 442 | }, | ||
| 443 | .positinal => try positionals.append(try parser(arg.value.?)), | ||
| 444 | } | ||
| 445 | } | ||
| 446 | } | ||
| 447 | } | ||
| 448 | |||
| 449 | var result_args = Arguments(Id, params, value_parsers, .slice){}; | ||
| 450 | inline for (@typeInfo(@TypeOf(arguments)).Struct.fields) |field| { | ||
| 451 | if (@typeInfo(field.field_type) == .Struct and | ||
| 452 | @hasDecl(field.field_type, "toOwnedSlice")) | ||
| 453 | { | ||
| 454 | const slice = @field(arguments, field.name).toOwnedSlice(allocator); | ||
| 455 | @field(result_args, field.name) = slice; | ||
| 456 | } else { | ||
| 457 | @field(result_args, field.name) = @field(arguments, field.name); | ||
| 458 | } | ||
| 459 | } | ||
| 460 | |||
| 461 | return ResultEx(Id, params, value_parsers){ | ||
| 462 | .args = result_args, | ||
| 463 | .positionals = positionals.toOwnedSlice(), | ||
| 464 | .allocator = allocator, | ||
| 465 | }; | ||
| 466 | } | ||
| 467 | |||
| 468 | pub fn ResultEx( | ||
| 469 | comptime Id: type, | ||
| 470 | comptime params: []const Param(Id), | ||
| 471 | comptime value_parsers: anytype, | ||
| 472 | ) type { | ||
| 473 | return struct { | ||
| 474 | args: Arguments(Id, params, value_parsers, .slice), | ||
| 475 | positionals: []const FindPositionalType(Id, params, value_parsers), | ||
| 476 | allocator: mem.Allocator, | ||
| 477 | |||
| 478 | pub fn deinit(result: *@This()) void { | ||
| 479 | deinitArgs(Id, params, value_parsers, .slice, result.allocator, &result.args); | ||
| 480 | result.allocator.free(result.positionals); | ||
| 481 | } | ||
| 482 | }; | ||
| 483 | } | ||
| 484 | |||
| 485 | fn FindPositionalType( | ||
| 486 | comptime Id: type, | ||
| 487 | comptime params: []const Param(Id), | ||
| 488 | comptime value_parsers: anytype, | ||
| 489 | ) type { | ||
| 490 | for (params) |param| { | ||
| 491 | const longest = param.names.longest(); | ||
| 492 | if (longest.kind == .positinal) | ||
| 493 | return ParamType(Id, param, value_parsers); | ||
| 494 | } | ||
| 495 | |||
| 496 | return []const u8; | ||
| 497 | } | ||
| 498 | |||
| 499 | fn ParamType( | ||
| 500 | comptime Id: type, | ||
| 501 | comptime param: Param(Id), | ||
| 502 | comptime value_parsers: anytype, | ||
| 503 | ) type { | ||
| 504 | const parser = switch (param.takes_value) { | ||
| 505 | .none => parsers.string, | ||
| 506 | .one, .many => @field(value_parsers, param.id.value()), | ||
| 507 | }; | ||
| 508 | return parsers.Result(@TypeOf(parser)); | ||
| 509 | } | ||
| 510 | |||
| 511 | fn deinitArgs( | ||
| 512 | comptime Id: type, | ||
| 513 | comptime params: []const Param(Id), | ||
| 514 | comptime value_parsers: anytype, | ||
| 515 | comptime multi_arg_kind: MultiArgKind, | ||
| 516 | allocator: mem.Allocator, | ||
| 517 | arguments: *Arguments(Id, params, value_parsers, multi_arg_kind), | ||
| 518 | ) void { | ||
| 519 | inline for (params) |param| { | ||
| 520 | const longest = comptime param.names.longest(); | ||
| 521 | if (longest.kind == .positinal) | ||
| 522 | continue; | ||
| 523 | if (param.takes_value != .many) | ||
| 524 | continue; | ||
| 525 | |||
| 526 | switch (multi_arg_kind) { | ||
| 527 | .slice => allocator.free(@field(arguments, longest.name)), | ||
| 528 | .list => @field(arguments, longest.name).deinit(allocator), | ||
| 529 | } | ||
| 530 | } | ||
| 380 | } | 531 | } |
| 381 | 532 | ||
| 533 | const MultiArgKind = enum { slice, list }; | ||
| 534 | |||
| 535 | fn Arguments( | ||
| 536 | comptime Id: type, | ||
| 537 | comptime params: []const Param(Id), | ||
| 538 | comptime value_parsers: anytype, | ||
| 539 | comptime multi_arg_kind: MultiArgKind, | ||
| 540 | ) type { | ||
| 541 | var fields: [params.len]builtin.TypeInfo.StructField = undefined; | ||
| 542 | |||
| 543 | var i: usize = 0; | ||
| 544 | for (params) |param| { | ||
| 545 | const longest = param.names.longest(); | ||
| 546 | if (longest.kind == .positinal) | ||
| 547 | continue; | ||
| 548 | |||
| 549 | const T = ParamType(Id, param, value_parsers); | ||
| 550 | const FieldType = switch (param.takes_value) { | ||
| 551 | .none => bool, | ||
| 552 | .one => ?T, | ||
| 553 | .many => switch (multi_arg_kind) { | ||
| 554 | .slice => []const T, | ||
| 555 | .list => std.ArrayListUnmanaged(T), | ||
| 556 | }, | ||
| 557 | }; | ||
| 558 | fields[i] = .{ | ||
| 559 | .name = longest.name, | ||
| 560 | .field_type = FieldType, | ||
| 561 | .default_value = switch (param.takes_value) { | ||
| 562 | .none => &false, | ||
| 563 | .one => &@as(?T, null), | ||
| 564 | .many => switch (multi_arg_kind) { | ||
| 565 | .slice => &@as([]const T, &[_]T{}), | ||
| 566 | .list => &std.ArrayListUnmanaged(T){}, | ||
| 567 | }, | ||
| 568 | }, | ||
| 569 | .is_comptime = false, | ||
| 570 | .alignment = @alignOf(FieldType), | ||
| 571 | }; | ||
| 572 | i += 1; | ||
| 573 | } | ||
| 574 | |||
| 575 | return @Type(.{ .Struct = .{ | ||
| 576 | .layout = .Auto, | ||
| 577 | .fields = fields[0..i], | ||
| 578 | .decls = &.{}, | ||
| 579 | .is_tuple = false, | ||
| 580 | } }); | ||
| 581 | } | ||
| 582 | |||
| 583 | test "" { | ||
| 584 | const params = comptime &.{ | ||
| 585 | parseParam("-a, --aa") catch unreachable, | ||
| 586 | parseParam("-b, --bb") catch unreachable, | ||
| 587 | parseParam("-c, --cc <str>") catch unreachable, | ||
| 588 | parseParam("-d, --dd <usize>...") catch unreachable, | ||
| 589 | parseParam("<str>") catch unreachable, | ||
| 590 | }; | ||
| 591 | |||
| 592 | var iter = args.SliceIterator{ | ||
| 593 | .args = &.{ "-a", "-c", "0", "something", "-d", "1", "--dd", "2" }, | ||
| 594 | }; | ||
| 595 | var res = try parseEx(Help, params, parsers.default, &iter, .{ | ||
| 596 | .allocator = testing.allocator, | ||
| 597 | }); | ||
| 598 | defer res.deinit(); | ||
| 599 | |||
| 600 | try testing.expect(res.args.aa); | ||
| 601 | try testing.expect(!res.args.bb); | ||
| 602 | try testing.expectEqualStrings("0", res.args.cc.?); | ||
| 603 | try testing.expectEqual(@as(usize, 1), res.positionals.len); | ||
| 604 | try testing.expectEqualStrings("something", res.positionals[0]); | ||
| 605 | try testing.expectEqualSlices(usize, &.{ 1, 2 }, res.args.dd); | ||
| 606 | } | ||
| 607 | |||
| 608 | test "empty" { | ||
| 609 | var iter = args.SliceIterator{ .args = &.{} }; | ||
| 610 | var res = try parseEx(u8, &.{}, parsers.default, &iter, .{ .allocator = testing.allocator }); | ||
| 611 | defer res.deinit(); | ||
| 612 | } | ||
| 613 | |||
| 614 | fn testErr( | ||
| 615 | comptime params: []const Param(Help), | ||
| 616 | args_strings: []const []const u8, | ||
| 617 | expected: []const u8, | ||
| 618 | ) !void { | ||
| 619 | var diag = Diagnostic{}; | ||
| 620 | var iter = args.SliceIterator{ .args = args_strings }; | ||
| 621 | _ = parseEx(Help, params, parsers.default, &iter, .{ | ||
| 622 | .allocator = testing.allocator, | ||
| 623 | .diagnostic = &diag, | ||
| 624 | }) catch |err| { | ||
| 625 | var buf: [1024]u8 = undefined; | ||
| 626 | var fbs = io.fixedBufferStream(&buf); | ||
| 627 | diag.report(fbs.writer(), err) catch return error.TestFailed; | ||
| 628 | try testing.expectEqualStrings(expected, fbs.getWritten()); | ||
| 629 | return; | ||
| 630 | }; | ||
| 631 | |||
| 632 | try testing.expect(false); | ||
| 633 | } | ||
| 634 | |||
| 635 | test "errors" { | ||
| 636 | const params = comptime [_]Param(Help){ | ||
| 637 | parseParam("-a, --aa") catch unreachable, | ||
| 638 | parseParam("-c, --cc <str>") catch unreachable, | ||
| 639 | }; | ||
| 640 | |||
| 641 | try testErr(¶ms, &.{"q"}, "Invalid argument 'q'\n"); | ||
| 642 | try testErr(¶ms, &.{"-q"}, "Invalid argument '-q'\n"); | ||
| 643 | try testErr(¶ms, &.{"--q"}, "Invalid argument '--q'\n"); | ||
| 644 | try testErr(¶ms, &.{"--q=1"}, "Invalid argument '--q'\n"); | ||
| 645 | try testErr(¶ms, &.{"-a=1"}, "The argument '-a' does not take a value\n"); | ||
| 646 | try testErr(¶ms, &.{"--aa=1"}, "The argument '--aa' does not take a value\n"); | ||
| 647 | try testErr(¶ms, &.{"-c"}, "The argument '-c' requires a value but none was supplied\n"); | ||
| 648 | try testErr( | ||
| 649 | ¶ms, | ||
| 650 | &.{"--cc"}, | ||
| 651 | "The argument '--cc' requires a value but none was supplied\n", | ||
| 652 | ); | ||
| 653 | } | ||
| 654 | |||
| 655 | pub const Help = struct { | ||
| 656 | desc: []const u8 = "", | ||
| 657 | val: []const u8 = "", | ||
| 658 | |||
| 659 | pub fn description(h: Help) []const u8 { | ||
| 660 | return h.desc; | ||
| 661 | } | ||
| 662 | |||
| 663 | pub fn value(h: Help) []const u8 { | ||
| 664 | return h.val; | ||
| 665 | } | ||
| 666 | }; | ||
| 667 | |||
| 382 | /// Will print a help message in the following format: | 668 | /// Will print a help message in the following format: |
| 383 | /// -s, --long <valueText> helpText | 669 | /// -s, --long <valueText> helpText |
| 384 | /// -s, helpText | 670 | /// -s, helpText |
| 385 | /// -s <valueText> helpText | 671 | /// -s <valueText> helpText |
| 386 | /// --long helpText | 672 | /// --long helpText |
| 387 | /// --long <valueText> helpText | 673 | /// --long <valueText> helpText |
| 388 | pub fn helpFull( | 674 | pub fn help(stream: anytype, comptime Id: type, params: []const Param(Id)) !void { |
| 389 | stream: anytype, | ||
| 390 | comptime Id: type, | ||
| 391 | params: []const Param(Id), | ||
| 392 | comptime Error: type, | ||
| 393 | context: anytype, | ||
| 394 | helpText: fn (@TypeOf(context), Param(Id)) Error![]const u8, | ||
| 395 | valueText: fn (@TypeOf(context), Param(Id)) Error![]const u8, | ||
| 396 | ) !void { | ||
| 397 | const max_spacing = blk: { | 675 | const max_spacing = blk: { |
| 398 | var res: usize = 0; | 676 | var res: usize = 0; |
| 399 | for (params) |param| { | 677 | for (params) |param| { |
| 400 | var cs = io.countingWriter(io.null_writer); | 678 | var cs = io.countingWriter(io.null_writer); |
| 401 | try printParam(cs.writer(), Id, param, Error, context, valueText); | 679 | try printParam(cs.writer(), Id, param); |
| 402 | if (res < cs.bytes_written) | 680 | if (res < cs.bytes_written) |
| 403 | res = @intCast(usize, cs.bytes_written); | 681 | res = @intCast(usize, cs.bytes_written); |
| 404 | } | 682 | } |
| @@ -412,13 +690,13 @@ pub fn helpFull( | |||
| 412 | 690 | ||
| 413 | var cs = io.countingWriter(stream); | 691 | var cs = io.countingWriter(stream); |
| 414 | try stream.writeAll("\t"); | 692 | try stream.writeAll("\t"); |
| 415 | try printParam(cs.writer(), Id, param, Error, context, valueText); | 693 | try printParam(cs.writer(), Id, param); |
| 416 | try stream.writeByteNTimes(' ', max_spacing - @intCast(usize, cs.bytes_written)); | 694 | try stream.writeByteNTimes(' ', max_spacing - @intCast(usize, cs.bytes_written)); |
| 417 | 695 | ||
| 418 | const help_text = try helpText(context, param); | 696 | const description = param.id.description(); |
| 419 | var help_text_line_it = mem.split(u8, help_text, "\n"); | 697 | var it = mem.split(u8, description, "\n"); |
| 420 | var indent_line = false; | 698 | var indent_line = false; |
| 421 | while (help_text_line_it.next()) |line| : (indent_line = true) { | 699 | while (it.next()) |line| : (indent_line = true) { |
| 422 | if (indent_line) { | 700 | if (indent_line) { |
| 423 | try stream.writeAll("\t"); | 701 | try stream.writeAll("\t"); |
| 424 | try stream.writeByteNTimes(' ', max_spacing); | 702 | try stream.writeByteNTimes(' ', max_spacing); |
| @@ -434,9 +712,6 @@ fn printParam( | |||
| 434 | stream: anytype, | 712 | stream: anytype, |
| 435 | comptime Id: type, | 713 | comptime Id: type, |
| 436 | param: Param(Id), | 714 | param: Param(Id), |
| 437 | comptime Error: type, | ||
| 438 | context: anytype, | ||
| 439 | valueText: fn (@TypeOf(context), Param(Id)) Error![]const u8, | ||
| 440 | ) !void { | 715 | ) !void { |
| 441 | if (param.names.short) |s| { | 716 | if (param.names.short) |s| { |
| 442 | try stream.writeAll(&[_]u8{ '-', s }); | 717 | try stream.writeAll(&[_]u8{ '-', s }); |
| @@ -458,66 +733,12 @@ fn printParam( | |||
| 458 | return; | 733 | return; |
| 459 | 734 | ||
| 460 | try stream.writeAll(" <"); | 735 | try stream.writeAll(" <"); |
| 461 | try stream.writeAll(try valueText(context, param)); | 736 | try stream.writeAll(param.id.value()); |
| 462 | try stream.writeAll(">"); | 737 | try stream.writeAll(">"); |
| 463 | if (param.takes_value == .many) | 738 | if (param.takes_value == .many) |
| 464 | try stream.writeAll("..."); | 739 | try stream.writeAll("..."); |
| 465 | } | 740 | } |
| 466 | 741 | ||
| 467 | /// A wrapper around helpFull for simple helpText and valueText functions that | ||
| 468 | /// cant return an error or take a context. | ||
| 469 | pub fn helpEx( | ||
| 470 | stream: anytype, | ||
| 471 | comptime Id: type, | ||
| 472 | params: []const Param(Id), | ||
| 473 | helpText: fn (Param(Id)) []const u8, | ||
| 474 | valueText: fn (Param(Id)) []const u8, | ||
| 475 | ) !void { | ||
| 476 | const Context = struct { | ||
| 477 | helpText: fn (Param(Id)) []const u8, | ||
| 478 | valueText: fn (Param(Id)) []const u8, | ||
| 479 | |||
| 480 | pub fn help(c: @This(), p: Param(Id)) error{}![]const u8 { | ||
| 481 | return c.helpText(p); | ||
| 482 | } | ||
| 483 | |||
| 484 | pub fn value(c: @This(), p: Param(Id)) error{}![]const u8 { | ||
| 485 | return c.valueText(p); | ||
| 486 | } | ||
| 487 | }; | ||
| 488 | |||
| 489 | return helpFull( | ||
| 490 | stream, | ||
| 491 | Id, | ||
| 492 | params, | ||
| 493 | error{}, | ||
| 494 | Context{ | ||
| 495 | .helpText = helpText, | ||
| 496 | .valueText = valueText, | ||
| 497 | }, | ||
| 498 | Context.help, | ||
| 499 | Context.value, | ||
| 500 | ); | ||
| 501 | } | ||
| 502 | |||
| 503 | pub const Help = struct { | ||
| 504 | msg: []const u8 = "", | ||
| 505 | value: []const u8 = "", | ||
| 506 | }; | ||
| 507 | |||
| 508 | /// A wrapper around helpEx that takes a Param(Help). | ||
| 509 | pub fn help(stream: anytype, params: []const Param(Help)) !void { | ||
| 510 | try helpEx(stream, Help, params, getHelpSimple, getValueSimple); | ||
| 511 | } | ||
| 512 | |||
| 513 | fn getHelpSimple(param: Param(Help)) []const u8 { | ||
| 514 | return param.id.msg; | ||
| 515 | } | ||
| 516 | |||
| 517 | fn getValueSimple(param: Param(Help)) []const u8 { | ||
| 518 | return param.id.value; | ||
| 519 | } | ||
| 520 | |||
| 521 | test "clap.help" { | 742 | test "clap.help" { |
| 522 | var buf: [1024]u8 = undefined; | 743 | var buf: [1024]u8 = undefined; |
| 523 | var slice_stream = io.fixedBufferStream(&buf); | 744 | var slice_stream = io.fixedBufferStream(&buf); |
| @@ -525,6 +746,7 @@ test "clap.help" { | |||
| 525 | @setEvalBranchQuota(10000); | 746 | @setEvalBranchQuota(10000); |
| 526 | try help( | 747 | try help( |
| 527 | slice_stream.writer(), | 748 | slice_stream.writer(), |
| 749 | Help, | ||
| 528 | comptime &.{ | 750 | comptime &.{ |
| 529 | parseParam("-a Short flag.") catch unreachable, | 751 | parseParam("-a Short flag.") catch unreachable, |
| 530 | parseParam("-b <V1> Short option.") catch unreachable, | 752 | parseParam("-b <V1> Short option.") catch unreachable, |
| @@ -556,18 +778,11 @@ test "clap.help" { | |||
| 556 | } | 778 | } |
| 557 | 779 | ||
| 558 | /// Will print a usage message in the following format: | 780 | /// Will print a usage message in the following format: |
| 559 | /// [-abc] [--longa] [-d <valueText>] [--longb <valueText>] <valueText> | 781 | /// [-abc] [--longa] [-d <T>] [--longb <T>] <T> |
| 560 | /// | 782 | /// |
| 561 | /// First all none value taking parameters, which have a short name are | 783 | /// First all none value taking parameters, which have a short name are printed, then non |
| 562 | /// printed, then non positional parameters and finally the positinal. | 784 | /// positional parameters and finally the positinal. |
| 563 | pub fn usageFull( | 785 | pub fn usage(stream: anytype, comptime Id: type, params: []const Param(Id)) !void { |
| 564 | stream: anytype, | ||
| 565 | comptime Id: type, | ||
| 566 | params: []const Param(Id), | ||
| 567 | comptime Error: type, | ||
| 568 | context: anytype, | ||
| 569 | valueText: fn (@TypeOf(context), Param(Id)) Error![]const u8, | ||
| 570 | ) !void { | ||
| 571 | var cos = io.countingWriter(stream); | 786 | var cos = io.countingWriter(stream); |
| 572 | const cs = cos.writer(); | 787 | const cs = cos.writer(); |
| 573 | for (params) |param| { | 788 | for (params) |param| { |
| @@ -607,7 +822,7 @@ pub fn usageFull( | |||
| 607 | try cs.writeAll(name); | 822 | try cs.writeAll(name); |
| 608 | if (param.takes_value != .none) { | 823 | if (param.takes_value != .none) { |
| 609 | try cs.writeAll(" <"); | 824 | try cs.writeAll(" <"); |
| 610 | try cs.writeAll(try valueText(context, param)); | 825 | try cs.writeAll(param.id.value()); |
| 611 | try cs.writeAll(">"); | 826 | try cs.writeAll(">"); |
| 612 | if (param.takes_value == .many) | 827 | if (param.takes_value == .many) |
| 613 | try cs.writeAll("..."); | 828 | try cs.writeAll("..."); |
| @@ -621,46 +836,15 @@ pub fn usageFull( | |||
| 621 | try cs.writeAll(" "); | 836 | try cs.writeAll(" "); |
| 622 | 837 | ||
| 623 | try cs.writeAll("<"); | 838 | try cs.writeAll("<"); |
| 624 | try cs.writeAll(try valueText(context, p)); | 839 | try cs.writeAll(p.id.value()); |
| 625 | try cs.writeAll(">"); | 840 | try cs.writeAll(">"); |
| 626 | } | 841 | } |
| 627 | } | 842 | } |
| 628 | 843 | ||
| 629 | /// A wrapper around usageFull for a simple valueText functions that | ||
| 630 | /// cant return an error or take a context. | ||
| 631 | pub fn usageEx( | ||
| 632 | stream: anytype, | ||
| 633 | comptime Id: type, | ||
| 634 | params: []const Param(Id), | ||
| 635 | valueText: fn (Param(Id)) []const u8, | ||
| 636 | ) !void { | ||
| 637 | const Context = struct { | ||
| 638 | valueText: fn (Param(Id)) []const u8, | ||
| 639 | |||
| 640 | pub fn value(c: @This(), p: Param(Id)) error{}![]const u8 { | ||
| 641 | return c.valueText(p); | ||
| 642 | } | ||
| 643 | }; | ||
| 644 | |||
| 645 | return usageFull( | ||
| 646 | stream, | ||
| 647 | Id, | ||
| 648 | params, | ||
| 649 | error{}, | ||
| 650 | Context{ .valueText = valueText }, | ||
| 651 | Context.value, | ||
| 652 | ); | ||
| 653 | } | ||
| 654 | |||
| 655 | /// A wrapper around usageEx that takes a Param(Help). | ||
| 656 | pub fn usage(stream: anytype, params: []const Param(Help)) !void { | ||
| 657 | try usageEx(stream, Help, params, getValueSimple); | ||
| 658 | } | ||
| 659 | |||
| 660 | fn testUsage(expected: []const u8, params: []const Param(Help)) !void { | 844 | fn testUsage(expected: []const u8, params: []const Param(Help)) !void { |
| 661 | var buf: [1024]u8 = undefined; | 845 | var buf: [1024]u8 = undefined; |
| 662 | var fbs = io.fixedBufferStream(&buf); | 846 | var fbs = io.fixedBufferStream(&buf); |
| 663 | try usage(fbs.writer(), params); | 847 | try usage(fbs.writer(), Help, params); |
| 664 | try testing.expectEqualStrings(expected, fbs.getWritten()); | 848 | try testing.expectEqualStrings(expected, fbs.getWritten()); |
| 665 | } | 849 | } |
| 666 | 850 | ||