In Progress
Unit 1, Lesson 21
In Progress

Custom Splat

Video transcript & code

A number of UNIX network utilities make use of the .netrc file to retrieve login credentials for remote hosts. This file normally lives in a user's home directory and is locked down to only be readable by that user. Here's an example of a .netrc file. Each entry consists of a machine name, a login, and a password.

machine rubytapas.com
  login avdi
  password xyzzy
machine example.org
  login marvin
  password plugh

For more information on the .netrc file format, look up the relevant manpage.

man netrc

There is a Ruby gem that makes it easy to read and write .netrc files. To use it, we can tell it to read in a .netrc file. Then we can look up a machine by name. The return value from this lookup is a two-element array. The first element is the login, and the second element is the password.

require 'netrc'
netrc = Netrc.read('.netrc')
entry = netrc['rubytapas.com']
entry[0]                        # => "avdi"
entry[1]                        # => "xyzzy"

You may remember from episode 81 that it's possible to implicitly splat out an array into separate variables. This gives us a convenient way to assign both the login and the password at once.

require 'netrc'
netrc = Netrc.read('.netrc')
login, password = netrc['rubytapas.com']
login                           # => "avdi"
password                        # => "xyzzy"

Looking at this interface, we feel like it might be nice if a .netrc entry were represented as a more meaningful object than an array. For instance, how about a Struct with login and password fields?

class Netrc
  Entry = Struct.new(:login, :password)
end

As you may recall from episode 20, Structs are very flexible objects. With a Struct representing a .netrc entry, we can access the username and password through method calls, or using Hash-style notation. But not only that, we can even access them using array indices - exactly the way the original two-element array worked.

class Netrc
  Entry = Struct.new(:login, :password)
end
entry = Netrc::Entry.new('avdi', 'xyzzy')
entry.login                     # => "avdi"
entry.password                  # => "xyzzy"
entry[:login]                   # => "avdi"
entry[:password]                # => "xyzzy"
entry[0]                        # => "avdi"
entry[1]                        # => "xyzzy"

In fact, it seems like our Entry Struct would be a perfect replacement for the old array-style entries. Except for one little problem.

When we try to assign the attributes of the entry to login and password local variables, using the implicit splatting semantics we relied on earlier, it doesn't work. Instead, the first variable receives the entire struct, and the second variable remains unset.

class Netrc
  Entry = Struct.new(:login, :password)
end
entry = Netrc::Entry.new('avdi', 'xyzzy')
login, password = entry
login                           # => #<struct Netrc::Entry login="avdi", password="xyzzy">
password                        # => nil

In episode 81 we said that implicit splatting only works on arrays and "array-like" objects. So what, exactly, qualifies an object as "array-like"? Well, as it turns out, Ruby makes this determination based on whether the object responds to the #to_ary method. Array objects respond to this method, but our struct does not.

class Netrc
  Entry = Struct.new(:login, :password)
end
entry = Netrc::Entry.new('avdi', 'xyzzy')

['avdi', 'xyzzy'].respond_to?(:to_ary) # => true
entry.respond_to?(:to_ary)      # => false

Fortunately, this is easy to change. Entry objects already respond to #to_a, which is standard Struct behavior. All we need to do, then, is alias their #to_a method to #to_ary. Now, Entry objects can be implicitly splatted out just like arrays.

class Netrc
  Entry = Struct.new(:login, :password) do
    alias_method :to_ary, :to_a
  end
end
entry = Netrc::Entry.new('avdi', 'xyzzy')
login, password = entry
login                           # => "avdi"
password                        # => "xyzzy"

Now we have a class that really would make a good replacement for the old two-element-array return value. It preserves the semantics of the original, so no existing client code should be affected by it. And it adds new, meaningful accessor methods.

This seemed like a good enough idea to me that I went ahead and sent a pull request to the netrc gem project, and it was accepted and merged in. It might even be rolled into the latest gem release by the time you watch this.

Happy hacking!

Responses