Project

General

Profile

Actions

Feature #22312

open

Add parent directory operations and recursive file removal

Feature #22312: Add parent directory operations and recursive file removal
1

Added by nobu (Nobuyoshi Nakada) 7 days ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:126687]

Description

I propose extending Dir.mkdir, Dir.rmdir, and File.unlink with options for operating on directory trees. These operations currently require FileUtils; providing them in core would also allow recursive removal to improve robustness by using native directory descriptors or Windows handles where available.

Proposed API

Dir.mkdir(path, permissions = 0777, perm: permissions, parents: false)
Dir.rmdir(path, parents: false, ignore_non_empty: false)
File.unlink(*paths, recursive: false)

The aliases Dir.delete, Dir.unlink, and File.delete receive the corresponding options. Calls without the new options retain their existing behavior.

Creating parent directories

Dir.mkdir(path, parents: true) creates missing parent directories and returns 0 if the target is already a directory. An existing non-directory raises an exception.

Dir.mkdir("build/cache/objects", parents: true, perm: 0700) # => 0

The requested permissions apply only to the final directory. New parents use 0777, subject to the process umask, and existing directories keep their permissions. Permissions remain ignored on Windows.

Removing empty parent directories

Dir.rmdir(path, parents: true) removes the target and then successive empty parents. It does not remove a filesystem root or the relative parent path .. Its return value remains 0.

The ignore_non_empty: option controls when removal stops successfully:

Value Target directory Parent directories
false (default) Raise if non-empty Raise if non-empty
true Stop successfully if non-empty Stop successfully if non-empty
:parents Require successful removal Stop successfully if non-empty or no longer present

Other errors raise exceptions. :parents matches the error handling of FileUtils.rmdir(path, parents: true) and has no effect without parents: true. Removal already completed is not rolled back on an error.

Dir.rmdir("build/cache/objects", parents: true, ignore_non_empty: :parents)

Parent removal currently uses path-based calls to rmdir, resolving the path again at each step. It does not guarantee protection against concurrent renames or symbolic-link substitution in intermediate path components. Such changes can redirect removal to unintended empty directories that the process has permission to remove. Avoid using this option where another process can modify the path concurrently.

Retaining descriptors for all ancestors would require a number of descriptors proportional to the path depth. Walking upward with openat(fd, "..") would bound descriptor usage, but could follow a directory into a different parent after a concurrent rename. The current proposal retains path-based parent removal with the limitation described above.

Recursive removal

File.unlink(*paths, recursive: true) also removes directories and their contents. It returns the number of supplied paths, rather than the number of entries removed.

File.unlink("build/cache", recursive: true) # => 1

Symbolic links at the target, including targets specified with trailing slashes, and links inside the tree are removed without traversing their targets in the absence of concurrent replacement. Filesystem roots and paths ending in . or .. are rejected.

Like FileUtils.rm_r with its default force: false, this option does not suppress errors: missing paths and other failures raise exceptions, possibly after partial removal.

The implementation uses descriptor-relative operations, including openat and unlinkat, on platforms supporting them. Opened directories remain accessible through their descriptors if renamed during traversal, so their contents may still be removed after a rename. Concurrent modifications can cause an exception; the operation is not atomic. Mounted filesystems inside the tree are traversed on non-Windows platforms.

On Windows, the implementation retains directory handles with sharing disabled and removes entries through their handles. Reparse points in the ancestor path are rejected; reparse points at or inside the target are removed without traversal. Existing handles that conflict with exclusive access cause an error.

On other platforms, a path-based fallback behaves similarly to FileUtils.remove_entry. It cannot prevent traversal through a symbolic link substituted concurrently for a directory, so it is unsuitable for trees another process can modify concurrently.

Possible force: option

An additional force: false keyword for File.unlink may be useful for cleanup operations. Combined with recursive: true, it could cover use cases currently served by FileUtils.rm_rf. This option is not implemented in the current patch.

The scope of error suppression needs discussion: should force: true ignore only missing paths, or suppress removal errors more broadly as FileUtils.rm_r(force: true) does? Its applicability without recursive: true and the return value when some supplied paths cannot be removed also need to be defined.

Implementation

Implementation branch

No data to display

Actions

Also available in: PDF Atom