From: "byroot (Jean Boussier) via ruby-core" Date: 2026-01-14T10:50:09+00:00 Subject: [ruby-core:124534] [Ruby Feature#21800] `Dir.foreach` and `Dir.each_child` to optionally yield `File::Stat` object alongside the children name Issue #21800 has been updated by byroot (Jean Boussier). > The lazy File::Stat My draft was just yielding a symbol: https://github.com/ruby/ruby/pull/15667, @nobu did the `File::Stat` version, I thought it was elegant, and degraded better on platforms where `d_type` is not supported. > The lazy File::Stat is concerning about the timing That doesn't concern me too much to be honest, given all file operation are subject to this sort of race conditions. But I understand why it concerns you. > Yielding f_type requires explicitly writing code to call File.stat (or File.lstat) for DT_UNKNOWN cases, which isn't very convenient (though it might be acceptable since this method isn't for casual use?). There is also the option of Ruby doing the `lstat` call to translate `DT_UNKNOWN` into the actual type. If we expose `DT_UNKNOWN` I'm worried users won't handle it correctly. From my understanding all the popular systems do support `f_type`, so it would be very hard to test for `DT_UNKNOWN` and would likely be broken. > It's unclear whether the lazy File::Stat should use lstat or stat. In my opinion it should be `lstat` because for this sort of code, circular symlinks, and following symlinks in general, is a concern. But I can be convinced otherwise. Also if we go toward a new method, we can always make it a keyword argument. > About the API, checking the block's arity to yield differently is not great. Agreed. I did it for simplicity and to avoid a discussion about naming, but I think it would be OK to make it a new method, e.g. - `Dir.scan(path) { |name, type| }` - `Dir.scan(path) [[name, type], ...]` > here are some options how to represent f_type, including (1) using the raw integer (e.g., comparing against constants like Dir::DT_UNKNOWN), or (2) using a symbol like :UNKNOWN. I don't have any strong opinion here. We a precedent with `File::Stat#ftype` that returns a String, but I don't think that makes for a very usable API: https://docs.ruby-lang.org/en/master/File/Stat.html#method-i-ftype On one hand, symbols are easier to discover when playing with the API in `irb` and such. On the other constants are easier to discover when searching the documentation and are typo-proof. I may have a slight preference for constants. ---------------------------------------- Feature #21800: `Dir.foreach` and `Dir.each_child` to optionally yield `File::Stat` object alongside the children name https://bugs.ruby-lang.org/issues/21800#change-116102 * Author: byroot (Jean Boussier) * Status: Open ---------------------------------------- When listing a directory, it's very common to need to know the type of each children, generally because you want to scan recursively. The naive way to do this is to call `stat(2)` for each children, but this is quite costly. This use case is common enough that `readdir` on most modern platforms do expose `struct dirent.d_type`, which allows to know the type of the child without an extra syscall: >From the `scandir` manpage: > d_type: This field contains a value indicating the file type, making it possible to avoid the expense of calling lstat(2) I wrote a quick prototype, and relying on `dirent.d_type` instead of `stat(2)` allows to recursively scan Ruby's repository twice as fast on my machine: https://github.com/ruby/ruby/pull/15667 Given that recursively scanning directories is a common task across many popular ruby tools (`zeitwerk`, `rubocop`, etc), I think it would be very valuable to provide this more efficient interface. In addition, @nobu noticed my prototype, and implemented a nicer version of it, where a `File::Stat` is yielded: https://github.com/ruby/ruby/commit/9acf67057b9bc6f855b2c37e41c1a2f91eae643a In that case the `File::Stat` is lazy, it's only if you access something other than file type, that the actual `stat(2)` call is emitted. I think this API is both more efficient and more convenient. ### Proposed API ```ruby Dir.foreach(path) { |name| } Dir.foreach(path) { |name, stat| } Dir.each_child(path) { |name| } Dir.each_child(path) { |name, stat| } Dir.new(path).each_child { |name| } Dir.new(path).each_child { |name, stat| } Dir.new(path).each { |name| } Dir.new(path).each { |name, stat| } ``` Also important to note, the `File::Stat` is expected to be equivalent to a `lstat(2)` call, as to be able to chose to follow symlinks or not. Basic use case: ```ruby def count_ruby_files(root) count = 0 queue = [root] while dir = queue.pop Dir.each_child(dir) do |name, stat| next if name.start_with?(".") if stat.directory? queue << File.join(dir, name) elsif stat.file? count += 1 if name.end_with?(".rb") end end end count end ``` -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/