patternrubyMinor
A text editor driven file renamer in Ruby
Viewed 0 times
filetexteditorrubydrivenrenamer
Problem
I wrote the following Ruby script several years ago and have been using it often ever since.
It opens a text editor with the list of files in current directory. You can then edit the file names as text. Once you save and exit the files are renamed.
The
Here is how one such directory looks:
I would like to use it as an example for a Ruby workshop. Could you please suggest best-practice and design pattern improvements?
It opens a text editor with the list of files in current directory. You can then edit the file names as text. Once you save and exit the files are renamed.
The
renamer allowed me to maintain spreadsheet-like file names thanks to the powerful column editing capabilities of vi (emacs also has those).Here is how one such directory looks:
I would like to use it as an example for a Ruby workshop. Could you please suggest best-practice and design pattern improvements?
#!/usr/bin/ruby
RM = '/bin/rm'
MV = '/bin/mv'
from = Dir.entries('.').sort; from.delete('.'); from.delete('..')
from.sort!
from.delete_if {|i| i =~ /^\./} # Hidden files
tmp = "/tmp/renamer.#{Time.now.to_i}.#{(rand * 1000).to_i}"
File.open(tmp, 'w') do |f|
from.each {|i| f.puts i}
end
ENV['EDITOR'] = 'vi' if ENV['EDITOR'].nil?
system("#{ENV['EDITOR']} #{tmp}")
to = File.open(tmp) {|f| f.readlines.collect{|l| l.chomp}}
`#{RM} #{tmp}`
if to.size != from.size
STDERR.puts "renamer: ERROR: number of lines changed"
exit(1)
end
from.each_with_index do |f, i|
puts `#{MV} -v --interactive "#{f}" "#{to[i]}"` unless f == to[i]
endSolution
RM = '/bin/rm'
MV = '/bin/mv'In general it's preferable to use the
FileUtils class rather than relying on shell utilities. Though in this particular case, you might want to stick at least with mv since the FileUtils.mv method does not have an :interactive option.tmp = "/tmp/renamer.#{Time.now.to_i}.#{(rand * 1000).to_i}"Ruby has a
Tempfile class which can generate a unique temporary file more reliably than this. You should use it.from.each {|i| f.puts i}Calling
puts on an array will puts each line individually, so the above can just be shortened to f.puts from.ENV['EDITOR'] = 'vi' if ENV['EDITOR'].nil?Can be shortened to
ENV['EDITOR'] ||= 'vi'. Though what you have isn't particularly verbose either, so it doesn't really matter much which one you choose.system("#{ENV['EDITOR']} #{tmp}")Use
system(ENV[EDITOR], tmp) instead. This way you get rid of the string interpolation and the code still works if either ENV['EDITOR'] or tmp should contain a space or other shell meta-character (not that they're particularly likely to, but it's a good idea to use the multiple-argument-form of system where ever possible).to = File.open(tmp) {|f| f.readlines.collect{|l| l.chomp}}Usually this could be replaced with
to = File.readlines(tmp).collect {|l| l.chomp}. However if you follow my suggestion of using Tempfile, that won't be an option any more.`#{RM} #{tmp}`If you use fileutils, this will just be
FileUtils.rm(tmp) (or rm(tmp) if you include FileUtils). If you don't want to use FileUtils, you should at least use system(RM, tmp) for the same reasons as above.However if you use
Tempfile, which you should, this becomes redundant anyway.from.each_with_index do |f, i|
puts `#{MV} -v --interactive "#{f}" "#{to[i]}"` unless f == to[i]
endTo iterate over two arrays in parallel, use
zip:from.zip(to) do |f, t|
system(MV, "-v", "--interactive", f, t) unless f == t
endNote that here using
system instead of backticks is especially important since one of the files in from or to containing spaces is actually somewhat likely.So with all my suggestions, your code should now look like this:
#!/usr/bin/env ruby
require 'tempfile'
MV = '/bin/mv'
from = Dir.glob('*').sort
ENV['EDITOR'] ||= 'vi'
to = nil
Tempfile.open("renamer") do |f|
f.puts from
f.close
system(ENV['EDITOR'], f.path)
f.open
to = f.readlines.collect {|l| l.chomp}
end
if to.size != from.size
STDERR.puts "renamer: ERROR: number of lines changed"
exit(1)
end
from.zip(to) do |f, t|
system(MV, "-v", "--interactive", f, t) unless f == t
endCode Snippets
RM = '/bin/rm'
MV = '/bin/mv'tmp = "/tmp/renamer.#{Time.now.to_i}.#{(rand * 1000).to_i}"from.each {|i| f.puts i}ENV['EDITOR'] = 'vi' if ENV['EDITOR'].nil?system("#{ENV['EDITOR']} #{tmp}")Context
StackExchange Code Review Q#645, answer score: 9
Revisions (0)
No revisions yet.