9. Directories

Introduction

entry = File.stat("/usr/bin/vi")
entry = File.stat("/usr/bin")
entry = File.stat(INFILE)

entry = File.stat("/usr/bin/vi")
ctime = entry.ctime
size  = entry.size

f = File.open(filename, "r")

## There is no -T equivalent in Ruby, but we can still test emptiness
if test(?s, filename)
  puts "#{filename} doesn't have text in it."
  exit
end

Dir.new("/usr/bin").each do |filename|
  puts "Inside /usr/bin is something called #{filename}"
end

Getting and Setting Timestamps

file = File.stat("filename")
readtime, writetime = file.atime, file.mtime
file.utime(readtime, writetime)  

SECONDS_PER_DAY = 60 * 60 * 24
file = File.stat("filename")
atime, mtime = file.atime, file.mtime

atime -= 7 * SECONDS_PER_DAY
mtime -= 7 * SECONDS_PER_DAY

File.utime(atime, mtime, file)
mtime = File.stat(file).mtime
File.utime(Time.new, mtime, file)
File.utime(Time.new, File.stat("testfile").mtime, file)

#-----------------------------
#!/usr/bin/ruby -w
## uvi - vi a file without changing it's access times

if ARGV.length != 1
  puts "usage: uvi filename"
  exit
end
file = ARGV[0]
atime, mtime = File.stat(file).atime, File.stat(file).mtime
system(ENV["EDITOR"] || "vi", file)
File.utime(atime, mtime, file)
#-----------------------------

Deleting a File

File.unlink(FILENAME)

err_flg = false
filenames.each do |file|
  begin
    File.unlink(file)
  rescue
    err_flg = $!
  end
end
err_flg and raise "Couldn't unlink all of #{filenames.join(" ")}: #{err_flg}"

File.unlink(file)

count = filenames.length
filenames.each do |file|
  begin
    File.unlink(file)
  rescue
    count -= 1
  end
end
if count != filenames.length
  STDERR.puts "could only delete #{count} of #{filenames.length} files"
end

Copying or Moving a File

require "ftools"
File.copy(oldfile, newfile)

infile  = File.open(oldfile, "r")
outfile = File.open(newfile, "w")

blksize = infile.stat.blksize
# This doesn't handle partial writes or ^Z
# like the Perl version does.
while (line = infile.read(blksize))
  outfile.write(line)
end

infile.close
outfile.close

system("cp #{oldfile} #{newfile}")    # unix
system("copy #{oldfile} #{newfile}")  # dos, vms

require "ftools"
File.copy("datafile.dat", "datafile.bak")
File.move("datafile.new", "datafile.dat")

Recognizing Two Names for the Same File

$seen = {} # must use global var to be seen inside of method below

def do_my_thing(filename)
    dev, ino = File.stat(filename).dev, File.stat(filename).ino
    unless $seen[[dev, ino]]
        # do something with $filename because we haven't
        # seen it before
    end
    $seen[[dev, ino]] = $seen[[dev, ino]].to_i + 1
end

files.each do |filename|
    dev, ino = File.stat(filename).dev, File.stat(filename).ino
    if !$seen.has_key?([dev, ino])
        $seen[[dev, ino]] = []
    end
    $seen[[dev, ino]].push(filename)
end

$seen.keys.sort.each do |devino|
    ino, dev = devino
    if $seen[devino].length > 1
        # $seen[devino] is a list of filenames for the same file
    end
end

Processing All Files in a Directory

Dir.open(dirname) do |dir|
    dir.each do |file|
        # do something with dirname/file
        puts file
    end
end
# Dir.close is automatic

# No -T equivalent in Ruby

dir.each do |file|
    next if file =~ /^\.\.?$/
    # ...
end

def plainfiles(dir)
    dh = Dir.open(dir)
    dh.entries.grep(/^[^.]/).
        map      {|file| "#{dir}/#{file}"}.
        find_all {|file| test(?f, file)}.
        sort
end

Globbing, or Getting a List of Filenames Matching a Pattern

list = Dir.glob("*.c")

dir = Dir.open(path)
files = dir.entries.grep(/\.c$/)
dir.close

files = Dir.glob("*.c")
files = Dir.open(path).entries.grep(/\.[ch]$/i)

dir = Dir.new(path)
files = dir.entries.grep(/\.[ch]$/i)

begin
  d = Dir.open(dir)
rescue Errno::ENOENT
  raise "Couldn't open #{dir} for reading: #{$!}"
end

files = []
d.each do |file|
  puts file
  next unless file =~ /\.[ch]$/i

  filename = "#{dir}/#{file}"
  # There is no -T equivalent in Ruby, but we can still test emptiness
  files.push(filename) if test(?s, filename)
end

dirs.entries.grep(/^\d+$/).
             map    { |file| [file, "#{path}/#{file}"]} .
             select { |file| test(?d, file[1]) }.
             sort   { |a,b|  a[0] <=> b[0] }.
             map    { |file| file[1] }

Processing All Files in a Directory Recursively

require 'find'
Find.find(dirlist) do |file|
  # do whatever
end

require 'find'
argv = ARGV.empty? ? %w{.} : ARGV
Find.find(*argv) do |file|
  print file, (test(?d, file) ? "/\n" : "\n")
end

require 'find'
argv = ARGV.empty? ? %w{.} : ARGV
sum = 0
Find.find(*argv) do |file|
  size = test(?s, file) || 0
  sum += size
end
puts "#{argv.join(' ')} contains #{sum} bytes"

