diff --git a/docsite/source/file-system-utilities.html.md b/docsite/source/file-system-utilities.html.md index 5668cff..919def7 100644 --- a/docsite/source/file-system-utilities.html.md +++ b/docsite/source/file-system-utilities.html.md @@ -69,4 +69,7 @@ files.directory?(path) # check if path is an executable (files and directories) files.executable?(path) + +# read entries from a directory +files.entries(path) ``` diff --git a/docsite/source/index.html.md b/docsite/source/index.html.md index 7356cba..5a84983 100644 --- a/docsite/source/index.html.md +++ b/docsite/source/index.html.md @@ -99,6 +99,9 @@ file.directory?(path) # check if path is an executable (files and directories) file.executable?(path) + +# read entries from a directory +files.entries(path) ``` ### Adapters diff --git a/lib/dry/files.rb b/lib/dry/files.rb index ce385d4..6470a66 100644 --- a/lib/dry/files.rb +++ b/lib/dry/files.rb @@ -855,6 +855,18 @@ def remove_block(path, target) remove_block(path, target) if match?(content, target) end + # Reads entries from a directory + # + # @param path [String,Pathname] the path to file + # + # @raise [Dry::Files::IOError] in case of I/O error + # + # @since 1.0.1 + # @api public + def entries(path) + adapter.entries(path) + end + private # @since 0.3.0 diff --git a/lib/dry/files/file_system.rb b/lib/dry/files/file_system.rb index 7b1ab64..d52f8b2 100644 --- a/lib/dry/files/file_system.rb +++ b/lib/dry/files/file_system.rb @@ -17,6 +17,10 @@ class FileSystem # @api private attr_reader :file_utils + # @since 1.0.1 + # @api private + attr_reader :dir + # Creates a new instance # # @param file [Class] @@ -25,9 +29,10 @@ class FileSystem # @return [Dry::Files::FileSystem] # # @since 0.1.0 - def initialize(file: File, file_utils: FileUtils) + def initialize(file: File, file_utils: FileUtils, dir: Dir) @file = file @file_utils = file_utils + @dir = dir end # Opens (or creates) a new file for both read/write operations. @@ -362,6 +367,22 @@ def executable?(path) file.executable?(path) end + # Get entries from a directory + # + # @see https://ruby-doc.org/3.2.2/Dir.html#method-c-entries + # + # @param [String,Pathname] the path to list entries for + # + # @raise [Dry::Files::IOError] in case of I/O error + # + # @since 1.0.1 + # @api private + def entries(path) + with_error_handling do + dir.entries(path) + end + end + private # Catch `SystemCallError` and re-raise a `Dry::Files::IOError`. diff --git a/lib/dry/files/memory_file_system.rb b/lib/dry/files/memory_file_system.rb index d7bd573..9c8cade 100644 --- a/lib/dry/files/memory_file_system.rb +++ b/lib/dry/files/memory_file_system.rb @@ -386,6 +386,24 @@ def executable?(path) node.executable? end + # Reads entries from a directory + # + # @param path [String,Pathname] the path to file + # @return [Array] the entries + # + # @raise [Dry::Files::IOError] in case of I/O error + # + # @since 1.0.1 + # @api private + def entries(path) + path = Path[path] + node = find(path) + raise IOError, Errno::ENOENT.new(path.to_s) if node.nil? + raise IOError, Errno::ENOTDIR.new(path.to_s) unless node.directory? + + [".", ".."] + Array(node.children&.keys) + end + private # @since 0.1.0 diff --git a/lib/dry/files/memory_file_system/node.rb b/lib/dry/files/memory_file_system/node.rb index 33823e6..14a32b7 100644 --- a/lib/dry/files/memory_file_system/node.rb +++ b/lib/dry/files/memory_file_system/node.rb @@ -97,6 +97,10 @@ def self.root # @api private attr_reader :segment, :mode + # @since 1.0.1 + # @api private + attr_reader :children + # Instantiate a new node. # It's a directory node by default. # diff --git a/spec/integration/dry/files_spec.rb b/spec/integration/dry/files_spec.rb index dd3ab04..acb9481 100644 --- a/spec/integration/dry/files_spec.rb +++ b/spec/integration/dry/files_spec.rb @@ -1968,4 +1968,18 @@ class Result expect(subject.executable?(path)).to be(true) end end + + describe "#entries" do + it "returns a list of entries for a directory" do + subject.touch(root.join("file-1.txt")) + subject.touch(root.join("file-2.txt")) + + expect(subject.entries(root)).to eq [ + ".", + "..", + "file-2.txt", + "file-1.txt" + ] + end + end end diff --git a/spec/unit/dry/files/file_system_spec.rb b/spec/unit/dry/files/file_system_spec.rb index 5508d4f..aa7091d 100644 --- a/spec/unit/dry/files/file_system_spec.rb +++ b/spec/unit/dry/files/file_system_spec.rb @@ -600,4 +600,29 @@ expect(subject.executable?(path)).to be(true) end end + + describe "#entries" do + it "returns entries for directory" do + subject.touch(root.join("file-1.txt")) + subject.touch(root.join("file-2.txt")) + + expect(subject.entries(root)).to eq [".", "..", "file-2.txt", "file-1.txt"] + end + + it "returns an array with only relative paths on an empty directory" do + subject.mkdir(root.join("empty")) + + expect(subject.entries(root.join("empty"))).to eq [".", ".."] + end + + it "raises error if directory doesn't exist" do + path = root.join("non-existent") + + expect { subject.entries(path) }.to raise_error do |exception| + expect(exception).to be_kind_of(Dry::Files::IOError) + expect(exception.cause).to be_kind_of(Errno::ENOENT) + expect(exception.message).to include(path.to_s) + end + end + end end diff --git a/spec/unit/dry/files/memory_file_system_spec.rb b/spec/unit/dry/files/memory_file_system_spec.rb index d4fe7f1..29c8591 100644 --- a/spec/unit/dry/files/memory_file_system_spec.rb +++ b/spec/unit/dry/files/memory_file_system_spec.rb @@ -626,4 +626,45 @@ def call(*) expect(subject.executable?(path)).to be(true) end end + + describe "#entries" do + it "raises an error when the path does not exist" do + path = subject.join("file-1.txt") + + expect { subject.entries(path) }.to raise_error do |exception| + expect(exception).to be_kind_of(Dry::Files::IOError) + expect(exception.cause).to be_kind_of(Errno::ENOENT) + expect(exception.message).to include(path.to_s) + end + end + + it "raises an error when the path is a file" do + path = subject.join("file-1.txt") + subject.touch(path) + + expect { subject.entries(path) }.to raise_error do |exception| + expect(exception).to be_kind_of(Dry::Files::IOError) + expect(exception.cause).to be_kind_of(Errno::ENOTDIR) + expect(exception.message).to include(path.to_s) + end + end + + it "returns entries when the path is a directory" do + subject.touch(subject.join("file-1.txt")) + subject.touch(subject.join("file-2.txt")) + + expect(subject.entries(subject.join)).to eq [ + ".", + "..", + "file-1.txt", + "file-2.txt" + ] + end + + it "returns an array with only relative paths on an empty directory" do + subject.mkdir("empty") + + expect(subject.entries(subject.join("empty"))).to eq [".", ".."] + end + end end