summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--clap.zig8
-rw-r--r--clap/types.zig59
-rw-r--r--clap/v2.zig1592
3 files changed, 1657 insertions, 2 deletions
diff --git a/clap.zig b/clap.zig
index 5b69704..5fab15d 100644
--- a/clap.zig
+++ b/clap.zig
@@ -2132,14 +2132,18 @@ test "usage" {
2132 2132
2133test { 2133test {
2134 _ = args; 2134 _ = args;
2135 _ = ccw;
2135 _ = parsers; 2136 _ = parsers;
2136 _ = streaming; 2137 _ = streaming;
2137 _ = ccw; 2138 _ = v1;
2139 _ = v2;
2138} 2140}
2139 2141
2140pub const args = @import("clap/args.zig"); 2142pub const args = @import("clap/args.zig");
2143pub const ccw = @import("clap/codepoint_counting_writer.zig");
2141pub const parsers = @import("clap/parsers.zig"); 2144pub const parsers = @import("clap/parsers.zig");
2142pub const streaming = @import("clap/streaming.zig"); 2145pub const streaming = @import("clap/streaming.zig");
2143pub const ccw = @import("clap/codepoint_counting_writer.zig"); 2146pub const v1 = @This();
2147pub const v2 = @import("clap/v2.zig");
2144 2148
2145const std = @import("std"); 2149const std = @import("std");
diff --git a/clap/types.zig b/clap/types.zig
new file mode 100644
index 0000000..926659a
--- /dev/null
+++ b/clap/types.zig
@@ -0,0 +1,59 @@
1pub fn isArrayListUnmanaged(comptime T: type) bool {
2 if (@typeInfo(T) != .@"struct" or !@hasDecl(T, "Slice"))
3 return false;
4
5 const ptr_info = switch (@typeInfo(T.Slice)) {
6 .pointer => |info| info,
7 else => return false,
8 };
9
10 return T == std.ArrayListAlignedUnmanaged(ptr_info.child, null) or
11 T == std.ArrayListAlignedUnmanaged(ptr_info.child, ptr_info.alignment);
12}
13
14test isArrayListUnmanaged {
15 try std.testing.expect(!isArrayListUnmanaged(u8));
16 try std.testing.expect(!isArrayListUnmanaged([]const u8));
17 try std.testing.expect(!isArrayListUnmanaged(struct {
18 pub const Slice = []const u8;
19 }));
20 try std.testing.expect(isArrayListUnmanaged(std.ArrayListUnmanaged(u8)));
21}
22
23pub fn allFieldsHaveDefaults(comptime T: type) bool {
24 const info = switch (@typeInfo(T)) {
25 .@"struct" => |s| s,
26 else => return false,
27 };
28
29 inline for (info.fields) |field| {
30 if (field.default_value == null)
31 return false;
32 }
33
34 return true;
35}
36
37test allFieldsHaveDefaults {
38 try std.testing.expect(!allFieldsHaveDefaults(u8));
39 try std.testing.expect(!allFieldsHaveDefaults([]const u8));
40 try std.testing.expect(allFieldsHaveDefaults(struct {}));
41 try std.testing.expect(allFieldsHaveDefaults(struct {
42 a: u8 = 0,
43 }));
44 try std.testing.expect(!allFieldsHaveDefaults(struct {
45 a: u8,
46 }));
47 try std.testing.expect(!allFieldsHaveDefaults(struct {
48 a: u8,
49 b: u8 = 0,
50 c: u8,
51 }));
52 try std.testing.expect(allFieldsHaveDefaults(struct {
53 a: u8 = 1,
54 b: u8 = 0,
55 c: u8 = 3,
56 }));
57}
58
59const std = @import("std");
diff --git a/clap/v2.zig b/clap/v2.zig
new file mode 100644
index 0000000..e427d6a
--- /dev/null
+++ b/clap/v2.zig
@@ -0,0 +1,1592 @@
1pub fn Params(comptime T: type) type {
2 const info = @typeInfo(T).@"struct";
3
4 var params: [info.fields.len + 2]std.builtin.Type.StructField = undefined;
5 const name_default_value: ?[]const u8 = null;
6 params[0] = .{
7 .name = "name",
8 .type = ?[]const u8,
9 .alignment = @alignOf(?[]const u8),
10 .default_value = @ptrCast(&name_default_value),
11 .is_comptime = false,
12 };
13
14 const description_default_value: []const u8 = "";
15 params[1] = .{
16 .name = "description",
17 .type = []const u8,
18 .alignment = @alignOf([]const u8),
19 .default_value = @ptrCast(&description_default_value),
20 .is_comptime = false,
21 };
22
23 var used_shorts = std.StaticBitSet(std.math.maxInt(u8) + 1).initEmpty();
24
25 // Reserver 'h' and 'v' for `--help` and `--version`
26 used_shorts.set('h');
27 used_shorts.set('v');
28
29 for (info.fields, params[2..]) |field, *param| {
30 const FieldType = field.type;
31 const field_info = @typeInfo(FieldType);
32
33 const Command = switch (field_info) {
34 .@"union" => |un| blk: {
35 var cmd_fields: [un.fields.len]std.builtin.Type.StructField = undefined;
36 for (un.fields, &cmd_fields) |un_field, *cmd_field| {
37 const CmdParam = Params(un_field.type);
38 const cmd_default_value = CmdParam{};
39 cmd_field.* = .{
40 .name = un_field.name,
41 .type = CmdParam,
42 .alignment = @alignOf(CmdParam),
43 .default_value = @ptrCast(&cmd_default_value),
44 .is_comptime = false,
45 };
46 }
47
48 break :blk @Type(.{ .@"struct" = .{
49 .layout = .auto,
50 .fields = &cmd_fields,
51 .decls = &.{},
52 .is_tuple = false,
53 } });
54 },
55 else => struct {},
56 };
57
58 const default_short = if (used_shorts.isSet(field.name[0])) null else blk: {
59 used_shorts.set(field.name[0]);
60 break :blk field.name[0];
61 };
62
63 const Param = struct {
64 short: ?u8 = default_short,
65 long: ?[]const u8 = field.name,
66 value: []const u8 = blk: {
67 var res_buf: [field.name.len]u8 = undefined;
68 for (&res_buf, field.name) |*r, c|
69 r.* = std.ascii.toUpper(c);
70
71 const res = res_buf;
72 break :blk &res;
73 },
74 description: []const u8 = "",
75 init: Init(FieldType) = defaultInit(FieldType, field.default_value),
76 deinit: Deinit(FieldType) = defaultDeinit(FieldType),
77 next: Next(FieldType) = defaultNext(FieldType),
78 parse: ParseInto(FieldType) = defaultParseInto(FieldType),
79 command: Command = .{},
80 required: bool = field.default_value == null,
81 kind: enum {
82 flag,
83 option,
84 positional,
85 positionals,
86 command,
87 } = switch (@typeInfo(field.type)) {
88 .@"union" => .command,
89 .bool => .flag,
90 else => .option,
91 },
92 };
93
94 const default_value = Param{};
95 param.* = .{
96 .name = field.name,
97 .type = Param,
98 .alignment = @alignOf(Param),
99 .default_value = @ptrCast(&default_value),
100 .is_comptime = false,
101 };
102 }
103
104 return @Type(.{ .@"struct" = .{
105 .layout = .auto,
106 .fields = &params,
107 .decls = &.{},
108 .is_tuple = false,
109 } });
110}
111
112fn Init(comptime T: type) type {
113 return ?*const fn (std.mem.Allocator) ParseError!T;
114}
115
116fn defaultInit(comptime T: type, comptime default_value: ?*const anyopaque) Init(T) {
117 if (default_value) |v| {
118 return struct {
119 fn init(_: std.mem.Allocator) ParseError!T {
120 return @as(*const T, @alignCast(@ptrCast(v))).*;
121 }
122 }.init;
123 }
124 if (types.allFieldsHaveDefaults(T)) {
125 return struct {
126 fn init(_: std.mem.Allocator) ParseError!T {
127 return .{};
128 }
129 }.init;
130 }
131
132 return switch (@typeInfo(T)) {
133 .void => return struct {
134 fn init(_: std.mem.Allocator) ParseError!T {
135 return {};
136 }
137 }.init,
138 .bool => return struct {
139 fn init(_: std.mem.Allocator) ParseError!T {
140 return false;
141 }
142 }.init,
143 .int, .float => return struct {
144 fn init(_: std.mem.Allocator) ParseError!T {
145 return 0;
146 }
147 }.init,
148 .optional => return struct {
149 fn init(_: std.mem.Allocator) ParseError!T {
150 return null;
151 }
152 }.init,
153 else => null,
154 };
155}
156
157fn Deinit(comptime T: type) type {
158 return ?*const fn (*T, std.mem.Allocator) void;
159}
160
161fn defaultDeinit(comptime T: type) Deinit(T) {
162 switch (@typeInfo(T)) {
163 .@"struct", .@"enum", .@"union" => {
164 if (!@hasDecl(T, "deinit"))
165 return null;
166 },
167 else => return null,
168 }
169
170 return switch (@TypeOf(T.deinit)) {
171 fn (*const T) void,
172 fn (*T) void,
173 fn (T) void,
174 => struct {
175 fn deinit(v: *T, _: std.mem.Allocator) void {
176 v.deinit();
177 }
178 }.deinit,
179 fn (*const T, std.mem.Allocator) void,
180 fn (*T, std.mem.Allocator) void,
181 fn (T, std.mem.Allocator) void,
182 => struct {
183 fn deinit(v: *T, gpa: std.mem.Allocator) void {
184 v.deinit(gpa);
185 }
186 }.deinit,
187 else => null,
188 };
189}
190
191fn Next(comptime T: type) type {
192 return ?*const fn (T) ParseError!T;
193}
194
195fn defaultNext(comptime T: type) Next(T) {
196 return switch (@typeInfo(T)) {
197 .bool => struct {
198 fn next(_: T) !bool {
199 return true;
200 }
201 }.next,
202 .int => struct {
203 fn next(i: T) !T {
204 return i + 1;
205 }
206 }.next,
207 else => null,
208 };
209}
210
211fn Parse(comptime T: type) type {
212 return ?*const fn ([]const u8) ParseError!T;
213}
214
215fn defaultParse(comptime T: type) Parse(T) {
216 return switch (@typeInfo(T)) {
217 .bool => struct {
218 fn parse(str: []const u8) ParseError!T {
219 const res = std.meta.stringToEnum(enum { false, true }, str) orelse
220 return error.ParsingFailed;
221 return res == .true;
222 }
223 }.parse,
224 .int => struct {
225 fn parse(str: []const u8) ParseError!T {
226 return std.fmt.parseInt(T, str, 0) catch
227 return error.ParsingFailed;
228 }
229 }.parse,
230 .@"enum" => struct {
231 fn parse(str: []const u8) ParseError!T {
232 return std.meta.stringToEnum(T, str) orelse
233 return error.ParsingFailed;
234 }
235 }.parse,
236 else => null,
237 };
238}
239
240fn ParseInto(comptime T: type) type {
241 return ?*const fn (*T, std.mem.Allocator, []const u8) ParseError!void;
242}
243
244fn defaultParseInto(comptime T: type) ParseInto(T) {
245 if (types.isArrayListUnmanaged(T)) {
246 const Child = @typeInfo(T.Slice).pointer.child;
247 const parseChild = defaultParse(Child) orelse return null;
248 return struct {
249 fn parseInto(list: *T, allocator: std.mem.Allocator, str: []const u8) ParseError!void {
250 const ptr = try list.addOne(allocator);
251 errdefer _ = list.pop();
252
253 ptr.* = try parseChild(str);
254 }
255 }.parseInto;
256 } else switch (@typeInfo(T)) {
257 .optional => |o| {
258 const parse = defaultParse(o.child) orelse return null;
259 return struct {
260 fn parseInto(ptr: *T, allocator: std.mem.Allocator, str: []const u8) ParseError!void {
261 _ = allocator;
262 ptr.* = try parse(str);
263 }
264 }.parseInto;
265 },
266 else => {
267 const parse = defaultParse(T) orelse return null;
268 return struct {
269 fn parseInto(ptr: *T, allocator: std.mem.Allocator, str: []const u8) ParseError!void {
270 _ = allocator;
271 ptr.* = try parse(str);
272 }
273 }.parseInto;
274 },
275 }
276}
277
278fn validateParams(comptime T: type, name: []const u8, opt: ParseOptions(T)) !void {
279 const stderr_writer = std.io.getStdErr().writer();
280 const stderr = opt.stderr orelse stderr_writer.any();
281
282 var res: anyerror!void = {};
283 var first_command: ?[]const u8 = null;
284 var first_positionals: ?[]const u8 = null;
285 const fields = @typeInfo(T).@"struct".fields;
286 inline for (fields) |field| switch (@field(opt.params, field.name).kind) {
287 .flag, .option, .positional, .positionals => {
288 const param = @field(opt.params, field.name);
289 if (param.init == null) {
290 try stderr.print("error: '{s}.{s}.init' is null\n", .{ name, field.name });
291 try stderr.print("note: could not infer 'init' for type '{s}'\n", .{@typeName(field.type)});
292 try stderr.print("note: or it was set to null by the caller (don't do that)\n\n", .{});
293 res = error.InvalidParameter;
294 }
295 if (param.kind == .flag and param.next == null) {
296 try stderr.print("error: '{s}.{s}.next' is null\n", .{ name, field.name });
297 try stderr.print("note: could not infer 'next' for type '{s}'\n", .{@typeName(field.type)});
298 try stderr.print("note: or it was set to null by the caller (don't do that)\n\n", .{});
299 res = error.InvalidParameter;
300 }
301 if (param.kind != .flag and param.parse == null) {
302 try stderr.print("error: '{s}.{s}.parse' is null\n", .{ name, field.name });
303 try stderr.print("note: could not infer 'parse' for type '{s}'\n", .{@typeName(field.type)});
304 try stderr.print("note: or it was set to null by the caller (don't do that)\n\n", .{});
305 res = error.InvalidParameter;
306 }
307 if (first_command) |command| {
308 if (param.kind == .positional or param.kind == .positionals) {
309 try stderr.print("error: cannot have positionals after a command\n", .{});
310 try stderr.print("note: '{s}.{s}' is the command\n", .{ name, command });
311 try stderr.print("note: '{s}.{s}' is the positional\n\n", .{ name, field.name });
312 res = error.InvalidParameter;
313 }
314 }
315 if (first_positionals) |positional| {
316 if (param.kind == .positional or param.kind == .positionals) {
317 try stderr.print("error: cannot have positionals after a positional taking many values\n", .{});
318 try stderr.print("note: '{s}.{s}' is the positional taking many values\n", .{ name, positional });
319 try stderr.print("note: '{s}.{s}' is the positional after it\n\n", .{ name, field.name });
320 res = error.InvalidParameter;
321 }
322 }
323
324 if (param.kind == .positionals and first_positionals == null)
325 first_positionals = field.name;
326 },
327 .command => case: {
328 if (first_positionals) |positional| {
329 try stderr.print("error: cannot have command after a positional taking many values\n", .{});
330 try stderr.print("note: '{s}.{s}' is the positional\n", .{ name, positional });
331 try stderr.print("note: '{s}.{s}' is the command\n\n", .{ name, field.name });
332 res = error.InvalidParameter;
333 }
334
335 const param = @field(opt.params, field.name);
336 const union_info = @typeInfo(field.type);
337 if (union_info != .@"union" or union_info.@"union".tag_type == null) {
338 try stderr.print(
339 "error: expected command '{s}.{s}' to be a tagged union, but found '{s}'\n\n",
340 .{ name, field.name, @typeName(field.type) },
341 );
342 res = error.InvalidParameter;
343 break :case;
344 }
345
346 if (first_command) |command| {
347 try stderr.print("error: only one field can be a command\n", .{});
348 try stderr.print("note: both '{s}.{s}' and '{s}.{s}' are commands\n\n", .{ name, command, name, field.name });
349 res = error.InvalidParameter;
350 break :case;
351 } else {
352 first_command = field.name;
353 }
354
355 const union_field = union_info.@"union".fields;
356 inline for (union_field) |cmd_field| {
357 const cmd_params = @field(param.command, cmd_field.name);
358 const cmd_opt = opt.withNewParams(cmd_field.type, cmd_params);
359
360 const new_name = try std.fmt.allocPrint(opt.gpa, "{s}.{s}", .{
361 name,
362 cmd_field.name,
363 });
364 defer opt.gpa.free(new_name);
365
366 validateParams(cmd_field.type, new_name, cmd_opt) catch |err| {
367 res = err;
368 };
369 }
370 },
371 };
372
373 return res;
374}
375
376fn testValidateParams(comptime T: type, opt: struct {
377 params: Params(T),
378 expected: anyerror!void,
379 expected_err: []const u8,
380}) !void {
381 const gpa = std.testing.allocator;
382
383 var err = std.ArrayList(u8).init(gpa);
384 const err_writer = err.writer();
385 defer err.deinit();
386
387 const actual = validateParams(T, "", .{
388 .gpa = gpa,
389 .params = opt.params,
390 .stderr = err_writer.any(),
391 });
392
393 try std.testing.expectEqualStrings(opt.expected_err, err.items);
394 try std.testing.expectEqualDeep(opt.expected, actual);
395}
396
397test validateParams {
398 try testValidateParams(struct { a: *const void }, .{
399 .params = .{ .a = .{ .kind = .flag } },
400 .expected = error.InvalidParameter,
401 .expected_err =
402 \\error: '.a.init' is null
403 \\note: could not infer 'init' for type '*const void'
404 \\note: or it was set to null by the caller (don't do that)
405 \\
406 \\error: '.a.next' is null
407 \\note: could not infer 'next' for type '*const void'
408 \\note: or it was set to null by the caller (don't do that)
409 \\
410 \\
411 ,
412 });
413 try testValidateParams(struct { a: *const void }, .{
414 .params = .{ .a = .{ .kind = .option } },
415 .expected = error.InvalidParameter,
416 .expected_err =
417 \\error: '.a.init' is null
418 \\note: could not infer 'init' for type '*const void'
419 \\note: or it was set to null by the caller (don't do that)
420 \\
421 \\error: '.a.parse' is null
422 \\note: could not infer 'parse' for type '*const void'
423 \\note: or it was set to null by the caller (don't do that)
424 \\
425 \\
426 ,
427 });
428 try testValidateParams(struct { a: *const void }, .{
429 .params = .{ .a = .{ .kind = .positional } },
430 .expected = error.InvalidParameter,
431 .expected_err =
432 \\error: '.a.init' is null
433 \\note: could not infer 'init' for type '*const void'
434 \\note: or it was set to null by the caller (don't do that)
435 \\
436 \\error: '.a.parse' is null
437 \\note: could not infer 'parse' for type '*const void'
438 \\note: or it was set to null by the caller (don't do that)
439 \\
440 \\
441 ,
442 });
443 try testValidateParams(struct { a: *const void }, .{
444 .params = .{ .a = .{ .kind = .positionals } },
445 .expected = error.InvalidParameter,
446 .expected_err =
447 \\error: '.a.init' is null
448 \\note: could not infer 'init' for type '*const void'
449 \\note: or it was set to null by the caller (don't do that)
450 \\
451 \\error: '.a.parse' is null
452 \\note: could not infer 'parse' for type '*const void'
453 \\note: or it was set to null by the caller (don't do that)
454 \\
455 \\
456 ,
457 });
458 try testValidateParams(struct { a: *const void }, .{
459 .params = .{ .a = .{ .kind = .command } },
460 .expected = error.InvalidParameter,
461 .expected_err =
462 \\error: expected command '.a' to be a tagged union, but found '*const void'
463 \\
464 \\
465 ,
466 });
467 try testValidateParams(struct { a: union(enum) {}, b: union(enum) {} }, .{
468 .params = .{
469 .a = .{ .kind = .command },
470 .b = .{ .kind = .command },
471 },
472 .expected = error.InvalidParameter,
473 .expected_err =
474 \\error: only one field can be a command
475 \\note: both '.a' and '.b' are commands
476 \\
477 \\
478 ,
479 });
480 try testValidateParams(struct { a: union(enum) {}, b: u8 }, .{
481 .params = .{
482 .a = .{ .kind = .command },
483 .b = .{ .kind = .positional },
484 },
485 .expected = error.InvalidParameter,
486 .expected_err =
487 \\error: cannot have positionals after a command
488 \\note: '.a' is the command
489 \\note: '.b' is the positional
490 \\
491 \\
492 ,
493 });
494 try testValidateParams(struct { a: union(enum) {}, b: u8 }, .{
495 .params = .{
496 .a = .{ .kind = .command },
497 .b = .{ .kind = .positionals },
498 },
499 .expected = error.InvalidParameter,
500 .expected_err =
501 \\error: cannot have positionals after a command
502 \\note: '.a' is the command
503 \\note: '.b' is the positional
504 \\
505 \\
506 ,
507 });
508 try testValidateParams(struct { a: u8, b: union(enum) {} }, .{
509 .params = .{
510 .a = .{ .kind = .positionals },
511 .b = .{ .kind = .command },
512 },
513 .expected = error.InvalidParameter,
514 .expected_err =
515 \\error: cannot have command after a positional taking many values
516 \\note: '.a' is the positional
517 \\note: '.b' is the command
518 \\
519 \\
520 ,
521 });
522 try testValidateParams(struct { a: u8, b: u8 }, .{
523 .params = .{
524 .a = .{ .kind = .positionals },
525 .b = .{ .kind = .positional },
526 },
527 .expected = error.InvalidParameter,
528 .expected_err =
529 \\error: cannot have positionals after a positional taking many values
530 \\note: '.a' is the positional taking many values
531 \\note: '.b' is the positional after it
532 \\
533 \\
534 ,
535 });
536 try testValidateParams(struct { a: u8, b: u8 }, .{
537 .params = .{
538 .a = .{ .kind = .positionals },
539 .b = .{ .kind = .positionals },
540 },
541 .expected = error.InvalidParameter,
542 .expected_err =
543 \\error: cannot have positionals after a positional taking many values
544 \\note: '.a' is the positional taking many values
545 \\note: '.b' is the positional after it
546 \\
547 \\
548 ,
549 });
550
551 try testValidateParams(struct { a: union(enum) {
552 a: struct { a: *const void },
553 b: struct { a: *const void },
554 c: struct { a: *const void },
555 d: struct { a: *const void },
556 e: struct {
557 a: union(enum) {},
558 b: union(enum) {},
559 },
560 } }, .{
561 .params = .{ .a = .{ .command = .{
562 .a = .{ .a = .{ .kind = .flag } },
563 .b = .{ .a = .{ .kind = .option } },
564 .c = .{ .a = .{ .kind = .positional } },
565 .d = .{ .a = .{ .kind = .positionals } },
566 .e = .{ .a = .{ .kind = .command }, .b = .{ .kind = .command } },
567 } } },
568 .expected = error.InvalidParameter,
569 .expected_err =
570 \\error: '.a.a.init' is null
571 \\note: could not infer 'init' for type '*const void'
572 \\note: or it was set to null by the caller (don't do that)
573 \\
574 \\error: '.a.a.next' is null
575 \\note: could not infer 'next' for type '*const void'
576 \\note: or it was set to null by the caller (don't do that)
577 \\
578 \\error: '.b.a.init' is null
579 \\note: could not infer 'init' for type '*const void'
580 \\note: or it was set to null by the caller (don't do that)
581 \\
582 \\error: '.b.a.parse' is null
583 \\note: could not infer 'parse' for type '*const void'
584 \\note: or it was set to null by the caller (don't do that)
585 \\
586 \\error: '.c.a.init' is null
587 \\note: could not infer 'init' for type '*const void'
588 \\note: or it was set to null by the caller (don't do that)
589 \\
590 \\error: '.c.a.parse' is null
591 \\note: could not infer 'parse' for type '*const void'
592 \\note: or it was set to null by the caller (don't do that)
593 \\
594 \\error: '.d.a.init' is null
595 \\note: could not infer 'init' for type '*const void'
596 \\note: or it was set to null by the caller (don't do that)
597 \\
598 \\error: '.d.a.parse' is null
599 \\note: could not infer 'parse' for type '*const void'
600 \\note: or it was set to null by the caller (don't do that)
601 \\
602 \\error: only one field can be a command
603 \\note: both '.e.a' and '.e.b' are commands
604 \\
605 \\
606 ,
607 });
608}
609
610pub const HelpParam = struct {
611 short: ?u8 = 'h',
612 long: ?[]const u8 = "help",
613 command: ?[]const u8 = "help",
614 description: []const u8 = "Print help",
615};
616
617pub const VersionParam = struct {
618 string: []const u8 = "0.0.0",
619 short: ?u8 = 'v',
620 long: ?[]const u8 = "version",
621 command: ?[]const u8 = "version",
622 description: []const u8 = "Print version",
623};
624
625pub const ExtraParams = struct {
626 help: HelpParam = .{},
627 version: VersionParam = .{},
628};
629
630pub fn ParseOptions(comptime T: type) type {
631 return struct {
632 gpa: std.mem.Allocator,
633 params: Params(T) = .{},
634 extra_params: ExtraParams = .{},
635
636 assignment_separators: []const u8 = "=",
637
638 /// The Writer used to write expected output like the help message when `-h` is passed. If
639 /// `null`, `std.io.getStdOut` will be used
640 stdout: ?std.io.AnyWriter = null,
641
642 /// The Writer used to write errors. `std.io.getStdErr` will be used. If `null`,
643 /// `std.io.getStdOut` will be used
644 stderr: ?std.io.AnyWriter = null,
645
646 pub fn withNewParams(opt: @This(), comptime T2: type, params: Params(T2)) ParseOptions(T2) {
647 var res: ParseOptions(T2) = undefined;
648 res.params = params;
649 inline for (@typeInfo(@This()).@"struct".fields) |field| {
650 if (comptime std.mem.eql(u8, field.name, "params"))
651 continue;
652
653 @field(res, field.name) = @field(opt, field.name);
654 }
655
656 return res;
657 }
658 };
659}
660
661pub const ParseError = error{
662 ParsingInterrupted,
663 ParsingFailed,
664} || std.mem.Allocator.Error;
665
666pub fn parseIter(it: anytype, comptime T: type, opt: ParseOptions(T)) ParseError!T {
667 switch (@import("builtin").mode) {
668 .Debug, .ReleaseSafe => {
669 validateParams(T, "", opt) catch @panic("Invalid parameters. See errors above.");
670 },
671 .ReleaseFast, .ReleaseSmall => {},
672 }
673
674 var parser = try Parser(@TypeOf(it), T).init(it, opt);
675 return parser.parse();
676}
677
678fn Parser(comptime Iter: type, comptime T: type) type {
679 return struct {
680 it: Iter,
681 opt: Options,
682 result: T,
683 has_been_set: HasBeenSet,
684 current_positional: usize,
685
686 stdout_writer: std.fs.File.Writer,
687 stderr_writer: std.fs.File.Writer,
688
689 const Options = ParseOptions(T);
690 const Field = std.meta.FieldEnum(T);
691 const HasBeenSet = std.EnumSet(Field);
692 const fields = @typeInfo(T).@"struct".fields;
693
694 fn init(it: Iter, opt: Options) ParseError!@This() {
695 var res = @This(){
696 .it = it,
697 .opt = opt,
698 .result = undefined,
699 .has_been_set = .{},
700 .current_positional = 0,
701
702 .stdout_writer = std.io.getStdOut().writer(),
703 .stderr_writer = std.io.getStdErr().writer(),
704 };
705 inline for (fields) |field| {
706 const param = @field(opt.params, field.name);
707 if (!param.required) {
708 const initValue = param.init orelse unreachable; // Shouldn't happen (validateParams)
709 @field(res.result, field.name) = try initValue(opt.gpa);
710 res.has_been_set.insert(@field(Field, field.name));
711 }
712 }
713
714 return res;
715 }
716
717 fn parse(parser: *@This()) ParseError!T {
718 errdefer {
719 // If we fail, deinit fields that can be deinited
720 inline for (fields) |field| continue_field_loop: {
721 const param = @field(parser.opt.params, field.name);
722 const deinit = param.deinit orelse break :continue_field_loop;
723 if (parser.has_been_set.contains(@field(Field, field.name)))
724 deinit(&@field(parser.result, field.name), parser.opt.gpa);
725 }
726 }
727
728 while (parser.it.next()) |arg| {
729 if (std.mem.eql(u8, arg, "--"))
730 break;
731
732 if (std.mem.startsWith(u8, arg, "--")) {
733 try parser.parseLong(arg[2..]);
734 } else if (std.mem.startsWith(u8, arg, "-")) {
735 try parser.parseShorts(arg[1..]);
736 } else if (try parser.parseCommand(arg)) {
737 return parser.result;
738 } else {
739 try parser.parsePositional(arg);
740 }
741 }
742
743 while (parser.it.next()) |arg|
744 try parser.parsePositional(arg);
745
746 inline for (fields) |field| {
747 const param = @field(parser.opt.params, field.name);
748 _ = param;
749 if (!parser.has_been_set.contains(@field(Field, field.name))) {
750 // TODO: Proper error. Required argument not specified
751 return error.ParsingFailed;
752 }
753 }
754
755 return parser.result;
756 }
757
758 fn parseLong(parser: *@This(), name: []const u8) ParseError!void {
759 if (parser.opt.extra_params.help.long) |h|
760 if (std.mem.eql(u8, name, h))
761 return parser.printHelp();
762 if (parser.opt.extra_params.version.long) |v|
763 if (std.mem.eql(u8, name, v))
764 return parser.printVersion();
765
766 inline for (fields) |field| switch (@field(parser.opt.params, field.name).kind) {
767 .flag => switch_case: {
768 const param = @field(parser.opt.params, field.name);
769 const long_name = param.long orelse break :switch_case;
770 if (!std.mem.eql(u8, name, long_name))
771 break :switch_case;
772
773 return parser.parseNext(@field(Field, field.name));
774 },
775 .option => switch_case: {
776 const param = @field(parser.opt.params, field.name);
777 const long_name = param.long orelse break :switch_case;
778 if (!std.mem.startsWith(u8, name, long_name))
779 break :switch_case;
780
781 const value = if (name.len == long_name.len) blk: {
782 break :blk parser.it.next() orelse {
783 // TODO: Report proper error
784 return error.ParsingFailed;
785 };
786 } else if (std.mem.indexOfScalar(u8, parser.opt.assignment_separators, name[long_name.len]) != null) blk: {
787 break :blk name[long_name.len + 1 ..];
788 } else {
789 break :switch_case;
790 };
791
792 return parser.parseValue(@field(Field, field.name), value);
793 },
794 .positional, .positionals, .command => {},
795 };
796
797 // TODO: Report proper error
798 return error.ParsingFailed;
799 }
800
801 fn parseShorts(parser: *@This(), shorts: []const u8) ParseError!void {
802 var i: usize = 0;
803 while (i < shorts.len)
804 i = try parser.parseShort(shorts, i);
805 }
806
807 fn parseShort(parser: *@This(), shorts: []const u8, pos: usize) ParseError!usize {
808 if (parser.opt.extra_params.help.short) |h|
809 if (shorts[pos] == h)
810 return parser.printHelp();
811 if (parser.opt.extra_params.version.short) |v|
812 if (shorts[pos] == v)
813 return parser.printVersion();
814
815 inline for (fields) |field| switch (@field(parser.opt.params, field.name).kind) {
816 .flag => switch_case: {
817 const param = @field(parser.opt.params, field.name);
818 const short_name = param.short orelse break :switch_case;
819 if (shorts[pos] != short_name)
820 break :switch_case;
821
822 try parser.parseNext(@field(Field, field.name));
823 return pos + 1;
824 },
825 .option => switch_case: {
826 const param = @field(parser.opt.params, field.name);
827 const short_name = param.short orelse break :switch_case;
828 if (shorts[pos] != short_name)
829 break :switch_case;
830
831 const value = if (pos + 1 == shorts.len) blk: {
832 break :blk parser.it.next() orelse {
833 // TODO: Report proper error
834 return error.ParsingFailed;
835 };
836 } else blk: {
837 const assignment_separators = parser.opt.assignment_separators;
838 const has_assignment_separator =
839 std.mem.indexOfScalar(u8, assignment_separators, shorts[pos + 1]) != null;
840 break :blk shorts[pos + 1 + @intFromBool(has_assignment_separator) ..];
841 };
842
843 try parser.parseValue(@field(Field, field.name), value);
844 return shorts.len;
845 },
846 .positional, .positionals, .command => {},
847 };
848
849 // TODO: Report proper error
850 return error.ParsingFailed;
851 }
852
853 fn parseCommand(parser: *@This(), arg: []const u8) ParseError!bool {
854 if (parser.opt.extra_params.help.command) |h|
855 if (std.mem.eql(u8, arg, h))
856 return parser.printHelp();
857 if (parser.opt.extra_params.version.command) |v|
858 if (std.mem.eql(u8, arg, v))
859 return parser.printVersion();
860
861 inline for (fields) |field| continue_field_loop: {
862 const union_field = switch (@typeInfo(field.type)) {
863 .@"union" => |u| u.fields,
864 else => continue,
865 };
866 const param = @field(parser.opt.params, field.name);
867 if (param.kind != .command)
868 break :continue_field_loop;
869
870 inline for (union_field) |cmd_field| continue_cmd_field_loop: {
871 const cmd_params = @field(param.command, cmd_field.name);
872 if (!std.mem.eql(u8, arg, cmd_params.name orelse cmd_field.name))
873 break :continue_cmd_field_loop;
874
875 var cmd_parser = try Parser(Iter, cmd_field.type).init(
876 parser.it,
877 parser.opt.withNewParams(cmd_field.type, cmd_params),
878 );
879
880 const cmd_result = try cmd_parser.parse();
881 const cmd_union = @unionInit(field.type, cmd_field.name, cmd_result);
882 @field(parser.result, field.name) = cmd_union;
883 parser.has_been_set.insert(@field(Field, field.name));
884 return true;
885 }
886 }
887
888 return false;
889 }
890
891 fn parsePositional(parser: *@This(), arg: []const u8) ParseError!void {
892 var i: usize = 0;
893 inline for (fields) |field| continue_field_loop: {
894 const param = @field(parser.opt.params, field.name);
895 const next_positional = switch (param.kind) {
896 .positional => parser.current_positional + 1,
897 .positionals => parser.current_positional,
898 else => break :continue_field_loop,
899 };
900 if (parser.current_positional != i) {
901 i += 1;
902 break :continue_field_loop;
903 }
904
905 try parser.parseValue(@field(Field, field.name), arg);
906 parser.current_positional = next_positional;
907 return;
908 }
909
910 // TODO: Proper error. Too many positionals
911 return error.ParsingFailed;
912 }
913
914 fn parseNext(parser: *@This(), comptime field: Field) ParseError!void {
915 const field_name = @tagName(field);
916 const param = @field(parser.opt.params, field_name);
917
918 if (!parser.has_been_set.contains(field)) {
919 const initValue = param.init orelse unreachable; // Shouldn't happen (validateParams)
920 @field(parser.result, field_name) = try initValue(parser.opt.gpa);
921 }
922
923 const next = param.next orelse unreachable; // Shouldn't happen (validateParams)
924 const field_ptr = &@field(parser.result, field_name);
925 field_ptr.* = try next(field_ptr.*);
926 parser.has_been_set.insert(field);
927 }
928
929 fn parseValue(parser: *@This(), comptime field: Field, value: []const u8) ParseError!void {
930 const field_name = @tagName(field);
931 const param = @field(parser.opt.params, field_name);
932
933 if (!parser.has_been_set.contains(field)) {
934 const initValue = param.init orelse unreachable; // Shouldn't happen (validateParams)
935 @field(parser.result, field_name) = try initValue(parser.opt.gpa);
936 }
937
938 const parseInto = param.parse orelse unreachable; // Shouldn't happen (validateParams)
939 try parseInto(&@field(parser.result, field_name), parser.opt.gpa, value);
940 parser.has_been_set.insert(field);
941 }
942
943 fn printHelp(parser: *@This()) ParseError {
944 help(parser.stdout(), T, .{
945 .params = parser.opt.params,
946 .extra_params = parser.opt.extra_params,
947 }) catch {};
948 return error.ParsingInterrupted;
949 }
950
951 fn printVersion(parser: *@This()) ParseError {
952 parser.stdout().writeAll(parser.opt.extra_params.version.string) catch {};
953 return error.ParsingInterrupted;
954 }
955
956 fn stdout(parser: *@This()) std.io.AnyWriter {
957 return parser.opt.stdout orelse parser.stdout_writer.any();
958 }
959
960 fn stderr(parser: *@This()) std.io.AnyWriter {
961 return parser.opt.stderr orelse parser.stderr_writer.any();
962 }
963 };
964}
965
966fn testParseIter(comptime T: type, opt: struct {
967 args: []const u8,
968 params: Params(T) = .{},
969
970 expected: anyerror!T,
971 expected_out: []const u8 = "",
972 expected_err: []const u8 = "",
973}) !void {
974 const gpa = std.testing.allocator;
975 var it = try std.process.ArgIteratorGeneral(.{}).init(gpa, opt.args);
976 defer it.deinit();
977
978 var out = std.ArrayList(u8).init(gpa);
979 const out_writer = out.writer();
980 defer out.deinit();
981
982 var err = std.ArrayList(u8).init(gpa);
983 const err_writer = err.writer();
984 defer err.deinit();
985
986 const actual = parseIter(&it, T, .{
987 .gpa = gpa,
988 .params = opt.params,
989 .stdout = out_writer.any(),
990 .stderr = err_writer.any(),
991 });
992 defer blk: {
993 var v = actual catch break :blk;
994 if (@hasDecl(T, "deinit"))
995 v.deinit(gpa);
996 }
997
998 try std.testing.expectEqualDeep(opt.expected, actual);
999 try std.testing.expectEqualStrings(opt.expected_out, out.items);
1000 try std.testing.expectEqualStrings(opt.expected_err, err.items);
1001}
1002
1003test "parseIterParams" {
1004 const S = struct {
1005 a: bool = false,
1006 b: u8 = 0,
1007 c: enum { a, b, c, d } = .a,
1008 d: std.ArrayListUnmanaged(usize) = .{},
1009 e: ?u8 = null,
1010
1011 fn deinit(s: *@This(), allocator: std.mem.Allocator) void {
1012 s.d.deinit(allocator);
1013 }
1014 };
1015
1016 try testParseIter(S, .{
1017 .args = "--a",
1018 .expected = .{ .a = true },
1019 });
1020 try testParseIter(S, .{
1021 .args = "-a",
1022 .expected = .{ .a = true },
1023 });
1024
1025 try testParseIter(S, .{
1026 .args = "--b",
1027 .expected = .{ .b = 1 },
1028 .params = .{ .b = .{ .kind = .flag } },
1029 });
1030 try testParseIter(S, .{
1031 .args = "-b",
1032 .expected = .{ .b = 1 },
1033 .params = .{ .b = .{ .kind = .flag } },
1034 });
1035 try testParseIter(S, .{
1036 .args = "-bb",
1037 .expected = .{ .b = 2 },
1038 .params = .{ .b = .{ .kind = .flag } },
1039 });
1040
1041 try testParseIter(S, .{
1042 .args = "-aabb",
1043 .expected = .{ .a = true, .b = 2 },
1044 .params = .{ .b = .{ .kind = .flag } },
1045 });
1046
1047 try testParseIter(S, .{
1048 .args = "--b 1",
1049 .expected = .{ .b = 1 },
1050 });
1051 try testParseIter(S, .{
1052 .args = "--b=2",
1053 .expected = .{ .b = 2 },
1054 });
1055
1056 try testParseIter(S, .{
1057 .args = "-b 1",
1058 .expected = .{ .b = 1 },
1059 });
1060 try testParseIter(S, .{
1061 .args = "-b=2",
1062 .expected = .{ .b = 2 },
1063 });
1064 try testParseIter(S, .{
1065 .args = "-b3",
1066 .expected = .{ .b = 3 },
1067 });
1068
1069 try testParseIter(S, .{
1070 .args = "-aab4",
1071 .expected = .{ .a = true, .b = 4 },
1072 });
1073
1074 try testParseIter(S, .{
1075 .args = "--c b",
1076 .expected = .{ .c = .b },
1077 });
1078 try testParseIter(S, .{
1079 .args = "--c=c",
1080 .expected = .{ .c = .c },
1081 });
1082
1083 try testParseIter(S, .{
1084 .args = "-c b",
1085 .expected = .{ .c = .b },
1086 });
1087 try testParseIter(S, .{
1088 .args = "-c=c",
1089 .expected = .{ .c = .c },
1090 });
1091 try testParseIter(S, .{
1092 .args = "-cd",
1093 .expected = .{ .c = .d },
1094 });
1095
1096 try testParseIter(S, .{
1097 .args = "-bbcd",
1098 .expected = .{ .b = 2, .c = .d },
1099 .params = .{ .b = .{ .kind = .flag } },
1100 });
1101
1102 var expected_items = [_]usize{ 0, 1, 2 };
1103 try testParseIter(S, .{
1104 .args = "-d 0 -d 1 -d 2",
1105 .expected = .{ .d = .{ .items = &expected_items, .capacity = 8 } },
1106 });
1107
1108 try testParseIter(S, .{
1109 .args = "-e 2",
1110 .expected = .{ .e = 2 },
1111 });
1112
1113 // Tests that `d` is not leaked when an error occurs
1114 try testParseIter(S, .{
1115 .args = "-d 0 -d 1 -d 2 -qqqq",
1116 .expected = error.ParsingFailed,
1117 });
1118}
1119
1120test "parseIterRequired" {
1121 const S = struct {
1122 a: bool = false,
1123 b: bool,
1124 };
1125
1126 try testParseIter(S, .{
1127 .args = "",
1128 .expected = error.ParsingFailed,
1129 });
1130 try testParseIter(S, .{
1131 .args = "-b",
1132 .expected = .{ .b = true },
1133 });
1134 try testParseIter(S, .{
1135 .args = "",
1136 .expected = error.ParsingFailed,
1137 .params = .{ .a = .{ .required = true } },
1138 });
1139 try testParseIter(S, .{
1140 .args = "-a",
1141 .expected = error.ParsingFailed,
1142 .params = .{ .a = .{ .required = true } },
1143 });
1144 try testParseIter(S, .{
1145 .args = "-b",
1146 .expected = error.ParsingFailed,
1147 .params = .{ .a = .{ .required = true } },
1148 });
1149 try testParseIter(S, .{
1150 .args = "-a -b",
1151 .expected = .{ .a = true, .b = true },
1152 .params = .{ .a = .{ .required = true } },
1153 });
1154}
1155
1156test "parseIterPositional" {
1157 const S = struct {
1158 a: bool = false,
1159 b: u8 = 0,
1160 c: enum { a, b, c, d } = .a,
1161 };
1162
1163 try testParseIter(S, .{
1164 .args = "true",
1165 .expected = .{ .a = true },
1166 .params = .{ .a = .{ .kind = .positional } },
1167 });
1168 try testParseIter(S, .{
1169 .args = "false",
1170 .expected = .{ .a = false },
1171 .params = .{ .a = .{ .kind = .positional } },
1172 });
1173 try testParseIter(S, .{
1174 .args = "0",
1175 .expected = .{ .b = 0 },
1176 .params = .{ .b = .{ .kind = .positional } },
1177 });
1178 try testParseIter(S, .{
1179 .args = "2",
1180 .expected = .{ .b = 2 },
1181 .params = .{ .b = .{ .kind = .positional } },
1182 });
1183 try testParseIter(S, .{
1184 .args = "a",
1185 .expected = .{ .c = .a },
1186 .params = .{ .c = .{ .kind = .positional } },
1187 });
1188 try testParseIter(S, .{
1189 .args = "c",
1190 .expected = .{ .c = .c },
1191 .params = .{ .c = .{ .kind = .positional } },
1192 });
1193
1194 try testParseIter(S, .{
1195 .args = "true 2 d",
1196 .expected = .{ .a = true, .b = 2, .c = .d },
1197 .params = .{
1198 .a = .{ .kind = .positional },
1199 .b = .{ .kind = .positional },
1200 .c = .{ .kind = .positional },
1201 },
1202 });
1203 try testParseIter(S, .{
1204 .args = "false 4 c",
1205 .expected = .{ .a = false, .b = 4, .c = .c },
1206 .params = .{
1207 .a = .{ .kind = .positional },
1208 .b = .{ .kind = .positional },
1209 .c = .{ .kind = .positional },
1210 },
1211 });
1212
1213 try testParseIter(S, .{
1214 .args = "false true",
1215 .expected = error.ParsingFailed,
1216 .params = .{ .a = .{ .kind = .positional } },
1217 });
1218 try testParseIter(S, .{
1219 .args = "false true",
1220 .expected = .{ .a = true },
1221 .params = .{ .a = .{ .kind = .positionals } },
1222 });
1223 try testParseIter(S, .{
1224 .args = "2 3",
1225 .expected = error.ParsingFailed,
1226 .params = .{ .b = .{ .kind = .positional } },
1227 });
1228 try testParseIter(S, .{
1229 .args = "2 3",
1230 .expected = .{ .b = 3 },
1231 .params = .{ .b = .{ .kind = .positionals } },
1232 });
1233 try testParseIter(S, .{
1234 .args = "c d",
1235 .expected = error.ParsingFailed,
1236 .params = .{ .c = .{ .kind = .positional } },
1237 });
1238 try testParseIter(S, .{
1239 .args = "c d",
1240 .expected = .{ .c = .d },
1241 .params = .{ .c = .{ .kind = .positionals } },
1242 });
1243
1244 try testParseIter(S, .{
1245 .args = "true 2 d d",
1246 .expected = error.ParsingFailed,
1247 .params = .{
1248 .a = .{ .kind = .positional },
1249 .b = .{ .kind = .positional },
1250 .c = .{ .kind = .positional },
1251 },
1252 });
1253 try testParseIter(S, .{
1254 .args = "true 2 c d",
1255 .expected = .{ .a = true, .b = 2, .c = .d },
1256 .params = .{
1257 .a = .{ .kind = .positional },
1258 .b = .{ .kind = .positional },
1259 .c = .{ .kind = .positionals },
1260 },
1261 });
1262}
1263
1264test "parseIterCommand" {
1265 const S = struct {
1266 a: bool = false,
1267 b: bool = false,
1268 command: union(enum) {
1269 sub1: struct { a: bool = false },
1270 sub2: struct { b: bool = false },
1271 },
1272 };
1273
1274 try testParseIter(S, .{
1275 .args = "sub1",
1276 .expected = .{ .command = .{ .sub1 = .{} } },
1277 });
1278 try testParseIter(S, .{
1279 .args = "sub1 --a",
1280 .expected = .{ .command = .{ .sub1 = .{ .a = true } } },
1281 });
1282 try testParseIter(S, .{
1283 .args = "--a --b sub1 --a",
1284 .expected = .{
1285 .a = true,
1286 .b = true,
1287 .command = .{ .sub1 = .{ .a = true } },
1288 },
1289 });
1290 try testParseIter(S, .{
1291 .args = "sub2",
1292 .expected = .{ .command = .{ .sub2 = .{} } },
1293 });
1294 try testParseIter(S, .{
1295 .args = "sub2 --b",
1296 .expected = .{ .command = .{ .sub2 = .{ .b = true } } },
1297 });
1298 try testParseIter(S, .{
1299 .args = "--a --b sub2 --b",
1300 .expected = .{
1301 .a = true,
1302 .b = true,
1303 .command = .{ .sub2 = .{ .b = true } },
1304 },
1305 });
1306
1307 try testParseIter(S, .{
1308 .args = "bob",
1309 .params = .{ .command = .{ .command = .{
1310 .sub1 = .{ .name = "bob" },
1311 .sub2 = .{ .name = "kurt" },
1312 } } },
1313 .expected = .{ .command = .{ .sub1 = .{} } },
1314 });
1315 try testParseIter(S, .{
1316 .args = "kurt",
1317 .params = .{ .command = .{ .command = .{
1318 .sub1 = .{ .name = "bob" },
1319 .sub2 = .{ .name = "kurt" },
1320 } } },
1321 .expected = .{ .command = .{ .sub2 = .{} } },
1322 });
1323}
1324
1325test "parseIterHelp" {
1326 const S = struct {
1327 alice: bool = false,
1328 bob: bool = false,
1329 ben: bool = false,
1330 kurt: usize = 0,
1331 command: union(enum) {
1332 cmd1: struct {
1333 kurt: bool = false,
1334 mark: bool = false,
1335 },
1336 cmd2: struct {
1337 jim: bool = false,
1338 frans: bool = false,
1339 },
1340 },
1341 };
1342
1343 const help_args = [_][]const u8{ "-h", "--help", "help" };
1344 for (help_args) |args| {
1345 try testParseIter(S, .{
1346 .args = args,
1347 .params = .{ .name = "testing-program" },
1348 .expected = error.ParsingInterrupted,
1349 .expected_out =
1350 \\Usage: testing-program [OPTIONS] [COMMAND]
1351 \\
1352 \\Commands:
1353 \\ cmd1
1354 \\ cmd2
1355 \\ help Print help
1356 \\ version Print version
1357 \\
1358 \\Options:
1359 \\ -a, --alice
1360 \\ -b, --bob
1361 \\ --ben
1362 \\ -k, --kurt <KURT>
1363 \\ -h, --help Print help
1364 \\ -v, --version Print version
1365 \\
1366 ,
1367 });
1368 try testParseIter(S, .{
1369 .args = args,
1370 .params = .{
1371 .name = "testing-program",
1372 .description = "This is a test",
1373 .alice = .{ .description = "Who is this?" },
1374 .bob = .{ .description = "Bob the builder" },
1375 .ben = .{ .description = "One of the people of all time" },
1376 .kurt = .{ .description = "No fun allowed" },
1377 .command = .{ .command = .{
1378 .cmd1 = .{ .name = "command1", .description = "Command 1" },
1379 .cmd2 = .{ .name = "command2", .description = "Command 2" },
1380 } },
1381 },
1382 .expected = error.ParsingInterrupted,
1383 .expected_out =
1384 \\This is a test
1385 \\
1386 \\Usage: testing-program [OPTIONS] [COMMAND]
1387 \\
1388 \\Commands:
1389 \\ command1 Command 1
1390 \\ command2 Command 2
1391 \\ help Print help
1392 \\ version Print version
1393 \\
1394 \\Options:
1395 \\ -a, --alice Who is this?
1396 \\ -b, --bob Bob the builder
1397 \\ --ben One of the people of all time
1398 \\ -k, --kurt <KURT> No fun allowed
1399 \\ -h, --help Print help
1400 \\ -v, --version Print version
1401 \\
1402 ,
1403 });
1404 }
1405}
1406
1407pub fn HelpOptions(comptime T: type) type {
1408 return struct {
1409 params: Params(T) = .{},
1410 extra_params: ExtraParams = .{},
1411 };
1412}
1413
1414const help_long_prefix_len = 4;
1415const help_value_prefix_len = 3;
1416const help_description_spacing = 2;
1417
1418pub fn help(writer: anytype, comptime T: type, opt: HelpOptions(T)) !void {
1419 const fields = @typeInfo(T).@"struct".fields;
1420
1421 var self_exe_path_buf: [std.fs.max_path_bytes]u8 = undefined;
1422 const program_name = opt.params.name orelse blk: {
1423 const self_exe_path = std.fs.selfExePath(&self_exe_path_buf) catch
1424 break :blk "program";
1425 break :blk std.fs.path.basename(self_exe_path);
1426 };
1427
1428 if (opt.params.description.len != 0) {
1429 try writer.writeAll(opt.params.description);
1430 try writer.writeAll("\n\n");
1431 }
1432
1433 try writer.writeAll("Usage: ");
1434 try writer.writeAll(program_name);
1435 try writer.writeAll(" [OPTIONS] [COMMAND]");
1436
1437 var padding: usize = 0;
1438 if (opt.extra_params.help.command) |h|
1439 padding = @max(padding, h.len);
1440 if (opt.extra_params.version.command) |v|
1441 padding = @max(padding, v.len);
1442
1443 inline for (fields) |field| switch (@field(opt.params, field.name).kind) {
1444 .flag, .option, .positional, .positionals => {},
1445 .command => {
1446 const param = @field(opt.params, field.name);
1447 inline for (@typeInfo(@TypeOf(param.command)).@"struct".fields) |cmd_field| {
1448 const cmd_param = @field(param.command, cmd_field.name);
1449 padding = @max(padding, (cmd_param.name orelse cmd_field.name).len);
1450 }
1451 },
1452 };
1453
1454 try writer.writeAll("\n\nCommands:\n");
1455 inline for (fields) |field| switch (@field(opt.params, field.name).kind) {
1456 .flag, .option, .positional, .positionals => {},
1457 .command => {
1458 const param = @field(opt.params, field.name);
1459 inline for (@typeInfo(@TypeOf(param.command)).@"struct".fields) |cmd_field| {
1460 const cmd_param = @field(param.command, cmd_field.name);
1461 try printCommand(writer, padding, .{
1462 .name = cmd_param.name orelse cmd_field.name,
1463 .description = cmd_param.description,
1464 });
1465 }
1466 },
1467 };
1468 if (opt.extra_params.help.command) |h|
1469 try printCommand(writer, padding, .{
1470 .name = h,
1471 .description = opt.extra_params.help.description,
1472 });
1473 if (opt.extra_params.version.command) |v|
1474 try printCommand(writer, padding, .{
1475 .name = v,
1476 .description = opt.extra_params.version.description,
1477 });
1478
1479 padding = 0;
1480 if (opt.extra_params.help.long) |h|
1481 padding = @max(padding, h.len + help_long_prefix_len);
1482 if (opt.extra_params.version.long) |v|
1483 padding = @max(padding, v.len + help_long_prefix_len);
1484 inline for (fields) |field| {
1485 const param = @field(opt.params, field.name);
1486
1487 var pad: usize = 0;
1488 if (param.long) |long|
1489 pad += long.len + help_long_prefix_len;
1490 if (param.kind == .option)
1491 pad += param.value.len + help_value_prefix_len;
1492 padding = @max(padding, pad);
1493 }
1494
1495 try writer.writeAll("\nOptions:\n");
1496 inline for (fields) |field| {
1497 const param = @field(opt.params, field.name);
1498 switch (param.kind) {
1499 .command, .positional, .positionals => {},
1500 .flag => try printParam(writer, padding, .{
1501 .short = param.short,
1502 .long = param.long,
1503 .description = param.description,
1504 }),
1505 .option => try printParam(writer, padding, .{
1506 .short = param.short,
1507 .long = param.long,
1508 .description = param.description,
1509 .value = param.value,
1510 }),
1511 }
1512 }
1513 try printParam(writer, padding, .{
1514 .short = opt.extra_params.help.short,
1515 .long = opt.extra_params.help.long,
1516 .description = opt.extra_params.help.description,
1517 });
1518 try printParam(writer, padding, .{
1519 .short = opt.extra_params.version.short,
1520 .long = opt.extra_params.version.long,
1521 .description = opt.extra_params.version.description,
1522 });
1523}
1524
1525fn printCommand(writer: anytype, padding: usize, command: struct {
1526 name: []const u8,
1527 description: []const u8,
1528}) !void {
1529 try writer.writeByteNTimes(' ', 4);
1530 try writer.writeAll(command.name);
1531 if (command.description.len != 0) {
1532 try writer.writeByteNTimes(' ', padding - command.name.len);
1533 try writer.writeAll(" ");
1534 try writer.writeAll(command.description);
1535 }
1536 try writer.writeAll("\n");
1537}
1538
1539fn printParam(writer: anytype, padding: usize, param: struct {
1540 short: ?u8,
1541 long: ?[]const u8,
1542 description: []const u8,
1543 value: ?[]const u8 = null,
1544}) !void {
1545 if (param.short == null and param.long == null)
1546 return;
1547
1548 try writer.writeByteNTimes(' ', 4);
1549 if (param.short) |short| {
1550 try writer.writeByte('-');
1551 try writer.writeByte(short);
1552 } else {
1553 try writer.writeAll(" ");
1554 }
1555 if (param.long) |long| {
1556 try writer.writeByte(if (param.short) |_| ',' else ' ');
1557 try writer.writeAll(" --");
1558 try writer.writeAll(long);
1559 }
1560 if (param.value) |value| {
1561 try writer.writeAll(" <");
1562 try writer.writeAll(value);
1563 try writer.writeAll(">");
1564 }
1565 if (param.description.len != 0) {
1566 var pad = padding;
1567 if (param.long) |long|
1568 pad -= (long.len + help_long_prefix_len);
1569 if (param.value) |value|
1570 pad -= (value.len + help_value_prefix_len);
1571 try writer.writeByteNTimes(' ', pad + help_description_spacing);
1572 try writer.writeAll(param.description);
1573 }
1574
1575 try writer.writeByte('\n');
1576}
1577
1578fn testHelp(comptime T: type, opt: struct {
1579 params: Params(T) = .{},
1580 expected: []const u8,
1581}) !void {
1582 var buf: [std.mem.page_size]u8 = undefined;
1583 var fbs = std.io.fixedBufferStream(&buf);
1584
1585 try help(fbs.writer(), T, .{
1586 .params = opt.params,
1587 });
1588 try std.testing.expectEqualStrings(opt.expected, fbs.getWritten());
1589}
1590
1591const types = @import("types.zig");
1592const std = @import("std");