In Progress
Unit 1, Lesson 1
In Progress

Pathname

Video transcript & code

In the last episode we used the File module to extract parts of filenames, like the directory, basename, and extension. The File module is fine for occasional filename-related usage. But when we have a lot of work having to do with filenames and paths, it becomes a little tedious to put File. in front of everything.

That's where the Pathname library comes in, which is the subject of today's episode. pathname is a sort of melting pot for Ruby filename-related methods, combining methods from several different modules including the File, Dir, FileUtils, and FileTest modules.

Let's start with how to make a pathname. First, we need to require the library. Then we can make a Pathname with Pathname.new.

require "pathname"

Pathname.new("~/.emacs")        # => #<Pathname:~/.emacs>

But there's a shortcut. Instead of .new, we can use the Pathname() conversion function.

require "pathname"

Pathname("~/.emacs")        # => #<Pathname:~/.emacs>

This is my preferred method for creating pathnames.

Now let's take a look at what we can do with this method.

First of all, we can expand paths to be fully-qualified.

require "pathname"

path = Pathname("193-pathname.org")
path = path.expand_path
# => #<Pathname:/home/avdi/Dropbox/rubytapas/193-pathname/193-pathname.org>

We have access to dirname, basename, and extname as instance methods. These work just like their File counterparts.

require "pathname"

path = Pathname("193-pathname.org")
path = path.expand_path
path.dirname                    # => #<Pathname:/home/avdi/Dropbox/rubytapas/193-pathname>
path.basename                   # => #<Pathname:193-pathname.org>
path.extname                    # => ".org"

Pathname also has some handy methods for cleaning up paths. For instance, if we have a path with a bunch of extra slashes or dots in it, we can send #cleanpath to it to get a cleaned-up version.

require "pathname"
Pathname("/home////avdi/./.emacs").cleanpath
# => #<Pathname:/home/avdi/.emacs>

There is also a helper for resolving symbolic links. Let's say we have a file, "FOO", and a symbolic link to that file called LINK_TO_FOO. When create a Pathname for LINK_TO_FOO and then expand and clean it, the resulting Pathname is still for LINK_TO_FOO. But if we instead use realpath, the resulting expanded pathname is for FOO, the target of the link.

require "pathname"
Pathname("LINK_TO_FOO").expand_path.cleanpath
# => #<Pathname:/home/avdi/Dropbox/rubytapas/193-pathname/LINK_TO_FOO>
Pathname("LINK_TO_FOO").realpath
# => #<Pathname:/home/avdi/Dropbox/rubytapas/193-pathname/FOO>

Pathname makes it easy to query for various properties of a file. We can test if it is a file, or if it is a directory. We can check its size on disk, or ask when it was last modified. This is just a sampling of the metadata that's available; see the documentation for a complete list.

require "pathname"
path = Pathname("193-pathname.org")
path.file?      # => true
path.directory? # => false
path.size       # => 2835
path.mtime      # => 2014-03-02 14:59:04 -0500

Making filesystem changes is easy as well. We can copy files, rename them, and delete them. We can even make whole directory trees and remove them.

require "pathname"

bar = Pathname("BAR")
bar.write("Hello, world!")
bar.rename("BAZ")
Pathname("BAZ").exist?          # => true
Pathname("BAZ").delete
Pathname("BAZ").exist?          # => false

Pathname("parent/child/grandchild").mkpath
Pathname("parent/child/grandchild").exist? # => true
Pathname("parent").rmtree
Pathname("parent").exist?       # => false

We can use a Pathname to open a file for reading or writing. This works just like the Kernel#open method.

require "pathname"

Pathname("test").open("w") do |f|
  f.write "Hello, world"
end

Pathname("test").open do |f|
  f.read                        # => "Hello, world"
end

Another cool thing about Pathname is that it's also compatible with Kernel#open. So if we have some existing code that uses Kernel#open, we can replace our strings with Pathname objects and it will still work.

require "pathname"

path = Pathname("test")
f = open(path)
f.read                          # => "Hello, world"

It can be convenient to process whole directories full of files as pathnames. To accomodate this usage, Pathname provides the .glob method. We can get an array of pathnames by saying Pathname.glob and passing a file glob pattern.

require "pathname"

Pathname.glob("*")
# => [#<Pathname:LINK_TO_FOO>
#     #<Pathname:xmptmp-in4834I6J.rb>,
#     #<Pathname:test>,
#     #<Pathname:xmptmp-out4834VEQ.rb>,
#     #<Pathname:193-pathname.org>,
#     #<Pathname:FOO>]

This can make for some very concise code. Let's make some directories. Then we'll select all the subdirectories in the current directory.

require "pathname"

%W[dir1 dir2 dir3].each do |d| Pathname(d).mkpath end

Pathname.glob("*").select(&:directory?)
# => [#<Pathname:dir2>, #<Pathname:dir1>, #<Pathname:dir3>]

So far I've shown just a few highlights of what can be found in the pathname library. There are many more methods I haven't covered. I definitely recommend looking up the documentation and learning about what else it provides. If you have a program which does more than a little work with files and directories, Pathname is definitely the way to go. Happy hacking!

Responses