UNIX Recursive file renaming based on patterns
March 17, 2005 7:34 AM   Subscribe

Unix folks: How can I recursively find all files with a certain string in their filename, replace that string with another string, and rename them with the new filename?

For example, I'd like to find all the files in all subfolders that contain "category040" and change them to "category080". So category04021.gif gets renamed to category08021.gif.

I'm relatively ignorant about UNIX, so forgive me if this is a no-brainer...
posted by turaho to Computers & Internet (19 answers total)
it's probably possible with just find, but life's too short to work out how (or i'm too stupid). what i do is use find to produce a list of files in a temp file, then emacs (which you can "program") to turn that list into a list of "mv" commands.

so, in this case, it would be something like:
find . -name "*80*" -print > tmp
emacs tmp
*stuff involve ^X^(... *
source tmp

of course, if the emacs stuff above is gibberish, this won't help. sorry.
posted by andrew cooke at 7:44 AM on March 17, 2005

I always use perl for these things... You'll need the File::Find module installed for this application. I could eschew that and use the find command and interpret the output, but I'm lazy.

perl -e '
use File::Find;

my $dir = "/top/level/directory/here";
find(\&rename_files, $dir);

sub rename_files {
  if ($File::Find::name =~ /category040/) {
    $oldfile = $File::Find::name;
    $File::Find::name =~ s/category040/category080/;
    `mv $oldfile $File::Find::name`;

This should do the trick. Untested, but if you know a smidgen of perl, you can hack it to work easily, right? Same with error checking, otherwise make sure you have backups before running this blindly. And of course, this is on the command-line (perl -e), remove the appropriate parts (perl -e and backticks) to put into a script, and we should be using strict mode. Again, I'm just lazy :)
posted by splice at 7:55 AM on March 17, 2005

In bash:
for file in *;do
newfile=$(echo $file | sed s/category040/category080/g)
test "$file" != "$newfile" && mv "$file" $newfile
posted by togdon at 7:56 AM on March 17, 2005

Eh... Dunno what I was smoking, but I dout File::Find::name is modifiable, so store that puppy in $newfile before doing the regexp replace and do the replace on $newfile instead, like so:

# Store $File::Find::name in both $oldfile and $newfile
$oldfile = $newfile = $File::Find::name;
$newfile =~ s/category040/category080/;
`mv $oldfile $newfile`;

That would probably work better. Course, I overuse perl way too much in these cases, instead of doing bash scripting and using sed, like togdon above. Probably easier. But I like perl.
posted by splice at 7:59 AM on March 17, 2005

find . *category040* | grep category040 | perl -p -e 's/^(.*)(category040)(.*$)/mv $1$2$3 $1category080$3/' | sh
posted by casu marzu at 8:00 AM on March 17, 2005

why doesn't
find . -name "*80" -exec mv {} `echo {} \| sed s/80/40/` \;
posted by andrew cooke at 8:05 AM on March 17, 2005

I got sick of the various alternatives for this so I wrote my own in Python. The nice thing about it is that it has a --dry-run option that lets me list all the changes I'm
$ renamer --helpusage: renamer [OPTION]... [FILE]...options:  --version             show program's version number and exit  -h, --help            show this help message and exit  Filename modifications:    -R STR, --remove=STR                        remove STR    -F OLD NEW, --fixed-string=OLD NEW                        change OLD to NEW    -P PATTERN NEW, --perl-regexp=PATTERN NEW                        change regexp /PATTERN/ to NEW  Miscellaneous:    -r, --recursive     recurse through subdirectories    -d, --directories   rename directories as well    -n, --dry-run       don't actually do anything
If you have Python 2.4 and will use it, I'll package it up and put it on the web for you.

Oops, just noticed I never implemented the regexp option, but fixed-string replacement is good enough for you.
posted by grouse at 8:11 AM on March 17, 2005

Ahem. The nice thing about it is that it has a --dry-run option that lets me list all the changes I'm about to make before I make them. Of course it's easy enough to do that with the find alternatives by sticking echo in front of the mv, I just got sick of find+sed for something I do so often.
posted by grouse at 8:12 AM on March 17, 2005

Andrew it should - although, you might want to add the -n flag to mv so that it won't inadvertendly overrite an existing file, or better yet, write a script to test before doing the move and write any failures to stdout or stderr so it can be logged. The other issue you'll have is that that it will change the first pattern it finds, you if you have a path that has an 80 in it, it will rename that and not the file. On top of that, it will rename directories too. Don't forget, the goal is to rename 040 to 080
How about:
find . -t f -name "category04*" -exec mv -n {} `echo {} \| sed s/category040/category080` \;
posted by plinth at 8:19 AM on March 17, 2005

find . -name "*80*" -exec bash -c "mv \$1 \`echo \$1 | sed s/80/40/\`" -- {} \;
on preview - i don't know why, but i need to invoke bash again (what i posted didn't work for me with cygwin bash)
posted by andrew cooke at 8:26 AM on March 17, 2005

See how much simpler renamer --recursive --fixed-string 80 40 is? ;)
posted by grouse at 9:01 AM on March 17, 2005

find . | perl -ne'chomp; next unless -e; $oldname = $_; s/aaa/bbb/; next if -e; rename $oldname, $_'

"Please give me a list of all file names from here down. Perl, take each member of that list, take off any trailing whitespace that find may have generated, and as long as that file exists, save the old name, change any occurrence of 'aaa' to 'bbb' in that name, and unless the new name already exists, rename the file to that. Repeat until done."

No need for perl modules, and *especially* no need for python... :)
posted by felix at 9:02 AM on March 17, 2005

felix's one-liner will not get the expected results if you have any directories with the pattern in it, so beware. Also it renames only the first occurrence of 'aaa', not "any" of them. And you will get nasty results if you have any files with a newline in the name, althought that was asking for trouble in the first place.
posted by grouse at 10:25 AM on March 17, 2005

Doesn't anyone use Awk anymore? Manual is online, should be a couple line program.
posted by 445supermag at 10:48 AM on March 17, 2005

Nope, no one uses awk anymore.

perl -MFile::Find -e 'find(sub {if (-f and /category040/) { $old = $_; s/category040/category080/g; rename $old, $_}}, ".")'

Doesn't affect symbolic links, which I'm considering a feature. If it's not, replace -f $_ with (-f or -l).
posted by Zed_Lopez at 11:47 AM on March 17, 2005

Wow, there are a lot of great answers here--I'd check mark all of you if I didn't think it would make this thread look silly.

Anyway, I only needed a quick and dirty solution for a one-time problem with very defined parameters, so the suggestions of andrew cooke, plinth and felix were good enough for me. But I'm definitely bookmarking this thread for future reference in case massive filenaming becomes part of my job and I need a more robust solution.

Thanks again, everyone.
posted by turaho at 12:25 PM on March 17, 2005

andrew cooke: It needs to be passed through a shell for tokenisation because you're passing the entire command to find as a single string. Try passing each element of ARGV (ie the command and its parameters) as a separate parameter.
posted by fvw at 1:17 PM on March 17, 2005

eh? this is what didn't work. i can't see how Try passing each element of ARGV (ie the command and its parameters) as a separate parameter applies to that. can you explain more? (i'm not sure if i'm confused or you, but my second version with bash in the exec does work).
posted by andrew cooke at 1:27 PM on March 17, 2005

One more nifty thing you can do in bash:

for i in `find .` ; do mv $i ${i/gory080/gory040}; done
posted by sfenders at 3:42 PM on March 17, 2005

« Older Why is this the universal representation of a...   |   Salvia Divinorum Newer »
This thread is closed to new comments.