authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2025-09-24 16:17:51-07:00
committergravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2025-10-03 16:29:08-07:00
log760127a7608f671daa60668a3c60a18058adff13
treeaac4a7ba0c391b6862c120389831ce61d05fc94d
parent07c3f9ef8e0a5a557dce70322334b0d1b49fe154

Add SelectiveWalker/walkSelectively and implement Walker in terms of it

This is a breaking change, since the fields of Walker have changed. The function APIs are unchanged, though.

2 files changed, 168 insertions(+), 55 deletions(-)

lib/std/fs/Dir.zig+107-55
......@@ -663,35 +663,17 @@ fn iterateImpl(self: Dir, first_iter_start_value: bool) Iterator {
663663 }
664664}
665665
666pub const Walker = struct {
667 stack: std.ArrayListUnmanaged(StackItem),
666pub const SelectiveWalker = struct {
667 stack: std.ArrayListUnmanaged(Walker.StackItem),
668668 name_buffer: std.ArrayListUnmanaged(u8),
669669 allocator: Allocator,
670670
671 pub const Entry = struct {
672 /// The containing directory. This can be used to operate directly on `basename`
673 /// rather than `path`, avoiding `error.NameTooLong` for deeply nested paths.
674 /// The directory remains open until `next` or `deinit` is called.
675 dir: Dir,
676 basename: [:0]const u8,
677 path: [:0]const u8,
678 kind: Dir.Entry.Kind,
679 };
680
681 const StackItem = struct {
682 iter: Dir.Iterator,
683 dirname_len: usize,
684 };
685
686671 /// After each call to this function, and on deinit(), the memory returned
687672 /// from this function becomes invalid. A copy must be made in order to keep
688673 /// a reference to the path.
689 pub fn next(self: *Walker) !?Walker.Entry {
690 const gpa = self.allocator;
691 while (self.stack.items.len != 0) {
692 // `top` and `containing` become invalid after appending to `self.stack`
693 var top = &self.stack.items[self.stack.items.len - 1];
694 var containing = top;
674 pub fn next(self: *SelectiveWalker) !?Walker.Entry {
675 while (self.stack.items.len > 0) {
676 const top = &self.stack.items[self.stack.items.len - 1];
695677 var dirname_len = top.dirname_len;
696678 if (top.iter.next() catch |err| {
697679 // If we get an error, then we want the user to be able to continue
......@@ -703,36 +685,22 @@ pub const Walker = struct {
703685 item.iter.dir.close();
704686 }
705687 return err;
706 }) |base| {
688 }) |entry| {
707689 self.name_buffer.shrinkRetainingCapacity(dirname_len);
708690 if (self.name_buffer.items.len != 0) {
709 try self.name_buffer.append(gpa, fs.path.sep);
691 try self.name_buffer.append(self.allocator, fs.path.sep);
710692 dirname_len += 1;
711693 }
712 try self.name_buffer.ensureUnusedCapacity(gpa, base.name.len + 1);
713 self.name_buffer.appendSliceAssumeCapacity(base.name);
694 try self.name_buffer.ensureUnusedCapacity(self.allocator, entry.name.len + 1);
695 self.name_buffer.appendSliceAssumeCapacity(entry.name);
714696 self.name_buffer.appendAssumeCapacity(0);
715 if (base.kind == .directory) {
716 var new_dir = top.iter.dir.openDir(base.name, .{ .iterate = true }) catch |err| switch (err) {
717 error.NameTooLong => unreachable, // no path sep in base.name
718 else => |e| return e,
719 };
720 {
721 errdefer new_dir.close();
722 try self.stack.append(gpa, .{
723 .iter = new_dir.iterateAssumeFirstIteration(),
724 .dirname_len = self.name_buffer.items.len - 1,
725 });
726 top = &self.stack.items[self.stack.items.len - 1];
727 containing = &self.stack.items[self.stack.items.len - 2];
728 }
729 }
730 return .{
731 .dir = containing.iter.dir,
697 const walker_entry: Walker.Entry = .{
698 .dir = top.iter.dir,
732699 .basename = self.name_buffer.items[dirname_len .. self.name_buffer.items.len - 1 :0],
733700 .path = self.name_buffer.items[0 .. self.name_buffer.items.len - 1 :0],
734 .kind = base.kind,
701 .kind = entry.kind,
735702 };
703 return walker_entry;
736704 } else {
737705 var item = self.stack.pop().?;
738706 if (self.stack.items.len != 0) {
......@@ -743,20 +711,44 @@ pub const Walker = struct {
743711 return null;
744712 }
745713
746 pub fn deinit(self: *Walker) void {
747 const gpa = self.allocator;
748 // Close any remaining directories except the initial one (which is always at index 0)
749 if (self.stack.items.len > 1) {
750 for (self.stack.items[1..]) |*item| {
751 item.iter.dir.close();
714 /// Traverses into the directory, continuing walking one level down.
715 pub fn enter(self: *SelectiveWalker, entry: Walker.Entry) !void {
716 if (entry.kind != .directory) {
717 @branchHint(.cold);
718 return;
719 }
720
721 var new_dir = entry.dir.openDir(entry.basename, .{ .iterate = true }) catch |err| {
722 switch (err) {
723 error.NameTooLong => unreachable,
724 else => |e| return e,
752725 }
726 };
727 errdefer new_dir.close();
728
729 try self.stack.append(self.allocator, .{
730 .iter = new_dir.iterateAssumeFirstIteration(),
731 .dirname_len = self.name_buffer.items.len - 1,
732 });
733 }
734
735 pub fn deinit(self: *SelectiveWalker) void {
736 self.name_buffer.deinit(self.allocator);
737 self.stack.deinit(self.allocator);
738 }
739
740 /// Leaves the current directory, continuing walking one level up.
741 pub fn leave(self: *SelectiveWalker) void {
742 var item = self.stack.pop().?;
743 if (self.stack.items.len != 0) {
744 @branchHint(.likely);
745 item.iter.dir.close();
753746 }
754 self.stack.deinit(gpa);
755 self.name_buffer.deinit(gpa);
756747 }
757748};
758749
759/// Recursively iterates over a directory.
750/// Recursively iterates over a directory, but requires the user to
751/// opt-in to recursing into each directory entry.
760752///
761753/// `self` must have been opened with `OpenOptions{.iterate = true}`.
762754///
......@@ -765,7 +757,9 @@ pub const Walker = struct {
765757/// The order of returned file system entries is undefined.
766758///
767759/// `self` will not be closed after walking it.
768pub fn walk(self: Dir, allocator: Allocator) Allocator.Error!Walker {
760///
761/// See also `walk`.
762pub fn walkSelectively(self: Dir, allocator: Allocator) !SelectiveWalker {
769763 var stack: std.ArrayListUnmanaged(Walker.StackItem) = .empty;
770764
771765 try stack.append(allocator, .{
......@@ -780,6 +774,64 @@ pub fn walk(self: Dir, allocator: Allocator) Allocator.Error!Walker {
780774 };
781775}
782776
777pub const Walker = struct {
778 inner: SelectiveWalker,
779
780 pub const Entry = struct {
781 /// The containing directory. This can be used to operate directly on `basename`
782 /// rather than `path`, avoiding `error.NameTooLong` for deeply nested paths.
783 /// The directory remains open until `next` or `deinit` is called.
784 dir: Dir,
785 basename: [:0]const u8,
786 path: [:0]const u8,
787 kind: Dir.Entry.Kind,
788 };
789
790 const StackItem = struct {
791 iter: Dir.Iterator,
792 dirname_len: usize,
793 };
794
795 /// After each call to this function, and on deinit(), the memory returned
796 /// from this function becomes invalid. A copy must be made in order to keep
797 /// a reference to the path.
798 pub fn next(self: *Walker) !?Walker.Entry {
799 const entry = try self.inner.next();
800 if (entry != null and entry.?.kind == .directory) {
801 try self.inner.enter(entry.?);
802 }
803 return entry;
804 }
805
806 pub fn deinit(self: *Walker) void {
807 self.inner.deinit();
808 }
809
810 /// Leaves the current directory, continuing walking one level up.
811 /// If the current entry is a directory entry, then the "current directory"
812 /// is the directory pertaining to the current entry.
813 pub fn leave(self: *Walker) void {
814 self.inner.leave();
815 }
816};
817
818/// Recursively iterates over a directory.
819///
820/// `self` must have been opened with `OpenOptions{.iterate = true}`.
821///
822/// `Walker.deinit` releases allocated memory and directory handles.
823///
824/// The order of returned file system entries is undefined.
825///
826/// `self` will not be closed after walking it.
827///
828/// See also `walkSelectively`.
829pub fn walk(self: Dir, allocator: Allocator) Allocator.Error!Walker {
830 return .{
831 .inner = try walkSelectively(self, allocator),
832 };
833}
834
783835pub const OpenError = error{
784836 FileNotFound,
785837 NotDir,
lib/std/fs/test.zig+61
......@@ -1810,6 +1810,67 @@ test "walker" {
18101810 try testing.expectEqual(expected_paths.kvs.len, num_walked);
18111811}
18121812
1813test "selective walker, skip entries that start with ." {
1814 var tmp = tmpDir(.{ .iterate = true });
1815 defer tmp.cleanup();
1816
1817 const paths_to_create: []const []const u8 = &.{
1818 "dir1/foo/.git/ignored",
1819 ".hidden/bar",
1820 "a/b/c",
1821 "a/baz",
1822 };
1823
1824 // iteration order of walker is undefined, so need lookup maps to check against
1825
1826 const expected_paths = std.StaticStringMap(void).initComptime(.{
1827 .{"dir1"},
1828 .{"dir1" ++ fs.path.sep_str ++ "foo"},
1829 .{"a"},
1830 .{"a" ++ fs.path.sep_str ++ "b"},
1831 .{"a" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c"},
1832 .{"a" ++ fs.path.sep_str ++ "baz"},
1833 });
1834
1835 const expected_basenames = std.StaticStringMap(void).initComptime(.{
1836 .{"dir1"},
1837 .{"foo"},
1838 .{"a"},
1839 .{"b"},
1840 .{"c"},
1841 .{"baz"},
1842 });
1843
1844 for (paths_to_create) |path| {
1845 try tmp.dir.makePath(path);
1846 }
1847
1848 var walker = try tmp.dir.walkSelectively(testing.allocator);
1849 defer walker.deinit();
1850
1851 var num_walked: usize = 0;
1852 while (try walker.next()) |entry| {
1853 if (entry.basename[0] == '.') continue;
1854 if (entry.kind == .directory) {
1855 try walker.enter(entry);
1856 }
1857
1858 testing.expect(expected_basenames.has(entry.basename)) catch |err| {
1859 std.debug.print("found unexpected basename: {f}\n", .{std.ascii.hexEscape(entry.basename, .lower)});
1860 return err;
1861 };
1862 testing.expect(expected_paths.has(entry.path)) catch |err| {
1863 std.debug.print("found unexpected path: {f}\n", .{std.ascii.hexEscape(entry.path, .lower)});
1864 return err;
1865 };
1866 // make sure that the entry.dir is the containing dir
1867 var entry_dir = try entry.dir.openDir(entry.basename, .{});
1868 defer entry_dir.close();
1869 num_walked += 1;
1870 }
1871 try testing.expectEqual(expected_paths.kvs.len, num_walked);
1872}
1873
18131874test "walker without fully iterating" {
18141875 var tmp = tmpDir(.{ .iterate = true });
18151876 defer tmp.cleanup();