6.7 Deleting Files Recursively
We now write a program that deletes files with specified extensions recursively from a list of files and directories given to it as command-line argument. The reader should read carefully the recursive programs given earlier in the chapter to follow this program. The program we write removes only regular files. It does not remove directories. In other words, after the program executes, the directories still exist although the files are all removed recursively. One may write an extension of this program that deletes a directory if all the files and sub-directories in it have been deleted. It is left as an exercise.
Program 6.11
#!/usr/bin/perl
use strict;
my @extensions = qw(dvi log aux);
#Return 1 if $file has an extension in the list @exts
#Otherwise, return 0
sub hasExtension{
my ($file, @exts) = @_;
my $ext;
foreach $ext (@exts){
if ($file =~ /\.$ext$/){
return 1;
}
}
return 0;
}
#delete files recursively
sub deleteR{
my @FDList = @_;
my $first = shift @FDList;
if (!$first){
return ();
}
elsif (-f $first and hasExtension($first, @extensions)){
unlink $first;
}
elsif (-d $first){
opendir DIR, $first or warn "Cannot open directory $first: $!";
my @files = readdir DIR;
closedir DIR;
@files = grep {$_ !~ /^[.]{1,2}$/} @files;
my @simpleFiles = grep -f, (map {"$first/$_"} @files);
my @directories = grep -d, (map {"$first/$_"} @files);
map {if (hasExtension ($_, @extensions)){
print "Deleting $_\n";
unlink $_;
}
} @simpleFiles;
map {deleteR ($_)} @directories;
}
}
#######main program
my @FDToDelete = @ARGV;
if (!@FDToDelete){ @FDToDelete = ".";}
map {deleteR $_} @FDToDelete;
The main part of the program is quite simple. If the program is not called with command-line arguments, the program looks at the current directory or the . directory. The main program maps the deleteR subroutine on every file given to it as a command-line argument, or ., otherwise.
The program has two subroutines: hasExtension and deleteR. hasExtension is a helper subroutine that returns true if the name of a file given to it as argument has an extension specified in the variable @extensions that is global to the whole program.
The main work is done in the subroutine deleteR. The structure of the recursive subroutine is similar to that of one of the recursive routines we have seen earlier in the chapter.
The subroutine deleteR is called with a list of files and directories. This list is called @FDList inside the subroutine. The termination condition for this recursive subroutine occurs if @FDList is empty. Otherwise, the subroutine looks at the first element of @FDList and does different things based on whether it is a file or a directory. This first element is called $first. If $first is a file and has one of the extensions specified as undesirable, the program unlinks the file. If it is a file and does not have such an extension, we do not do anything.
If $first is a directory, we opendir it, and read the list of files in it. From this list of files we remove . and .. that correspond to the current and the parent directories, respectively. We then separate out the files and the directories in the current directory into the lists @simpleFiles and @directories, respectively. In applying the
-f and -d tests to check if it is a file or a directory, we must provide the qualified name of the file with respect to the current directory. That is why we prepend the names with $first/ when we map the -f and -d tests on the list of files and directories.
Next, the subroutine looks at the list of files called @simpleFiles and maps a block of statements on this list. This block of statements deletes a file if it has one of the pre-specified extensions. Finally, the subroutine calls itself recursively on every directory. This is done by using map as given below.
map {deleteR ($_)} @directories;
The program prints the names of files it deletes to inform the user. It may be a good idea to modify this deleteR subroutine so that it asks the user for confirmation before deleting a file.