diff options
| author | 2022-03-23 17:21:10 +0100 | |
|---|---|---|
| committer | 2022-03-23 21:48:20 +0100 | |
| commit | 651dc2c3d68322847c5d17fe3bdcb926ad044c90 (patch) | |
| tree | 37020a94cedba76a4a993e5bab7739f3fbce0101 /clap.zig | |
| parent | Workaround infinit loop caused by `try` inside `inline for` (diff) | |
| download | zig-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.zig | 343 |
1 files changed, 279 insertions, 64 deletions
| @@ -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 | ||
| 136 | fn 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 | ||
| 153 | fn expectParam(expect: Param(Help), actual: Param(Help)) !void { | 274 | fn 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 | ||
| 165 | test "parseParam" { | 286 | test "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 |