Helper scripts in BBEdit packages

I didn’t put this in yesterday’s post because it was already 1,600 words long, but I need to write it down before I forget. It’s the set of tricks I learned—mostly from Patrick Woolsey of Bare Bones support—on how to put a complicated BBEdit package together.

By “complicated,” I mean a package in which the user-facing scripts—the ones you see in a menu—call other scripts that you want to include in the package but don’t want users to call directly. The tricks concern where to store these helper scripts and how to call them from the user-facing scripts.

Where to store them is easy. A BBEdit package is simply a folder whose name has been given a .bbpackage extension. According to the BBEdit manual, the folder structure for the package should look like this:

BBEdit package structure

Basically, everything inside the Contents folder is optional. If you have clippings, you’ll need the Clippings folder to put them in. Likewise for the Language Modules, Preview CSS, Preview Templates, Scripts, and Text Filters folders. That leaves the Resources folder and the Info.plist file, which the manual mysteriously says “are currently not required, and are reserved for future use.”

According to Patrick, the future is now. When I asked where helper scripts should go, he told me to put them in the Resources folder, which makes sense, given its name.

That leaves us with the problem of how to call a helper script stored in a package’s Resources folder, which is a little trickier because it depends on what’s doing the calling.

If the user-facing script is a Unix script (that is, a shell, Perl, Python, Ruby, etc. script), then the working directory for the script is the folder in which it’s located—typically the Scripts or Text Filters folder of the package. So when you call a helper script in Resources, you need to use a relative directory path to climb up and then down the directory tree. An example might help.

Let’s say you have a helper script called upper in the packages Resources folder. It’s a Python script that takes the text fed to it in stdin and returns an upper case version of it:

1:  #!/usr/bin/python
3:  from sys import stdin, stdout
4:  original =
5:  stdout.write(original.upper())

Now let’s say you want to create a Text Filter that calls upper. Here’s a shell script that would work.

1:  #!/bin/bash
3:  ../Resources/upper

Notice the relative directory path used to find upper. That’s where upper is located relative to the shell script in the Text Filters folder.

This is, of course, a contrived example. It’s far too simple to require a helper script, but the principle of using a relative path applies in more complicated situations. The same idea works in Clippings, too. For example, the “Flickr original image” clipping in my WP-MD package is saved in the folder inside the package’s Clippings folder and is defined this way:


Here you see that the relative path needs to climb up two levels before climbing down into the Resources folder.

AppleScripts don’t follow the same rules as Unix scripts. Say you have this AppleScript, called PWD, saved in your package’s Scripts folder:

tell application "BBEdit" to set selection to do shell script "pwd"

Does it insert the path to the directory it’s stored in? No, it inserts a single slash (/), the path to the root directory. So getting the path to the package’s Resources directory can’t be done through do shell script "pwd".

There is, however, a similar AppleScript construct that works: path to me. Here’s how I get the POSIX-style path to a package’s Resources directory in an AppleScript stored in the package’s Scripts directory:

tell application "Finder" to set cPath to container of container of (path to me) as text
set rPath to (quoted form of POSIX path of cPath) & "Resources/"

The variable me refers to the AppleScript itself. Its container is the Scripts directory, and the container of that is the Contents directory. Therefore, the cPath variable created by the first line is an absolute path to the package’s Contents folder. We need to use tell application "Finder" because container isn’t part of regular AppleScript, it’s in the Finder’s dictionary.1 The second line then gets the POSIX form of cPath, makes sure it’s properly quoted (very important because the absolute path will include at least one space), and tacks on the Resources subdirectory.

With rPath defined this way, you can call a helper script in the Resources folder via something like

do shell script rPath & "upper"

Several of the scripts in my WP-MD package use this technique.

If I remember correctly, TextMate has some environment variables defined to help the bundle programmer access directories within the bundle to call helper scripts. BBEdit package programming lacks that convenience, but once you know these tricks, adding helper scripts to your packages is pretty easy.

  1. It’s also in the System Events dictionary, so `tell application “System Events” would also work. I went with the Finder because it takes less time to type.