Thursday, March 31, 2011

Ruby gotcha: Hash default

Ruby lets you specify a default value when you create a Hash. Otherwise, if a key isn't found, it returns nil:
h = {}
=> {}
h['foo']
=> nil
h = Hash.new(1)
=> {}
h['foo']
=> 1

Great! That works fine when the default value is a number, but try a String, and you may see something weird:
h = Hash.new("default")
=> {} # empty hash created
h['foo']
=> "default" # 'foo' has the default value
h['bar'] += " value"
=> "default value" # 'bar' gets the default, plus " value" appended
h['baz'].slice!(0..1) # whoa, wait! what's going on here?
=> "de"
h['foo']
=> "fault" # what happened to foo?
h['bar']
=> "default value" # bar is still ok!
h
=> {"bar"=>"default value"} # but it's the only thing in the hash!

What's going on here? In short, a bunch of stuff that makes sense in retrospect, once you know what's happening under the hood, but isn't very intuitive.

The first thing is that calling h['foo'] doesn't add key 'foo' to the hash with the default value; it just returns the default. It doesn't add a key to the hash unless we actually set a value, like we do for 'bar'.

What about the "h['baz'].slice!(0..1)" line? That's actually modifying the default string in place, not just for baz, but for everyone. It still doesn't set a value for baz. That's a little surprising. If you do slice() instead of slice!(), it doesn't modify anything.
h['baz'].slice(0..1)
=> "de"
h['baz']
=> "default"

To modify baz and only baz, you need to do something like this:
h['baz'] += ""                 # set the value in baz to the default plus an empty string
=> "default"
h['baz'].slice!(0..1) # modify baz in place
=> "de"
h['foo']
=> "default" # foo still the default
h['baz']
=> "fault" # baz modified

Ok, that's a quirky edge case, but what's the big deal? Why am I writing a post about this? Because if set the default to an empty array, it really isn't going to work the way you want:
h = Hash.new([])
=> {}
h['foo']
=> []
h['foo'].push "red"
=> ["red"]
h['bar']
=> ["red"]
h['bar'].push "green"
=> ["red", "green"]

Instead of having an empty array associated with each key, there's one array shared by all keys. (I actually ran across this problem while trying to set a default hash, and it was much less obvious what had gone wrong.)
Now, you can create a Hash that will do what you want, but you need to pass a code block to the constructor:
h = Hash.new {|hash, key| hash[key] = []}
=> {}
h['foo']
=> []
h['foo'].push "red"
=> ["red"]
h['bar'].push "green"
=> ["green"]

So it turns out that this constructor is what you need to use pretty much anytime your default is a container or complex object. That's going to me handy for me to know, at least.

Tuesday, March 29, 2011

Time Capsule + Linux

I just spent a while figuring out how to access my Apple Time Capsule from my Linux box (Ubuntu 10.10). Turns out it's really easy, but there's a bit of a trick. You can do it through the "Connect to Server..." dialog on the "Places" menu on the panel.

Service type is "Windows Share" (which is a bit unintuitive).
Server is the IP address of your Time Capsule.
Share is what tripped me up. It's the name of the name of the Time Capsule as it appears in your Mac's /Volumes directory. Note that this may not be how it shows up in the Finder. My Time Capsule shows up in the Finder as "Colin MacDonald's Time Capsule", but it's mounted as /Volumes/Time Capsule Drive. "Time Capsule Drive" (with spaces) was what I needed for the Share name.
Folder should be "data". This is an existing directory on the Time Capsule drive. (If you look at the root directory for the Time Capsule when it's mounted, you'll also see a .sparsebundle file for each Mac you have backed up.)
User Name is your username on the Time Capsule. It's probably the same as your username on your Mac. (You'll be prompted for the matching password later.)
Domain Name is "WORKGROUP".
Bookmark is optional. It just gives you a handy name for the folder in your file browser (Nautilus).

Once you've mounted the Time Capsule, it will show up in Nautilus as a share. This will be its root directory, not the "data" folder. If you created a bookmark, it will also show up in Nautilus, but it will point to the "data" folder.

Of course, since it's a Windows share, I lose my Linux file permissions when I copy things there, which limits its use as a backup drive. I expect there'll be another post when I get that figured out.

Saturday, October 16, 2010

vim custom syntax

I've been known to write blog posts. I tend to draft them in a text file, just because it's easier to edit in vim than in a text box and I get an automatic backup. The format is sorta HTML, but not quite: I don't have to bother with <p> tags, etc. To avoid confusion, I give them a file extension of .blog, but I want vim to highlight them like .html files.

Getting vim to do this is actually pretty straightforward, but figuring out how was a pain. The info is scattered across a bunch of help files and cluttered with references to old versions and non-recommended options. So I'm pulling it together here.

First, create a ~/.vim/syntax/blog.vim file containing:
runtime! syntax/xhtml.vim

This says to invoke the xhtml syntax file. (You could do plain html instead.)

Then you need to tell vim to use the blog.vim syntax when you're editing a .blog file. To do this, create a ~/.vim/ftdetect/blog.vim file containing:
au BufRead,BufNewFile *.blog        set filetype=blog

That's it.

Tuesday, July 6, 2010

gnupg-agent installation

I ran into a little Ubuntu package installation quirk the other day. I wanted to install gpg-agent on a remote server. It's a headless server, so it doesn't have any window manager crap installed. gpg-agent has to be able to prompt you for keystore passphrases, and normally, it would pop up a window for that. So when I first tried to install gpg-agent, it said it would also have to install this alarming list of xwindows and GTK packages. Didn't want that.

What's actually going on is that gpg-agent just has a "pinentry" dependency. The default pinentry is is pinentry-gtk2, which of course depends on gtk and xwindows and all that. But there's also an curses (text mode) pinentry package that satisfies the dependency. That's what you'd want if you're ssh-ing into the machine. If that's already installed, gpg-agent won't ask for anything more.

So the commands are:
sudo apt-get install pinentry-curses
sudo apt-get install gnupg-agent

Saturday, July 3, 2010

About

Hi! My name's Colin. I'm a programmer. Often, in the course of my work, I spend Way Too Much Time beating my head against the computer, trying to figure out why some bit of software doesn't work the way it should (or how it's supposed to work in the first place). Sometimes, I'm saved that frustration by some clever person somewhere out there on the web who's already figured it out and written about it. This blog is my way of returning the favor, in a fuzzy, karmic sense. It's basically a dumping ground for random technical minutiae. It's a reference, rather than something you'd actually want to subscribe to.

Jekyll Pages List

Getting a list of pages in a Jekyll site is easy, but paring that down to just the top-level files and the index pages is tricky. {% for p...