Scripts for WordPress and BBEdit, Part 1

About this time last year, I switched from TextMate to BBEdit and wrote a few scripts to help me blog directly from my new editor. As I explained in these two posts, the goal of these scripts was to be editor-agnostic—I wanted them built as Unix utilities that communicated entirely through stdin and stdout. I’d then call them from shorter scripts, possibly AppleScripts, that included all the BBEdit-specific code.

The scripts have worked quite well, but they aren’t in the same state they were a year ago, so I thought I’d post an update. Over the next few days, I’ll be posting about several little scripts I’ve built up over the past year. When I’m done I’ll put them in one or two BBEdit packages and make them available on GitHub.

First, a script for publishing articles to the blog. It’s called Publish Post.py, and I keep it in the folder ~/Dropbox/Application Support/BBEdit/Text Filters.1 It then appears in BBEdit’s menus as Text▸Apply Text Filter▸Publish Post, where I’ve assigned it the keyboard shortcut ⌃⌘P.

Here’s the script:

python:
  1:  #!/usr/bin/python
  2:  
  3:  import xmlrpclib
  4:  import sys
  5:  from datetime import datetime, timedelta
  6:  import pytz
  7:  import keyring
  8:  import subprocess
  9:  
 10:  '''
 11:  Take text from standard input in the format
 12:  
 13:    Title: Blog post title
 14:    Keywords: key1, key2, etc
 15:  
 16:    Body of post after the first blank line.
 17:  
 18:  and publish it to my WordPress blog. Return in standard output
 19:  the same post after publishing. It will then have more header
 20:  fields (see hFields for the list) and can be edited and re-
 21:  published again and again.
 22:  
 23:  The goal is to work the same way TextMate's Blogging Bundle does
 24:  but with fewer headers.
 25:  '''
 26:  
 27:  # The blog's XMLRPC URL and username.
 28:  url = 'http://mysite.com/path/to/xmlrpc.php'
 29:  user = 'myname'
 30:  
 31:  # Time zones. WP is trustworthy only in UTC.
 32:  utc = pytz.utc
 33:  myTZ = pytz.timezone('US/Central')
 34:  
 35:  # The header fields and their metaWeblog synonyms.
 36:  hFields = [ 'Title', 'Keywords', 'Date', 'Post',
 37:              'Slug', 'Link', 'Status', 'Comments' ]
 38:  wpFields = [ 'title', 'mt_keywords', 'date_created_gmt',  'postid',
 39:               'wp_slug', 'link', 'post_status', 'mt_allow_comments' ]
 40:  h2wp = dict(zip(hFields, wpFields))
 41:  
 42:  # Get the password from Keychain.
 43:  pw = keyring.get_password(url, user)
 44:  
 45:  def makeContent(header):
 46:    "Make the content dict from the header dict."
 47:    content = {}
 48:    for k, v in header.items():
 49:      content.update({h2wp[k]: v})
 50:    content.update(description=body)
 51:    return content
 52:  
 53:  # Read and parse the source.
 54:  source = sys.stdin.read()
 55:  header, body = source.split('\n\n', 1)
 56:  header = dict( [ x.split(': ', 1) for x in header.split('\n') ])
 57:  
 58:  # The publication date may or may not be in the header.
 59:  if 'Date' in header:
 60:    # Get the date from the string in the header.
 61:    dt = datetime.strptime(header['Date'], "%Y-%m-%d %H:%M:%S")
 62:    dt = myTZ.localize(dt)
 63:    header['Date'] = xmlrpclib.DateTime(dt.astimezone(utc))
 64:  else:
 65:    # Use the current date and time.
 66:    dt = myTZ.localize(datetime.now())
 67:    header.update({'Date': xmlrpclib.DateTime(dt.astimezone(utc))})
 68:  
 69:  # Connect and upload the post.
 70:  blog = xmlrpclib.Server(url)
 71:  
 72:  # It's either a new post or and old one that's been revised.
 73:  if 'Post' in header:
 74:    # Revising an old post.
 75:    postID = int(header['Post'])
 76:    del header['Post']
 77:    content = makeContent(header)
 78:    blog.metaWeblog.editPost(postID, user, pw, content, True)
 79:  else:
 80:    # Publishing a new post.
 81:    content = makeContent(header)
 82:    postID = blog.metaWeblog.newPost(0, user, pw, content, True)
 83:  
 84:  # Return the post as text in header/body format for possible editing.
 85:  post = blog.metaWeblog.getPost(postID, user, pw)
 86:  header = ''
 87:  for f in hFields:
 88:    if f == 'Date':
 89:      # Change the date from UTC to local and from DateTime to string.
 90:      dt = datetime.strptime(post[h2wp[f]].value, "%Y%m%dT%H:%M:%S")
 91:      dt = utc.localize(dt).astimezone(myTZ)
 92:      header += "%s: %s\n" % (f, dt.strftime("%Y-%m-%d %H:%M:%S"))
 93:    else:
 94:      header += "%s: %s\n" % (f, post[h2wp[f]])
 95:  print header.encode('utf8')
 96:  print
 97:  print post['description'].encode('utf8')
 98:  
 99:  # Open the published post in the default browser.
