More Address Book scripting

I’m still cleaning up my Address Book in preparation for a large mailing to my company’s clients. Today I wrote a couple of scripts, one in AppleScript and the other in Perl, to make address labels.

I already had a script for printing address labels, but it’s meant for Avery 5164 labels, the big ones you’d put on a box or a large envelope. What I need now is a script for Avery 5363 labels, more suited for a small envelope or large postcard.

Before we get into the scripts, you’re probably wondering why I don’t just use the label printing utility built in to Address Book. The easy answer is that Address Book doesn’t know about the 5363 layout, so I can’t print from there. But that’s not the real answer. I’m sure there’s some label printing application for the Mac that knows about 5363s, or maybe a Pages or FileMaker template that I could flow the addresses into, but I never even thought about these solutions because

  1. Given that I’ve already written several label-printing scripts, I knew I could whip out a new one with just a few edits.1
  2. I like the full control I get with my own scripts.

The new script is called pplabels (print postcard labels), and it’s a minor edit of my older palabels script. New label dimensions, three columns instead of two, but basically the same script.

perl:
  1:  #!/usr/bin/perl
  2:  
  3:  use Getopt::Std;
  4:  
  5:  # Usage/help message.
  6:  $usage = <<USAGE;
  7:  Usage: pflabels [options] [filename]
  8:  Print address labels on Avery 5363 sheets
  9:  
 10:    -r m : start at row m (range: 1..8; default: 1)
 11:    -c n : start at column n (range 1..3; default: 1)
 12:    -h   : print this message
 13:  
 14:  If no filename is given, use STDIN. A label entry is a plain text
 15:  series of non-blank lines. Blank lines separate entries.
 16:  USAGE
 17:  
 18:  # Set up geometry constants for Avery 5363.
 19:  $topmargin = 0.125;
 20:  $poleft = 0.25;
 21:  $pomiddle = 3.10;
 22:  $poright = 5.93;
 23:  $lheight = 1.375;
 24:  
 25:  # get starting point from command line if present
 26:  getopts('hr:c:', \%opt);
 27:  die $usage if ($opt{h});
 28:  
 29:  $row = int($opt{r}) || 1;    # chop off any fractional parts
 30:  $col = int($opt{c}) || 1;    # or set defaults
 31:  
 32:  # Bail out if position options are out of bounds
 33:  die $usage unless (($row >= 1 and $row <= 8) and 
 34:                     ($col >= 1 and $col <= 3));
 35:  
 36:  # Set initial horizontal and vertical positions.
 37:  if ($col == 1) {
 38:    $po = $poleft;
 39:  } elsif ($col == 2){
 40:    $po = $pomiddle;
 41:  } else {
 42:    $po = $poright;
 43:  }
 44:  $sp = ($topmargin + ($row - 1)*$lheight);
 45:  
 46:  # Various outputs.
 47:  open OUT, "| groff -P-m | lpr";     # for printing directly
 48:  # open OUT, "| groff | ps2pdf - ";  # for making a PDF
 49:  # open OUT, "> labels.rf";          # for debugging
 50:  select OUT;
 51:  
 52:  # Set up document.
 53:  print <<SETUP;
 54:  .ps 11
 55:  .vs 13
 56:  .nf
 57:  .ll 2.4i
 58:  
 59:  SETUP
 60:  
 61:  # The troff code for formatting a single entry, with placeholders for
 62:  # positioning on the page. The magic numbers embedded in the formatting
 63:  # commands make the layout look nice.
 64:  $label = <<ENTRY;
 65:  .sp |%.2fi
 66:  .po %.2fi
 67:  .ft H
 68:  %s
 69:  ENTRY
 70:  
 71:  # Slurp all the input into an array of entries.
 72:  $/ = "";
 73:  @entries = <>;
 74:  @sortedentries = sort @entries;   # too lazy to sort on last name
 75:  
 76:  $bp = 0;                  # we don't want to start with a page break
 77:  
 78:  foreach $addr (@sortedentries) {
 79:    # Eliminate trailing whitespace
 80:    $addr =~ s/\s+$//;
 81:    # Scoot the address down if it's less than 6 lines long.
 82:    $nl = split(/\n/, $addr);
 83:    if ($nl <= 4) {
 84:      $addr = ".sp\n" . $addr;
 85:    } elsif ($nl == 5) {
 86:      $addr = ".sp 6.5p\n" . $addr;
 87:    }
 88:  
 89:    # Break page if we ran off the end.
 90:    if ($bp) {
 91:      print "\n.bp\n";      # issue the page break command
 92:      $bp = 0;              # reset flag
 93:    }
 94:    
 95:    # Print the label.
 96:    printf $label, $sp, $po, $addr;
 97:    
 98:    # Now we set up for the next entry.
 99:    if ($col == 1){       # last entry was in the left column
100:      $col = 2;             # so the next will be in
101:      $po = $pomiddle;      # the middle column
102:    } elsif ($col == 2){  # last entry was in the middle column
103:      $col = 3;             # so the next will be in
104:      $po = $poright;       # the right column
105:    } else {              # last was in the right column
106:      $col = 1;             # so the next will be in
107:      $po = $poleft;        # the left column
108:      $row++;               # of the next row
109:      if ($row > 8) {      # we're at the end of the page
110:        $bp = 1;            # page break flag
111:        $row = 1;           # new page starts at top row
112:      }
113:      $sp = ($topmargin + ($row - 1)*$lheight);
114:    }
115:  }

