This is a quick script I whipped up to collapse some folder trees down to individual folders. The specific situation that arose was my formerly-itunes collection (it's currently organized Music\artist\album\songs.(mp3|m4a)
and I'd really prefer it to be Music\artist\song.(mp3|m4a)
).
#!/usr/bin/ruby
require 'optparse'
require 'pp'
require 'fileutils'
### parsing command line options via optparse, nothing to see here
options={}
optparse = OptionParser.new do|opts|
opts.on('-h', '--help',
'Display this screen') do
puts opts
exit
end
opts.on('-t', '--type a,b,c',
Array, 'Specify filetypes to flatten out (defaults to mp3,m4a)') do|t|
options[:types] = t
end
end
optparse.parse!
### the setup (ideally, this would be in a `let` statement)
origin = Dir.pwd
if not options[:types]
options[:types] = ["mp3", "m4a"]
end
### the actual work
ARGV.each do |target|
FileUtils.cd("#{origin}/#{target}")
options[:types].each{|ext| `find -iname '*.#{ext}' -exec mv {} ./ \\\;`}
`find -depth -type d -empty -exec rmdir {} \\\;`
end
FileUtils.cd(origin)
Am I doing anything eye-stabbingly wrong? Is there a better way of handling any of the above?
Specifically, I suspect that I could replace the find calls with native Ruby methods, but I can't see how without complicating it significantly. I also suspect that there's a better way of writing FileUtils.cd("#{origin}/#{target}")
, but a cursory reading of Dir doesn't reveal anything. Any tips?
#!/usr/bin/ruby
require 'optparse'
require 'pp'
require 'fileutils'
### parsing command line options via optparse
options={:types => ["mp3", "m4a"]}
optparse = OptionParser.new do|opts|
opts.on('-h', '--help', 'Display this screen') do
puts opts
exit
end
opts.on('-t', '--type a,b,c', Array, 'Specify filetypes to flatten out (comma separated, defaults to mp3,m4a)') do|t|
options[:types] = t
end
end
optparse.parse!
### the actual work
ARGV.each do |target|
FileUtils.cd(target) do
options[:types].each{|ext| `find -iname '*.#{ext}' -exec mv {} ./ \\\;`} ##move all matched files into [target]
`find -depth -type d -empty -exec rmdir {} \\\;` ##recursively delete all empty folders in [target]
end
end
options
is now initialized beforehand rather than being checked after.parse!
- using the block option on
FileUtils.cd
- removed useless final
cd
back - tried the
glob
option, but it just errors without continuing if it hits a matched file intarget
(ie. one that needs to stay where it is). This seems to happen when I pass:force => TRUE
too. Thefind
call notes such files, but still flattens what it can. Theglob
route also doesn't seem to be case-insensitive.
1 Answer 1
options={}
# ...
if not options[:types]
options[:types] = ["mp3", "m4a"]
end
I would initialize options
with the default values first, instead of checking whether the option is set afterwards. I.e.
options = {:types => ["mp3", "m4a"]}
options[:types].each{|ext| `find -iname '*.#{ext}' -exec mv {} ./ \\\;`}
This can be done with a glob. For this I'd first change options[:types]
to be string (i.e. replace Array
with String
in the optparse code and use "mp3,m4a"
instead of ["mp3", "m4a"]
as the default. Then you can use Dir.glob
with FileUtils.mv
like this:
Dir.glob("**/*.{#{ ext }}") {|f| FileUtils.mv(f, ".")}
I suspect that I could replace the find calls with native Ruby methods, but I can't see how without complicating it significantly.
For the first one see above. However the second one would indeed be more complicated to replace, so I'd just stick with the find
call.
I also suspect that there's a better way of writing
FileUtils.cd("#{origin}/#{target}")
Yes, there is: cd
optionally takes a block. When given a block, cd
will cd into the given directory, execute the block, and then cd back. So you can get rid of origin
and just do
FileUtils.cd(target) do
...
end
FileUtils.cd(origin)
Even without using the block version of cd
the above line would be completely unnecessary. Changing the directory at the end of a script has no effect. Any cd performed in a process will never affect the parent process.