melguin has asked for the wisdom of the Perl Monks concerning the following question:

I was recently looking for a script to recursively change all files and directories to lowercase. Unfortunately, I use this only often enough to where when I need it again, I'm at a different job and don't have my old machine (and thus the script I found or re-wrote). This time I found an old snippet by the honorable vroom and modified it a bit to be recursive and non-interactive:

#!/usr/bin/perl -w use strict; my $base = shift @ARGV; $base ||= "."; lc_filenames($base); sub lc_filenames{ my($dir)=@_; $dir||="."; opendir DIR, $dir; while(defined(my $file=readdir DIR)){ if ($file =~ /^\.+$/) {next} if(-f ("$dir/$file")){ if($file=~/[A-Z]/){ my $newname=lc($file); rename("$dir/$file","$dir/$newname"); } } elsif(-d ("$dir/$file")){ if($file=~/[A-Z]/){ my $newname=lc($file); rename("$dir/$file","$dir/$newname"); $file = $newname; } lc_filenames("$dir/$file"); } } }
My problem is that once the subroutine calls itself then returns, it doesn't seem to continue reading the directory. I think I ran into this a long while back, but I can't remember for the life of me what the problem is.

Replies are listed 'Best First'.
Re: problems returning from recursive subroutine
by benn (Vicar) on Apr 18, 2003 at 09:01 UTC
    the problem is that your readdir loop is getting trashed by the re-entrant sub - each time you recurse, you lose the previous directory loop. Something like this works (tested too!)...
    #!/usr/bin/perl -w use strict; lc_filenames(shift @ARGV); sub lc_filenames{ my($dir)=@_; $dir||="."; opendir DIR, $dir; my @files = readdir DIR; closedir DIR; foreach my $file(@files) { next if ($file =~ /^\.+$/); lc_filenames("$dir/$file") if(-d ("$dir/$file")); rename("$dir/$file", "$dir/".lc($file)) if($file=~/[A-Z]/); } }
    Cheers, Ben
Re: problems returning from recursive subroutine
by Jenda (Abbot) on Apr 18, 2003 at 09:37 UTC

    The DIR handle is GLOBAL. Therefore the recursive call clobbers (overwrites) the "parent"'s dirhandle so when the call returns the dirhandle is at the end of the list and therefore cannot return anything.

    You have to add

    local *DIR;
    into the subroutine, above the opendir or use a lexical dirhandle:
    sub lc_filenames{ my($dir)=@_; $dir||="."; opendir my $DIR, $dir; while(defined(my $file=readdir $DIR)){ ...
    This syntax will not work in older Perls! You may need to add use FileHandle; and declare the $DIR above the opendir like this:
    my $DIR; opendir $DIR, $dir;

    Another nit. You do not close the dirhandle! You should!

    Jenda
    Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.
       -- Rick Osborne

    Edit by castaway: Closed small tag in signature

      You do not close the dirhandle! You should!

      Why? Perl will close the handle automatically when it goes out of scope. Closing it yourself only makes sense if you are interested in the return value, and are going to do something different depending on the value. Would you expect a close() to fail, and if it fails, what action would you take?

      Abigail

        I would offer that the reason to explicitly close everything is to tell your maintainer that you've done so.

        Another, more important, reason is the same reason I always put the trailing comma in a list of stuff - what if I add more stuff?!? Then, the implicit close happens later, and that may not be good.

        Of course, best, in my opinion, is to limit your exposure to connections like handles and $sth's to as small a block as possible, just in case.

        ------
        We are the carpenters and bricklayers of the Information Age.

        Don't go borrowing trouble. For programmers, this means Worry only about what you need to implement.

        Please remember that I'm crufty and crochety. All opinions are purely mine and all code is untested, unless otherwise specified.

        In fact close can indeed fail because, for instance, the disk is full and so it cannot flush buffers. In which case you probably want to report that and possibly want to stop processing. And closing a pipe can give you all sorts of error information.

        Also according to the documentation for 5.6.1, an explicit close resets $. while an implicit one due to a following open does not. If you are reporting $. and want that to be accurate, then it is better to do an explicit close whether or not you pay attention to its return value.

•Re: problems returning from recursive subroutine
by merlyn (Sage) on Apr 18, 2003 at 11:11 UTC
    And of course, the proper way to do this is to use File::Find. Yours improperly follows symlinks, which means you could get in to an infinite loop if the symlink points to a directory somewhere upward in the tree.

    Using proper technology, your code becomes simply:

    use File::Find; finddepth sub { return if /^\./; my $new = lc $_; return if $new eq $_ or -e $new; rename $_, $new or warn "Cannot rename $File::Find::name to $File::F +ind::dir/$new: $!"; }, $ARGV[0] || ".";

    -- Randal L. Schwartz, Perl hacker
    Be sure to read my standard disclaimer if this is a reply.

      Without knowing more about the context, it's hard to say whether following symlink is appropriate or not. For all we know, your solution is wrong because it improperly does not follow symlinks, which means part of the directory structure isn't updated.

      Abigail

        If symlinks must be followed, then still the safest is to use modern technology:
        use File::Find; finddepth { wanted => sub { return if /^\./; my $new = lc $_; return if $new eq $_ or -e $new; rename $_, $new or warn "Cannot rename $File::Find::name to $File: +:Find::dir/$new: $!"; }, follow => 1, }, $ARGV[0] || ".";

        -- Randal L. Schwartz, Perl hacker
        Be sure to read my standard disclaimer if this is a reply.

Re: problems returning from recursive subroutine
by Abigail-II (Bishop) on Apr 18, 2003 at 08:46 UTC
    I don't see anything obviously wrong, but perhaps the rename or opendir are failing. You aren't checking, just assuming it works. Note also that you are modifying the directory while looping over it - this might give unexpected results on your OS. Better to read all entries of the directory into an array right after opening the directory, and to loop over that.

    And always check the return values of your system calls.

    Abigail

Re: problems returning from recursive subroutine (file::find instead)
by Aristotle (Chancellor) on Apr 18, 2003 at 11:21 UTC
    As long as it's the results you're after, I wouldn't bother debugging it when a working alternative is so simple to write.
    use File::Find; push @ARGV, '.' unless @ARGV; finddepth(sub { my $new = lc; return if $new eq $_; rename $_, $new unless -e $new; }, @ARGV);
    Using finddepth rather than find is important because directories will otherwise be renamed before File::Find (at that point futily) attempts to recurse into them.

    Makeshifts last the longest.

      rename $_, $new unless -e $new;
      Funny ;D On some FSs, -e 'foo' == -e 'Foo', so you should take that out. Besides, it's redundant on FSs that are case sensitive.

      update: heh, here's a different approach then

      use File::Find; @ARGV ||= '.'; find( { preprocess => \&pp }, @ARGV ); sub pp { for(@_){ my $new = lc $_; next if $new eq $_; unless(-e $new){ rename $_, $new; $_ = $new; } } return @_; }


      MJD says you can't just make shit up and expect the computer to know what you mean, retardo!
      I run a Win32 PPM repository for perl 5.6x+5.8x. I take requests.
      ** The Third rule of perl club is a statement of fact: pod is sexy.

        It's not redundant on filesystems that are case sensitive, because then you may have both a file named Foo and foo. Rename Foo to foo would destroy the original content of foo.

        The program itself would be redundant on case insensitive file systems.

        Abigail

        $ perl -e '@ARGV ||= "."' Can't modify array dereference in logical or assignment (||=) at -e li +ne 1, at EOF
        Don't get your contexts mixed up. The left side of a logical operator is always in scalar context. Also don't forget - rename may fail, so if you're going to return the new filename you should be sure the renaming succeeded. Overall this seems more like a job for map:
        sub pp { map { my $new = lc $_; ($new eq $_ or -e $new) ? $_ : rename($_, $new) ? $new : $_; } @_; }

        Makeshifts last the longest.

Re: problems returning from recursive subroutine
by zby (Vicar) on Apr 18, 2003 at 08:02 UTC
    If the directory did indeed have some uppercase letters than this call: lc_filenames("$dir/$file"); would process a nonexisting directory (it is after you renamed the "$dir/$file").
      Wouldn't the line above it:
      $file = $newname;
      solve that by making $file (which in this case is a directory) the lowercase version? Or perhaps there's something else I'm not seeing.
        Ah - sorry!