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.

  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 copy

2 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.