http://www.perlmonks.org?node_id=302113
Category: Utility Scripts
Author/Contact Info /msg Aristotle
Description:

Update: obsolete, please check rename 0.3 - now with two extra cupholders instead.

You probably know the script that comes with Perl. Initially, I started hacking on it because I didn't want to pull out the old rename binary for very simple substitutions, but found it too cumbersome to write a Perl s/// for the same job. Then, feeping creaturism set in and I started adding more and more little stuff.. eventually, it grew to something I wouldn't want to miss from life on the command line.

#!/usr/bin/perl
use strict;
use warnings;

=head1 NAME

rename - renames multiple files

=head1 SYNOPSIS

 rename [ -0 ] [ -d ] [ -f ] [ -v ] perlexpr [ files ]
 rename -s [ -0 ] [ -d ] [ -f ] [ -g ] [ -v ] from to [ files ]
 rename -z [ -0 ] [ -d ] [ -f ] [ -v ] [ -z ] [ -z ] [ files ]
 rename -h

=head1 DESCRIPTION

C<rename> renames the filenames supplied according to the rules specif
+ied. If a given filename is not modified, it will not be renamed. If 
+no filenames are given on the command line, filenames will be read vi
+a standard input.

For example, to translate uppercase names to lower, you'd use

 rename 'y/A-Z/a-z/' *

To rename all files matching C<*.bak> to strip the extension, you migh
+t say

 rename -s .bak '' *.bak

Although if any of the files has C<.bak> in another part of its filena
+me as well, you'll have to resort to

 rename 's/\.bak$//' *.bak

If you have a directory full of inconveniently named files, you can us
+e C<-z> to clean them up for you.

=head1 ARGUMENTS

=over 4

=item B<-h>, B<--help>

Browse the manpage.

=item I<nothing>

A C<perlexpr> parameter is expected, which should be a Perl expression
+ that assumes the filename in the C<$_> variable and modifies it for 
+the filenames to be renamed.

=item B<-s>, B<--subst>, B<--simple>

Perform a simple textual substitution of C<from> to C<to>. The C<from>
+ and C<to> parameters must immediately follow the argument.

This is equivalent to supplying a C<perlexpr> of C<s/\Qfrom/to/;>.

=item B<-z>, B<--sanitize>

If you specify this option once, (consecutive) blanks in filenames wil
+l be replaced by underscores. If you specify it twice, control charac
+ters in filenames will also be substituted by underscores. If you spe
+cify it thrice, filenames will also be converted to all lowercase.

=back

=head1 OPTIONS

=over 4

=item B<-0>, B<--null>

When reading file names from C<STDIN>, split on null bytes instead of 
+newlines. This is useful in combination with GNU find's C<-print0> op
+tion, GNU grep's C<-Z> option, and GNU sort's C<-z> option, to name j
+ust a few. B<Only valid if no filenames have been given on the comman
+dline.>

=item B<-d>, B<--dryrun>

Show how the files would be renamed, but don't actually do anything.

=item B<-f>, B<--force>

Rename even when a file with the destination name already exists.

=item B<-g>, B<--global>

In simple mode, substitute all occurences of the string given instead 
+of only the first one. B<Only valid if you specified C<-s>.>

=item B<-v>, B<--verbose>

Print additional information about the operations executed.

=back

=head1 ENVIRONMENT

No environment variables are used.

=head1 AUTHORS

Larry Wall, Robin Barker, Aristotle Pagaltzis

=head1 SEE ALSO

mv(1), perl(1), find(1), grep(1), sort(1)

=head1 DIAGNOSTICS

If you give an invalid Perl expression you'll get a syntax error.

=head1 BUGS

None currently known.

=cut

use Pod::Usage;
use Getopt::Long;

Getopt::Long::Configure('bundling', 'no_ignore_case');
GetOptions(
    'h|help'         => \my $opt_help,
    's|subst|simple' => \my $opt_simple,
    'z|sanitize+'    => \my $opt_sanitize,
    '0|null!'        => \my $opt_null,
    'f|force!'       => \my $opt_force,
    'g|global!'      => \my $opt_global,
    'd|dryrun!'      => \my $opt_dryrun,
    'v|verbose!'     => \my $opt_verbose,
) or pod2usage( -verbose => 1 );

pod2usage( -verbose => 2 ) if $opt_help;

