# Scripting your scripting

A couple of days ago, Rob Trew tweeted a link to a shell script he wrote that prints out an emoji clock face based on the time it’s given.

🕟.sh
Simple bash .sh gets emoji clockface (stdout|clipboard) with specified time (to nearest preceding half hour)

github.com/RobTrew/txtque…

ComplexPoint (@ComplexPoint) Jul 22 2014 10:31 AM

It sounded like a fun little programming project and possibly even useful.1 Because I wanted to round to the nearest half hour instead of the nearest preceding half hour, and because my shell scripting is abysmal, I decided to rewrite it in Python.

I knew immediately that the key feature of my script would be a dictionary that mapped a time written as text, like “8:30,” to the corresponding emoji, 🕣. The rest of the script would just be input, output, and rounding. Here’s the script:

python:
1:  #!/usr/bin/python
2:  # -*- coding: utf-8 -*-
3:
4:  from sys import argv
5:  from time import strftime
6:
7:  clocks = {'12:00': '🕛', '12:30': '🕧',  '1:00': '🕐',  '1:30': '🕜',
8:             '2:00': '🕑',  '2:30': '🕝',  '3:00': '🕒',  '3:30': '🕞',
9:             '4:00': '🕓',  '4:30': '🕟',  '5:00': '🕔',  '5:30': '🕠',
10:             '6:00': '🕕',  '6:30': '🕡',  '7:00': '🕖',  '7:30': '🕢',
11:             '8:00': '🕗',  '8:30': '🕣',  '9:00': '🕘',  '9:30': '🕤',
12:            '10:00': '🕙', '10:30': '🕥', '11:00': '🕚', '11:30': '🕦'}
13:
14:  # Get the argument, which should be a time string, like "8:10."
15:  # If there's no argument, use the current time.
16:  try:
17:    oclock = argv[1]
18:  except IndexError:
19:    oclock = strftime('%I:%M')
20:
21:  hour, minute = [ int(x) for x in oclock.split(':') ]
22:
23:  # Round to the nearest half-hour, because there are no
24:  # emoji clockfaces for other times.
25:  rminute = int(round(float(minute)/30)*30)
26:  if rminute == 60:
27:    rminute = 0
28:    hour = 1 if hour == 12 else hour + 1
29:
30:  print clocks['%d:%02d' % (hour, rminute)]


The dictionary is defined in Lines 7–12. By including the encoding line at the top of the script, I was able to include the clock faces directly in the source code instead of messing around with hex character codes.

But the script itself isn’t the point of this post. What I really want to talk about is how I wrote it. I knew what Lines 7–12 were going to look like, and I really didn’t want to type it out. Not only would it be tedious, I’d almost certainly make an error somewhere. So I used a combination of IPython and BBEdit’s search-and-replace tools to build Lines 7–12 step by step.

Step 1 was generating a list of times. I used these commands in an IPython session to print a list of times, one per line:

python:
for h in range(1, 13):
print "'%d:00'\n'%d:30'" % (h, h)


I copied this into a BBEdit window, moved the last two lines, 12:00 and 12:30, to the top, and did this find-and-replace:

This gave me a single line that looked like this,

12:00', '12:30', '1:00', '1:30', '2:00', '2:30', '3:00', '3:30',
'4:00', '4:30', '5:00', '5:30', '6:00', '6:30', '7:00', '7:30',
'8:00', '8:30', '9:00', '9:30', '10:00', '10:30', '11:00', '11:30', '


which I’ve broken up here for ease of reading. I cleaned up the front and rear to turn it into a Python list assignment,

python:
times = ['12:00', '12:30', '1:00', '1:30', '2:00', '2:30', '3:00',
'3:30', '4:00', '4:30', '5:00', '5:30', '6:00', '6:30', '7:00',
'7:30', '8:00', '8:30', '9:00', '9:30', '10:00', '10:30', '11:00', '11:30']


which I entered into my IPython session.

Step 2 was to do a similar thing for the clock faces. I “typed” them in, one per line, using the Character Viewer,

and then converted them into a list assignment that I entered into IPython.

python:
faces = ['🕛', '🕧', '🕐', '🕜', '🕑', '🕝', '🕒', '🕞', '🕓',
'🕟', '🕔', '🕠', '🕕', '🕡', '🕖', '🕢', '🕗', '🕣', '🕘', '🕤',
'🕙', '🕥', '🕚', '🕦']


Now comes the fun part. With times and faces defined, it was easy to generate most of the dictionary assignment statement with

