Ruby: how do I recursively find and remove empty directories? - ruby

I'm trying to write some ruby that would recursively search a given directory for all empty child directories and remove them.
Thoughts?
Note: I'd like a script version if possible. This is both a practical need and a something to help me learn.

In ruby:
Dir['**/*'] \
.select { |d| File.directory? d } \
.select { |d| (Dir.entries(d) - %w[ . .. ]).empty? } \
.each { |d| Dir.rmdir d }

Looking at the examples from kch, dB. and Vishnu above, I've put together a one-liner that I think is a more elegant solution:
Dir['**/'].reverse_each { |d| Dir.rmdir d if Dir.entries(d).size == 2 }
I use '**/' instead of '/**/*' for the glob, which only returns directories, so I don't have to test whether it's a directory later. I'm using reverse_each instead of sort.reverse.each as it's shorter and supposedly more efficient, according to this post. I prefer Dir.entries(d).size == 2 to (Dir.entries(d) - %w[ . .. ]).empty? because it's a little easier to read and understand, although (Dir.entries(d) - %w[ . .. ]).empty? would probably work better if you had to run your script on Windows.
I've tested this quite a bit on Mac OS X and it works well, even with recursive empty directories.

You have to delete in reverse order, otherwise if you have an empty directory foo with a subdirectory bar you will delete bar, but not foo.
Dir.glob(dir + "/**/*").select { |d|
File.directory?(d)
}.reverse_each { |d|
if ((Dir.entries(d) - %w[ . .. ]).empty?)
Dir.rmdir(d)
end
}

Why not just use shell?
find . -type d -empty -exec rmdir '{}' \;
Does exactly what you want.

Dir['/Users/path/Movies/incompleteAnime/foom/**/*']. \
select { |d| File.directory? d }. \
sort.reverse. \
each {|d| Dir.rmdir(d) if Dir.entries(d).size == 2}
just like the first example, but the first example doesn't seem to handle the recursive bit. The sort and reverse ensures we deal with the most nested directories first.
I suppose sort.reverse could be written as sort {|a,b| b <=> a} for efficiency

I've tested this script on OS X, but if you are on Windows, you'll need to make changes.
You can find the files in a directory, including hidden files, with Dir#entries.
This code will delete directories which become empty once you've deleted any sub-directories.
def entries(dir)
Dir.entries(dir) - [".", ".."]
end
def recursively_delete_empty(dir)
subdirs = entries(dir).map { |f| File.join(dir, f) }.select { |f| File.directory? f }
subdirs.each do |subdir|
recursively_delete_empty subdir
end
if entries(dir).empty?
puts "deleting #{dir}"
Dir.rmdir dir
end
end

Dir.glob('**/*').each do |dir|
begin
Dir.rmdir dir if File.directory?(dir)
# rescue # this can be dangereous unless used cautiously
rescue Errno::ENOTEMPTY
end
end

module MyExtensions
module FileUtils
# Gracefully delete dirs that are empty (or contain empty children).
def rmdir_empty(*dirs)
dirs.each do |dir|
begin
ndel = Dir.glob("#{dir}/**/", File::FNM_DOTMATCH).count do |d|
begin; Dir.rmdir d; rescue SystemCallError; end
end
end while ndel > 0
end
end
end
module ::FileUtils
extend FileUtils
end
end

For a pure shell solution, I found this very useful
find "$dir" -depth -type d |
while read sub; do
[ "`cd "$sub"; echo .* * ?`" = ". .. * ?" ] || continue
echo rmdir "$sub"
#rmdir "$sub"
done
But if you have gnu-find installed (not universal yet)...
find . -depth -type d -empty -printf "rmdir %p\n"
this uses find with xargs...
find . -depth -type d -print0 |
xargs -0n1 sh -c '[ "`cd "$0"; echo .* * ?`" = ". .. * ?" ] &&
echo "rmdir $0";'

Related

Ruby and shell : get the same output with ls

