As you suspect, this can be done in fewer lines, but it's not a one-liner.
use strict;
use warnings;
use diagnostics;
Always a good idea. Consider use warnings FATAL => "all"
so that you don't miss any.
my $filename = <STDIN>;
A command-line argument or environment variable is the typical way to do this, in Perl and most other languages. Directory variables oughtn't be named $filename
.
my $dir = ( shift or $ENV{TIMELAPSE_DIRECTORY} or die "usage: $0 directory\n" );
chdir $filename or die "Couldn't change the directory: $!\n";
…
rename ($file, $new_name) or die "Couldn't rename the file: $!";
The fat-arrow =>
can replace a comma and improve readability in some cases. Dropping parens is another good readability boost, when done judiciously. Unless the line number where die
occurred is interesting, suppress it by appending a newline to the message. And finally, it's a good habit to include in error messages the data that produced them, as in:
rename $file => $new_name or die "Couldn't rename '$file' to '$new_name': $!\n";
my @files = <*>;
#gives a list of all filehandles in the given directory
Filenames, not filehandles. It would be reasonable to filter this list to contain actual files, excluding directories:
my @files = grep -f, <*>;
And maybe even by name:
my @files = grep { -f and /\.( png | jpe?g | tga )$/xi } <*>;
my $amount_of_digits = int( log($#files)/log(10) +1);
$#files
is the largest index in that zero-based array. With 10 files, the last is $files[9]
and the math returns 1
instead of the 2
we need. The size of the array is one bigger and retrieved as @files
in scalar context (log()
imposes scalar context for us, which is convenient).
width
is a good name for this variable.
Avoid an uncaught exception by checking that @files
is non-empty.
And the parens around int
can be omitted.
die "nothing to do!\n" unless @files;
my $width = int log(@files)/log(10) + 1;
But this is still no good! log(1000)/log(10)
is 3
in Perl and in real life. But int( log(1000)/log(10) )
is 2
! This happens because floating-point math is imperfect. Luckily Perl will let us cheat by taking the length of a number that's been silently converted to a string:
my $width = length scalar @files;
foreach my $file (@files){
for
is the idiomatic alternative to foreach
.
if ($file =~ /(\d+)/){
my $amount_of_padded_zeroes = $amount_of_digits - length($1);
if($amount_of_padded_zeroes > 0){
my $new_number = '0' x $amount_of_padded_zeroes . $1;
(my $new_name = $file) =~ s/$1/$new_number/;
print "Changing name too: $new_name\n";
sprintf
is the function to use for leading zeroes. It can go right in the s///
replacement by using the /e
xecute modifier. If the replace fails, skip to next
file.
(my $new_name = $file) =~ s/(\d+)/ sprintf "%0${width}d" => $1 /e or next;
rename ($file, $new_name) or die "Couldn't rename the file: $!";
It's good practice to ensure you have something to do, and that you aren't overwriting an existing file here:
next if $new_name eq $file;
die "$new_name (from $file) already exists!\n" if -f $new_name;
print "Program completed. Press any key to continue.\n";
my $einde = <STDIN>;
I think you know how I feel about this. Should you decide to keep it, only the Enter key will actually proceed.
Putting it all together:
use strict;
use warnings FATAL => 'all';
use diagnostics;
my $dir = ( shift or $ENV{TIMELAPSE_DIRECTORY} or die "usage: $0 directory\n" );
chdir $dir or die "Couldn't change directory to $dir: $!\n";
my @files = grep { -f and /\.( png | jpe?g | tga )$/xi } <*>;
die "nothing to do!\n" unless @files;
my $width = length scalar @files;
for my $file (@files) {
(my $new_name = $file) =~ s/(\d+)/ sprintf "%0${width}d" => $1 /e or next;
next if $new_name eq $file;
die "$new_name (from $file) already exists!\n" if -f $new_name;
rename $file => $new_name or die "Couldn't rename '$file' to '$new_name': $!\n";
}
print "Program completed.\n"
Reader exercise: improve this program to number the files from 1 to n, even if the original numbers don't start at 1 or have gaps!