Posts Tagged ‘perl’
Who’s up with nmap, revisited
August 19th, 2010 at 11:30 pm
I am, sad to say, the de facto network administrator for my small company. Today, one of my partners was having a connectivity problem, and while I was solving that problem (some cabling got disconnected), I learned that my little network probing script, whosup, needed to be updated.
Originally, whosup was a one-line shell script that ran nmap with certain parameters,
nmap -sP 192.168.1.*
which told nmap to do a “ping scan” of my local network and report the hosts that respond. The reason I made a script out of such a simple command is that I didn’t use nmap very often and couldn’t remember the options from one use to the next.
Two things have changed since the last time I used whosup:
- We now have a few Vista machines at work, and they don’t respond to the ping scan unless I run it as the superuser.
Nmaphas been upgraded, and the current version is more verbose in its output and is in the process of changing its ping scan option.
To account for these changes and to do a little future-proofing, I changed whosup from a shell script to Perl:
1: #!/usr/bin/perl
2:
3: @nmap = `sudo nmap -sn 192.168.1.0/24 2>&1`;
4: for (@nmap) {
5: print "$1\n" if /^Nmap scan.+?(\d+\.\d+\.\d+\.\d+).*/;
6: }
Line 3 runs nmap as the superuser (prompting me for my administrative password) and puts the output lines into the @nmap list.
- The
-snoption is the “no port scan” option used for host discovery. The older-sPoption, which-snreplaces, still works in Version 5.21 but is being phased out. - The
192.168.1.0/24tellsnmapto look at all the IPs whose first three bytes are 192, 168, and 1. Three bytes is 24 bits, which is where the/24comes from. - Finally, the
2>&1is the standardbashredirection operator which merges standard error into standard output. I did this becausenmapoften writes messages to standard error, and I wanted the rest of the program to filter them out.
Lines 4-6 then filter the nmap output lines. The nmap output from Line 3 looks something like this,
Starting Nmap 5.21 ( http://nmap.org ) at 2010-08-19 22:16 CDT
Nmap scan report for 192.168.1.1
Host is up (0.0053s latency).
MAC Address: 00:11:22:33:44:55 (Cisco-Linksys)
Nmap scan report for 192.168.1.10
Host is up (0.0038s latency).
MAC Address: 00:11:22:33:44:55 (Brother Industries)
Nmap scan report for 192.168.1.100
Host is up (0.034s latency).
MAC Address: 00:11:22:33:44:55 (Apple Computer)
Nmap scan report for 192.168.1.101
Host is up (0.035s latency).
MAC Address: 00:11:22:33:44:55 (Billionton Systems)
Nmap scan report for 192.168.1.103
Host is up (0.27s latency).
MAC Address: 00:11:22:33:44:55 (Unknown)
Nmap scan report for 192.168.1.107
Host is up (0.27s latency).
MAC Address: 00:11:22:33:44:55 (Apple)
Nmap scan report for 192.168.1.109
Host is up.
Nmap scan report for 192.168.1.110
Host is up (0.16s latency).
MAC Address: 00:11:22:33:44:55 (Sony Computer Entertainment)
Nmap done: 256 IP addresses (8 hosts up) scanned in 10.23 seconds
which has way too much information.1 I just want the IP numbers of the hosts that respond. Line 5 plucks out the IP numbers and prints them, one per line. The final whosup output looks like this:
192.168.1.1
192.168.1.10
192.168.1.100
192.168.1.101
192.168.1.105
192.168.1.109
192.168.1.110
A simple list of the hosts connected to the network. When I ran it at work today, I found a host I didn’t know about. Turns out one of my other partners has an old router in his office acting as hub.
-
The MAC addresses in the output above have been changed to protect the innocent (how many of you are old enough to get that reference?), but the rest of the output is real. ↩
Open URLs in Default Browser
August 4th, 2010 at 9:45 am
You’ve probably read this post at Daring Fireball in which John Gruber lays out an OS X service, written as an Automator workflow, that opens all the URLs in the currently selected text. If you haven’t, go read it now, because we’re going to talk about how to simplify it and make it work with browsers other than Safari and WebKit.
♫ Tall and tan and young and lovely, the girl from… ♬
Oh, you’re back. Good. Let’s talk about the code.
Assuming you’ve downloaded John’s workflow, you can open it up in Automator and scan through the code in the Run Shell Script pane.

But that’s not very convenient. You’d be better off looking at it in the Gist that John created for the script. Or maybe just look at it here:
1: #!/usr/bin/env perl
2:
3: # Description: http://daringfireball.net/2010/08/open_urls_in_safari_tabs
4: # License: See below.
5: # http://gist.github.com/507356
6:
7: use strict;
8: use warnings;
9: use URI::Escape;
10:
11: my $text = do { local $/; <> };
12: my @urls;
13: my $url_regex = qr{(?xi)
14: \b
15: ( # Capture 1: entire matched URL
16: (?:
17: [a-z][\w-]+: # URL protocol and colon
18: (?:
19: /{1,3} # 1-3 slashes
20: | # or
21: [a-z0-9%] # Single letter or digit or '%'
22: # (Trying not to match e.g. "URI::Escape")
23: )
24: | # or
25: www\d{0,3}[.] # "www.", "www1.", "www2." … "www999."
26: | # or
27: [a-z0-9.\-]+[.][a-z]{2,4}/ # looks like domain name followed by a slash
28: )
29: (?: # One or more:
30: [^\s()<>]+ # Run of non-space, non-()<>
31: | # or
32: \(([^\s()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels
33: )+
34: (?: # End with:
35: \(([^\s()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels
36: | # or
37: [^\s`!()\[\]{};:'".,<>?«»“”‘’] # not a space or one of these punct chars
38: )
39: )
40: };
41:
42: # Build an AppleScript-syntax list of the URLs in the input text.
43: while ($text =~ m{$url_regex}g) {
44: my $u = $1;
45: $u =~ s{([\x{80}-\x{ffff}])}{uri_escape($1)}eg; # Encode non-ASCII characters
46: push @urls, qq{"$u"};
47: }
48: my $urls_as_applescript_list = join ", ", @urls;
49:
50: # Get the user's default web browser; if it isn't Safari or WebKit, use Safari
51: # in the AppleScript. (And also use Safari if the backticks command fails.)
52: my $browser = `export VERSIONER_PERL_PREFER_32_BIT=yes; /usr/bin/perl -MMac::InternetConfig -le 'print +(GetICHelper "http")[1]'`;
53: chomp $browser;
54: unless ($browser =~ /^(?:Safari|WebKit)$/i) {
55: $browser = "Safari";
56: }
57:
58: # Create and run the AppleScript.
59: my $applescript = <<"END_SCRIPT";
60: set _url_list to {$urls_as_applescript_list}
61:
62: tell application "$browser"
63: -- activate # Uncomment to have browser activate when invoked
64: make new document
65: set _w to window 1
66: repeat with _url in _url_list
67: tell _w to make new tab with properties {URL:_url}
68: end repeat
69: tell _w to close tab 1 -- the empty tab the window started with
70: end tell
71: END_SCRIPT
72:
73: system("/usr/bin/osascript", "-e", $applescript);
74:
75: __END__
76:
77: LICENSE
78:
79: http://www.opensource.org/licenses/mit-license.php
80:
81: Copyright (c) 2010 John Gruber
82:
83: Permission is hereby granted, free of charge, to any person obtaining a copy
84: of this software and associated documentation files (the "Software"), to deal
85: in the Software without restriction, including without limitation the rights
86: to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
87: copies of the Software, and to permit persons to whom the Software is
88: furnished to do so, subject to the following conditions:
89:
90: The above copyright notice and this permission notice shall be included in
91: all copies or substantial portions of the Software.
92:
93: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
94: IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
95: FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
96: AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
97: LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
98: OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
99: THE SOFTWARE.
Lines 13-40 define the URL-hunting regular expression John has used elsewhere. Lines 43-47 pass that regex across the input text and extract the URLs it finds into a list called @urls.
The rest of the script, from Line 48 through Line 73, concerns itself with detecting the default browser and then creating and running an AppleScript that opens the URLs in that browser. Unfortunately, because AppleScript support is lacking in popular browsers like Chrome and Firefox, the service is really set up to work primarily with Safari and WebKit.1 If neither of these is your default browser, the URLs will open in Safari.
We can fix that by using the open command instead of AppleScript. Replace lines 48-73 with this:
48:
49: # Simplification by Dr. Drang (drdrang@gmail.com) on 8/4/2010. See description at
50: # http://www.leancrew.com/all-this/2010/08/open-urls-in-default-browser/
51: for (@urls) {
52: `/usr/bin/open -g $_`; # opens without bringing to browser to foreground
53: # `/usr/bin/open $_`; # opens and brings browser to foreground
54: }
The -g option to open keeps the browser behind the current application. If you want it to come forward right away, just comment Line 52 and uncomment Line 53. You can get the full script with my changes at this Gist.2
You can download the whole workflow, called “Open URLs in Default Browser,” as a zipped file. Unzip it and drag it into your ~/Library/Services folder, just like John’s original
I’ve tested this against four browsers: Safari, Firefox, Chrome, and Camino. It works in all of them, but you may get different behavior depending on the initial state of the browser. Here’s how the URLs open under various conditions:
| Browser | Unlaunched | No open windows | An open window |
|---|---|---|---|
| Safari | in new tabs | in new tabs | in new tabs |
| Firefox | in new windows, one extra window | in new windows | in new tabs |
| Chrome | in new tabs, one extra tab | in new tabs | in new tabs |
| Camino | in new tabs | in new tabs | in new tabs |
The “extra” window or tab you’ll sometimes get in Firefox and Chrome will contain whatever page you have set that browser to start with.
Recognize that I have all my browsers set up to open links from other programs in new tabs. If that weren’t true, they’d probably all be opening the URLs in new windows.

I have no idea why Firefox works the way it does. I even have the Tab Mix Plus add-on installed, but it still wants to open each URL in its own window unless there’s already an open window.
Update 8/6/10
In the comments, Matthew McVickar posits (correctly) that the problem with Firefox is that it’s too slow. He suggests changing the non-comment portion of Line 52 from
`/usr/bin/open -g $_`;
to
`/usr/bin/open -g $_; sleep .5`;
to give Firefox a half-second delay between open commands.
I’ve tried this out and it does make Firefox behave correctly (i.e., open the links in new tabs instead of new windows) when FF is already running but has no open windows. It’s not a long enough delay, though, to work when Firefox is starting from scratch. On my work computer—a 2.16 GHz Intel Core 2 Duo iMac—I need to increase the delay to 4-5 seconds to get Firefox to work right. That’s just too much, and would wreck the behavior for everyone else just to get Firefox working for one use case.
So I’ve decided to leave my script as is. If you’re a Firefox user and the new window thing annoys you (it’ll only happen if FF is unlaunched or has no open windows when you invoke the Service), you can add a sleep command as Matthew suggests.
I should also mention that this doesn’t work exactly the way John’s service does, even if Safari is your default browser. John’s opens a new window first, then fills it with tabs for each of the found URLs. Mine will put the URLs in new tabs of the current window, opening a new window only if there isn’t one already up on the screen.
When I started looking at John’s code, it wasn’t my intention to change it; I just wanted to look at how he detects the default browser in Perl and see if I could adapt that to Python. When I read it, though, I realized it could be both shortened and generalized at the same time. The open command is one of Apple’s greatest gifts to scripters and command-line users.
-
In this case, WebKit means the experimental browser, not the rendering engine. ↩
-
Update: this is now a fork of Gruber’s gist, so you can follow the edit history if that’s the sort of thing you like to do. ↩
A quick script for Avery 5160 labels
July 24th, 2010 at 11:26 pm
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:
- The logic needs to expand to accommodate three horizontal positions instead of two.
- The various margins need to be adjusted to reflect the narrower labels.
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:
- Blank lines separate labels.
- Lines that start with a hash (#) are in bold.
- A vertical bar acts as a tab to a right-justified tab stop at the right margin.
- All other lines are centered.
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.
-
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. ↩
-
By the way, if you ever need to create some fake names, I suggest The Name Generator. ↩
Ack
June 18th, 2010 at 11:03 pm
At the end of last week’s post about trusses, I mentioned that I had found an old photo of a roof truss by greping for the word “truss” in a folder of project reports on my work computer. I should not have grep’d; I should have ack’d.
Ack is a Perl script that’s meant to be a replacement of and improvement on the venerable Unix search tool grep. I’d read about ack quite a while ago—there’s a TextMate bundle called Ack in Project that’s well thought of—but kept forgetting to install it. Now that I have it installed, I see some obvious improvements.
- Speed. I haven’t done any formal timing tests, but
ackis clearly faster. - Presentation.
Ackshows its results in a much clearer format. I must say, though, that I’m not thrilled with some of the colors it uses in the output—maybe those can be changed in the.ackrcconfiguration file. - Better regular expressions. And by that I mean Perl’s regular expressions.
- Subdirectory search by default. I usually want my searches to walk down into the subdirectories but am always forgetting to turn on
grep’s recursive search option (-r), so having recursive search as the default is a big help. - No searching in revision control subdirectories. For me, that means the default is to avoid my
.gitfolders, butackalso knows to avoid folders named.svn,.hg,.bzr,CVS,RCS, and so on.
A couple of ack’s defaults are not to my liking.
- It doesn’t search in files that it considers “text” files. Since a lot of my searching is in just these kinds of files—mostly Markdown—this default works against me.
- It’s searches are case-sensitive. This is no different than
grep’s default, but that doesn’t mean I like having to always use the-iswitch.
These defaults betray ack’s origins as a programmer’s tool. Programmers usually want to search through their source code and avoid extraneous hits in, say, the documentation files. And most of today’s languages are case-sensitive, so it only makes sense for the default searches to be so as well.
Luckily for me, ack’s defaults can be changed through an .ackrc file in my home directory. Right now my .ackrc has just two lines:
--type=text
--smart-case
The first line tells ack to go ahead and search the text files. The second tells it to do case-insensitive searches unless the search term has a capital letter. The format of the .ackrc is simple: each line represents a command line switch and is written just as if you were adding it to an ack command—there’s no special dotfile syntax.
I’ll probably add more lines to my .ackrc as I learn more about it. For now, I’m just wondering why I didn’t switch from grep earlier.
More Avery labels
February 4th, 2010 at 4:02 pm
This week I had to create lots of small labels to attach to laboratory samples. To make this easier, I modified my file folder label script to handle the smaller Avery 5167 labels, the kind usually thought of as return address labels.
The new program, called ptlabels (“print tiny labels”), follows the same logic as the old one and uses the same command-line options. You can tell it which row and column to start on through the -r and -c options. The input format is also the same:
- Individual labels are separated by a blank line.
- Header lines, which get printed in bold, are designated by a leading hash mark.
- Header lines can have a left-justified and right-justified part; they’re separated by a vertical bar.
- Headers apply to all subsequent labels until a new header line is encountered.
As an example, this input
#Lorem project|1234
Sample 1
Sample 2
Sample 3
Sample 4
Sample 5
Sample 6
Sample 7
Sample 8
#Dolor project|9876
Sample 1
Sample 2
Sample 3
Sample 4
passed to
ptlist -r 3 -c 2
generates this output

As with the file folder label script, I find it easiest to run the script in TextMate via Filter Through Command… (⌥⌘R).

The script is in Perl, because that was my main language back when I wrote the original version. Rewriting in from scratch in Python would have been a waste of time.
1: #!/usr/bin/perl
2:
3: use Getopt::Std;
4:
5: # Usage/help message.
6: $usage = <<USAGE;
7: Usage: ptlabels [options] [filename]
8: Print tiny labels on Avery 5167 sheets
9:
10: -r m : start at row m (range: 1..20; default: 1)
11: -c n : start at column n (range 1..4; 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 5167.
26: $topmargin = 0.55;
27: $pocol[1] = 0.45;
28: $pocol[2] = 2.50;
29: $pocol[3] = 4.55;
30: $pocol[4] = 6.60;
31: $lheight = 0.50;
32:
33: # get starting point from command line if present
34: getopts('hr:c:', \%opt);
35: die $usage if ($opt{h});
36:
37: $row = int($opt{r}) || 1; # chop off any fractional parts and
38: $col = int($opt{c}) || 1;
39:
40: # Bail out if position options are out of bounds
41: die $usage unless (($row >= 1 and $row <= 20) and
42: ($col >= 1 and $col <= 4));
43:
44: # Set initial horizontal and vertical positions.
45: $po = $pocol[$col];
46: $sp = ($topmargin + ($row - 1)*$lheight);
47:
48: # Pipe output through groff to printer (manual feed).
49: open OUT, "| groff | lpr -o ManualFeed=True";
50: # Change to PDF before sending to printer.
51: # open OUT, "| groff | ps2pdf - - | lpr -o ManualFeed=True";
52: # Preview output instead of printing directly.
53: # open OUT, "| groff | ps2pdf - - | open -a /Applications/Preview.app";
54: # Print raw troff code for debugging.
55: # open OUT, "> labels.rf";
56: select OUT;
57:
58: # Set up document.
59: print <<SETUP;
60: .vs 12
61: .nf
62: .ll 1.50i
63: .ta 1.50iR
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: .ps 10
74: .ft HB
75: %s
76: .ps 10
77: .ft H
78: .ce 2
79: %s
80: .ce 0
81: ENTRY
82:
83: # Slurp all the input into an array of entries.
84: $/ = "";
85: @entries = <>;
86:
87: $bp = 0; # we don't want to start with a page break
88:
89: foreach $body (@entries) {
90: # Parse and transform the header and body.
91: if ($body =~ /^#/) { # it's a header line
92: ($header, $body) = split(/\n/, $body, 2);
93: $header = substr($header, 1);
94: $header =~ s/\|/\t/;
95: }
96: $body =~ s/\s+$//;
97:
98: # Break page if we ran off the end.
99: if ($bp) {
100: print "\n.bp\n"; # issue the page break command
101: $bp = 0; # reset flag
102: }
103:
104: # Print the label.
105: printf $label, $sp, $po, $header, $body;
106:
107: # Now we set up for the next entry.
108: $col = ($col % 4) + 1; # step to next column
109: $po = $pocol[$col];
110: if ($col == 1) { # we just went down a row
111: $row++;
112: if ($row > 20) { # we just went off the bottom
113: $bp = 1; # start a new page
114: $row = 1; # at the top
115: }
116: $sp = ($topmargin + ($row - 1)*$lheight);
117: }
118: }
The geometry constants in Lines 26-31 were initially set by making measurements of the labels and then adjusted through trial and error until the printing was nicely aligned with the die cuts on the label sheets. The final values are based not only on the sheet geometry, but also on how the labels pass through my printer via the manual feed slot. For good alignment on another printer, the values might need adjusting by a few hundredths.
Line 49 was also written with my default printer in mind. Because it’s a PostScript printer, I can take the PostScript output directly from groff and pipe it to lpr—no need to convert it first to PDF and no need to use lpr’s -P option to tell it which printer to use. (The -o option should be self-explanatory.)
Line 51 (commented out) is an example of what you may need to do if you don’t have a PostScript printer.
51: # open OUT, "| groff | ps2pdf - - | lpr -o ManualFeed=True";
Ps2pdf is part of Ghostscript, an open source suite of PostScript utilities that doesn’t come with OS X, but which I find invaluable. The two hyphens after ps2pdf tell it to use standard input and output instead of files on disk.
A more Mac-like possibility is shown in Line 53:
53: # open OUT, "| groff | ps2pdf - - | open -a /Applications/Preview.app";
This generates the PDF and opens it in Preview so you can see it before printing. I’m a wild and impetuous sort of guy, so I just send it off the printer and let the ink fall where it may.
My script hall of fame
January 27th, 2010 at 1:25 pm
Many of my posts here have been about the writing—or rewriting or rerewriting—of scripts to automate the dull, repetitive, clicky-click tasks so common to computer use. While almost all of these scripts have been worthwhile, a few have proved so useful that I use them on a weekly or even daily basis. These are the member of my personal scripting hall of fame.
Folder labels
I like the project file folders in my office to have crisp laser-printed labels. There are plenty of templates out there for Avery labels and their clones, but they’re usually made for MS Word or some other program I don’t use. Also, there’s a lot of repetitive typing associated with those templates, and I obviously want to avoid that. So I wrote a script called pflabels that takes plain text input in a simple format and generates output for printing on a sheet of 1″×4″ labels (Avery 5261 or the equivalent).

The input looks like this:
#Kernighan Building|4215
Drawings
Contract
Correspondence
Photographs and
videotape
#Ossanna Residence|4332
Report
Correspondence
The headings, which have the project name and number separated by the pipe character (|), are denoted with an initial hash symbol (#), and individual labels are separated by blank lines. If I’m going to make several labels for the same project (which is usually the case), I need only enter the heading once. The script takes two options, -r and -c, which tell it which row and column to start on, so it can print on label sheets that have been partially used.
I generally type up my label input in TextMate and then run pflabels on that input via the Text>Filter Through Command… (⌥⌘R) command.
Screenshot uploader
This script, called snapftp:
- Takes a snapshot of some portion of my computer screen.
- Names it.
- (Optionally) resizes it.
- (Optionally) uploads it to my blog.
- Puts the URL of the uploaded image on the clipboard for pasting into a post.
I’ve configured FastScripts to run snapftp with the ⌃⌥⌘4 key combination, a minor variation on the Apple-standard ⌘⇧4 key combo.

There are certainly commercial products that do some or all of these things, but none are so perfectly tuned to my way of working. I can’t imagine going back to writing posts like this without it.
Markdown links in TextMate
I write almost everything in Markdown, mainly because I can forget about the formatting and just type, but also because it’s so easy to read. To keep the visual clutter to an absolute minimum, I use reference-style links, which puts all the URLs at the bottom of the document, out of the flow of the text.

I use three TextMate command/snippet/macros to do this:
- One for inserting links as I type, triggered by ⌃L.
- One for inserting links after I’ve already typed out the text, triggered by ⌃⌥L.
- One for inserting Google’s I Feel Lucky link on text already typed, triggered by ⌃⌥⇧L.
The first two are described here, and the third is described here. They’re a bit complicated to set up but are wonderfully simple to use. I should probably turn them into a Bundle for easier installation.
URL getters for TextExpander
This started out as a set of scripts for getting the URL (or a shortened version of the URL) of the page showing on the visible tab of the frontmost Safari window, and the scripts were triggered by key combinations defined in FastScripts. Then, after seeing this tip by Jeff Gamet, I turned them into a set of TypeIt4Me snippets. When I switched from TypeIt4Me to TextExpander, I moved the snippet over and that’s what I use today.
The two workhorses are:
- The snippet triggered by
;furl, which gets the aforementioned URL and inserts it. - The snippet triggered by
;surl, which gets the URL, shortens it through the Metamark shortening service (run by perl.org), and inserts it.
I tend to use the first when writing posts like this or email and the second when using links on Twitter. The great advantage is that I can add a link to a page I’ve been reading without having to switch back to Safari to get it.
More recently, I added a few more snippets for getting the URLs of the first, second, third, and fourth Safari tabs, regardless of whether they are frontmost.
BBC Radio recording scripts for Audio Hijack Pro
These scripts, written in Python and AppleScript, differ from those described above in that I never actually run these scripts myself. They’re run automatically on a schedule set in Audio Hijack Pro, and they record and tag certain BBC Radio 2 shows and put them into my iTunes library for syncing with my iPod. In effect, they turn shows that aren’t podcasts into podcasts for me.
The scripts are described here and they’re also available in this GitHub repository.
Library loan tracking
This script, like the set of BBC scripts, is run automatically—in this case, by a launchd process. Every morning, it logs in to my local library and gathers information on all the items my family has checked out or on hold. It then sends that information to my wife and me in a nicely formatted email.

Now it’s true that my library emails us a notice shortly before an item is due, but the advantage of this system is that we see everything at once and can gather up all the books that will be due in the next several days before going to the library to make a return. And we don’t have to remember to sign on to the library’s web site; the information is delivered to us every day.
Suspend and sleep screen
This is a pretty recent script, but I’ve come to love it. Without logging me out, it suspends my user session and puts the display to sleep. Seeing the Desktop swing around in that cube animation (which I’ve been a fan of since Andy Hertzfeld used a simplified version of it in Switcher) has become the visual signal that my day at work is over.





