A quick script for Avery 5160 labels

An occasional theme of these blog posts is the value of scripting as a way of showing your computer who’s boss. If you can’t write scripts—and here I’m adopting a broad definition of scripts to include macros and other customizations—you become a slave to your computer, forced to do things the way it wants you to instead of the way that’s most natural and efficient for you.

Tonight I helped my wife and a friend who needed to print up a bunch of labels for the neighborhood swim team. The computer program the swim team uses to run the meets and keep track of times can print labels, but not the type of label they needed. What they needed was a label for each swimmer who swam faster than the “city time”1 in one or more events this year. The labels were to be stuck to the backs of ribbons given out at tomorrow morning’s awards ceremony. (Oh yes, I forgot to mention: there was a looming deadline for these labels.)

What we had was a printed list of all the swimmers who were to get ribbons and the events in which they’d made city times. What we needed was to get that information printed on a set of Avery 5160 labels. Some division of labor was in order.

The list went to our daughter, the fastest non-professional typist I know. I asked her to retype the list of names and events into a text file with a simple format and email it to me. Yes, we could have transferred the file over the network here in the house, but it’s usually fastest to use the tools you’re most familiar with.

While she was typing, I pulled up my ancient Perl code for printing file folder labels and started modifying it. That script was written for Avery 5161 label sheets, which have two columns of ten labels each. The 5160 sheets have three columns of ten labels each, so the necessary changes were obvious:

These changes went smoothly, and after a few syntax errors—programming in Python has gotten me out of the habit of ending statements with a semicolon—the script was up and running. It takes a text file that looks like this:2

#Irene Hartnett|2010
50 Free, 50 Back, 50 Breast
50 Fly, 100 IM

#Rosalina Reial|2010
50 Fly

#Darrin Schrick|2010
50 Back, 50 Fly

#Douglas Dunnam|2010
50 Free, 50 Back, 50 Breast
50 Fly, 100 IM

#Carmina Jaworsky|2010
50 Fly

#Elliott Oland|2010
50 Free, 50 Back, 50 Breast
50 Fly, 100 IM

#Salena Angel|2010
25 Back

#Elroy Tigert|2010
50 Free

#Robin Turiano|2010
50 Back

#Jolyn Mcclerkin|2010
50 Free, 50 Breast
50 Fly, 100 IM

#James Wolf|2010
50 Free, 50 Back, 50 Breast
50 Fly, 100 IM

#Renea Addison|2010
50 Free, 50 Back, 50 Breast
50 Fly, 100 IM

#Jesse Bautista|2010
25 Free, 50 Free, 25 Back
25 Breast, 25 Fly

#Gail Tanous|2010
50 Free, 50 Breast, 50 Fly

#Miguel Loarca|2010
25 Free, 50 Free, 25 Back
25 Breast, 25 Fly

and generates a PDF that looks like this:

I didn’t ask my daughter to type in the hash marks, vertical bars, or the year; I added those to the file with a couple of search-and-replace commands. I used the same formatting rules as my file folder label program:

