From 88e2a62b49b60c8606f729193b9a9ec5ebfd734a Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Fri, 2 Aug 2024 22:00:11 -0400 Subject: [PATCH] Add `DirEntry`-based `readdir` methods Co-Authored-By: Ian Butterworth Co-Authored-By: Simeon David Schaub --- NEWS.md | 3 ++ base/exports.jl | 1 + base/file.jl | 35 ++++++++++++++++----- doc/src/base/file.md | 1 + stdlib/REPL/src/REPLCompletions.jl | 9 +++--- stdlib/REPL/src/docview.jl | 4 +-- test/file.jl | 49 +++++++++++++++++++++++++----- 7 files changed, 79 insertions(+), 23 deletions(-) diff --git a/NEWS.md b/NEWS.md index 2e9b32befe342..ff8ce6475a025 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 ------------------------ diff --git a/base/exports.jl b/base/exports.jl index dbe12f933e597..791b40d8539ff 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -836,6 +836,7 @@ export close, closewrite, countlines, + DirEntry, eachline, readeach, eof, diff --git a/base/file.jl b/base/file.jl index e1b8e8a748fae..c92a65b92892f 100644 --- a/base/file.jl +++ b/base/file.jl @@ -8,6 +8,7 @@ export chown, cp, cptree, + DirEntry, diskstat, hardlink, mkdir, @@ -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 @@ -983,13 +993,14 @@ 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 @@ -997,13 +1008,23 @@ returned objects without further stat calls. However, for some filesystems, the 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 @@ -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}() diff --git a/doc/src/base/file.md b/doc/src/base/file.md index 22799f882bb26..81cf8aa059b9b 100644 --- a/doc/src/base/file.md +++ b/doc/src/base/file.md @@ -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 diff --git a/stdlib/REPL/src/REPLCompletions.jl b/stdlib/REPL/src/REPLCompletions.jl index 609a7b4d81bc0..a62bef4549320 100644 --- a/stdlib/REPL/src/REPLCompletions.jl +++ b/stdlib/REPL/src/REPLCompletions.jl @@ -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 @@ -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) @@ -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 @@ -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) diff --git a/stdlib/REPL/src/docview.jl b/stdlib/REPL/src/docview.jl index 5086aa0c9485c..d9204f25e1cab 100644 --- a/stdlib/REPL/src/docview.jl +++ b/stdlib/REPL/src/docview.jl @@ -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 @@ -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 diff --git a/test/file.jl b/test/file.jl index f82b2a0fd8f39..8f4f2e1d7ac5a 100644 --- a/test/file.jl +++ b/test/file.jl @@ -26,13 +26,45 @@ 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") @@ -40,7 +72,8 @@ if !Sys.iswindows() || Sys.windows_version() >= Sys.WINDOWS_VISTA_VER 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") @@ -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) @@ -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