jlh's assorted stuff

Pretty random

Posts Tagged ‘rename’

Directory renamer script (dirrename)

Saturday, October 18th, 2008

Did you ever have to rename many files at once, using a certain pattern? For example rename “DSCXXX.JPG” to “Picture XXX.jpg”, where XXX goes from 001 to 123? Of course you could write a shell one-liner to do the job, but it’s much simpler and quicker if you can load the list of file names into your favourite text editor and then use standard editor features (search & replace) to rename the files. This is exactly what this script does: You call it without arguments and it loads all file names of the current directory into $EDITOR, let’s you edit them, and when you’re done, it renames all files accordingly. It’s simple, yet powerful, even though it lacks many bells and whistles yet. It correctly handles cycles, so you can exchange two file names if you like.

For convenience, copy the following into /usr/local/bin/dirrename and make it executable:

#!/usr/bin/perl -w

# Copyright (c) 2008 jlh (jlh at gmx dot ch)
#
# This bit of code is released under the GPL version 2 or (at your option) any
# later version.

use strict;
use warnings;

my $get_tmp_name_n = 0;

sub get_tmp_name {
	my $tmp;

	while (1) {
		$get_tmp_name_n++;
		$tmp = ".__dirrename_tmp_${get_tmp_name_n}__";
		last if !-e $tmp;
	}

	return $tmp;
}

# check if we have write access to .

if (!-w '.') {
	print "I do not have write access to this directory.\n";
	exit 1;
}

# read the list of files to rename

opendir my $h, '.' or die;
my @list = sort grep { !/^\./ } readdir $h;
closedir $h;

# write it to a file

my $tmp = "/tmp/pid.$";
open $h, '>', $tmp or die;
print $h join("\n", @list), "\n";
close $h;

# let the user edit it

my $editor = $ENV{EDITOR} || 'vi';
system("$editor '$tmp'");

# read it back in

open $h, '<', $tmp or die;
my @newlist = map { my $a = $_; chomp $a; $a; } <$h>;
close $h;
unlink $tmp;

# compare

if (@list != @newlist) {
	print "Number of lines has been changed!";
	exit 1;
}

# check for errors

my $error = 0;

my %from;
$from{$_}++ for @list;
my %to;
$to{$_}++ for @newlist;

for (keys %to) {
	if ($to{$_} > 1) {
		print "error: more than one file wants to rename to '$_'\n";
		$error = 1;
	}
	if (!exists $from{$_} and -e $_) {
		print "error: target exists already: $_\n";
		$error = 1;
	}
}

exit $error if $error;

# build a trivial strategy to rename the file (rename all files to a temporary
# name, then all temporary names are renamed to the target names; this avoids
# cycles and dependencies)

my @strategy;

for (0 .. $#list) {
	my ($old, $new) = ($list[$_], $newlist[$_]);
	next if $old eq $new;
	my $tmp = &get_tmp_name;
	unshift @strategy, [ $old, $tmp ];
	push @strategy, [ $tmp, $new ];
}

# now optimize this strategy.  this avoids unnecessary renaming but takes more
# time to plan.  it would probably be faster without doing this, but it was fun
# to write it.  :)

while (1) {

	my $done_something = 0;

	for (my $i = 0; $i < @strategy; $i++) { # regular loop because we splice @strategy in here
		my $target = $strategy[$i][1];
		my $j = undef;
		for ($i + 1 .. $#strategy) {
			if ($strategy[$_][1] eq $target) {
				die "Assertion failure...\n";
			}
			if ($strategy[$_][0] eq $target) {
				$j = $_;
				last;
			}
		}
		if (defined $j) {
			my $target_target = $strategy[$j][1];

			# see if we can merge $strategy[$i] and $strategy[$j] into $strategy[$i]...

			my $ok = 1;
			for ($i + 1 .. $j - 1) {
				if ($strategy[$_][0] eq $target_target or $strategy[$_][1] eq $target_target) {
					$ok = 0;
					last;
				}
			}

			if ($ok) {
				# ...yes we can, do it
				#print "reducing chain: $strategy[$i][0] -> $target -> $target_target (up)\n";
				$strategy[$i][1] = $target_target;
				splice @strategy, $j, 1;
				next;
			} 

			# we couldn't, try to merge $strategy[$i] and $strategy[$j] into $strategy[$j] instead

			my $from = $strategy[$i][0];
			$ok = 1;
			for ($i + 1 .. $j - 1) {
				if ($strategy[$_][0] eq $from or $strategy[$_][1] eq $from) {
					$ok = 0;
					last;
				}
			}

			if ($ok) {
				# ...yes we can, do it
				#print "reducing chain: $strategy[$i][0] -> $target -> $target_target (down)\n";
				$strategy[$j][0] = $from;
				splice @strategy, $i, 1;
				$i--;
				next;
			}
		}
	}

	last unless $done_something;
}

# execute it

print "Performing " . scalar(@strategy) . " rename operations\n";

for (@strategy) {
	#print "$_->[0] -> $_->[1]\n";
	rename $_->[0], $_->[1];
}