Some Keyboard Maestro Terminal macros

A couple of weeks ago, after nearly a decade of heavy use, I quit FastScripts and removed it from the list of Login Items on both of my computers. I’m not unhappy with FastScripts—quite the contrary—but it’s silly to run both it and Keyboard Maestro, and I’m finally comfortable enough with KM to use it exclusively. This has meant converting scripts into KM macros, which usually involves nothing more than pasting the script into an Execute Shell Script or Execute AppleScript action. In some cases, though, I took the opportunity to rewrite scripts I wasn’t especially happy with and to simplify them by taking advantage of Keyboard Maestro’s built-in features. That’s what I did with a couple of scripts that manipulate shell output in the Terminal.

As you probably know, the bash shell keeps track of your command history, which allows you to rerun commands with just a keystroke or two. This history, however, applies only to the commands themselves, not to their output. I often find myself wanting to use the output of a previous command as the argument to my next command. Or I want to take the output and paste it into a document. Both of these are easily accomplished if I remember to pipe the output to pbcopy, but I often forget to do that until after the command is run.

One solution, which I wrote about a few years ago, is to use the Terminal’s AppleScript dictionary to put the output of the last command onto the clipboard. I wrote a Python script that did just that, using Hamish Sanderson’s now-deprecated appscript module. Although appscript still works with Terminal, it can’t be trusted to continue to work, so I decided to remove that dependency when I brought the script over to Keyboard Maestro.

Here’s the Copy Last Output macro:

KM Copy Last Output

It’s a single action that executes the following script and puts its output on the clipboard:

python:
1:  #!/usr/bin/python
2:  
3:  import applescript
4:  
5:  ioCmd = 'tell application "Terminal" to get history of selected tab of window 1'
6:  inout = applescript.asrun(ioCmd).split('\n$ ')
7:  print '\n'.join(inout[-2].split('\n')[1:-1])

The first thing you should notice is the import on Line 3. I wrote the applescript module as a sort of half-assed replacement for appscript. It allows me to write AppleScript code into a string, run it, and collect the output into a variable, all within a Python script. The module itself is very simple:

python:
 1:  #!/usr/bin/python
 2:  
 3:  import subprocess
 4:  
 5:  def asrun(ascript):
 6:    "Run the given AppleScript and return the standard output and error."
 7:  
 8:    osa = subprocess.Popen(['osascript', '-'],
 9:                           stdin=subprocess.PIPE,
10:                           stdout=subprocess.PIPE)
11:    return osa.communicate(ascript)[0]
12:  
13:  def asquote(astr):
14:    "Return the AppleScript equivalent of the given string."
15:    
16:    astr = astr.replace('"', '" & quote & "')
17:    return '"{}"'.format(astr)

It has just two functions: asrun and asquote. The former runs an AppleScript command and returns the output; the latter rewrites a string with quotes that AppleScript understands.

Getting back to the macro, Line 5 defines a short AppleScript that gets the history of the frontmost Terminal session and Line 6 runs it. This returns everything—input, output, and the prompts. The split('\n$ ') at the end of Line 6 breaks the history up at the prompt lines, turning it into a list.

This split is specific to my prompt. I use a two-line bash prompt in which the first line is the current working directory and the second is just a dollar sign. Here’s an example of a Terminal session, and how it gets split into a list.

term-splits

I’ve colored the five items in the list, and put their indexes along the right side of the screenshot. So the last output is going to be the second-to-last item in the list (inout[-2]), minus its first and last lines (split('\n')[1:-1]), which are the command itself and the directory, respectively. Line 7 extracts just what we want.

Sometimes I don’t want all of the last output, just the final line of it. For that, I have another macro:

KM Copy Last Output Line

This is also a single action that executes a script and puts its output on the clipboard. The script is

python:
1:  #!/usr/bin/python
2:  
3:  import applescript
4:  
5:  ioCmd = 'tell application "Terminal" to get history of selected tab of window 1'
6:  inout = applescript.asrun(ioCmd).split('\n$ ')
7:  print inout[-2].split('\n')[-2]

Lines 1–6 are the same as in the Copy Last Output macro. Line 7 prints just the second-to-last line of the second-to-last item in the inout list, which is the last line of the last output.

If you go back and look at the original versions of these scripts, you’ll see that these are simpler, partly because appscript used an unnatural syntax to coerce AppleScript into Python, and partly because the scripts had to handle the clipboard themselves. Keyboard Maestro’s built-in system for putting the output onto the clipboard is a delight to use.

It may well be that the zsh and fish shells keep track of output history and don’t require Terminal scripting like this. If so, I’m happy for users of those shells, but I still have no intention of switching. I have too many years of bash under my belt to change now.