The script reads in a set of addresses in a plain text file that looks like this

John H. Kernighan
Thompson Industries
1337 Knuth Boulevard
Chicago, IL 60606

Cynthia P. Ossanna
Gamma, Helm, Johnson
  Vlissides & Booth
314 East Addison Street
Suite 2200
New York, NY 10005

where the addresses are laid out just as if you were going to type them directly on the label, with blank lines separating the entries. The script inserts the appropriate troff codes, processes the result through groff, and sends the PostScript file produced by groff to the printer, which waits for the Avery 5363 sheets to be manually fed to it.

The printing can be started on any of the 24 label positions on the sheet by passing options to pplabels. For example,

pplabels -r 3 -c 2 addresses.txt

will put the first label in the middle column of the third row and continue from there.

The code is fairly straightforward. Most of it is concerned with setting the initial label position (Lines 18-44) and then figuring out the next label position (Lines 98-113). There are a couple of things worth noting:

OK, that’s how the labels get printed. Where does the input file come from? It comes from the Address Book and this AppleScript:

 1:  set out to ""
 2:  tell application "Address Book"
 3:    set cList to the selection
 4:    repeat with c in cList
 5:      tell c
 6:        set addrs to (every address whose label is "work")
 7:        set addr to item 1 of addrs
 8:        set out to out & first name & " "
 9:        if middle name is not missing value then
10:          set out to out & middle name & " "
11:        end if
12:        set out to out & last name
13:        if suffix is not missing value then
14:          set out to out & " " & suffix
15:        end if
16:        set out to out & return & organization
17:        if length of (organization as text) > 30 then
18:          set out to out & "*"
19:        end if
20:        set out to out & return
21:        tell addr
22:          set out to out & street & return ¬
23:            & city & ", " & state & " " & zip & return
24:          if country is not "USA" then
25:            set out to out & country & return
26:          end if
27:        end tell
28:        
29:      end tell
30:      set out to out & return
31:    end repeat
32:  end tell
33:  --get out
34:  set the clipboard to out

This takes the contacts I have selected and puts their addresses—in the format given above (almost)—on the clipboard. I then paste that into a text file, and it’s ready to be processed by pplabels.

Almost.

Take a look at Lines 17 and 18. If the company name is more than 30 characters long, the script puts an asterisk after it. I need that because several of my clients are law firms with long names—Goodman, Lieber, Kurtzberg & Holliway, LLP, for example—that need to be split into two lines. Because I don’t want line breaks at ampersands, I edit the input file by hand rather than trust a line-breaking algorithm. The asterisks help me find the lines to split.

If you have some experience scripting the Address Book, you may be wondering why I don’t just use

set out to out & name

instead of all the conditionals in Lines 8-15. The reason is my Address Book entries include titles (Mr., Ms., Dr.) that I don’t want on the label.

Similarly, I don’t use

set out to out & formatted address

but in this case, it’s not just me being persnickety. The formatted address includes weird characters between the city and the state and between the state and the zip. These characters are invisible in a text editor, but show up as an accented a in the printed copy2 produced by pplabels. I’m sure there’s a way to handle that character, but it’s easier to avoid it entirely. I just don’t use Perl enough anymore to make it worth the effort to learn its version of the Unicode sandwich.

Remember when I said the addresses in the pplabels input file weren’t in any rational order? Line 3 of this AppleScript is the reason. It returns the list of selected contacts in an order that probably makes sense to a hashing algorithm but doesn’t make sense to humans. And because AppleScript has no builtin sorting command (I know, unbelievable, right?), and I no intention of writing a sort routine in AppleScript, the addresses this script puts on the clipboard are unsorted.

At present, the pplabels script isn’t in my GitHub repository of similar scripts, but I’ll probably put it there soon.


  1. These scripts were first written in the late ’90s when I was using Linux and had to write my own scripts, as there were no templates or label-making utilities available. 

  2. If I remembered which accent it is, I’d tell you, but I don’t. And I don’t feel like screwing around with the script to find out.