back to _why's Estate


Wearing Ruby Slippers To Work

(Also available in French.)

This is Part One of an ongoing series to get us all comfortable using Ruby in the harshest of conditions. Hopefully, by the end of this series, you will feel as attached to Ruby as you are to your leg. And your work can't keep you from taking your leg with you to the office.

Next time someone frowns on you for using Ruby just scream, "YOU CAN'T TAKE AWAY MY LEG!!" Stick up for yourself. I may just start an abuse hotline for that kind of bigotry.

Now, seriously. Seriously. You can't use Ruby at work, can you? You'd be hijacking the project! You can't make the rules, can you? You don't want to be the pusher, do you?

No, hey, we're not crazy enough to storm the castle. We're just going to wear the ruby slippers quietly on our feet. Just watch for a second.

Slippers That Shuffle Through
(Using Dir.glob and File#grep)

Not long ago a friend asked me how to recursively search his PHP scripts for a string. He had a lot of big binary files and templates in those directories that could have really bogged down a plain grep. I couldn't think of a way to use grep to make this happen, so I figured using find and grep together would be my best bet.

I ended up with this:

  find . -name "*.php" -exec grep 'search_string' {} \; -print

I'm not great at chaining together shell commands. It's fun and momentarily challenging, but often I hunt around on man pages for awhile and sometimes I wish I knew sed better. Can't keep it all straight all the time. You know?

But, why would I want to know sed better when I know Ruby? Say, if I started formulating my shell command magic in Ruby, then I'd be able to be even more expressive if I needed.

Here's the above file search reworked in Ruby:

  Dir['**/*.php'].each do |path|
    File.open( path ) do |f|
      f.grep( /search_string/ ) do |line|
        puts path, ':', line
      end
    end
  end

I think Ruby has a wealth of handy ways of managing the filesystem. Dir::glob is a big one for me. Above, I used the shortcut for Dir::glob called Dir::[]. The globbing expression can contain a few special characters: ? to match any single character, * to match any set of characters within a given filename, ** to match any set of characters within a complete path (meaning ** is recursive).

The Dir::glob returns an Array of matching filenames. Thereafter, I'm cycling through each file, opening it up and checking each line for a match.

Notice the f.grep line. The File class has an each method, which iterates through each line in a text file. Also, the File class has the Enumerable module mixed in. This mixin provides the grep method, which will try each of those lines against the regular expression and pass the block each line which matches.

Your first reaction may be, "Well, that's quite a bit wordier than the original." And I just have to shrug and let it be. "It's a lot easier to extend," I say. And it works across platforms.

The moral is: use your Ruby slippers to elegantly shuffle through your files.

Slippers That Clean-up
(Using FileUtils and Find)

Okay, so I've been using Ruby to handle any shell command which require more than a single pipe. And, of course, no shell scripting allowed.

I needed to check a directory into CVS that was kind of a mess. All kinds of Old and Trash folders. And images that I didn't want to check into CVS. I started deleting files, but it was time-consuming and decided upon a script to scan a directory and copy over the good files into a new directory.

  require 'find'
  require 'fileutils'

  include FileUtils::Verbose
  from_dir = File.expand_path( ARGV[0] ) + "/" 
  to_dir = File.expand_path( ARGV[1] ) + "/" 

  makedirs( to_dir )
  chdir( to_dir ) do
    Find.find( from_dir ) do |path|
      fname = path.sub( from_dir, '' )
      fdir, base = File.split( fname )
      if FileTest.directory? path
        Find.prune if base[0] == ?. or 
                    base =~ /^(?:CVS|images|Old|Trash|delme|bak)/i
        makedirs( fname )
      elsif FileTest.file? path
        unless base =~ /(?:\.jpg|\.jpeg|\.pdf|\.gif|\.psd|~)$/
          copy path, fname
        end
      end
    end
  end

A bunch of great modules at work here. I'll start with FileUtils, cause it's the simplest.

The FileUtils module simply provides many of the same file-level commands you see at any shell prompt. In the script above, you can see copy and chdir (aka cd) at work.

I've mixed-in the FileUtils::Verbose version of the module, though, which is perfect for shell scripting. This version of the module prints to the terminal the full text of the operation. So you can see what's going on when you run the script.

I'm also using the Find module, which simple recurses through every file under a directory. You can tell the find to avoid searching a directory by issuing a Find.prune. This is exactly what I'm doing to ensure the copy skips any Old or Trash directories. Notice that I'm also skipping any directory which start with a dot.

These modules come with Ruby 1.8, so it's all kosher. If you have 1.9 installed (or you've actually got Ri working with 1.8), then you can get docs for these modules with: ri Find and ri FileUtils.

So, I'm saying: use your Ruby slippers to clean up. It's like you've got brushes under your feet!

Slippers That Merge

I ended up doing a bit more than the above script, though. You see, each of these directories represented a manual versioning of code. So, not only did I want to clean the directory, but then I wanted to merge it into a checked out copy of a CVS module.

  require 'find'
  require 'fileutils'

  include FileUtils::Verbose
  checkout_dir = File.expand_path( ARGV[0] ) + "/" 
  add_dir = File.expand_path( ARGV[1] ) + "/" 

  checked = {}
  chdir( checkout_dir ) do
    # 1. compare directories, copying missing or updated text files,
    #  skipping binary files, skipping Old/Trash/delme/bak
    #  and renaming .htaccess to htaccess.sample.
    Find.find( add_dir ) do |path|
      fname = path.sub( add_dir, '' )
      fdir, base = File.split( fname )
      if FileTest.directory? path
        Find.prune if base[0] == ?. or 
                    base =~ /^(?:CVS|images|Old|Trash|delme|bak)/i
        unless File.exists?( fname ) or fname.empty?
          mkdir fname
          `cvs add #{ fname }`
        end
      elsif FileTest.file? path
        base_to = base.dup
        base_to = "htaccess.sample" if base_to == ".htaccess" 
        unless base =~ /(?:\.jpg|\.jpeg|\.pdf|\.gif|\.psd|~)$/
          path_to = fdir == "." ? base_to : File.join( fdir, base_to )
          unless File.exists? path_to
            copy path, path_to
            `cvs add #{ path_to }`
          else  
            copy path, path_to
          end
          checked[path_to] = true
        end
      end
    end

    # 2. mark for deletion any missing files
    Find.find( checkout_dir ) do |path|
      fname = path.sub( checkout_dir, '' )
      fdir, base = File.split( fname )
      if FileTest.directory? path
        Find.prune if base[0]  ?. or
                      base  "CVS" 
      elsif FileTest.file? path
        unless checked[fname]
          rm fname
          `cvs rm #{ fname }`
        end
      end
    end
  end

You're seeing a lot of the same use of Find and FileUtils here. But I also needed to get out to CVS. So I just used the backticks and went out to the shell. It's not that we need to avoid the shell. I like FileUtils because it looks nice and it outputs to the terminal.

So, a Bit

Yeah, so we got a bit of Ruby in today. It's the little sidekick automating the chores. It's not all over the place, yet. But it has its place.

Ruby has saved us a bit of manual labor today. In a few weeks I'll go over how you can use Ruby to help you manage your brain work and scheduling.

(Many thanks to my good friend Sleeper for the French translation!.)


by why the lucky stiff

july 2, 2004