summaryrefslogtreecommitdiff
path: root/clap.zig
diff options
context:
space:
mode:
authorGravatar Jimmi Holst Christensen2022-03-23 17:21:10 +0100
committerGravatar Komari Spaghetti2022-03-23 21:48:20 +0100
commit651dc2c3d68322847c5d17fe3bdcb926ad044c90 (patch)
tree37020a94cedba76a4a993e5bab7739f3fbce0101 /clap.zig
parentWorkaround infinit loop caused by `try` inside `inline for` (diff)
downloadzig-clap-651dc2c3d68322847c5d17fe3bdcb926ad044c90.tar.gz
zig-clap-651dc2c3d68322847c5d17fe3bdcb926ad044c90.tar.xz
zig-clap-651dc2c3d68322847c5d17fe3bdcb926ad044c90.zip
Refactor parseParam into a state machine
This new parser stops when it hits something that looks like a new parameter. This is the precurser to parsing multiple parameters in a single function.
Diffstat (limited to 'clap.zig')
-rw-r--r--clap.zig343
1 files changed, 279 insertions, 64 deletions
diff --git a/clap.zig b/clap.zig
index a578615..d55ccc2 100644
--- a/clap.zig
+++ b/clap.zig
@@ -91,63 +91,184 @@ pub fn parseParam(line: []const u8) !Param(Help) {
91 // * Someone points out how this is a really bad idea. 91 // * Someone points out how this is a really bad idea.
92 @setEvalBranchQuota(std.math.maxInt(u32)); 92 @setEvalBranchQuota(std.math.maxInt(u32));
93 93
94 var found_comma = false; 94 var res = Param(Help){};
95 var it = mem.tokenize(u8, line, " \t"); 95 var start: usize = 0;
96 var param_str = it.next() orelse return error.NoParamFound; 96 var state: enum {
97 97 start,
98 const short_name = if (!mem.startsWith(u8, param_str, "--") and 98
99 mem.startsWith(u8, param_str, "-")) 99 start_of_short_name,
100 blk: { 100 end_of_short_name,
101 found_comma = param_str[param_str.len - 1] == ','; 101
102 if (found_comma) 102 before_long_name_or_value_or_description,
103 param_str = param_str[0 .. param_str.len - 1]; 103
104 104 before_long_name,
105 if (param_str.len != 2) 105 start_of_long_name,
106 return error.InvalidShortParam; 106 first_char_of_long_name,
107 107 rest_of_long_name,
108 const short_name = param_str[1]; 108
109 if (!found_comma) { 109 before_value_or_description,
110 var res = parseParamRest(it.rest()); 110
111 res.names.short = short_name; 111 first_char_of_value,
112 return res; 112 rest_of_value,
113 } 113 end_of_one_value,
114 second_dot_of_multi_value,
115 third_dot_of_multi_value,
116
117 before_description,
118 before_description_new_line,
119
120 rest_of_description,
121 rest_of_description_new_line,
122 } = .start;
123 for (line) |c, i| switch (state) {
124 .start => switch (c) {
125 ' ', '\t', '\n' => {},
126 '-' => state = .start_of_short_name,
127 '<' => state = .first_char_of_value,
128 else => return error.InvalidParameter,
129 },
114 130
115 param_str = it.next() orelse return error.NoParamFound; 131 .start_of_short_name => switch (c) {
116 break :blk short_name; 132 '-' => state = .first_char_of_long_name,
117 } else null; 133 'a'...'z', 'A'...'Z', '0'...'9' => {
134 res.names.short = c;
135 state = .end_of_short_name;
136 },
137 else => return error.InvalidParameter,
138 },
139 .end_of_short_name => switch (c) {
140 ' ', '\t' => state = .before_long_name_or_value_or_description,
141 '\n' => state = .before_description_new_line,
142 ',' => state = .before_long_name,
143 else => return error.InvalidParameter,
144 },
118 145
119 const long_name = if (mem.startsWith(u8, param_str, "--")) blk: { 146 .before_long_name => switch (c) {
120 if (param_str[param_str.len - 1] == ',') 147 ' ', '\t' => {},
121 return error.TrailingComma; 148 '-' => state = .start_of_long_name,
149 else => return error.InvalidParameter,
150 },
151 .start_of_long_name => switch (c) {
152 '-' => state = .first_char_of_long_name,
153 else => return error.InvalidParameter,
154 },
155 .first_char_of_long_name => switch (c) {
156 'a'...'z', 'A'...'Z', '0'...'9' => {
157 start = i;
158 state = .rest_of_long_name;
159 },
160 else => return error.InvalidParameter,
161 },
162 .rest_of_long_name => switch (c) {
163 'a'...'z', 'A'...'Z', '0'...'9' => {},
164 ' ', '\t' => {
165 res.names.long = line[start..i];
166 state = .before_value_or_description;
167 },
168 '\n' => {
169 res.names.long = line[start..i];
170 state = .before_description_new_line;
171 },
172 else => return error.InvalidParameter,
173 },
122 174
123 break :blk param_str[2..]; 175 .before_long_name_or_value_or_description => switch (c) {
124 } else if (found_comma) { 176 ' ', '\t' => {},
125 return error.TrailingComma; 177 ',' => state = .before_long_name,
126 } else if (short_name == null) { 178 '<' => state = .first_char_of_value,
127 return parseParamRest(mem.trimLeft(u8, line, " \t")); 179 else => {
128 } else null; 180 start = i;
181 state = .rest_of_description;
182 },
183 },
129 184
130 var res = parseParamRest(it.rest()); 185 .before_value_or_description => switch (c) {
131 res.names.long = long_name; 186 ' ', '\t' => {},
132 res.names.short = short_name; 187 '<' => state = .first_char_of_value,
133 return res; 188 else => {
134} 189 start = i;
190 state = .rest_of_description;
191 },
192 },
193 .first_char_of_value => switch (c) {
194 '>' => return error.InvalidParameter,
195 else => {
196 start = i;
197 state = .rest_of_value;
198 },
199 },
200 .rest_of_value => switch (c) {
201 '>' => {
202 res.takes_value = .one;
203 res.id.val = line[start..i];
204 state = .end_of_one_value;
205 },
206 else => {},
207 },
208 .end_of_one_value => switch (c) {
209 '.' => state = .second_dot_of_multi_value,
210 ' ', '\t' => state = .before_description,
211 '\n' => state = .before_description_new_line,
212 else => {
213 start = i;
214 state = .rest_of_description;
215 },
216 },
217 .second_dot_of_multi_value => switch (c) {
218 '.' => state = .third_dot_of_multi_value,
219 else => return error.InvalidParameter,
220 },
221 .third_dot_of_multi_value => switch (c) {
222 '.' => {
223 res.takes_value = .many;
224 state = .before_description;
225 },
226 else => return error.InvalidParameter,
227 },
135 228
136fn parseParamRest(line: []const u8) Param(Help) { 229 .before_description => switch (c) {
137 if (mem.startsWith(u8, line, "<")) blk: { 230 ' ', '\t' => {},
138 const len = mem.indexOfScalar(u8, line, '>') orelse break :blk; 231 '\n' => state = .before_description_new_line,
139 const takes_many = mem.startsWith(u8, line[len + 1 ..], "..."); 232 else => {
140 const help_start = len + 1 + @as(usize, 3) * @boolToInt(takes_many); 233 start = i;
141 return .{ 234 state = .rest_of_description;
142 .takes_value = if (takes_many) .many else .one,
143 .id = .{
144 .desc = mem.trim(u8, line[help_start..], " \t"),
145 .val = line[1..len],
146 }, 235 },
147 }; 236 },
237 .before_description_new_line => switch (c) {
238 ' ', '\t', '\n' => {},
239 '-' => break,
240 else => {
241 start = i;
242 state = .rest_of_description;
243 },
244 },
245 .rest_of_description => switch (c) {
246 '\n' => state = .rest_of_description_new_line,
247 else => {},
248 },
249 .rest_of_description_new_line => switch (c) {
250 ' ', '\t', '\n' => {},
251 '-' => {
252 res.id.desc = mem.trimRight(u8, line[start..i], " \t\n\r");
253 break;
254 },
255 else => state = .rest_of_description,
256 },
257 } else switch (state) {
258 .rest_of_description, .rest_of_description_new_line => {
259 res.id.desc = mem.trimRight(u8, line[start..], " \t\n\r");
260 },
261 .rest_of_long_name => res.names.long = line[start..],
262 .end_of_short_name,
263 .end_of_one_value,
264 .before_value_or_description,
265 .before_description,
266 .before_description_new_line,
267 => {},
268 else => return error.InvalidParameter,
148 } 269 }
149 270
150 return .{ .id = .{ .desc = mem.trim(u8, line, " \t") } }; 271 return res;
151} 272}
152 273
153fn expectParam(expect: Param(Help), actual: Param(Help)) !void { 274fn expectParam(expect: Param(Help), actual: Param(Help)) !void {
@@ -163,68 +284,162 @@ fn expectParam(expect: Param(Help), actual: Param(Help)) !void {
163} 284}
164 285
165test "parseParam" { 286test "parseParam" {
166 try expectParam(Param(Help){ 287 try expectParam(.{
288 .id = .{},
289 .names = .{ .short = 's' },
290 }, try parseParam("-s"));
291
292 try expectParam(.{
293 .id = .{},
294 .names = .{ .long = "str" },
295 }, try parseParam("--str"));
296
297 try expectParam(.{
298 .id = .{},
299 .names = .{ .short = 's', .long = "str" },
300 }, try parseParam("-s, --str"));
301
302 try expectParam(.{
303 .id = .{ .val = "str" },
304 .names = .{ .long = "str" },
305 .takes_value = .one,
306 }, try parseParam("--str <str>"));
307
308 try expectParam(.{
309 .id = .{ .val = "str" },
310 .names = .{ .short = 's', .long = "str" },
311 .takes_value = .one,
312 }, try parseParam("-s, --str <str>"));
313
314 try expectParam(.{
167 .id = .{ .desc = "Help text", .val = "val" }, 315 .id = .{ .desc = "Help text", .val = "val" },
168 .names = .{ .short = 's', .long = "long" }, 316 .names = .{ .short = 's', .long = "long" },
169 .takes_value = .one, 317 .takes_value = .one,
170 }, try parseParam("-s, --long <val> Help text")); 318 }, try parseParam("-s, --long <val> Help text"));
171 319
172 try expectParam(Param(Help){ 320 try expectParam(.{
173 .id = .{ .desc = "Help text", .val = "val" }, 321 .id = .{ .desc = "Help text", .val = "val" },
174 .names = .{ .short = 's', .long = "long" }, 322 .names = .{ .short = 's', .long = "long" },
175 .takes_value = .many, 323 .takes_value = .many,
176 }, try parseParam("-s, --long <val>... Help text")); 324 }, try parseParam("-s, --long <val>... Help text"));
177 325
178 try expectParam(Param(Help){ 326 try expectParam(.{
179 .id = .{ .desc = "Help text", .val = "val" }, 327 .id = .{ .desc = "Help text", .val = "val" },
180 .names = .{ .long = "long" }, 328 .names = .{ .long = "long" },
181 .takes_value = .one, 329 .takes_value = .one,
182 }, try parseParam("--long <val> Help text")); 330 }, try parseParam("--long <val> Help text"));
183 331
184 try expectParam(Param(Help){ 332 try expectParam(.{
185 .id = .{ .desc = "Help text", .val = "val" }, 333 .id = .{ .desc = "Help text", .val = "val" },
186 .names = .{ .short = 's' }, 334 .names = .{ .short = 's' },
187 .takes_value = .one, 335 .takes_value = .one,
188 }, try parseParam("-s <val> Help text")); 336 }, try parseParam("-s <val> Help text"));
189 337
190 try expectParam(Param(Help){ 338 try expectParam(.{
191 .id = .{ .desc = "Help text" }, 339 .id = .{ .desc = "Help text" },
192 .names = .{ .short = 's', .long = "long" }, 340 .names = .{ .short = 's', .long = "long" },
193 }, try parseParam("-s, --long Help text")); 341 }, try parseParam("-s, --long Help text"));
194 342
195 try expectParam(Param(Help){ 343 try expectParam(.{
196 .id = .{ .desc = "Help text" }, 344 .id = .{ .desc = "Help text" },
197 .names = .{ .short = 's' }, 345 .names = .{ .short = 's' },
198 }, try parseParam("-s Help text")); 346 }, try parseParam("-s Help text"));
199 347
200 try expectParam(Param(Help){ 348 try expectParam(.{
201 .id = .{ .desc = "Help text" }, 349 .id = .{ .desc = "Help text" },
202 .names = .{ .long = "long" }, 350 .names = .{ .long = "long" },
203 }, try parseParam("--long Help text")); 351 }, try parseParam("--long Help text"));
204 352
205 try expectParam(Param(Help){ 353 try expectParam(.{
206 .id = .{ .desc = "Help text", .val = "A | B" }, 354 .id = .{ .desc = "Help text", .val = "A | B" },
207 .names = .{ .long = "long" }, 355 .names = .{ .long = "long" },
208 .takes_value = .one, 356 .takes_value = .one,
209 }, try parseParam("--long <A | B> Help text")); 357 }, try parseParam("--long <A | B> Help text"));
210 358
211 try expectParam(Param(Help){ 359 try expectParam(.{
212 .id = .{ .desc = "Help text", .val = "A" }, 360 .id = .{ .desc = "Help text", .val = "A" },
213 .names = .{}, 361 .names = .{},
214 .takes_value = .one, 362 .takes_value = .one,
215 }, try parseParam("<A> Help text")); 363 }, try parseParam("<A> Help text"));
216 364
217 try expectParam(Param(Help){ 365 try expectParam(.{
218 .id = .{ .desc = "Help text", .val = "A" }, 366 .id = .{ .desc = "Help text", .val = "A" },
219 .names = .{}, 367 .names = .{},
220 .takes_value = .many, 368 .takes_value = .many,
221 }, try parseParam("<A>... Help text")); 369 }, try parseParam("<A>... Help text"));
222 370
223 try testing.expectError(error.TrailingComma, parseParam("--long, Help")); 371 try expectParam(.{
224 try testing.expectError(error.TrailingComma, parseParam("-s, Help")); 372 .id = .{
225 try testing.expectError(error.InvalidShortParam, parseParam("-ss Help")); 373 .desc =
226 try testing.expectError(error.InvalidShortParam, parseParam("-ss <val> Help")); 374 \\This is
227 try testing.expectError(error.InvalidShortParam, parseParam("- Help")); 375 \\ help spanning multiple
376 \\ lines
377 ,
378 },
379 .names = .{ .long = "aa" },
380 .takes_value = .none,
381 }, try parseParam(
382 \\--aa This is
383 \\ help spanning multiple
384 \\ lines
385 \\
386 ));
387
388 try expectParam(.{
389 .id = .{ .desc = "This msg should end and the newline cause of new param" },
390 .names = .{ .long = "aa" },
391 .takes_value = .none,
392 }, try parseParam(
393 \\--aa This msg should end and the newline cause of new param
394 \\--bb This should not end up being parsed
395 \\
396 ));
397
398 try expectParam(.{
399 .id = .{},
400 .names = .{ .short = 'a' },
401 .takes_value = .none,
402 }, try parseParam(
403 \\-a
404 \\--bb
405 \\
406 ));
407
408 try expectParam(.{
409 .id = .{},
410 .names = .{ .long = "aa" },
411 .takes_value = .none,
412 }, try parseParam(
413 \\--aa
414 \\--bb
415 \\
416 ));
417
418 try expectParam(.{
419 .id = .{ .val = "q" },
420 .names = .{ .short = 'a' },
421 .takes_value = .one,
422 }, try parseParam(
423 \\-a <q>
424 \\--bb
425 \\
426 ));
427
428 try expectParam(.{
429 .id = .{ .val = "q" },
430 .names = .{ .short = 'a' },
431 .takes_value = .many,
432 }, try parseParam(
433 \\-a <q>...
434 \\--bb
435 \\
436 ));
437
438 try testing.expectError(error.InvalidParameter, parseParam("--long, Help"));
439 try testing.expectError(error.InvalidParameter, parseParam("-s, Help"));
440 try testing.expectError(error.InvalidParameter, parseParam("-ss Help"));
441 try testing.expectError(error.InvalidParameter, parseParam("-ss <val> Help"));
442 try testing.expectError(error.InvalidParameter, parseParam("- Help"));
228} 443}
229 444
230/// Optional diagnostics used for reporting useful errors 445/// Optional diagnostics used for reporting useful errors