python:
print ",  ".join("'%s': '%s'" % (t, f) for t, f in zip(times, faces) )


That gave me this string

'12:00': '🕛',  '12:30': '🕧',  '1:00': '🕐',  '1:30': '🕜',
'2:00': '🕑',  '2:30': '🕝',  '3:00': '🕒',  '3:30': '🕞',
'4:00': '🕓',  '4:30': '🕟',  '5:00': '🕔',  '5:30': '🕠',
'6:00': '🕕',  '6:30': '🕡',  '7:00': '🕖',  '7:30': '🕢',
'8:00': '🕗',  '8:30': '🕣',  '9:00': '🕘',  '9:30': '🕤',
'10:00': '🕙',  '10:30': '🕥',  '11:00': '🕚',  '11:30': '🕦'


where, again, I’ve added line breaks here to make it easier to read.

A few quick edits turned that string into

python:
clocks = {'12:00': '🕛', '12:30': '🕧',  '1:00': '🕐',  '1:30': '🕜',
'2:00': '🕑',  '2:30': '🕝',  '3:00': '🕒',  '3:30': '🕞',
'4:00': '🕓',  '4:30': '🕟',  '5:00': '🕔',  '5:30': '🕠',
'6:00': '🕕',  '6:30': '🕡',  '7:00': '🕖',  '7:30': '🕢',
'8:00': '🕗',  '8:30': '🕣',  '9:00': '🕘',  '9:30': '🕤',
'10:00': '🕙', '10:30': '🕥', '11:00': '🕚', '11:30': '🕦'}


which is the nicely formatted dictionary assignment statement you see in Lines 7–12.

Does this seem like more work than just typing it out? It wasn’t. One of the great things about writing scripts frequently is that you become fluent enough to know instinctively how cobble together little bits of code like this. You also learn that some things, like the beginning and end of a list assignment statement, are more efficiently done by typing than by programming, so you don’t waste time trying to script those parts.

Sometimes your first instinct is a little off. I initially wrote the line that built the dictionary string this way:

python:
print ", ".join("'%s': '%s'" % (t, f) for t, f in zip(times, faces) )


Do you see the difference? There’s only one space after the comma in the joining string instead of two. When I saw the output of this statement, I realized that to get the times lined up nicely I’d need a mixture of one and two spaces, depending on whether the hour was a one- or two-digit number. Since there are nine one-digit hours and only three two-digit hours, it would be faster to put two spaces everywhere and delete one in front of the two-digit hours. I reran the line with two spaces after the comma and saved myself twelve edits.

I’m sure there are still better ways to do this, further tricks that would have saved me even more typing. But even though I didn’t save as much time as I might have, I still saved a lot, mainly because I automated all the quotes, colons, and commas that I tend to mistype.

1. It is useful for Rob. He makes links in FoldingText to his timed Reminders and includes the appropriate clock face in the link text.

# Customer service

## Level One

I put my mom on a plane this afternoon to fly back home after a visit. She’s been having cognitive problems for a while and airports—especially one as big as O’Hare—are not a familiar environment for her, so I wanted to make sure I could take her to the gate and see her off. Other relatives would be picking her up at the other end—a very small airport that’s only a few steps from gate to baggage claim.

To be allowed through security without a ticket or boarding pass requires something called a gate pass. The airline’s website said that gate passes were available for people accompanying travelers who need assistance, and I called the airline a few days ago to confirm that I’d be able to get one at the ticket desk when we checked her bag.

You will not be surprised to hear that the people at the ticket desk—both our initial agent and his superior—had no idea how to issue me a gate pass. Curiously, the agent did ask for my photo ID, even though he had no idea what to do with it. Force of habit, I guess. Eventually, the supervisor hit upon the idea of sending us to the Special Services desk, where we would become someone else’s problem.

The agent at the Special Services desk knew everything about gate passes and told me right away that I wouldn’t be able to get one. “They’re being very tight with those.”

When I explained to the agent that I’d been told by the airline that I could get a gate pass, she told me with great confidence that the people manning the airline’s 800 number didn’t know anything. But she took my driver’s license, typed my information into her computer, and my gate pass printed out immediately.

“Do you know which gate you’re going to?” she asked sharply as she handed it over.

“No, I haven’t checked yet. I wasn’t sure until just now that I was going to get in.”

“Well, it’s F6A. It’s right on the pass.” There was a note of triumph in her voice, as it was clear she had bested me.

The rest of our visit to O’Hare went smoothly. We didn’t even use up all the extra “snafu time” I’d put into our schedule, so we were able to walk slowly to the gate. Most important, Mom got home safely and wasn’t stressed.