pod2usage( -verbose => 1 ) if $opt_global and not $opt_simple;

sub DEBUG { print "@_\n" if $opt_verbose }
sub INFO  { print "@_\n" if $opt_verbose or $opt_dryrun }
sub ERROR { print "@_\n" }

my $code =
    $opt_simple ? do {
        pod2usage( -verbose => 1 ) if @ARGV < 3;
        my $from = shift @ARGV;
        my $to = shift @ARGV;
        $opt_global
            ? sub { s/\Q$from/$to/ }
            : sub { s/\Q$from/$to/g };
    } :
    $opt_sanitize ? sub {
        s/[_[:blank:]]+/_/g;
        s/[_[:cntrl:]]+/_/g if $opt_sanitize > 1;
        s/([[:upper:]]+)/\L$1/g if $opt_sanitize > 2;
    } :
    do {
        my $perlexpr = shift;
        pod2usage( -verbose => 1 ) if not defined $perlexpr;
        my $evaled = eval "sub { $perlexpr }";
        die $@ if $@;
        $evaled;
    };

pod2usage( -verbose => 1 ) if $opt_null and @ARGV;

if (!@ARGV) {
    INFO("Reading filenames from STDIN");
    @ARGV = do {
        if($opt_null) {
            INFO("Splitting on null bytes");
            local $/ = "\0";
        }
        <STDIN>;
    };
    chomp @ARGV;
}

for (@ARGV) {
    my $oldname = $_;

    $code->();

    if($oldname eq $_) {
        DEBUG("'$oldname' unchanged");
        next;
    }

    WARN("'$oldname' not renamed: '$_' already exists"), next
        if not $opt_force and -e;

    if ($opt_dryrun or rename $oldname, $_) {
        INFO("'$oldname' renamed to '$_'");
    }
    else {
        ERROR("Can't rename '$oldname' to '$_': $!");
    }
}

INFO('Dry run, no changes were made.') if $opt_dryrun;
Replies are listed 'Best First'.
Re: rename - an improved version of the script which comes with Perl
by graff (Chancellor) on Oct 26, 2003 at 01:54 UTC
    That is sweet -- much more flexible/usable (in terms of renaming files) than the home-grown script that I wrote for myself years ago to do sort of the same thing. (But then, my script is actually meant to do anything you want with files or any list, not just renaming.)

    Just one squib about your choice of semantics for the "-z" option: downcasing names is already pretty easy (in fact, I assume you can specify it separately, while still using "-z"). But something that's not so easy, and that comes up all too often, is whole bunches of file names that happen to include characters like  [<>|!=\&\$\#\(\)\[\]\{\}\\] (probably others as well), which tend to have special meanings in shell commands. When the unwitting shell user happens to do a quick "copy/paste" of such a name onto a command line, all hell breaks loose.

    (I've seen file names like this created by "wget -r ..." as well as by "innovative" windows users putting files with "helpful" names onto volumes that are used by both samba and nfs.)

    So, since there's already a way to downcase, but removing shell metacharacters can be an important issue in "sanitizing", how about the thrice-used "-z" handling the latter instead? (Note that a space really just another shell metacharacter -- so it makes sense to handle other such characters with the same option.)

    (BTW, does "thrice used" mean "-zzz" or "-z -z -z"? I guess with the longer args it's an easier guess: "--sanitize --sanitize --sanitize", but that's a lot to put on the command line...)

      You're right about the shell metacharacters - and their substitution should really happen along with the control characters. I don't know why I thought [:cntrl:] would include them, it logically doesn't. To me the space is more than just another metacharacter - it breaks more things than the others. (Trivially fixed, contrived example: for F in * ; do ls $F ; done)

      You can bundle short options, as you can see from the source - Getopt::Long::Configure('bundling', 'no_ignore_case');. But you can't specify downcasing independently - that's why I included it by having a third -z. Besides, I kinda liked the -zzz visual.. maybe I should add a -Z option as well? ;^) Now that I think about it though, maybe sanitizing names should indeed not be a simple option, not a mode.

      I also discovered another independent derivative of Larry's rename script on CPAN just after I posted this one, with rather different goals (and pretty messy innards).

      So there's plenty of things to move around and more yet to add. Expect a New And Improved version in the near future. :) (For now, this version scratches me quite excellently, and I have other things to do, so the pressure is low.)

      Makeshifts last the longest.