2

What is the Perl way of removing a directory and then all empty parent directories up to the first non-empty one? In other words, what could one use instead of:

system 'rmdir', '-p', $directory;

which, in starting with d, would first remove d and then c, and then b, but not a, since a would still contain x, like this:

a
a/b
a/b/c
a/b/c/d
a/x

resulting in

a
a/x

It's not the built-in rmdir, as it can only remove a single directory. (doc)

It's not finddepth ( sub {rmdir}, '.' ), using File::Find, as it removes children, not parents. (doc)

It's not the remove_tree function of the File::Path module either, since it not only removes children directories but files as well. (doc)

Notice, remove_tree and finddepth work in the opposite direction of Bash rmdir --parent.

n.r.
  • 1,900
  • 15
  • 20

3 Answers3

5
use Path::Tiny qw( path );

my $p = path('a/b/c/d');

while (!$p->is_rootdir()) {
    if (!rmdir($p)) {
        last if $!{ENOTEMPTY};
        die("Can't remove \"$p\": $!\n");
    }

    $p = $p->parent;
}

Notes:

  • Efficient. By checking the result of rmdir instead of using ->children or ->iterator, this solution is avoids needless calls to readdir.

  • No race conditions. Unlike the solutions that use readdir (via ->children or ->iterator), this solution doesn't suffer from a race condition.

  • This solution also avoids the redundant -d check used by an earlier solution.

  • This solution, unlike those before it, will handle a tree that's empty except for the directories to remove.

ikegami
  • 367,544
  • 15
  • 269
  • 518
4

AFAIK this doesn't exist. You can write it yourself fairly easily with Path::Tiny. It's a simple recursive function.

#!/usr/bin/env perl

use strict;
use warnings;
use v5.10;

use Carp;
use Path::Tiny;
use autodie;

sub Path::Tiny::rmdir_if_empty {
    my $self = shift;

    # Stop when we reach the parent.
    # You can't rmdir('.') anyway.
    return if $self eq '.';

    croak "$self is not a directory" if !$self->is_dir;

    # Stop if the directory contains anything.
    # I use an iterator to avoid a possibly very long list.
    my $iter = $self->iterator;
    return if defined $iter->();

    # rmdir will not delete a non-empty directory, a second safeguard
    rmdir $self;

    return $self->parent->rmdir_if_empty;
}

path("a/b/c/d")->rmdir_if_empty;
Schwern
  • 153,029
  • 25
  • 195
  • 336
  • It would stop at `a`. – n.r. Apr 13 '17 at 20:53
  • @n.r. Turns out it would stop, but only because `rmdir('.')` fails. I fixed it to stop at the root of the given path, empty or not. Now `path("c/d")->rmdir_if_empty` won't delete `b`. – Schwern Apr 13 '17 at 21:06
  • @ikegami To make the recursion work better, and because Path::Tiny doesn't subclass well (peek inside `Path::Tiny->new` to see why). Adding a method is safe-ish, safer than altering an existing one. You can do it as a function. YMMV. – Schwern Apr 13 '17 at 21:08
  • 1
    The non-intrusive solution (`sub rmdir_if_empty { ... return rmdir_if_empty($self->parent); } rmdir_if_empty(path("a/b/c/d"));`) recurses just as easily. Noone suggested altering an existing method. – ikegami Apr 13 '17 at 21:10
  • Needless `readdir`. Race condition (from the use of `readdir`). Doesn't handle a drive that just contains `a/b/c/d`. – ikegami Apr 13 '17 at 21:24
  • @ikegami Can you provide any details about those assertions? Ahh, I see your answer now, very clever! – Schwern Apr 13 '17 at 21:25
3

As always, using my favorite Path::Tiny

use 5.014;
use warnings;
use Path::Tiny;
use autodie;

my $p = path('a/b/c/d');     # starting
die "$p is not a dir" unless -d $p;

while( ! $p->children ) {    # if it is empty
        rmdir $p;            # remove it
        $p = $p->parent;     # go upward
}
clt60
  • 62,119
  • 17
  • 107
  • 194
  • 1
    Needless initial `-d` (since `->children` will perform the same check). Needless `readdir`. Race condition (from the use of `readdir`). Doesn't handle a drive that just contains `a/b/c/d`. – ikegami Apr 13 '17 at 21:24