Pathname of the Righteous 2

Posted by Justin Reagor Thu, 30 Apr 2009 00:25:00 GMT

Lately, I've noticed a lot of scripts taking advantage of File, Dir, FileUtils and even FileTest. Nothing odd, and these are fully warranted for most. They get the job done just fine, end of story.

But Ruby is special. Like little yellow bus special. It's about these things called Object instances, not Class/singleton methods. Maybe you'll agree with me, maybe you won't... but I'm sure you'll enjoy reading more about what Ruby is packaged to offer.

The Golden Child

Pathname is what this article is about. It's part of the standard library which comes included with MRI's Ruby distribution. You might have seen Pathname used like this...

require 'pathname'
$: << Pathname(__FILE__).dirname.join('lib', 'bacon', 'bits').to_s

...which is normally this...

$: << File.expand_path(File.join(File.dirname(__FILE__), 'lib', 'bacon', 'bits'))

BTW: $: is called the "load path", I'm sure you've seen plenty of errors about it... read here

This is because Pathname represents a full file name path in your filesystem. Only as a Ruby Object instance. See what I mean? Using Ruby this way just makes you want to rub your tummy like Santa after cookies and milk.

Alil Deeper

Most people stop there. They say to themselves, "Man, that looks great but you're not really saving lines of code. I mean, you just daisy chain method calls and every time I use this I have to explicitly require it".

Thinking this way is just wrong, because "All functionality from File, FileTest, and some from Dir and FileUtils is included, in an unsurprising way. It is essentially a facade for all of these, and more".

What does this mean? Well, when you find some crazy file handling method...

def some_files
  @file_list = Dir.entries(DOC_PATH)
  @file_list.reject! { |entry| entry =~ /^\./ }
  @pretty_file_list = []
  @file_list.each do |file|
    file_path = File.join(DOC_PATH, file)
    if ! File.directory?(file_path)
      tmp = {}
      tmp[:name] = file
      tmp[:date] = File.ctime(file_path)
      tmp[:size] = sprintf("%.2f", (File.size(file_path) / 1.0.kilobytes)) + " kb"

      @pretty_file_list << tmp
    end
  end
  @pretty_file_list.sort! { |x,y| x[:date] <=> y[:date] }
end

You can refactor it like so...

def some_files
  @file_list = Pathname(DOC_PATH).children.
    delete_if(&:directory?).
    sort_by(&:ctime).inject([]) do |file|
      {
        :name => file.to_s,
        :date => file.ctime,
        :size => sprintf("%.2f", (file.size / 1.0.kilobytes)) + " kb"
      }
    end
end

Personally, I would just pass around the actual Pathname instances without building this hash because Pathname provides an immense amount of convenience for handling files wrapped into one library.

The library uses this example...

require 'pathname'
p = Pathname.new("/usr/bin/ruby")
size = p.size              # 27662
isdir = p.directory?       # false
dir  = p.dirname           # Pathname:/usr/bin
base = p.basename          # Pathname:ruby
dir, base = p.split        # [Pathname:/usr/bin, Pathname:ruby]
data = p.read
p.open { |f| _ }
p.each_line { |line| _ }

With Strings

Just remember that Pathname returns instances, not Strings. So methods that use #to_s will work like normal.

puts Pathname('.').expand_path
=> "/home/justin/Kinetic/articles"

p Pathname('.').expand_path
=> #<Pathname:'/home/justin/Kinetic/articles'>

You can even concatenate String's to Pathname's to build a path, as you would between normal strings.

p1 = Pathname.new("/usr/lib")   # Pathname:/usr/lib
p2 = p1 + "ruby/1.8"            # Pathname:/usr/lib/ruby/1.8
p3 = p1.parent                  # Pathname:/usr
p4 = p2.relative_path_from(p3)  # Pathname:lib/ruby/1.8
pwd = Pathname.pwd              # Pathname:/home/gavin
pwd.absolute?                   # true
p5 = Pathname.new "."           # Pathname:.
p5 = p5 + "music/../articles"   # Pathname:music/../articles
p5.cleanpath                    # Pathname:articles
p5.realpath                     # Pathname:/home/gavin/articles
p5.children                     # [Pathname:/home/gavin/articles/linux, ...]

However, you will need to call #to_s. For instance, when adding to the load path array, which normally contains expanded path strings.

From up top, notice the #to_s...

$: << Pathname(__FILE__).dirname.join('lib', 'bacon', 'bits').to_s

But when you're requiring you won't need it.

require Pathname(__FILE__).dirname.join('lib', 'bacon', 'bits')

In Conclusion

So that's really it... when not to use Pathname? Well, you will want to check to make sure that alternative Ruby implementation support it. Classes like File and Dir definitely will be supported by many, but Pathname might not. Also, if you believe you'll be doing major binary work, heavy File CRUD or possibly need to extend File writing operations... please don't use this. I also believe Windows support has gotten better in Ruby 1.9.1, but its a little flacky in older versions. But who uses Windows?

Simple stuff really, but I hope to see more of you using this instead of all those File classes and their class method calls. Even when you need to do very few things with Pathname, just require it, because I'm sure you'll find some use for it later!

Comments

Leave a response

  1. Avatar
    grosser about 5 hours later:
    awesome! i never heard of this and will try to use/refactor my old code, hopefully it also works good in 1.8.6... PS: your comment form has a bug, if you press submit once, and the name is missing, submit is disabled and you have to reload/renter the comment
  2. Avatar
    cheapRoc about 13 hours later:
    This blog needs a serious overhaul in general... I've never been a fan of pre-packaged Typo or Wordpress. Thanks for pointing it out though because we were unaware!
Comments