badapplescript primary

Bad AppleScript: Subroutines and file paths

When I learned to program, programs had line numbers. You’d type GOTO 10 or GOSUB 5000 and that would control the flow of your program. When I first saw a programming language that didn’t have line numbers–I think it was Pascal—I couldn’t comprehend it.

Well, we’ve come a long way. I can’t type GOSUB 5000 anymore, but subroutines can be incredibly valuable in simplifying even Bad AppleScripts.

Many of my colleagues and I write our articles for Macworld in Bare Bones Software’s BBEdit text editor, using the Markdown plain-text markup language invented by John Gruber. One of the nice things about working on the Web is not having to standardize on any single app or even one style—in the end, all that matters is that we paste our story into our Web-based posting tool in HTML format.

So I wrote an AppleScript that takes what I’ve written in BBEdit—even this very article—and converts it into something ready for pasting into that system. Yes, I could just use Gruber’s original Markdown perl script, but I need to make a whole bunch of other changes in order to make a story Macworld-ready. And a few reusable subroutines have made my life much easier.

on replaceme(theFind, theReplace)
    tell application "BBEdit"
        replace theFind using theReplace searching in text of text window 1 options {search mode:grep, starting at top:true, wrap around:true, backwards:false, case sensitive:false, match words:false, extend selection:false}
    end tell
end replaceme

This routine, replaceme, is simple—it’s basically a one-line BBEdit command—but it’s quite a time saver. If I want to replace all incidences of “cat” in my BBEdit document with “dog” using that command, I’d need to write:

tell application "BBedit" to replace theFind using theReplace searching in text of text window 1 options {search mode:grep, starting at top:true, wrap around:true, backwards:false, case sensitive:false, match words:false, extend selection:false}

Instead, I just write:

replaceme("cat", "dog")

Handy, huh? The replaceme subroutine uses the pattern-matching syntax known as grep. I found that occasionally the symbols that grep uses kind of get in the way, so I created a second subroutine, replacemeliteral, that searches for literal text and doesn’t use grep. With these two subroutines, I can do a lot.

Style check

Some of the things my script does have nothing to do with proper HTML formatting. They have to do with my own brain, which has a style guide that diverges from the official IDG style guide. My script fixes some of my most common personal mistakes:

replacemeliteral("web site", "website")
replacemeliteral("e-mail", "email")
replacemeliteral("e-book", "ebook")
replacemeliteral("Ethernet", "ethernet")

We also had a rash of writers who insisted of writing keyboard shortcuts with a plus symbol instead of a dash, which is our style: i.e., Command-Shift-3 instead of Command+Shift+3. Two pattern-matching replaces make quick work of that.

replaceme("(Command|Control|Option|Shift)\+(Command|Control|Option|Shift)\+(.)", "\1-\2-\3")
replaceme("(Command|Control|Option|Shift)\+(.)", "\1-\2")

Among many other little style issues, there’s also a big check to make sure that the hyperlinks in the document go to our live server, not the one we use to preview stories (which the outside world can’t see):

replacemeliteral("preview-gate.www.idgesg.", "www.")

Is my script aware of its location?

I distribute this script to my editors using BBEdit’s bbpackage format, which allows me to send a single file that contains a bundle of scripts and other stuff. Part of the reason for this is that my script uses two different perl scripts distributed by Gruber: Markdown (which converts Markdown-formatted text into HTML) and SmartyPants (which generates “smart” quotes and other fancy characters).

But in order for BBEdit to run those scripts, it needs to know where they’re located on my Mac’s hard drive. That’s easy—they’re located in the Text Filters folder, which is adjacent to the Scripts folder that my script lives inside. Which means that if I can figure out where my script is, I can figure out where those other scripts live.

set thePath to (path to me as text)

Boom! So easy. One line and we’re done. I now have a variable called thePath that contains the location of my script on my hard drive in a format like this: Macintosh HD:Users:jsnell:Dropbox:Application Support:BBEdit:Packages:IDG.bbpackage:Contents:Scripts:CMS.