I got a folder with the following zip files :
13162.zip 14864.zip 19573.zip 20198.zip
In console, when i run :
cd my_folder; echo `ls *{.zip,.ZIP}`
I got the following output (which is perfect) :
ls: cannot access *.ZIP: No such file or directory
13162.zip 14864.zip 19573.zip 20198.zip
Now when in ruby i try the same :
cmd= "cd my_folder; echo `ls {*.zip,*.ZIP}`";
puts `#{cmd}`
It only display :
ls: cannot access {*.zip,*.ZIP}: No such file or directory
=> nil
I try this solution :
Getting output of system() calls in Ruby
But it seem not work in my case.
How can i get the same output in ruby and in shell ?
Ruby Only
You can use Dir.glob with File::FNM_CASEFOLD for case-insensitive search :
Dir.chdir 'my_folder' do
Dir.glob('*.zip', File::FNM_CASEFOLD).each do |zip_file|
puts zip_file
end
end
#=>
# 19573.zip
# 13162.zip
# 14864.zip
# 20198.zip
# 12345.zIp
Ruby + bash
You can use find for case-insensitive search :
paths = `find my_folder -maxdepth 1 -iname '*.zip'`.split
#=> ["my_folder/19573.zip", "my_folder/13162.zip", "my_folder/14864.zip", "my_folder/20198.zip", "my_folder/12345.zIp"]
-printf '%P' can also be used to only display the filenames :
files = `find my_folder -maxdepth 1 -iname '*.zip' -printf '%P\n'`.split
#=> ["19573.zip", "13162.zip", "14864.zip", "20198.zip", "12345.zIp"]
I think this should work directly on the terminal:
echo 'system("ls *ZIP,*zip")' | ruby
or create a ruby file with the following contents
system("cd my_folder; ls {*.zip,*.ZIP}")
and then execute it. Once you write ls, you don't need echo!

Changing filenames in Ruby when all files have no type?

I have a directory filled with 5 files with no filetype (perhaps their filetype is '.txt' - I am uncertain), named "file1", "file2"...
I am trying to convert them to CSV format with the following code:
require('fileutils')
folder_path = "correct_folder_path"
Dir.foreach(folder_path) do |f|
next if f == '.' || f == '..'
#confirm inputs are correct (they are)
#p f
#p f+".csv"
File.rename(f, f+".csv")
end
I have p'd out f to confirm everything is working, but the line
File.rename(f,f+".csv")
is throwing the error: "in `rename': No such file or directory... (Errno::ENOENT)"
Does anyone know why this isn't working?
With Dir and File
You could change the directory to folder_path. If some files might have '.txt' extension, you need to remove the extension first in order not to get a .txt.csv file :
folder_path = "correct_folder_path"
Dir.chdir(folder_path) do
Dir.foreach(".") do |f|
next if File.directory?(f)
basename = File.basename(f, '.*')
new_file = basename + '.csv'
p f
p new_file
## Uncomment when you're sure f and new_file are correct :
# File.rename(f, new_file) unless f == new_file
end
end
With Pathname
With Pathname, it's usually much easier to filter and rename files :
require 'pathname'
folder_path = "correct_folder_path"
Pathname.new(folder_path).children.each do |f|
next if f.directory?
p f
p f.sub_ext('.csv')
## Uncomment if you're sure f and subext are correct :
# f.rename(f.sub_ext('.csv'))
end
The paths returned by Dir.foreach are relative to the folder_path that you passed in. Your call to File.rename tries to rename a file in the current working directory, which is probably not the same directory as that specified by folder_path.
You can make the rename succeed by prepending folder_path to the filename:
f = File.join(folder_path, f)
File.rename(f, f + ".csv")
One alternative:
require 'pathname'
folder.children.each do |child|
# Other logic here
child.rename(child.dirname + (child.basename.to_s + '.csv'))
end

Getting Errno::ENOENT during Dir.foreach block when I know file exists?

