Skip to content

Add DirEntry-based readdir methods #55358

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ New library features
the uniquing checking ([#53474])
* `RegexMatch` objects can now be used to construct `NamedTuple`s and `Dict`s ([#50988])
* `Lockable` is now exported ([#54595])
* New methods `readdir(DirEntry, path::String)` and `readdir(e::DirEntry)` will now return directory contents
along with the type of the entries in a vector of new `DirEntry` objects to provide more efficient `isfile`
etc. checks ([#55358])

Standard library changes
------------------------
Expand Down
1 change: 1 addition & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,7 @@ export
close,
closewrite,
countlines,
DirEntry,
eachline,
readeach,
eof,
Expand Down
35 changes: 28 additions & 7 deletions base/file.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export
chown,
cp,
cptree,
DirEntry,
diskstat,
hardlink,
mkdir,
Expand Down Expand Up @@ -949,11 +950,20 @@ const UV_DIRENT_BLOCK = Cint(7)

"""
DirEntry
DirEntry(path::String)

A type representing a filesystem entry that contains the name of the entry, the directory, and
the raw type of the entry. The full path of the entry can be obtained lazily by accessing the
`path` field. The type of the entry can be checked for by calling [`isfile`](@ref), [`isdir`](@ref),
`path` field.

Public fields:
- `dir::String`: The directory containing the entry.
- `name::String`: The name of the entry.
- `path::String`: The full path of the entry, lazily constructed from `dir` and `name`. Also accessible via `joinpath(entry)`.

The type of the entry can be checked for by calling [`isfile`](@ref), [`isdir`](@ref),
[`islink`](@ref), [`isfifo`](@ref), [`issocket`](@ref), [`ischardev`](@ref), and [`isblockdev`](@ref)
on the entry object.
"""
struct DirEntry
dir::String
Expand Down Expand Up @@ -983,27 +993,38 @@ isblockdev(obj::DirEntry) = (isunknown(obj) || islink(obj)) ? isblockdev(obj.pat
realpath(obj::DirEntry) = realpath(obj.path)

"""
_readdirx(dir::AbstractString=pwd(); sort::Bool = true) -> Vector{DirEntry}
readdir(::Type{DirEntry}, dir::Union{AbstractString,DirEntry}=pwd(); sort::Bool = true) -> Vector{DirEntry}
readdir(entry::DirEntry; sort::Bool=true) -> Vector{DirEntry}

Return a vector of [`DirEntry`](@ref) objects representing the contents of the directory `dir`,
or the current working directory if not given. If `sort` is true, the returned vector is
sorted by name.

Unlike [`readdir`](@ref), `_readdirx` returns [`DirEntry`](@ref) objects, which contain the name of the
The [`DirEntry`](@ref) objects that are returned contain the name of the
file, the directory it is in, and the type of the file which is determined during the
directory scan. This means that calls to [`isfile`](@ref), [`isdir`](@ref), [`islink`](@ref), [`isfifo`](@ref),
[`issocket`](@ref), [`ischardev`](@ref), and [`isblockdev`](@ref) can be made on the
returned objects without further stat calls. However, for some filesystems, the type of the file
cannot be determined without a stat call. In these cases the `rawtype` field of the [`DirEntry`](@ref))
object will be 0 (`UV_DIRENT_UNKNOWN`) and [`isfile`](@ref) etc. will fall back to a `stat` call.

# Examples
```julia
for obj in _readdirx()
isfile(obj) && println("\$(obj.name) is a file with path \$(obj.path)")
for entry in readdir(DirEntry, ".")
if isfile(entry)
println("\$(entry.name) is a file with path \$(entry.path)")
continue
end
isdir(entry) || continue
for entry2 in readdir(entry)
...
end
end
```
"""
_readdirx(dir::AbstractString=pwd(); sort::Bool=true) = _readdir(dir; return_objects=true, sort)::Vector{DirEntry}
readdir(::Type{DirEntry}, dir::AbstractString=pwd(); sort::Bool=true) = _readdir(dir; return_objects=true, sort)::Vector{DirEntry}
readdir(::Type{DirEntry}, entry::DirEntry; sort::Bool=true) = readdir(entry; sort)::Vector{DirEntry}
readdir(entry::DirEntry; sort::Bool=true) = readdir(DirEntry, entry.path; sort)::Vector{DirEntry}

function _readdir(dir::AbstractString; return_objects::Bool=false, join::Bool=false, sort::Bool=true)
# Allocate space for uv_fs_t struct
Expand Down Expand Up @@ -1093,7 +1114,7 @@ function walkdir(root; topdown=true, follow_symlinks=false, onerror=throw)
end
return
end
entries = tryf(_readdirx, root)
entries = tryf(p -> readdir(DirEntry, p), root)
entries === nothing && return
dirs = Vector{String}()
files = Vector{String}()
Expand Down
1 change: 1 addition & 0 deletions doc/src/base/file.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Base.Filesystem.pwd
Base.Filesystem.cd(::AbstractString)
Base.Filesystem.cd(::Function)
Base.Filesystem.readdir
Base.Filesystem.DirEntry
Base.Filesystem.walkdir
Base.Filesystem.mkdir
Base.Filesystem.mkpath
Expand Down
9 changes: 4 additions & 5 deletions stdlib/REPL/src/REPLCompletions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ using Core: Const
const CC = Core.Compiler
using Base.Meta
using Base: propertynames, something, IdSet
using Base.Filesystem: _readdirx

abstract type Completion end

Expand Down Expand Up @@ -335,7 +334,7 @@ function cache_PATH()
end

path_entries = try
_readdirx(pathdir)
readdir(DirEntry, pathdir)
catch e
# Bash allows dirs in PATH that can't be read, so we should as well.
if isa(e, Base.IOError) || isa(e, Base.ArgumentError)
Expand Down Expand Up @@ -398,9 +397,9 @@ function complete_path(path::AbstractString;
end
entries = try
if isempty(dir)
_readdirx()
readdir(DirEntry)
elseif isdir(dir)
_readdirx(dir)
readdir(DirEntry, dir)
else
return Completion[], dir, false
end
Expand Down Expand Up @@ -1435,7 +1434,7 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif
complete_loading_candidates!(suggestions, s, dir)
end
isdir(dir) || continue
for entry in _readdirx(dir)
for entry in readdir(DirEntry, dir)
pname = entry.name
if pname[1] != '.' && pname != "METADATA" &&
pname != "REQUIRE" && startswith(pname, s)
Expand Down
4 changes: 1 addition & 3 deletions stdlib/REPL/src/docview.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import Base.Docs: doc, formatdoc, parsedoc, apropos

using Base: with_output_color, mapany, isdeprecated, isexported

using Base.Filesystem: _readdirx

using InteractiveUtils: subtypes

using Unicode: normalize
Expand Down Expand Up @@ -365,7 +363,7 @@ function find_readme(m::Module)::Union{String, Nothing}
path = dirname(mpath)
top_path = pkgdir(m)
while true
for entry in _readdirx(path; sort=true)
for entry in readdir(DirEntry, path; sort=true)
isfile(entry) && (lowercase(entry.name) in ["readme.md", "readme"]) || continue
return entry.path
end
Expand Down
49 changes: 41 additions & 8 deletions test/file.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,54 @@ let err = nothing
end
end

@testset "readdir" begin
@test ispath("does/not/exist") == false
@test isdir("does/not/exist") == false
@test_throws Base.IOError readdir("does/not/exist")
@test_throws Base.IOError readdir(DirEntry, "does/not/exist")

mktempdir() do dir
touch(joinpath(dir, "afile.txt"))
mkdir(joinpath(dir, "adir"))
touch(joinpath(dir, "adir", "bfile.txt"))
@test length(readdir(dir)) == 2
@test readdir(dir) == map(e->e.name, readdir(DirEntry, dir))
for p in readdir(dir, join=true)
if isdir(p)
@test only(readdir(p)) == "bfile.txt"
else
@test isfile(p)
@test p == joinpath(dir, "afile.txt")
end
end
for e in readdir(DirEntry, dir)
if isdir(e) || continue
@test only(readdir(e)).name == "bfile.txt"
@test only(readdir(DirEntry, e)).name == "bfile.txt"
else
@test isfile(e)
@test e.name == "afile.txt"
end
end
end
end

if !Sys.iswindows() || Sys.windows_version() >= Sys.WINDOWS_VISTA_VER
dirlink = joinpath(dir, "dirlink")
symlink(subdir, dirlink)
@test stat(dirlink) == stat(subdir)
@test readdir(dirlink) == readdir(subdir)
@test map(o->o.names, Base.Filesystem._readdirx(dirlink)) == map(o->o.names, Base.Filesystem._readdirx(subdir))
@test realpath.(Base.Filesystem._readdirx(dirlink)) == realpath.(Base.Filesystem._readdirx(subdir))
@test isempty(readdir(DirEntry, dirlink))
@test isempty(readdir(DirEntry, subdir))

# relative link
relsubdirlink = joinpath(subdir, "rel_subdirlink")
reldir = joinpath("..", "adir2")
symlink(reldir, relsubdirlink)
@test stat(relsubdirlink) == stat(subdir2)
@test readdir(relsubdirlink) == readdir(subdir2)
@test Base.Filesystem._readdirx(relsubdirlink) == Base.Filesystem._readdirx(subdir2)
@test isempty(readdir(DirEntry, relsubdirlink))
@test isempty(readdir(DirEntry, subdir2))

# creation of symlink to directory that does not yet exist
new_dir = joinpath(subdir, "new_dir")
Expand All @@ -59,7 +92,7 @@ if !Sys.iswindows() || Sys.windows_version() >= Sys.WINDOWS_VISTA_VER
mkdir(new_dir)
touch(foo_file)
@test readdir(new_dir) == readdir(nedlink)
@test realpath.(Base.Filesystem._readdirx(new_dir)) == realpath.(Base.Filesystem._readdirx(nedlink))
@test realpath.(readdir(DirEntry, new_dir)) == realpath.(readdir(DirEntry, nedlink))

rm(foo_file)
rm(new_dir)
Expand Down Expand Up @@ -1444,10 +1477,10 @@ rm(dirwalk, recursive=true)
touch(randstring())
end
@test issorted(readdir())
@test issorted(Base.Filesystem._readdirx())
@test map(o->o.name, Base.Filesystem._readdirx()) == readdir()
@test map(o->o.path, Base.Filesystem._readdirx()) == readdir(join=true)
@test count(isfile, readdir(join=true)) == count(isfile, Base.Filesystem._readdirx())
@test issorted(readdir(DirEntry))
@test map(o->o.name, readdir(DirEntry)) == readdir()
@test map(o->o.path, readdir(DirEntry)) == readdir(join=true)
@test count(isfile, readdir(join=true)) == count(isfile, readdir(DirEntry))
end
end
end
Expand Down