Except … I don’t want the path all the way to my file. I want the path to the folder that contains the folder my script lives inside, because that’s the parent folder of that other folder where the Markdown and SmartyPants scripts live. To do this, I need to lop off the last two items in that path. Here’s how:

set the oldDelims to AppleScript's text item delimiters
set AppleScript's text item delimiters to ":"
set thePathItems to text items of thePath
set AppleScript's text item delimiters to the oldDelims
set parsedPath to items 1 thru ((count of items of thePathItems) - 2) of thePathItems
set AppleScript's text item delimiters to ":"
set parentPath to parsedPath as string
set AppleScript's text item delimiters to the oldDelims

I use one of the oldest tricks in the AppleScript book, AppleScript's text item delimiters. Turns out that AppleScript has a built-in way of chopping text into individual elements, delimited by some character. For example, a list of words is delimited by the space character that separates them. A comma-delimited line of data such as 12,13,14,15,16 is delimited by, well, the comma. In the case of file paths, they’re delimited by a colon.

Here’s what the code above does:

set the oldDelims to AppleScript's text item delimiters

Saves whatever AppleScript’s current text item delimiters are into the variable oldDelims. This is just good housekeeping practice; if you keep monkeying with AppleScript’s delimiters, other parts of your script can freak out.

set AppleScript's text item delimiters to ":"

Now the colon will determine how my text is delimited!

set thePathItems to text items of thePath

This converts my text variable, thePath, into a variable that’s a list of individual items—the ones delimited by the colon. At this point, a string like Macintosh HD:Users:jsnell would become a list with three items in it: Macintosh HD, Users, and jsnell.

set AppleScript's text item delimiters to the oldDelims

Principles of good housekeeping require that you put the delimiters back where you left them.

set parsedPath to items 1 thru ((count of items of thePathItems) - 2) of thePathItems

This makes a new variable, parsedPath, that contains a subset of the items in thePathItems. Namely, item 1 through the item that’s two from the end. The phrase count of items of theVariable is a very useful construction in AppleScript, because it allows you to count backward. In this case, count of items of thePathItems is the total number of items in the list, and by subtracting 2 from that count, I’m lopping off the last two items in the list.

Now here’s another neat trick:

set AppleScript's text item delimiters to ":"
set parentPath to parsedPath as string
set AppleScript's text item delimiters to the oldDelims

AppleScript’s text item delimiters also can be modified to insert separators between items when you’re turning a list back into a string. In the example above, if I omitted the first and third lines (and kept the delimiters to the AppleScript default—which is nothing), the variable parentPath would look something like this: Macintosh HDUsersjsnell. I want a colon-delimited file path like the one I got at the beginning! So again I change the delimiters, then set the new variable, and get something formatted like Macintosh HD:Users:jsnell.

Now that I know the name of the folder above the folder my script is in, I can build the paths for the two perl scripts I need, the ones who live next door:

set SmartyPants to parentPath & ":Text Filters:SmartyPants.pl"
set Markdown to parentPath & ":Text Filters:Markdown.pl"

Once I’ve got that path set, I can use it to get BBEdit to run that script on my text document using a simple command:

tell application "BBEdit"
    run unix filter Markdown with replacing selection
end tell

The only other tricky thing in the entire script is making sure that my script works on a copy of the story, not the original. In BBEdit, this is how I accomplish that:

tell application "BBEdit"
    set theItem to (contents of window 1 as text)
    set theDocument to (make new text document of window 1)
    set encoding of theDocument to "Unicode (UTF-8)"
    set text of theDocument to theItem
    set name of theDocument to "HTML output"
    set source language of theDocument to "HTML"
end tell

In the end, I’ve got a script that converts Markdown to HTML, runs the SmartyPants filter to educate all our quote marks, and walks through a bunch of other quirks of our style guide and content-management system in order to make a document that’s fit to be pasted and posted.

Subscribe to the Create Newsletter

Comments