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
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:

• Line 74 sorts the addresses by the first name of the recipient. As we will soon see, the addresses in the input file will not be any rational order, and I thought it would be useful to give them some structure before printing. But I was too lazy to write the comparison subroutines necessary to sort by anything other than first name. If I find myself using this script often, I’ll probably add options to sort by last name or zip code.
• Lines 82-87 adjust the vertical position of an individual address to make it reasonably close to centered on it label. It does this by looking at the address and scootching the top line down if it has fewer than six lines. Three- and four-line addresses get adjusted by one line height; five-line addresses get adjusted by half a line height.

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 ""
3:    set cList to the selection
4:    repeat with c in cList
5:      tell c
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
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. ↩︎