require 'find'
argv = ARGV.empty? ? %w{.} : ARGV
saved_size, saved_name = -1, ""
Find.find(*argv) do |file|
  size = test(?s, file) || 0
  next unless test(?f, file) && size > saved_size
  saved_size = size
  saved_name = file
end
puts "Biggest file #{saved_name} in #{argv.join(' ')} is #{saved_size}"

require 'find'
argv = ARGV.empty? ? %w{.} : ARGV
age, name = nil
Find.find(*argv) do |file|
  mtime = File.stat(file).mtime
  next if age && age > mtime
  age = mtime
  name = file
end
puts "#{name} #{age}"

#-----------------------------
#!/usr/bin/ruby -w
# fdirs - find all directories
require 'find'
argv = ARGV.empty? ? %w{.} : ARGV
File.find(*argv) { |file| puts file if test(?d, file) }
#-----------------------------

Removing a Directory and Its Contents

require 'fileutils'

puts "Usage #{$0} dir ..." if ARGV.empty?
ARGV.each do |dir|
  FileUtils.rmtree(dir)
end

Renaming Files

require 'ftools'
names.each do |file|
  newname = file
  begin
    File.move(file, newname)
  rescue Errno::EPERM
    $stderr.puts "Couldn't rename #{file} to #{newname}: #{$!}"
  end
end

require 'ftools'
op = ARGV.empty? ? (raise "Usage: rename expr [files]\n") : ARGV.shift
argv = ARGV.empty? ? $stdin.readlines.map { |f| f.chomp } : ARGV
argv.each do |file|
  was = file
  file = eval("file.#{op}")
  File.move(was, file) unless was == file
end

Splitting a Filename into Its Component Parts

base = File.basename(path)
dir  = File.dirname(path)
# ruby has no fileparse equivalent
dir, base = File.split(path)
ext = base.scan(/\..*$/).to_s

path = '/usr/lib/libc.a'
file = File.basename(path)
dir  = File.dirname(path)

puts "dir is #{dir}, file is #{file}"
# dir is /usr/lib, file is libc.a

path = '/usr/lib/libc.a'
dir, filename = File.split(path)
name, ext = filename.split(/(?=\.)/)
puts "dir is #{dir}, name is #{name}, ext is #{ext}"
#   NOTE: The Ruby code prints
#   dir is /usr/lib, name is libc, extension is .a
#     while the Perl code prints a '/' after the directory name
#   dir is /usr/lib/, name is libc, extension is .a

# No fileparse_set_fstype() equivalent in ruby

def extension(path)
    ext = path.scan(/\..*$/).to_s
    ext.sub(/^\./, "")
end

Program: symirror

#-----------------------------
#!/usr/bin/ruby -w
# symirror - build spectral forest of symlinks

require 'find'
require 'fileutils'

raise "usage: #{$0} realdir mirrordir" unless ARGV.size == 2

srcdir,dstdir = ARGV
srcmode = File::stat(srcdir).mode
Dir.mkdir(dstdir, srcmode & 07777) unless test(?d, dstdir)

# fix relative paths
Dir.chdir(srcdir) {srcdir = Dir.pwd}
Dir.chdir(dstdir) {dstdir = Dir.pwd}

Find.find(srcdir) do |srcfile| 
    if test(?d, srcfile)
        dest = srcfile.sub(/^#{srcdir}/, dstdir)
        dmode = File::stat(srcfile).mode & 07777
        Dir.mkdir(dest, dmode) unless test(?d, dest)
        a = Dir["#{srcfile}/*"].reject{|f| test(?d, f)}
        FileUtils.ln_s(a, dest)
    end
end

Program: lst

# we use the Getopt/Declare library here for convenience:
#   http://raa.ruby-lang.org/project/getoptdeclare/
#-----------------------------
#!/usr/bin/ruby -w
# lst - list sorted directory contents (depth first)

require 'find'
require 'etc'
require "Getopt/Declare"

# Note: in the option-spec below there must by at least one hard
# tab in between each -option and its description. For example
#    -i <tab> read from stdin

opts = Getopt::Declare.new(<<'EOPARAM')
    ============
    Input Format:
        -i      read from stdin
    ============
    Output Format:
        -l      long listing
        -r      reverse listing
    ============
    Sort on: (one of)
        -m      mtime (modify time - default)
                {$sort_criteria = :mtime}
        -u      atime (access time)
                {$sort_criteria = :atime}
        -c      ctime (inode change time)
                {$sort_criteria = :ctime}
        -s      size
                {$sort_criteria = :size}
        [mutex: -m -u -c -s]

EOPARAM

$sort_criteria ||= :mtime
files = {}
DIRS = opts['-i'] ? $stdin.readlines.map{|f|f.chomp!} : ARGV
DIRS.each do |dir|
    Find.find(dir) do |ent|
        files[ent] = File::stat(ent)
    end
end
entries = files.keys.sort_by{|f| files[f].send($sort_criteria)}
entries = entries.reverse unless opts['-r']

entries.each do |ent|
    unless opts['-l']
        puts ent
        next
    end
    stats = files[ent]
    ftime = stats.send($sort_criteria == :size ? :mtime : $sort_criteria)
    printf "%6d %04o %6d %8s %8s %8d %s %s\n",
        stats.ino,
        stats.mode & 07777,
        stats.nlink,
        ETC::PASSWD[stats.uid].name,
        ETC::GROUP[stats.gid].name,
        stats.size,
        ftime.strftime("%a %b %d %H:%M:%S %Y"),
        ent
end