summaryrefslogtreecommitdiff
path: root/clap.zig
diff options
context:
space:
mode:
Diffstat (limited to 'clap.zig')
-rw-r--r--clap.zig332
1 files changed, 144 insertions, 188 deletions
diff --git a/clap.zig b/clap.zig
index 9a18bac..8846d3b 100644
--- a/clap.zig
+++ b/clap.zig
@@ -8,9 +8,7 @@ const testing = std.testing;
8pub const args = @import("clap/args.zig"); 8pub const args = @import("clap/args.zig");
9 9
10test "clap" { 10test "clap" {
11 _ = args; 11 testing.refAllDecls(@This());
12 _ = ComptimeClap;
13 _ = StreamingClap;
14} 12}
15 13
16pub const ComptimeClap = @import("clap/comptime.zig").ComptimeClap; 14pub const ComptimeClap = @import("clap/comptime.zig").ComptimeClap;
@@ -64,229 +62,196 @@ pub fn Param(comptime Id: type) type {
64/// Takes a string and parses it to a Param(Help). 62/// Takes a string and parses it to a Param(Help).
65/// This is the reverse of 'help' but for at single parameter only. 63/// This is the reverse of 'help' but for at single parameter only.
66pub fn parseParam(line: []const u8) !Param(Help) { 64pub fn parseParam(line: []const u8) !Param(Help) {
67 var z: usize = 0; 65 var found_comma = false;
68 var res = Param(Help){
69 .id = Help{
70 // For testing, i want to be able to easily compare slices just by pointer,
71 // so I slice by a runtime value here, so that zig does not optimize this
72 // out. Maybe I should write the test better, geeh.
73 .msg = line[z..z],
74 .value = line[z..z],
75 },
76 };
77
78 var it = mem.tokenize(line, " \t"); 66 var it = mem.tokenize(line, " \t");
79 var param_str = it.next() orelse return error.NoParamFound; 67 var param_str = it.next() orelse return error.NoParamFound;
80 if (!mem.startsWith(u8, param_str, "--") and mem.startsWith(u8, param_str, "-")) { 68
81 const found_comma = param_str[param_str.len - 1] == ','; 69 const short_name = if (!mem.startsWith(u8, param_str, "--") and
70 mem.startsWith(u8, param_str, "-"))
71 blk: {
72 found_comma = param_str[param_str.len - 1] == ',';
82 if (found_comma) 73 if (found_comma)
83 param_str = param_str[0 .. param_str.len - 1]; 74 param_str = param_str[0 .. param_str.len - 1];
84 75
85 if (param_str.len != 2) 76 if (param_str.len != 2)
86 return error.InvalidShortParam; 77 return error.InvalidShortParam;
87 78
88 res.names.short = param_str[1]; 79 const short_name = param_str[1];
89 if (!found_comma) { 80 if (!found_comma) {
90 var help_msg = it.rest(); 81 var res = parseParamRest(it.rest());
91 if (it.next()) |next| blk: { 82 res.names.short = short_name;
92 if (mem.startsWith(u8, next, "<")) {
93 const start = mem.indexOfScalar(u8, help_msg, '<').? + 1;
94 const len = mem.indexOfScalar(u8, help_msg[start..], '>') orelse break :blk;
95 res.id.value = help_msg[start..][0..len];
96 if (mem.startsWith(u8, help_msg[start + len + 1 ..], "...")) {
97 res.takes_value = .Many;
98 help_msg = help_msg[start + len + 1 + 3 ..];
99 } else {
100 res.takes_value = .One;
101 help_msg = help_msg[start + len + 1 ..];
102 }
103 }
104 }
105
106 res.id.msg = mem.trim(u8, help_msg, " \t");
107 return res; 83 return res;
108 } 84 }
109 85
110 param_str = it.next() orelse return error.NoParamFound; 86 param_str = it.next() orelse return error.NoParamFound;
111 } 87 break :blk short_name;
112 88 } else null;
113 if (mem.startsWith(u8, param_str, "--")) {
114 res.names.long = param_str[2..];
115 89
90 const long_name = if (mem.startsWith(u8, param_str, "--")) blk: {
116 if (param_str[param_str.len - 1] == ',') 91 if (param_str[param_str.len - 1] == ',')
117 return error.TrailingComma; 92 return error.TrailingComma;
118 93
119 var help_msg = it.rest(); 94 break :blk param_str[2..];
120 if (it.next()) |next| blk: { 95 } else if (found_comma) {
121 if (mem.startsWith(u8, next, "<")) { 96 return error.TrailingComma;
122 const start = mem.indexOfScalar(u8, help_msg, '<').? + 1; 97 } else if (short_name == null) {
123 const len = mem.indexOfScalar(u8, help_msg[start..], '>') orelse break :blk; 98 return parseParamRest(mem.trimLeft(u8, line, " \t"));
124 res.id.value = help_msg[start..][0..len]; 99 } else null;
125 if (mem.startsWith(u8, help_msg[start + len + 1 ..], "...")) { 100
126 res.takes_value = .Many; 101 var res = parseParamRest(it.rest());
127 help_msg = help_msg[start + len + 1 + 3 ..]; 102 res.names.long = param_str[2..];
128 } else { 103 res.names.short = short_name;
129 res.takes_value = .One; 104 return res;
130 help_msg = help_msg[start + len + 1 ..]; 105}
131 }
132 }
133 }
134 106
135 res.id.msg = mem.trim(u8, help_msg, " \t"); 107fn parseParamRest(line: []const u8) Param(Help) {
136 return res; 108 if (mem.startsWith(u8, line, "<")) blk: {
109 const len = mem.indexOfScalar(u8, line, '>') orelse break :blk;
110 const takes_many = mem.startsWith(u8, line[len + 1 ..], "...");
111 const help_start = len + 1 + @as(usize, 3) * @boolToInt(takes_many);
112 return Param(Help){
113 .takes_value = if (takes_many) .Many else .One,
114 .id = .{
115 .msg = mem.trim(u8, line[help_start..], " \t"),
116 .value = line[1..len],
117 },
118 };
137 } 119 }
138 120
139 return error.NoParamFound; 121 return Param(Help){ .id = .{ .msg = mem.trim(u8, line, " \t") } };
122}
123
124fn expectParam(expect: Param(Help), actual: Param(Help)) void {
125 testing.expectEqualStrings(expect.id.msg, actual.id.msg);
126 testing.expectEqualStrings(expect.id.value, actual.id.value);
127 testing.expectEqual(expect.names.short, actual.names.short);
128 testing.expectEqual(expect.takes_value, actual.takes_value);
129 if (expect.names.long) |long| {
130 testing.expectEqualStrings(long, actual.names.long.?);
131 } else {
132 testing.expectEqual(@as(?[]const u8, null), actual.names.long);
133 }
140} 134}
141 135
142test "parseParam" { 136test "parseParam" {
143 var z: usize = 0; 137 expectParam(Param(Help){
144 var text: []const u8 = "-s, --long <value> Help text";
145 testing.expectEqual(Param(Help){
146 .id = Help{ 138 .id = Help{
147 .msg = find(text, "Help text"), 139 .msg = "Help text",
148 .value = find(text, "value"), 140 .value = "value",
149 }, 141 },
150 .names = Names{ 142 .names = Names{
151 .short = 's', 143 .short = 's',
152 .long = find(text, "long"), 144 .long = "long",
153 }, 145 },
154 .takes_value = .One, 146 .takes_value = .One,
155 }, try parseParam(text)); 147 }, try parseParam("-s, --long <value> Help text"));
156 148 expectParam(Param(Help){
157 text = "-s, --long <value>... Help text";
158 testing.expectEqual(Param(Help){
159 .id = Help{ 149 .id = Help{
160 .msg = find(text, "Help text"), 150 .msg = "Help text",
161 .value = find(text, "value"), 151 .value = "value",
162 }, 152 },
163 .names = Names{ 153 .names = Names{
164 .short = 's', 154 .short = 's',
165 .long = find(text, "long"), 155 .long = "long",
166 }, 156 },
167 .takes_value = .Many, 157 .takes_value = .Many,
168 }, try parseParam(text)); 158 }, try parseParam("-s, --long <value>... Help text"));
169 159 expectParam(Param(Help){
170 text = "--long <value> Help text";
171 testing.expectEqual(Param(Help){
172 .id = Help{ 160 .id = Help{
173 .msg = find(text, "Help text"), 161 .msg = "Help text",
174 .value = find(text, "value"), 162 .value = "value",
175 }, 163 },
176 .names = Names{ 164 .names = Names{
177 .short = null, 165 .short = null,
178 .long = find(text, "long"), 166 .long = "long",
179 }, 167 },
180 .takes_value = .One, 168 .takes_value = .One,
181 }, try parseParam(text)); 169 }, try parseParam("--long <value> Help text"));
182 170 expectParam(Param(Help){
183 text = "-s <value> Help text";
184 testing.expectEqual(Param(Help){
185 .id = Help{ 171 .id = Help{
186 .msg = find(text, "Help text"), 172 .msg = "Help text",
187 .value = find(text, "value"), 173 .value = "value",
188 }, 174 },
189 .names = Names{ 175 .names = Names{
190 .short = 's', 176 .short = 's',
191 .long = null, 177 .long = null,
192 }, 178 },
193 .takes_value = .One, 179 .takes_value = .One,
194 }, try parseParam(text)); 180 }, try parseParam("-s <value> Help text"));
195 181 expectParam(Param(Help){
196 text = "-s, --long Help text";
197 testing.expectEqual(Param(Help){
198 .id = Help{ 182 .id = Help{
199 .msg = find(text, "Help text"), 183 .msg = "Help text",
200 .value = text[z..z], 184 .value = "",
201 }, 185 },
202 .names = Names{ 186 .names = Names{
203 .short = 's', 187 .short = 's',
204 .long = find(text, "long"), 188 .long = "long",
205 }, 189 },
206 .takes_value = .None, 190 .takes_value = .None,
207 }, try parseParam(text)); 191 }, try parseParam("-s, --long Help text"));
208 192 expectParam(Param(Help){
209 text = "-s Help text";
210 testing.expectEqual(Param(Help){
211 .id = Help{ 193 .id = Help{
212 .msg = find(text, "Help text"), 194 .msg = "Help text",
213 .value = text[z..z], 195 .value = "",
214 }, 196 },
215 .names = Names{ 197 .names = Names{
216 .short = 's', 198 .short = 's',
217 .long = null, 199 .long = null,
218 }, 200 },
219 .takes_value = .None, 201 .takes_value = .None,
220 }, try parseParam(text)); 202 }, try parseParam("-s Help text"));
221 203 expectParam(Param(Help){
222 text = "--long Help text";
223 testing.expectEqual(Param(Help){
224 .id = Help{ 204 .id = Help{
225 .msg = find(text, "Help text"), 205 .msg = "Help text",
226 .value = text[z..z], 206 .value = "",
227 }, 207 },
228 .names = Names{ 208 .names = Names{
229 .short = null, 209 .short = null,
230 .long = find(text, "long"), 210 .long = "long",
231 }, 211 },
232 .takes_value = .None, 212 .takes_value = .None,
233 }, try parseParam(text)); 213 }, try parseParam("--long Help text"));
234 214 expectParam(Param(Help){
235 text = "--long <A | B> Help text";
236 testing.expectEqual(Param(Help){
237 .id = Help{ 215 .id = Help{
238 .msg = find(text, "Help text"), 216 .msg = "Help text",
239 .value = find(text, "A | B"), 217 .value = "A | B",
240 }, 218 },
241 .names = Names{ 219 .names = Names{
242 .short = null, 220 .short = null,
243 .long = find(text, "long"), 221 .long = "long",
244 }, 222 },
245 .takes_value = .One, 223 .takes_value = .One,
246 }, try parseParam(text)); 224 }, try parseParam("--long <A | B> Help text"));
225 expectParam(Param(Help){
226 .id = Help{
227 .msg = "Help text",
228 .value = "A",
229 },
230 .names = Names{
231 .short = null,
232 .long = null,
233 },
234 .takes_value = .One,
235 }, try parseParam("<A> Help text"));
236 expectParam(Param(Help){
237 .id = Help{
238 .msg = "Help text",
239 .value = "A",
240 },
241 .names = Names{
242 .short = null,
243 .long = null,
244 },
245 .takes_value = .Many,
246 }, try parseParam("<A>... Help text"));
247 247
248 testing.expectError(error.NoParamFound, parseParam("Help"));
249 testing.expectError(error.TrailingComma, parseParam("--long, Help")); 248 testing.expectError(error.TrailingComma, parseParam("--long, Help"));
250 testing.expectError(error.NoParamFound, parseParam("-s, Help")); 249 testing.expectError(error.TrailingComma, parseParam("-s, Help"));
251 testing.expectError(error.InvalidShortParam, parseParam("-ss Help")); 250 testing.expectError(error.InvalidShortParam, parseParam("-ss Help"));
252 testing.expectError(error.InvalidShortParam, parseParam("-ss <value> Help")); 251 testing.expectError(error.InvalidShortParam, parseParam("-ss <value> Help"));
253 testing.expectError(error.InvalidShortParam, parseParam("- Help")); 252 testing.expectError(error.InvalidShortParam, parseParam("- Help"));
254} 253}
255 254
256fn find(str: []const u8, f: []const u8) []const u8 {
257 const i = mem.indexOf(u8, str, f).?;
258 return str[i..][0..f.len];
259}
260
261pub fn Args(comptime Id: type, comptime params: []const Param(Id)) type {
262 return struct {
263 arena: std.heap.ArenaAllocator,
264 clap: ComptimeClap(Id, args.OsIterator, params),
265 exe_arg: ?[]const u8,
266
267 pub fn deinit(a: *@This()) void {
268 a.clap.deinit();
269 a.arena.deinit();
270 }
271
272 pub fn flag(a: @This(), comptime name: []const u8) bool {
273 return a.clap.flag(name);
274 }
275
276 pub fn option(a: @This(), comptime name: []const u8) ?[]const u8 {
277 return a.clap.option(name);
278 }
279
280 pub fn options(a: @This(), comptime name: []const u8) []const []const u8 {
281 return a.clap.options(name);
282 }
283
284 pub fn positionals(a: @This()) []const []const u8 {
285 return a.clap.positionals();
286 }
287 };
288}
289
290/// Optional diagnostics used for reporting useful errors 255/// Optional diagnostics used for reporting useful errors
291pub const Diagnostic = struct { 256pub const Diagnostic = struct {
292 arg: []const u8 = "", 257 arg: []const u8 = "",
@@ -319,15 +284,7 @@ fn testDiag(diag: Diagnostic, err: anyerror, expected: []const u8) void {
319 var buf: [1024]u8 = undefined; 284 var buf: [1024]u8 = undefined;
320 var slice_stream = io.fixedBufferStream(&buf); 285 var slice_stream = io.fixedBufferStream(&buf);
321 diag.report(slice_stream.outStream(), err) catch unreachable; 286 diag.report(slice_stream.outStream(), err) catch unreachable;
322 287 testing.expectEqualStrings(expected, slice_stream.getWritten());
323 const actual = slice_stream.getWritten();
324 if (!mem.eql(u8, actual, expected)) {
325 debug.warn("\n============ Expected ============\n", .{});
326 debug.warn("{}", .{expected});
327 debug.warn("============= Actual =============\n", .{});
328 debug.warn("{}", .{actual});
329 testing.expect(false);
330 }
331} 288}
332 289
333test "Diagnostic.report" { 290test "Diagnostic.report" {
@@ -343,6 +300,35 @@ test "Diagnostic.report" {
343 testDiag(.{ .name = .{ .long = "cc" } }, error.SomethingElse, "Error while parsing arguments: SomethingElse\n"); 300 testDiag(.{ .name = .{ .long = "cc" } }, error.SomethingElse, "Error while parsing arguments: SomethingElse\n");
344} 301}
345 302
303pub fn Args(comptime Id: type, comptime params: []const Param(Id)) type {
304 return struct {
305 arena: std.heap.ArenaAllocator,
306 clap: ComptimeClap(Id, args.OsIterator, params),
307 exe_arg: ?[]const u8,
308
309 pub fn deinit(a: *@This()) void {
310 a.clap.deinit();
311 a.arena.deinit();
312 }
313
314 pub fn flag(a: @This(), comptime name: []const u8) bool {
315 return a.clap.flag(name);
316 }
317
318 pub fn option(a: @This(), comptime name: []const u8) ?[]const u8 {
319 return a.clap.option(name);
320 }
321
322 pub fn options(a: @This(), comptime name: []const u8) []const []const u8 {
323 return a.clap.options(name);
324 }
325
326 pub fn positionals(a: @This()) []const []const u8 {
327 return a.clap.positionals();
328 }
329 };
330}
331
346/// Parses the command line arguments passed into the program based on an 332/// Parses the command line arguments passed into the program based on an
347/// array of `Param`s. 333/// array of `Param`s.
348pub fn parse( 334pub fn parse(
@@ -422,6 +408,7 @@ fn printParam(
422 408
423 try stream.print("--{}", .{l}); 409 try stream.print("--{}", .{l});
424 } 410 }
411
425 switch (param.takes_value) { 412 switch (param.takes_value) {
426 .None => {}, 413 .None => {},
427 .One => try stream.print(" <{}>", .{valueText(context, param)}), 414 .One => try stream.print(" <{}>", .{valueText(context, param)}),
@@ -516,22 +503,7 @@ test "clap.help" {
516 "\t-d, --dd <V3> \tBoth option.\n" ++ 503 "\t-d, --dd <V3> \tBoth option.\n" ++
517 "\t-d, --dd <V3>...\tBoth repeated option.\n"; 504 "\t-d, --dd <V3>...\tBoth repeated option.\n";
518 505
519 const actual = slice_stream.getWritten(); 506 testing.expectEqualStrings(expected, slice_stream.getWritten());
520 if (!mem.eql(u8, actual, expected)) {
521 debug.warn("\n============ Expected ============\n", .{});
522 debug.warn("{}", .{expected});
523 debug.warn("============= Actual =============\n", .{});
524 debug.warn("{}", .{actual});
525
526 var buffer: [1024 * 2]u8 = undefined;
527 var fba = std.heap.FixedBufferAllocator.init(&buffer);
528
529 debug.warn("============ Expected (escaped) ============\n", .{});
530 debug.warn("{x}\n", .{expected});
531 debug.warn("============ Actual (escaped) ============\n", .{});
532 debug.warn("{x}\n", .{actual});
533 testing.expect(false);
534 }
535} 507}
536 508
537/// Will print a usage message in the following format: 509/// Will print a usage message in the following format:
@@ -629,23 +601,7 @@ fn testUsage(expected: []const u8, params: []const Param(Help)) !void {
629 var buf: [1024]u8 = undefined; 601 var buf: [1024]u8 = undefined;
630 var fbs = io.fixedBufferStream(&buf); 602 var fbs = io.fixedBufferStream(&buf);
631 try usage(fbs.outStream(), params); 603 try usage(fbs.outStream(), params);
632 604 testing.expectEqualStrings(expected, fbs.getWritten());
633 const actual = fbs.getWritten();
634 if (!mem.eql(u8, actual, expected)) {
635 debug.warn("\n============ Expected ============\n", .{});
636 debug.warn("{}\n", .{expected});
637 debug.warn("============= Actual =============\n", .{});
638 debug.warn("{}\n", .{actual});
639
640 var buffer: [1024 * 2]u8 = undefined;
641 var fba = std.heap.FixedBufferAllocator.init(&buffer);
642
643 debug.warn("============ Expected (escaped) ============\n", .{});
644 debug.warn("{x}\n", .{expected});
645 debug.warn("============ Actual (escaped) ============\n", .{});
646 debug.warn("{x}\n", .{actual});
647 testing.expect(false);
648 }
649} 605}
650 606
651test "usage" { 607test "usage" {