Nathan Grigg

BBEdit filter: Rewrite shebang

This BBEdit filter allows you to cycle through choices for a script’s shebang line. It is a fun little script, and has been surprisingly useful to me.

BBEdit text filters are shell scripts that read from stdin and write to stdout. When you select a filter from the “Apply Text Filter” submenu of the “Text” menu, the text of the current document is fed through your shell script, and the document text is replaced by the output. If some text is selected, only that part passes through the script.

My Python script is super simple. It reads the first line of the file and determines the name of the interpreter. Then it calculates three possible shebang lines: the stock system command (e.g. /usr/bin/python), the one on my custom path (e.g. /usr/local/bin/python), and the env one (e.g. /usr/bin/env python). Then it cycles from one to the next each time you run it through the filter.

As an added bonus, the script is forgiving. So if I want a Python script, I can just write python on the first line, and the filter will change it to #!/usr/bin/python. This is good for me, because for some reason it always takes me three or four seconds to remember if it should be #! or !#. (At least only one of these makes sense. I have even worse problems remembering the diference between $! and !$ in bash.)

The python script

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/usr/bin/python

import sys
import os

LOCAL = "/usr/local/bin:/usr/local/python/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/texbin:/Users/grigg/bin"
SYSTEM = "/usr/bin:/bin:/usr/sbin:/sbin"

def which(command, path):
    """Emulate the 'which' utility"""
    for p in path.split(':'):
        full_command = os.path.join(p, command)
        if os.path.isfile(full_command) and os.access(full_command, os.X_OK):
            return full_command
    return ""

transformations = [lambda s: which(s, SYSTEM),
                    lambda s: which(s, LOCAL),
                    lambda s: "/usr/bin/env " + s]

# deal with the first line
line = original_line = next(sys.stdin).strip('\n')
if line[:2] == "#!":
    line = line[2:].strip()
base = line.rsplit('/', 1)[-1]
if base[:4] == 'env ':
    base = base[4:].strip()
if ' ' in base:
    base, args = base.split(' ', 1)
    args = ' ' + args
else:
    args = ''

# do the transformations
options = [T(base) for T in transformations]
# filter out the empty ones while appending args
options = [o + args for o in options if o]
# if the only one is the /usr/bin/env, don't do anything
if len(options) <= 1:
    print original_line
else:
    dedupe = list(set(options))
    if line in dedupe:
        dedupe.sort()
        index = dedupe.index(line)
        line = dedupe[(index + 1) % len(dedupe)] # cycle
    else:
        # can't cycle, just use the first option
        line = options[0]
    print "#!" + line

# print every other line
for line in sys.stdin: print line,

The possible transformations are listed beginning on line 17. The order of this list sometimes matters; it determines which transformation should be used if the current shebang doesn’t match any of the options (see line 49).

The current shebang is interpreted on lines 22 through 32. It’s pretty basic, just using the first word after the last slash as the interpreter. That should cover most use cases.

(I am very happy that when I write base[:4] in line 26, Python doesn’t complain if base has fewer than four characters. Contrast this with AppleScript, which fails if base is short and you say text 1 thru 4 of base. You can get around this by testing base begins with "env ". Sorry for the tangent.)

In lines 42 through 46, we get to the cycling. I deduplicate the list, alphabetize, look up the current shebang in the list, and cycle to the next. It is fun how the filter uses the first line of the input file to essentially save state, so it knows where to go next time it is run.