Command line utilities for the Address Book

The Mac gives you many scripting language choices. My preferences, from most-liked to least-liked, are

  1. Perl, at the top of the list because I have a decade of experience with it.
  2. Ruby, coming on strong as I get more accustomed to object-oriented thinking.
  3. Bash shell, which I really hate because of its bizarre syntax, especially with assignment statements; but which is often the best choice when a script is nothing more than a few commands strung together.
  4. AppleScript, programming in which is--for me, at least--nothing more than an exercise in trial and error.

So this post and one I plan to write tomorrow are about how I used my two least-liked scripting languages to put together a couple of useful utilities. I won’t talk about how long it took me to write them, because that would be embarrassing; and I won’t talk about my frustrations, because that would double the length of the post and make it hard for me to get a good night’s sleep.

The first utility is called plainaddress, and it extracts the name and address information from an Address Book entry and returns it formatted for a letter or an envelope or an address label. For example, if I invoke it from the command line this way

plainaddress John Smith  

it will spit back this

John L. Smith
Smith and Associates
1234 Main Street
Suite 222
Centerville, IL 60606

The plainaddress script itself is just a couple of lines of bash:

#!/bin/bash
osascript ~/bin/plainaddress.scpt "$*"

This is saved in my ~/bin folder, which is in my $PATH, and made executable with chmod +x ~/bin/plainaddress.

The only interesting part of this script is the way it uses the $* variable to collect all the arguments and quotes them to present a single argument (possibly with spaces) to the plainaddress.scpt AppleScript.

As you might expect, plainaddress.scpt is where the real work is done. Here’s the listing:

on searchName(theName)
  tell application "Address Book"
    set nameWords to every word in theName
    set personList to every person whose name¬
    contains the first item of nameWords
    repeat
      if the length of nameWords = 1 then
        exit repeat
      else
        set newPersonList to {}
        set nameWords to rest of nameWords
        repeat with p in personList
          if name of p contains first item of nameWords then
            set end of newPersonList to p
          end if
        end repeat
        set personList to newPersonList
      end if
    end repeat

    return the first item of personList
  end tell
end searchName

on fullName(p)
  tell application "Address Book" to set fn to name of p
  set titles to {"Mr.", "Ms.", "Miss", "Mrs.", "Dr."}
  set AppleScript's text item delimiters to " "
  set nameWords to text items of fn
  if first item of nameWords is in titles then
    set nameWords to rest of nameWords
  end if
  return nameWords as string
end fullName

on mailingAddress(p)
  tell application "Address Book" to set addr to¬
  formatted address of address of p as string
  set AppleScript's text item delimiters to "
"
  set addrLines to text items of addr
  if last item of addrLines is "USA" then
    set addrLines to reverse of rest of reverse of addrLines
  end if
  return addrLines as string
end mailingAddress

on run argv
  set nameaddr to ""
  try
    set contact to searchName(item 1 of argv)
    set nameaddr to fullName(contact) & "
"
    tell application "Address Book"
      if exists (organization of contact) then
        set nameaddr to nameaddr & (organization of contact) & "
"
      end if
    end tell
    set nameaddr to nameaddr & mailingAddress(contact)
    return nameaddr
  end try
end run

This was written in the Script Editor and saved as a script in my ~/bin folder.

The searchName function is my attempt to create an AppleScript function that mimics the way the Address Book search field works. If you type “John Smith” into the Address Book search field, the entry for John L. Smith will be found--you don’t have to include the middle initial in your search. But, as far as I can tell, there is no comparable AppleScript command in the Address Book dictionary. If you try

get every person whose name contains "John Smith"

you won’t find John L. Smith because of the missing middle initial.

How searchName gets around this is by

  1. splitting the search string into words (substrings separated by spaces);
  2. getting the list of people whose name contains the first word;
  3. refining that list of people, keeping only those whose name also contains the second word; and
  4. repeating step 3 for the subsequent words in the search string.
  5. returning the first person in the refined list.

If there’s only one word in the search string, steps 3 and 4 are skipped. If there are only two words in the search string, step 4 is skipped. If more than one entry matches the search, only the first entry is returned; this makes the subsequent code easier to write and has never posed a problem for me--I know my Address Book well enough to know how much of a name is needed to insure a unique result.

The on run portion of the script uses searchName to get the Address Book entry for the person we want, then plucks out the address information and formats it. I think it pretty much speaks for itself. The only comment I have is that I hate how the Script Editor changed every "\n" I typed into a literal newline; it really messes up the indentation.

Although they were a pain to write (OK, I said I wasn’t going to talk about that), these scripts are a pleasure to use, and I use them all the time. I especially like that I don’t have to quote the search string if it contains spaces. It’s so much more natural to write

plainaddress John Smith

than

plainaddress "John Smith"

Actually, it’s even more natural to write

plainaddress john smith

which works because AppleScript’s string comparison is case-insensitive.

You may be wondering why I didn’t wrap the AppleScript into the shell script by using a here document, creating only one script instead of two. It’s because I’ve never gotten here documents to work in shell scripts the way I’ve gotten them to work in Perl. So, after beating my head against that particular wall for a while (right, said I wasn’t going to talk about that), I decided to wimp out and use two files. Sue me.

Update
After looking over the AppleScript, I decided I could make it easier to read by factoring out the parts that get the contact’s name and address. The script you see now reflects those changes. Both fullName and mailingAddress use the text item delimiters property to split a string into a list and then construct a string from a list. Between the splitting and reconstructing, the lists are manipulated using some of the ideas AppleScript borrowed from Lisp; not surprisingly, Apple chose to use the names “first” and “rest” instead of “car” and “cdr.”

Tags: