| ... | ... | @@ -522,6 +522,236 @@ pub const ArgIteratorWasi = struct { |
| 522 | 522 | } |
| 523 | 523 | }; |
| 524 | 524 | |
| 525 | /// Iterator that implements the Windows command-line parsing algorithm. |
| 526 | /// |
| 527 | /// This iterator faithfully implements the parsing behavior observed in `CommandLineToArgvW` with |
| 528 | /// one exception: if the command-line string is empty, the iterator will immediately complete |
| 529 | /// without returning any arguments (whereas `CommandLineArgvW` will return a single argument |
| 530 | /// representing the name of the current executable). |
| 531 | pub const ArgIteratorWindows = struct { |
| 532 | allocator: Allocator, |
| 533 | /// Owned by the iterator. |
| 534 | cmd_line: []const u8, |
| 535 | index: usize = 0, |
| 536 | /// Owned by the iterator. Long enough to hold the entire `cmd_line` plus a null terminator. |
| 537 | buffer: []u8, |
| 538 | start: usize = 0, |
| 539 | end: usize = 0, |
| 540 | |
| 541 | pub const InitError = error{ OutOfMemory, InvalidCmdLine }; |
| 542 | |
| 543 | /// `cmd_line_w` *must* be an UTF16-LE-encoded string. |
| 544 | /// |
| 545 | /// The iterator makes a copy of `cmd_line_w` converted UTF-8 and keeps it; it does *not* take |
| 546 | /// ownership of `cmd_line_w`. |
| 547 | pub fn init(allocator: Allocator, cmd_line_w: [*:0]const u16) InitError!ArgIteratorWindows { |
| 548 | const cmd_line = std.unicode.utf16leToUtf8Alloc(allocator, mem.sliceTo(cmd_line_w, 0)) catch |err| switch (err) { |
| 549 | error.DanglingSurrogateHalf, |
| 550 | error.ExpectedSecondSurrogateHalf, |
| 551 | error.UnexpectedSecondSurrogateHalf, |
| 552 | => return error.InvalidCmdLine, |
| 553 | error.OutOfMemory => return error.OutOfMemory, |
| 554 | }; |
| 555 | errdefer allocator.free(cmd_line); |
| 556 | |
| 557 | const buffer = try allocator.alloc(u8, cmd_line.len + 1); |
| 558 | errdefer allocator.free(buffer); |
| 559 | |
| 560 | return .{ |
| 561 | .allocator = allocator, |
| 562 | .cmd_line = cmd_line, |
| 563 | .buffer = buffer, |
| 564 | }; |
| 565 | } |
| 566 | |
| 567 | /// Returns the next argument and advances the iterator. Returns `null` if at the end of the |
| 568 | /// command-line string. The iterator owns the returned slice. |
| 569 | pub fn next(self: *ArgIteratorWindows) ?[:0]const u8 { |
| 570 | return self.nextWithStrategy(next_strategy); |
| 571 | } |
| 572 | |
| 573 | /// Skips the next argument and advances the iterator. Returns `true` if an argument was |
| 574 | /// skipped, `false` if at the end of the command-line string. |
| 575 | pub fn skip(self: *ArgIteratorWindows) bool { |
| 576 | return self.nextWithStrategy(skip_strategy); |
| 577 | } |
| 578 | |
| 579 | const next_strategy = struct { |
| 580 | const T = ?[:0]const u8; |
| 581 | |
| 582 | const eof = null; |
| 583 | |
| 584 | fn emitBackslashes(self: *ArgIteratorWindows, count: usize) void { |
| 585 | for (0..count) |_| emitCharacter(self, '\\'); |
| 586 | } |
| 587 | |
| 588 | fn emitCharacter(self: *ArgIteratorWindows, char: u8) void { |
| 589 | self.buffer[self.end] = char; |
| 590 | self.end += 1; |
| 591 | } |
| 592 | |
| 593 | fn yieldArg(self: *ArgIteratorWindows) [:0]const u8 { |
| 594 | self.buffer[self.end] = 0; |
| 595 | const arg = self.buffer[self.start..self.end :0]; |
| 596 | self.end += 1; |
| 597 | self.start = self.end; |
| 598 | return arg; |
| 599 | } |
| 600 | }; |
| 601 | |
| 602 | const skip_strategy = struct { |
| 603 | const T = bool; |
| 604 | |
| 605 | const eof = false; |
| 606 | |
| 607 | fn emitBackslashes(_: *ArgIteratorWindows, _: usize) void {} |
| 608 | |
| 609 | fn emitCharacter(_: *ArgIteratorWindows, _: u8) void {} |
| 610 | |
| 611 | fn yieldArg(_: *ArgIteratorWindows) bool { |
| 612 | return true; |
| 613 | } |
| 614 | }; |
| 615 | |
| 616 | // The essential parts of the algorithm are described in Microsoft's documentation: |
| 617 | // |
| 618 | // - <https://learn.microsoft.com/en-us/cpp/cpp/main-function-command-line-args?view=msvc-170#parsing-c-command-line-arguments> |
| 619 | // - <https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw> |
| 620 | // |
| 621 | // David Deley explains some additional undocumented quirks in great detail: |
| 622 | // |
| 623 | // - <https://daviddeley.com/autohotkey/parameters/parameters.htm#WINCRULES> |
| 624 | // |
| 625 | // Code points <= U+0020 terminating an unquoted first argument was discovered independently by |
| 626 | // testing and observing the behavior of 'CommandLineToArgvW' on Windows 10. |
| 627 | |
| 628 | fn nextWithStrategy(self: *ArgIteratorWindows, comptime strategy: type) strategy.T { |
| 629 | // The first argument (the executable name) uses different parsing rules. |
| 630 | if (self.index == 0) { |
| 631 | var char = if (self.cmd_line.len != 0) self.cmd_line[0] else 0; |
| 632 | switch (char) { |
| 633 | 0 => { |
| 634 | // Immediately complete the iterator. |
| 635 | // 'CommandLineToArgvW' would return the name of the current executable here. |
| 636 | return strategy.eof; |
| 637 | }, |
| 638 | '"' => { |
| 639 | // If the first character is a quote, read everything until the next quote (then |
| 640 | // skip that quote), or until the end of the string. |
| 641 | self.index += 1; |
| 642 | while (true) : (self.index += 1) { |
| 643 | char = if (self.index != self.cmd_line.len) self.cmd_line[self.index] else 0; |
| 644 | switch (char) { |
| 645 | 0 => { |
| 646 | return strategy.yieldArg(self); |
| 647 | }, |
| 648 | '"' => { |
| 649 | self.index += 1; |
| 650 | return strategy.yieldArg(self); |
| 651 | }, |
| 652 | else => { |
| 653 | strategy.emitCharacter(self, char); |
| 654 | }, |
| 655 | } |
| 656 | } |
| 657 | }, |
| 658 | else => { |
| 659 | // Otherwise, read everything until the next space or ASCII control character |
| 660 | // (not including DEL) (then skip that character), or until the end of the |
| 661 | // string. This means that if the command-line string starts with one of these |
| 662 | // characters, the first returned argument will be the empty string. |
| 663 | while (true) : (self.index += 1) { |
| 664 | char = if (self.index != self.cmd_line.len) self.cmd_line[self.index] else 0; |
| 665 | switch (char) { |
| 666 | 0 => { |
| 667 | return strategy.yieldArg(self); |
| 668 | }, |
| 669 | '\x01'...' ' => { |
| 670 | self.index += 1; |
| 671 | return strategy.yieldArg(self); |
| 672 | }, |
| 673 | else => { |
| 674 | strategy.emitCharacter(self, char); |
| 675 | }, |
| 676 | } |
| 677 | } |
| 678 | }, |
| 679 | } |
| 680 | } |
| 681 | |
| 682 | // Skip spaces and tabs. The iterator completes if we reach the end of the string here. |
| 683 | while (true) : (self.index += 1) { |
| 684 | const char = if (self.index != self.cmd_line.len) self.cmd_line[self.index] else 0; |
| 685 | switch (char) { |
| 686 | 0 => return strategy.eof, |
| 687 | ' ', '\t' => continue, |
| 688 | else => break, |
| 689 | } |
| 690 | } |
| 691 | |
| 692 | // Parsing rules for subsequent arguments: |
| 693 | // |
| 694 | // - The end of the string always terminates the current argument. |
| 695 | // - When not in 'inside_quotes' mode, a space or tab terminates the current argument. |
| 696 | // - 2n backslashes followed by a quote emit n backslashes. If in 'inside_quotes' and the |
| 697 | // quote is immediately followed by a second quote, one quote is emitted and the other is |
| 698 | // skipped, otherwise, the quote is skipped. Finally, 'inside_quotes' is toggled. |
| 699 | // - 2n + 1 backslashes followed by a quote emit n backslashes followed by a quote. |
| 700 | // - n backslashes not followed by a quote emit n backslashes. |
| 701 | var backslash_count: usize = 0; |
| 702 | var inside_quotes = false; |
| 703 | while (true) : (self.index += 1) { |
| 704 | const char = if (self.index != self.cmd_line.len) self.cmd_line[self.index] else 0; |
| 705 | switch (char) { |
| 706 | 0 => { |
| 707 | strategy.emitBackslashes(self, backslash_count); |
| 708 | return strategy.yieldArg(self); |
| 709 | }, |
| 710 | ' ', '\t' => { |
| 711 | strategy.emitBackslashes(self, backslash_count); |
| 712 | backslash_count = 0; |
| 713 | if (inside_quotes) |
| 714 | strategy.emitCharacter(self, char) |
| 715 | else |
| 716 | return strategy.yieldArg(self); |
| 717 | }, |
| 718 | '"' => { |
| 719 | const char_is_escaped_quote = backslash_count % 2 != 0; |
| 720 | strategy.emitBackslashes(self, backslash_count / 2); |
| 721 | backslash_count = 0; |
| 722 | if (char_is_escaped_quote) { |
| 723 | strategy.emitCharacter(self, '"'); |
| 724 | } else { |
| 725 | if (inside_quotes and |
| 726 | self.index + 1 != self.cmd_line.len and |
| 727 | self.cmd_line[self.index + 1] == '"') |
| 728 | { |
| 729 | strategy.emitCharacter(self, '"'); |
| 730 | self.index += 1; |
| 731 | } |
| 732 | inside_quotes = !inside_quotes; |
| 733 | } |
| 734 | }, |
| 735 | '\\' => { |
| 736 | backslash_count += 1; |
| 737 | }, |
| 738 | else => { |
| 739 | strategy.emitBackslashes(self, backslash_count); |
| 740 | backslash_count = 0; |
| 741 | strategy.emitCharacter(self, char); |
| 742 | }, |
| 743 | } |
| 744 | } |
| 745 | } |
| 746 | |
| 747 | /// Frees the iterator's copy of the command-line string and all previously returned |
| 748 | /// argument slices. |
| 749 | pub fn deinit(self: *ArgIteratorWindows) void { |
| 750 | self.allocator.free(self.buffer); |
| 751 | self.allocator.free(self.cmd_line); |
| 752 | } |
| 753 | }; |
| 754 | |
| 525 | 755 | /// Optional parameters for `ArgIteratorGeneral` |
| 526 | 756 | pub const ArgIteratorGeneralOptions = struct { |
| 527 | 757 | comments: bool = false, |
| ... | ... | @@ -754,7 +984,7 @@ pub fn ArgIteratorGeneral(comptime options: ArgIteratorGeneralOptions) type { |
| 754 | 984 | /// Cross-platform command line argument iterator. |
| 755 | 985 | pub const ArgIterator = struct { |
| 756 | 986 | const InnerType = switch (builtin.os.tag) { |
| 757 | | .windows => ArgIteratorGeneral(.{}), |
| 987 | .windows => ArgIteratorWindows, |
| 758 | 988 | .wasi => if (builtin.link_libc) ArgIteratorPosix else ArgIteratorWasi, |
| 759 | 989 | else => ArgIteratorPosix, |
| 760 | 990 | }; |
| ... | ... | @@ -774,10 +1004,7 @@ pub const ArgIterator = struct { |
| 774 | 1004 | return ArgIterator{ .inner = InnerType.init() }; |
| 775 | 1005 | } |
| 776 | 1006 | |
| 777 | | pub const InitError = switch (builtin.os.tag) { |
| 778 | | .windows => InnerType.InitUtf16leError, |
| 779 | | else => InnerType.InitError, |
| 780 | | }; |
| 1007 | pub const InitError = InnerType.InitError; |
| 781 | 1008 | |
| 782 | 1009 | /// You must deinitialize iterator's internal buffers by calling `deinit` when done. |
| 783 | 1010 | pub fn initWithAllocator(allocator: Allocator) InitError!ArgIterator { |
| ... | ... | @@ -786,7 +1013,7 @@ pub const ArgIterator = struct { |
| 786 | 1013 | } |
| 787 | 1014 | if (builtin.os.tag == .windows) { |
| 788 | 1015 | const cmd_line_w = os.windows.kernel32.GetCommandLineW(); |
| 789 | | return ArgIterator{ .inner = try InnerType.initUtf16le(allocator, cmd_line_w) }; |
| 1016 | return ArgIterator{ .inner = try InnerType.init(allocator, cmd_line_w) }; |
| 790 | 1017 | } |
| 791 | 1018 | |
| 792 | 1019 | return ArgIterator{ .inner = InnerType.init() }; |
| ... | ... | @@ -877,6 +1104,133 @@ pub fn argsFree(allocator: Allocator, args_alloc: []const [:0]u8) void { |
| 877 | 1104 | return allocator.free(aligned_allocated_buf); |
| 878 | 1105 | } |
| 879 | 1106 | |
| 1107 | test "ArgIteratorWindows" { |
| 1108 | const t = testArgIteratorWindows; |
| 1109 | |
| 1110 | try t( |
| 1111 | \\"C:\Program Files\zig\zig.exe" run .\src\main.zig -target x86_64-windows-gnu -O ReleaseSafe -- --emoji=🗿 --eval="new Regex(\"Dwayne \\\"The Rock\\\" Johnson\")" |
| 1112 | , &.{ |
| 1113 | \\C:\Program Files\zig\zig.exe |
| 1114 | , |
| 1115 | \\run |
| 1116 | , |
| 1117 | \\.\src\main.zig |
| 1118 | , |
| 1119 | \\-target |
| 1120 | , |
| 1121 | \\x86_64-windows-gnu |
| 1122 | , |
| 1123 | \\-O |
| 1124 | , |
| 1125 | \\ReleaseSafe |
| 1126 | , |
| 1127 | \\-- |
| 1128 | , |
| 1129 | \\--emoji=🗿 |
| 1130 | , |
| 1131 | \\--eval=new Regex("Dwayne \"The Rock\" Johnson") |
| 1132 | , |
| 1133 | }); |
| 1134 | |
| 1135 | // Empty |
| 1136 | try t("", &.{}); |
| 1137 | |
| 1138 | // Separators |
| 1139 | try t("aa bb cc", &.{ "aa", "bb", "cc" }); |
| 1140 | try t("aa\tbb\tcc", &.{ "aa", "bb", "cc" }); |
| 1141 | try t("aa\nbb\ncc", &.{ "aa", "bb\ncc" }); |
| 1142 | try t("aa\r\nbb\r\ncc", &.{ "aa", "\nbb\r\ncc" }); |
| 1143 | try t("aa\rbb\rcc", &.{ "aa", "bb\rcc" }); |
| 1144 | try t("aa\x07bb\x07cc", &.{ "aa", "bb\x07cc" }); |
| 1145 | try t("aa\x7Fbb\x7Fcc", &.{"aa\x7Fbb\x7Fcc"}); |
| 1146 | try t("aa🦎bb🦎cc", &.{"aa🦎bb🦎cc"}); |
| 1147 | |
| 1148 | // Leading/trailing whitespace |
| 1149 | try t(" ", &.{""}); |
| 1150 | try t(" aa bb ", &.{ "", "aa", "bb" }); |
| 1151 | try t("\t\t", &.{""}); |
| 1152 | try t("\t\taa\t\tbb\t\t", &.{ "", "aa", "bb" }); |
| 1153 | try t("\n\n", &.{ "", "\n" }); |
| 1154 | try t("\n\naa\n\nbb\n\n", &.{ "", "\naa\n\nbb\n\n" }); |
| 1155 | |
| 1156 | // Executable name with quotes/backslashes |
| 1157 | try t("\"aa bb\tcc\ndd\"", &.{"aa bb\tcc\ndd"}); |
| 1158 | try t("\"", &.{""}); |
| 1159 | try t("\"\"", &.{""}); |
| 1160 | try t("\"\"\"", &.{ "", "" }); |
| 1161 | try t("\"\"\"\"", &.{ "", "" }); |
| 1162 | try t("\"\"\"\"\"", &.{ "", "\"" }); |
| 1163 | try t("aa\"bb\"cc\"dd", &.{"aa\"bb\"cc\"dd"}); |
| 1164 | try t("aa\"bb cc\"dd", &.{ "aa\"bb", "ccdd" }); |
| 1165 | try t("\"aa\\\"bb\"", &.{ "aa\\", "bb" }); |
| 1166 | try t("\"aa\\\\\"", &.{"aa\\\\"}); |
| 1167 | try t("aa\\\"bb", &.{"aa\\\"bb"}); |
| 1168 | try t("aa\\\\\"bb", &.{"aa\\\\\"bb"}); |
| 1169 | |
| 1170 | // Arguments with quotes/backslashes |
| 1171 | try t(". \"aa bb\tcc\ndd\"", &.{ ".", "aa bb\tcc\ndd" }); |
| 1172 | try t(". aa\" \"bb\"\t\"cc\"\n\"dd\"", &.{ ".", "aa bb\tcc\ndd" }); |
| 1173 | try t(". ", &.{"."}); |
| 1174 | try t(". \"", &.{ ".", "" }); |
| 1175 | try t(". \"\"", &.{ ".", "" }); |
| 1176 | try t(". \"\"\"", &.{ ".", "\"" }); |
| 1177 | try t(". \"\"\"\"", &.{ ".", "\"" }); |
| 1178 | try t(". \"\"\"\"\"", &.{ ".", "\"" }); |
| 1179 | try t(". \"\"\"\"\"\"", &.{ ".", "\"\"" }); |
| 1180 | try t(". \" \"", &.{ ".", " " }); |
| 1181 | try t(". \" \"\"", &.{ ".", " \"" }); |
| 1182 | try t(". \" \"\"\"", &.{ ".", " \"" }); |
| 1183 | try t(". \" \"\"\"\"", &.{ ".", " \"" }); |
| 1184 | try t(". \" \"\"\"\"\"", &.{ ".", " \"\"" }); |
| 1185 | try t(". \" \"\"\"\"\"\"", &.{ ".", " \"\"" }); |
| 1186 | try t(". \\\"", &.{ ".", "\"" }); |
| 1187 | try t(". \\\"\"", &.{ ".", "\"" }); |
| 1188 | try t(". \\\"\"\"", &.{ ".", "\"" }); |
| 1189 | try t(". \\\"\"\"\"", &.{ ".", "\"\"" }); |
| 1190 | try t(". \\\"\"\"\"\"", &.{ ".", "\"\"" }); |
| 1191 | try t(". \\\"\"\"\"\"\"", &.{ ".", "\"\"" }); |
| 1192 | try t(". \" \\\"", &.{ ".", " \"" }); |
| 1193 | try t(". \" \\\"\"", &.{ ".", " \"" }); |
| 1194 | try t(". \" \\\"\"\"", &.{ ".", " \"\"" }); |
| 1195 | try t(". \" \\\"\"\"\"", &.{ ".", " \"\"" }); |
| 1196 | try t(". \" \\\"\"\"\"\"", &.{ ".", " \"\"" }); |
| 1197 | try t(". \" \\\"\"\"\"\"\"", &.{ ".", " \"\"\"" }); |
| 1198 | try t(". aa\\bb\\\\cc\\\\\\dd", &.{ ".", "aa\\bb\\\\cc\\\\\\dd" }); |
| 1199 | try t(". \\\\\\\"aa bb\"", &.{ ".", "\\\"aa", "bb" }); |
| 1200 | try t(". \\\\\\\\\"aa bb\"", &.{ ".", "\\\\aa bb" }); |
| 1201 | } |
| 1202 | |
| 1203 | fn testArgIteratorWindows(cmd_line: []const u8, expected_args: []const []const u8) !void { |
| 1204 | const cmd_line_w = try std.unicode.utf8ToUtf16LeWithNull(testing.allocator, cmd_line); |
| 1205 | defer testing.allocator.free(cmd_line_w); |
| 1206 | |
| 1207 | // next |
| 1208 | { |
| 1209 | var it = try ArgIteratorWindows.init(testing.allocator, cmd_line_w); |
| 1210 | defer it.deinit(); |
| 1211 | |
| 1212 | for (expected_args) |expected| { |
| 1213 | if (it.next()) |actual| { |
| 1214 | try testing.expectEqualStrings(expected, actual); |
| 1215 | } else { |
| 1216 | return error.TestUnexpectedResult; |
| 1217 | } |
| 1218 | } |
| 1219 | try testing.expect(it.next() == null); |
| 1220 | } |
| 1221 | |
| 1222 | // skip |
| 1223 | { |
| 1224 | var it = try ArgIteratorWindows.init(testing.allocator, cmd_line_w); |
| 1225 | defer it.deinit(); |
| 1226 | |
| 1227 | for (0..expected_args.len) |_| { |
| 1228 | try testing.expect(it.skip()); |
| 1229 | } |
| 1230 | try testing.expect(!it.skip()); |
| 1231 | } |
| 1232 | } |
| 1233 | |
| 880 | 1234 | test "general arg parsing" { |
| 881 | 1235 | try testGeneralCmdLine("a b\tc d", &.{ "a", "b", "c", "d" }); |
| 882 | 1236 | try testGeneralCmdLine("\"abc\" d e", &.{ "abc", "d", "e" }); |