Renaming Files in Bulk

Problem

You want to rename a bunch of files programmatically: for instance, to normalize the filename case or to change the extensions.

Solution

Use the Find module in the Ruby standard library. Heres a method that renames files according to the results of a code block. It returns a list of files it couldn rename, because their proposed new name already existed:

require find module Find def rename(*paths) unrenamable = [] find(*paths) do |file| next unless File.file? file # Skip directories, etc. path, name = File.split(file) new_name = yield name if new_name and new_name != name new_path = File.join(path, new_name) if File.exists? new_path unrenamable << file else puts " Renaming #{file} to #{new_path}" if $DEBUG File.rename(file, new_path) end end end return unrenamable end module_function(:rename) end

This addition to the Find module makes it easy to do things like convert all filenames to lowercase. Ill create some dummy files to demonstrate:

require fileutils tmp_dir = mp_files Dir.mkdir(tmp_dir) [CamelCase.rb, OLDFILE.TXT, OldFile.txt].each do |f| FileUtils.touch(File.join(tmp_dir, f)) end tmp_dir = File.join(tmp_dir, subdir) Dir.mkdir(tmp_dir) [i_am_SHOUTING, I_AM_SHOUTING].each do |f| FileUtils.touch(File.join(tmp_dir, f)) end

Now lets convert these filenames to lowercase:

$DEBUG = true Find.rename(./) { |file| file.downcase } # Renaming ./tmp_files/subdir/I_AM_SHOUTING to ./tmp_files/subdir/i_am_shouting # Renaming ./tmp_files/OldFile.txt to ./tmp_files/oldfile.txt # Renaming ./tmp_files/CamelCase.rb to ./tmp_files/camelcase.rb # => ["./OldFile.txt", "./dir/i_am_SHOUTING"]

Two of the files couldn be renamed, because oldfile.txt and subdir/i_am_shouting were already taken.

Lets add a ".txt" extension to all files that have no extension:

Find.rename(./) { |file| file + .txt unless file.index(.) } # Renaming ./tmp_files/subdir/i_am_shouting to ./tmp_files/subdir/i_am_shouting.txt # Renaming ./tmp_files/subdir/i_am_SHOUTING to ./tmp_files/subdir/i_am_SHOUTING.txt # # => []

Discussion

Renaming files in bulk is a very common operation, but theres no standard command-line application to do it because renaming operations are best described algorithmically.

The Find.rename method makes several simplifying assumptions. It assumes that you want to rename regular files and not directories. It assumes that you can decide on a new name for a file based solely on its filename, not on its full path. It assumes that youll handle in some other way the files it couldn rename.

Another implementation might make different assumptions: it might yield both path and name, and use autoversioning to guarantee that it can rename every file, although not necessary to the exact filename returned by the code block. It all depends on your needs.

Perhaps the most common renaming operation is modifying the extensions of files. Heres a method that uses Find.rename to make this kind of operation easier:

module Find def change_extensions(extension_mappings, *paths) rename(*paths) do |file| base, extension = file.split(/(.*)./)[1..2] new_extension = extension extension_mappings.each do |re, ext| if re.match(extension) new_extension = ext break end end "#{base}.#{new_extension}" end end module_function(:change_extensions) end

This code uses Find.change_extensions to normalize a collection of images. All JPEG files will be given the extension ".jpg", all PNG files the extension ".png", and all GIF files the extension ".gif".

Again, well create some dummy image files to test:

tmp_dir = mp_graphics Dir.mkdir(tmp_dir) [my.house.jpeg, Construction.Gif, DSC1001.JPG, 52.PNG].each do |f| FileUtils.touch(File.join(tmp_dir, f)) end

Now, lets rename:

Find.change_extensions({/jpe?g/i => jpg, /png/i => png, /gif/i => gif}, tmp_dir) # Renaming tmp_graphics/52.PNG to tmp_graphics/52.png # Renaming tmp_graphics/DSC1001.JPG to tmp_graphics/DSC1001.jpg # Renaming tmp_graphics/Construction.Gif to tmp_graphics/Construction.gif # Renaming tmp_graphics/my.house.jpeg to tmp_graphics/my.house.jpg

See Also

Категории