## Level Two

Once Mom was on the plane, I drove to my office to knock off a few todos before leaving tomorrow morning on a weeklong business trip. One of the items on my list was to write down our network’s new IP numbers.

Earlier in the week, our company’s service provider had reworked our connections and installed a new router and interface hardware. In the process, our block of IP numbers was changed. I asked the installer why we couldn’t keep the old numbers—they are, after all, under the control of the provider, and we weren’t switching providers. He gave one of those syntactically convoluted answers that’s easily translated to “I don’t know, but I don’t want you to know that I don’t know.” I didn’t pursue it because

1. it wouldn’t do any good; and
2. it’s not a big deal to work with the new numbers.

I’d probably want access to company network while I was out of town, so I needed to write down the new numbers to take with me. But doubt—partly from the installer’s word jumble, partly from my still-fresh experience at O’Hare, but mostly from too many disappointing experiences with IT professionals—made me decide to test the new IP numbers before I left the office.

None of them worked. Not a single one.

After some network inquiry, diagnosis, and testing, I learned what the new numbers really were. They weren’t even close to what the installer had given us. Not a single octet was the same, nor was there any chance that digits had been transposed. What we’d been given was just thoroughly wrong, as if it had come from a random number generator.

## Upshot

As I sit here typing and enjoying a beverage, I don’t feel angry or outraged, nor am I disheartened by the sad state of the world. I’m just happy I planned for these mistakes and for others that didn’t happen. It’s something I wouldn’t have done when I was younger, and I would have ended this day upset. Now I see my interactions with customer service as a sort of strategy game: can I plan my way around the obstacles the game will put in my way? Today I came out on top. Tomorrow is another round.

# Quicker Markdown linking with TextExpander

I listened to the Mac Power Users TextExpander episode today and noticed that I come off as a bit of a dick, always correcting others. This was surprising only inasmuch as I wasn’t on the show. Normally, I have to be present to demonstrate my dickishness, but David and Katie have dealt with me enough to limn my essence without me. Still, as Marvin and Tammi say, ain’t nothing like the real thing, baby. Let me demonstrate by explaining why I think David’s Markdown link snippet could use some improvement.

He describes it starting at about 38:30. The snippet uses the Mac clipboard to paste a URL into the proper spot in a Markdown link structure. To use it, David

1. Switches to Safari, presumably through ⌘-Tab.
2. Selects the URL of the current page with ⌘L.
3. Copies it to the clipboard with ⌘C.
4. Switches to his text editor with ⌘-Tab again.
5. Invokes his snippet by typing its abbreviation.

He saves a little time by keeping the ⌘ key pressed through some of the steps, but there’s still too much context shifting and too many repeated keystrokes. With a little AppleScript, all he’d need is Step 5.

Here’s my first attempt,

and here’s the AppleScript it uses:

applescript:
1:  tell application "System Events"
2:    set numSafari to count (every process whose name is "Safari")
3:    set numChrome to count (every process whose name is "Google Chrome")
4:  end tell
5:
6:  if numSafari > 0 then
7:    tell application "Safari" to set fURL to URL of front document
8:  else
9:    if numChrome > 0 then
10:      tell application "Google Chrome"
11:        set frontIndex to active tab index of front window
12:        set fURL to URL of tab frontIndex of front window
13:      end tell
14:
15:    end if
16:  end if
17:
18:  get "[%|](" & fURL & ")"


Lines 1-16 get the URL of the current page in either Safari or Chrome. These lines were stolen directly from my trusty ;furl snippet, whose history can be traced back to this old AppleScript I used to invoke through Quicksilver.

Line 18 does something pretty cool. It returns a string that looks something like this