I currently have the path set to:
$: << "C:/Users/fireforkingjesus/ruby/note/"
I have these things in this folder: ruby/note/notes/
!IMPORTANT - folder
just.txt
inventory.txt
weapons.txt
And if I put this code it works fine:
Dir.foreach("notes/"){|file| puts file}
Outputs:
.
..
!IMPORTANT
just.txt
inventory.txt
weapons.txt
But THESE codes do not work:
Dir.foreach("notes/"){|file| puts file if File.exist?(file)}
This above^^ puts
.
..
and
Dir.foreach("notes/"){|file| puts File.exist?(file)}
Returns true 2 times and false 4 times
Dir.foreach("notes/"){ |file| puts File.ftype(file)}
This above^^ returns: "No such file or directory -!IMPORTANT" (Errno::ENOENT)
I've even done this:
f = "inventory.txt"
puts "yes before foreach" if File.exist?("notes/inventory.txt")
Dir.foreach("notes/"){ |file|
puts "yes in foreach" if File.exist?("inventory.txt")
puts File.ftype(f)
}
I get:
yes before foreach
No such file or directory -inventory.txt (Errno::ENOENT)`
I have Ruby 2.0, Windows 8. I thought maybe permissions? or something idk
and I also tried it on my dad's Win vista
Look a little closer at your output:
Dir.foreach("notes/"){|file| puts file}
# ----------^^^^^^^^
produces:
.
..
!IMPORTANT
just.txt
inventory.txt
weapons.txt
Dir.foreach is handing the basenames to the block so you'll have to put the path back on if Dir isn't looking at the current directory:
Dir.foreach("notes/") { |file| puts File.ftype("#{notes}/file") }
Everything works fine with . and .. of course, those exist in every directory.

File reading problem

f = File.open("/test/serverlist.txt", "r")
list = f.readlines
list.each do|servers|
File.open('/test/results.txt','w') do |b|
servers.each do |p|
r = `ping -n 1 #{p}`
b.puts r
end
end
It reads the serverlist file, and returns a string. The serverlist file contains the following IP addresses:
192.168.150.254
192.168.120.2
Are you looking to read each line from the file and then do something with like this.
fout = File.open('/test/results.txt','w')
File.open("/test/serverlist.txt", "r").each_line do |server|
server.chomp!
r = `ping -n 1 #{server}`
fout.puts r
end
I don't think you will need to iterate over the server line itself, and with a few style mods added and ping(1) arguments changed, I would suggest...
open 'serverlist.txt', 'r' do |f|
open '/tmp/results.txt', 'w' do |b|
f.readlines.each do |server|
b.puts `ping -c 1 -t 1 #{server}`
end
end
end
Just use b.write in place of b.puts
if you're using linux you could just go for
File.open("serverlist.txt").each { |addy| `echo "#{`ping -c 1 #{addy}`}" >> result.txt` }
and be done with it
well .. maybe add
`echo "# server-availability" > result.txt`
before the above line so the file gets reset every time you call this

Recursively convert all folder and file names to lower-case or upper-case

I have a folder structure like follows.
-FOO
-BAG
Rose.TXT
-BAR
JaCk.txt
I need the following output.
-foo
-bag
rose.txt
-bar
jack.txt
I realize you want ruby code, but I present to you a one liner to run in your shell:
for i in `find * -depth`; do (mv $i `echo $i|tr [:upper:] [:lower:]`); done
as found here: http://ubuntuforums.org/showthread.php?t=244738
Run it once, and it should do the trick.
Update
Ruby Code:
Dir.glob("./**/*").each do |file|
File.rename(file, file.downcase) #or upcase if you want to convert to uppercase
end
Dir["**/*"].each {|f| File.rename(f, f.downcase)}
The accepted answer does not work: when it tries to convert a directory first and then a file in that directory.
Here is the code that does work:
Dir.glob("./**/*").sort{|x| x.size}.each do |name|
x = name.split('/')
newname = (x[0..-2] + [x[-1].downcase]).join('/')
File.rename(name, newname)
end
(it sorts the list by length, so the direcotry will be converted after the file in it)
Recursive directory listing :
http://www.mustap.com/rubyzone_post_162_recursive-directory-listing
Uppercase/lowercase conversion :
http://www.programmingforums.org/thread5455.html
Enjoy :)
If you want to rename your files recursively, you can use **/*
folder = "/home/prince"
Dir["#{folder}/**/*"].each {|file| File.rename(file, file.downcase)}
If you just want the file array output in lowercase
Dir["#{folder}/**/*"].map(&:downcase)
Just a bit of Find.find is all you need:
require 'find'
Find.find(directory) do |path|
if(path != '.' && path != '..')
File.rename(path, path.downcase)
end
end

Resources