100:  subprocess.call(['open', post['link']])

Apart from some fiddle changes here and there, the biggest difference between this version and the one from a year ago is my use of the Keychain to retrieve the blog’s password. I do this through the cross-platform keyring library, an extraordinarily easy library to use that seems to be under active development.

If you go back and read that first post from a year ago, you’ll see that I was originally reading the password from a dotfile in my home directory. Two commenters, including the redoubtable Daniel Jalkut, took me to task for that, and I eventually decided they were right. At first I thought I’d use a subprocess call to the security command, but then I learned about keyring and decided that was the way to go. To make sure I had the password stored in Keychain in a way that was keyed to the blog URL and username, I ran this little interactive Python session:

import keyring
keyring.set_password('http://mysite.com/path/to/xmlrpc.php', 'myname', 'mypassword')

Once that was set—and I could confirm that it was by searching in Keychain Access—I knew the retrieval done in Line 43 would work.

The script works like this: I write my post in BBEdit with a brief header at the top that includes the title and keywords (tags).

Title: Scripts for WordPress and BBEdit
Keywords: blogging, bbedit, programming, python
Date: 2013-08-21 23:25:11

About this time last year, I switched from TextMate to [BBEdit][3]
and wrote a few scripts to help me blog directly from my new editor.
As I explained in [these][1] [two][2] posts, the goal of these scripts
was to be editor-agnostic—I wanted them built as Unix utilities that
communicated entirely through `stdin` and `stdout`. I'd then call them
from shorter scripts, possibly AppleScripts, that included all the
BBEdit-specific code.

The scripts have worked quite well, but they aren't in the same state
they were a year ago, so I thought I'd post an update. Over the next

The Date header line is optional. I include it only if I want the post to be published in the future.

When I select Text▸Apply Text Filter▸Publish Post, the post is published (Line 82), and the text in the editor window is updated with new header lines:

Title: Scripts for WordPress and BBEdit
Keywords: bbedit, blogging, programming, python
Date: 2013-08-21 23:25:11
Post: 2114
Slug: scripts-for-wordpress-and-bbedit
Link: http://leancrew.com/all-this/2013/08/scripts-for-wordpress-and-bbedit/
Status: publish
Comments: 0


About this time last year, I switched from TextMate to [BBEdit][3]
and wrote a few scripts to help me blog directly from my new editor.
As I explained in [these][1] [two][2] posts, the goal of these scripts
was to be editor-agnostic—I wanted them built as Unix utilities that
communicated entirely through `stdin` and `stdout`. I'd then call them
from shorter scripts, possibly AppleScripts, that included all the
BBEdit-specific code.

The scripts have worked quite well, but they aren't in the same state
they were a year ago, so I thought I'd post an update. Over the next

If I need to edit the post, I can do so with it in this form and use Publish Post to update it. Because it has the Post line in the header, the script knows that it’s an existing post and acts accordingly in Line 78.

The nicest thing about this is that there’s no need for a BBEdit-specific wrapper script. Scripts stored in the Filters folder are treated by BBEdit in a very Unix-like way, with the content of the current editing window acting as stdin and stdout.

You might argue that I’m taking liberties with the concept of a filter. That the main value of this script is in its side effect—the part that does the posting—rather than in the minor alteration of the header. And you’d be right. Fortunately, I don’t have a CS degree and don’t lie awake at night worrying about things like that.

A few months ago, Ben Brooks was casting about for a simple script that would allow him to post to his WordPress site via Keyboard Maestro. I know nothing about KM, but after a bit of back-and-forth, we came up with a variation on this script that worked for him. He may have switched to something else by now, but last I knew he was still using it.


  1. Recall that BBEdit allows you to keep your scripts in subfolders of either ~/Library/Application Support or ~/Dropbox/Application Support. I choose the latter because it helps me keep my iMac and MacBook Air working the same way.