Python and the Mac Clipboard

I wanted a handy way to convert a feed url to the redirected url. For example, Reeder puts this url on the clipboard:

“http://feeds.harvardbusiness.org/~r/harvardbusiness/~3/_folqII9-_8/beware-of-the-short-term-opera.html”

But when I make a post, I want this url:

“http://blogs.hbr.org/hbsfaculty/2011/12/beware-of-the-short-term-opera.html”

There are a number of reasonable ways to do this, but I like Python so that’s what I went with.

It’s a Hack!

I’m an inquisitive hack. I know just enough of these technologies to be dangerous. I accept that risk. Anyone else running these scripts should accept the same risks. I’ve omitted all of the standard try blocks from the scripts for brevity. They really should have more error handling. I’ve hung many apps with poorly crafted scripts.

I like to build my own solutions when I can. It provides an opportunity to learn something new, but it also forces me examine my workflows and motivations. For example, I’ve abandoned several “tools” in favor of consolidating into more simple options. The script in this post will make it’s way into Keyboard Maestro.

Getting The URL

Get the redirect URL is fairly trivial in Python

:::python
import urllib2

feedUrl = 'http://feeds.harvardbusiness.org/~r/harvardbusiness/~3/_folqII9-_8/beware-of-the-short-term-opera.html'
f = urllib2.urlopen(feedUrl)
source = f.read()
f.close()

newurl = f.geturl()
newurl = newurl.split('?')[0]
print newurl

The “f.geturl()” function grabs the redirected url from the feed. But that url also has some garbage at the end indicating that it also came from a feed:

“http://blogs.hbr.org/hbsfaculty/2011/12/beware-of-the-short-term-opera.html?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+harvardbusiness+%28HBR.org%29”

The second to last line of the Python script splits the url at the “?” and grabs the first bit of the string.

All well and good. This does extract the base url I wanted. But now I need a better way to set the feed url. I don’t want to open the script every time and edit the feedUrl variable. I could do this Keyboard Maestro but I wanted a more general solution. That means Python needs access to the OS X clipboard.

Clipboard and Python

There are a number of ways I could do this. I could first grab the clipboard in the shell environment then pass in the result to the python script when it is run. That’s fine, but I wanted something a little more direct. I wanted Python to get the clipboard from within the script.

Subprocess

I found this very thorough and very interesting tutorial for making a cross OS clipboard sharing script. In it, there are two nice methods that I’ve lifted.

:::Python
import subprocess

def getClipboardData():
 p = subprocess.Popen(['pbpaste'], stdout=subprocess.PIPE)
 retcode = p.wait()
 data = p.stdout.read()
 return data

def setClipboardData(data):
 p = subprocess.Popen(['pbcopy'], stdin=subprocess.PIPE)
 p.stdin.write(data)
 p.stdin.close()
 retcode = p.wait()

These are pretty clever tricks (to me) and something I never thought of. Subprocess allows Python to spawn a shell command for pbpaste and pipe the results to a variable. How cool is that. One method gets the clipboard contents and the other puts content back on the clipboard.

Putting it all together and I get this script for converting the current clipboard url to a normal address.

:::Python
import urllib2
import subprocess

def getClipboardData():
 p = subprocess.Popen(['pbpaste'], stdout=subprocess.PIPE)
 retcode = p.wait()
 data = p.stdout.read()
 return data


def setClipboardData(data):
 p = subprocess.Popen(['pbcopy'], stdin=subprocess.PIPE)
 p.stdin.write(data)
 p.stdin.close()
 retcode = p.wait()

f = urllib2.urlopen(getClipboardData())
source = f.read()
f.close()
newurl = f.geturl()
newurl = newurl.split('?')[0]
setClipboardData(newurl)

PyObjC

This process seemed a bit indirect. It means I run a shell command to run a python script that then spawns a shell command to get the clipboard. There’s nothing wrong with that as far as I can see, but I wanted to learn more about Python on the Mac. So, I kept reading.

PyObjC is a Python library for accessing the Objective C frameworks in Python. It should provide a native mechanism for accessing the OS X clipboard and provide additional features not available through the subprocess methods above.

After some reading and fiddling[1], I ended up with a version of the above script that uses the NSPasteboard class to get and set the clipboard instead of a using the shell command.

:::Python
import urllib2
from Foundation import *
from AppKit import 

# Sets the clipboard to a string
def pbcopy(s):
 pb = NSPasteboard.generalPasteboard()
 pb.declareTypes_owner_([NSStringPboardType], None)
 newStr = NSString.stringWithString_(s)
 newData = newStr.nsstring().dataUsingEncoding_(NSUTF8StringEncoding)
 pb.setData_forType_(newData, NSStringPboardType)

# Gets the clipboard contents
def pbpaste():
 pb = NSPasteboard.generalPasteboard()
 content = pb.stringForType_(NSStringPboardType)
 return content

f = urllib2.urlopen(pbpaste())
source = f.read()
f.close()
newurl = f.geturl()
newurl = newurl.split('?')[0]
pbcopy(newurl)

Conclusion

I found this whole process interesting and educational. At the very least I have a better appreciation for the clipboard and how complex a clipboard item can be. But it also gave me a way to make more generalized macros. Applications like TextExpander and Keyboard Maestro can access the clipboard through token replacements, but if I’m embedding a script, I might as well handle the clipboard myself. That means that the same script can be used in multiple application, including Launchbar and the terminal.


  1. I Found this great script on Pastebin. I wish I could give attribution to the creator. It’s quite marvelous. Not only did it give a clear example of accessing the clipboard, there are a number of other examples of manipulating the clipboard text.  ↩