Here’s the script that does it:

  1:  #!/usr/bin/perl
  2:  
  3:  use Getopt::Std;
  4:  
  5:  # Usage/help message.
  6:  $usage = <<USAGE;
  7:  Usage: prlabels [options] [filename]
  8:  Print file folder labels on Avery 5160 sheets
  9:  
 10:    -r m : start at row m (range: 1..10; 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:  
 17:  The first line of an entry is special. If it starts with a #, then it's
 18:  considered a header line. Everything in the header line up to the | is
 19:  printed flush left in bold and everything after the | is printed flush
 20:  right in bold. Subsequent lines are printed centered in normal weight.
 21:  If the first line of an entry doesn't start with #, it uses the header
 22:  of the previous entry.
 23:  USAGE
 24:  
 25:  # Set up geometry constants for Avery 5160.
 26:  $topmargin = 0.60;
 27:  $poleft = 0.4;
 28:  $pomiddle = 3.20;
 29:  $poright = 5.95;
 30:  $lheight = 1;
 31:  
 32:  # get starting point from command line if present
 33:  getopts('hr:c:', \%opt);
 34:  die $usage if ($opt{h});
 35:  
 36:  $row = int($opt{r}) || 1;    # chop off any fractional parts and
 37:  $col = int($opt{c}) || 1;
 38:  
 39:  # Bail out if position options are out of bounds
 40:  die $usage unless (($row >= 1 and $row <= 10) and 
 41:                     ($col >= 1 and $col <= 3));
 42:  
 43:  # Set initial horizontal and vertical positions.
 44:  if ($col == 1) {
 45:    $po = $poleft;
 46:  } elsif ($col == 2) {
 47:    $po = $pomiddle;
 48:  } else {
 49:    $po = $poright;
 50:  }
 51:  $sp = ($topmargin + ($row - 1)*$lheight);
 52:  
 53:  # Pipe output through groff and ps2pdf.
 54:  open OUT, "| groff | ps2pdf -";
 55:  # open OUT, "> labels.rf";    # for debugging
 56:  select OUT;
 57:  
 58:  # Set up document.
 59:  print <<SETUP;
 60:  .ps 11
 61:  .vs 15
 62:  .ll 2.20i
 63:  .ta 2.20iR
 64:  
 65:  SETUP
 66:  
 67:  # The troff code for formatting a single entry, with placeholders for
 68:  # positioning on the page. The magic numbers embedded in the formatting
 69:  # commands make the layout look nice.
 70:  $label = <<ENTRY;
 71:  .sp |%.2fi
 72:  .po %.2fi
 73:  .ft HB
 74:  %s
 75:  .ft H
 76:  .ce 3
 77:  %s
 78:  .ce 0
 79:  ENTRY
 80:  
 81:  # Slurp all the input into an array of entries.
 82:  $/ = "";
 83:  @entries = <>;
 84:  
 85:  $bp = 0;                  # we don't want to start with a page break
 86:  
 87:  foreach $body (@entries) {
 88:    # Parse and transform the header and body.
 89:    if ($body =~ /^#/) {    # it's a header line
 90:      ($header, $body) = split(/\n/, $body, 2);
 91:      $header = substr($header, 1);
 92:      $header =~ s/\|/\t/;
 93:    }  
 94:    $body =~ s/\s+$//;
 95:  
 96:    # Break page if we ran off the end.
 97:    if ($bp) {
 98:      print "\n.bp\n";      # issue the page break command
 99:      $bp = 0;              # reset flag
100:    }
101:    
102:    # Print the label.
103:    printf $label, $sp, $po, $header, $body;
104:    
105:    # Now we set up for the next entry.
106:    if ($col == 1){       # last entry was in the left column
107:      $col = 2;             # so the next will be in
108:      $po = $pomiddle;      # the middle column
109:    } elsif ($col == 2) { # last entry was in the middle column
110:      $col = 3;             # so the next will be in
111:      $po = $poright;       # the right column
112:    } else {              # last was in the right column
113:      $col = 1;             # so the next will be in
114:      $po = $poleft;        # the left column
115:      $row++;               # of the next row
116:      if ($row > 10) {      # we're at the end of the page
117:        $bp = 1;            # page break flag
118:        $row = 1;           # new page starts at top row
119:      }
120:      $sp = ($topmargin + ($row - 1)*$lheight);
121:    }
122:  }

I fed the input file into this script and sent the resulting PDF to the printer. Lots of labels in very little time.

More important, we now have a tool for doing this again and again. Maybe next year we’ll be able to save a step by getting the swim team’s program to spit out a text file instead of a printed list that has to be retyped.


  1. This is a somewhat arbitrary “wheat from chaff” separator. It’s called a city time because those who beat it get to compete in a city-wide meet at the end of the season. 

  2. By the way, if you ever need to create some fake names, I suggest The Name Generator