[%|](http://macsparky.com/tesnippets/)


where what’s inside the parentheses is the URL of the current page. The cool part, though, is the %| inside the brackets. This is the TextExpander code for “put the cursor here after the expansion,” which is exactly what happens when this snippet is run. So this snippet gets the URL, creates the Markdown inline link and positions the cursor right where you want it to type the link text. No context shifting, no fiddling with the clipboard.

This, by the way, is why TextExpander is such a great tool. Not only can you use it to run scripts, but the output of those scripts will then be interpreted as TextExpander variables.

In fact, that last line could be written this way,

applescript:
18: get "[%filltext:name=link text:default=link text:width=32%](" & fURL & ")"


which would use a single-line TextExpander fill-in to get the link text. Unfortunately, TextExpander shows you the whole script while you’re doing the fill-in:

This works, but… blech. There’s a better way.

If you look through the popup menu that shows the TextExpander variables you can embed in a snippet, you’ll notice that all of your previously defined snippets are available, which means you can nest one snippet inside another. The best way to use a fill-in for the link text is this way:

The content of this snippet is

[%filltext:name=link text:default=link text:width=32%](%snippet:;furl%)


where the thing in the parentheses is a call to my ;furl snippet.

When this is invoked, you see a much cleaner interface with the desired URL already in place:

Now we’re talkin’. No context shifting, no fiddling with the clipboard, and a clean fill-in interface. All you need to type is the abbreviation and the link text.

Maybe you don’t want to link to the current tab. No problem. Similar snippets can use the URLs of other tabs. I have snippets that will get the URLs of the first through the sixth tabs (counting left to right) of the frontmost Safari or Chrome window. Those could be nested just like ;furl:

[%filltext:name=link text:default=link text:width=32%](%snippet:;1url%)


What would be really nice is if these could be put into a popup menu, but TextExpander doesn’t allow that much magic yet.

I should mention that despite my love of script-driven snippets, David and Katie are exactly right when they say that TextExpander’s greatest value comes from simple, text-only snippets. There is so much repetition in business writing, especially correspondence, you’ll get your money’s worth from TextExpander even if you never exercise the AppleScript parts. (But you’ll have more fun if you do.)

# Path Finder trial

I’ve tried out Path Finder a half-dozen times over the years, but I’ve always gone back to the Finder, mainly because Cocoatech never seemed to be able to prevent the Finder from worming its way back onto my screen. I started another Path Finder trial at the beginning of this week, and I’m surprised to say that I’m still using it on both of my machines. This is longer than any attempt. But that doesn’t mean I’m completely sold on it.1

The most frustrating part of Path Finder is the behavior of its bottom drawer—a feature I both love and hate. I have it set to act as a terminal, which is the default behavior, and I’ve set up a keyboard shortcut, ⌃⌥⌘B, to toggle it. It’s nice to have instant access to shell commands in the directory I’m viewing.

Which is the first problem: the terminal access isn’t quite instant. When the bottom drawer appears, focus is not switched from the file browser to the terminal. This is inexplicable. I open the drawer because I want to type a shell command right now, and I have to believe that’s the common use case. By not handling the focus correctly, Cocoatech is creating an inefficiency in a feature that should be all about efficiency.

The aesthetics of the drawer are weird, too. The bottom edge of the file browser casts a deep shadow on the drawer, as if the drawer were far behind the browser—even when the drawer has focus. Worse, the shadow falls on the top two or three lines of the drawer, reducing the contrast between the text and the background. If I wanted to read gray on gray, I’d visit Daring Fireball.

Path Finder also seems to think that what I’m doing in one window is what I want to do from now on. I’m used to certain Finder window characteristics, like the size and the sorting order, being “sticky,” but Path Finder takes that too far. If, for example, I have the drawer open on the current window, every subsequent window I create will start with the drawer open. I don’t understand why anyone would want that.

Today I ran into the worst example of Path Finder’s drawer/terminal behavior. A USB thumb drive was attached to my iMac and a Path Finder window was showing its root directory. I needed to run a couple of shell commands in that directory, so I opened the bottom drawer and typed them in.2 Some time later I closed the window and tried to eject the drive. No dice.

The instance of bash that was still using that directory was the one that had been running in the bottom drawer of the window I’d closed. It should have been killed by Path Finder when I closed the window, but because it wasn’t, I was stuck with an unejectable drive. I opened Activity Monitor and rummaged through the list of processes until I found the instance of bash that was causing the trouble. I had to Force Quit it to be able to eject the thumb drive.

After some experimenting tonight, I’ve learned that in some cases I can eject the thumb drive after opening a Path Finder terminal drawer in it. It depends, bizarrely enough, on whether I close the drawer before closing the window: if I close the window without closing the drawer first, I can eject the drive; but if I close the drawer first, I can’t. This is the kind of bug that should have been wrung out of a program before it gets to version 6.5.

So I’m torn. Path Finder has become quite good at fending off the Finder, but now that I’m able to use it for an extended period, I’m running into annoyances I’d never seen in my earlier, shorter tests. On the other hand, the Finder has plenty of annoyances and limitations that Path Finder doesn’t. I’ll probably give Path Finder another week of continuous use and then return to the Finder. If the Finder seems unusable to me, I’ll know that Path Finder has won me over.

1. Strictly speaking, I am sold. I bought it as part of some bundle last year. But you know what I mean.

2. After changing the focus, of course.