ROT13: Odyssey Two

If you had told me ten years ago that I would spend the waning days of the decade discussing, programming, and writing about ROT13, I would’ve thought you’d gone soft in the head. But here I am, with a handful of ROT13 shortcuts on my iPhone and iPads, preparing to write about the ins and outs of doing a ridiculously simple task on ridiculously powerful hardware.

It started as described in my last post: a few tweets encoded in ROT13, a couple of decoding shortcuts written by Matt Cassinelli and Jason Snell, and my attempt to combine and tweak those shortcuts to handle a few edge cases. After the post went up (and Jason linked to it with his own post), there was a small Twitter discussion about other ways the shortcut could’ve been written. That discussion exploded when John Gruber entered the room and dropped this:

@jsnell @drdrang @leoncowle This whole thread is fun but it just shows how ridiculous it is that the Shortcuts doesn’t allow, you know, actual scripting. I’m mean it’s ROT13.
   John Gruber    Dec 26, 2019 – 10:45 PM

In other words, “Dear boy, why don’t you just try acting?”

Apart from the replies arguing about what is and isn’t scripting, there was some good input on how to embed calls to JavaScript and Python in shortcuts and how to make Shortcuts itself do things that “real” scripting languages can do. I fiddled around with some of these ideas and ended up with a small number of shortcuts that all do basically the same thing as the one I describe in the last post but with different ways of handling the ROT13 transformation.

The point of this exercise was not ROT13, it was expanding my toolbox of techniques so I’d know about more options to solve real problems in Shortcuts. I (and, I suspect, you) have done the same thing in other contexts when learning new techniques in Perl, Python, Scheme, JavaScript etc. It’s how we grow as programmers, even amateur ones.

The shortcuts I built fall into three basic categories:

  1. Shortcuts that call a prebuilt ROT13 action provided by a third-party app.
  2. Shortcuts that call a ROT13 script written by me in a third-party scripting app.
  3. Shortcuts that do the ROT13 transformation entirely within Shortcuts itself.

The shortcut I built in the last post is an example of the first category. You can also use a free app called ESSE, which also supplies an action to Shortcuts from which you can choose from a variety of text transformations.1 Comparing the two shortcuts, you can see that they are identical except for the one step that does the ROT13 transformation.

Text Case and ESSE comparison

There are other text transform apps in the App Store. Any of them that include ROT13 and provide a transformation action to Shortcuts could be used exactly the same way.

In this category, Shortcuts is acting as a glue language: preparing data and calling on another app to process it. This sort of behavior is a common feature of scripting languages like AppleScript, Perl, Python, Ruby, and the shell.

In the second category are these two shortcuts: the one on the left calls out to Scriptable to do the ROT13 transformation, and the one on the right calls out to Pythonista.

ROT13 shortcuts using Scriptable and Pythonista

Scriptable is a good modern iOS citizen in that it includes commands that allow it to accept data from and return data to Shortcuts. The ROT13 script that’s called by the shortcut is this:

javascript:
 1:  // Create a dictionary that maps characters to their ROT13 transform
 2:  var pchars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
 3:  var tchars = "nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM";
 4:  var trans = {};
 5:  for (var i=0; i<pchars.length; i++) {
 6:    trans[pchars[i]] = tchars[i];
 7:  }
 8:  
 9:  // Get the input from Shortcuts, convert it, send it back
10:  var s = args.shortcutParameter;
11:  s = s.split('').map(x =>  trans[x] ? trans[x] : x).join('');
12:  Script.setShortcutOutput(s);
13:  Script.complete()

I don’t want to discuss the pure ROT13 portion of this code2. The lines that are important for communicating with Shortcuts are 10, 12, and 13.

Lines 10 and 12 do pretty much what you expect. Line 10 gets whatever is passed into Scriptable from Shortcuts and saves it to a variable. Line 12 then sends the result back out to Shortcuts. Line 13 is a weird kludge. For reasons I don’t fully understand, telling Scriptable that it’s finished is good practice because it can speed up a script.

The speed difference can be dramatic. Here’s how the shortcut runs when Scriptable includes that line:

And here’s how it runs with that line commented out:

No one would want to use the shortcut if it sat around like that.

One last thing about the Scriptable-based shortcut. You’ll note that after the conversion from HTML to rich text, the rich text is then put into a text block before sending it to Scriptable. Without that step, Scriptable returned lots of junk, probably because it understands rich text and doesn’t automatically treat it as plain text. None of the other shortcuts needed that step.

Thanks to Rosemary Orchard, Federico Viticci, and Majd Koshakji for their tweets regarding Scriptable and Shortcuts.

I said above that Scriptable is a good modern iOS citizen. Pythonista is… not. While the Run Script action for Pythonista includes a parameter for passing data into a Python script, I’ve found that feature unreliable in iOS/iPad 13. The only safe way to get data into and out of Pythonista is through the clipboard. This means either destroying what was on the clipboard originally or adding Shortcuts code to save the clipboard before calling Pythonista and restore it after returning, which is what I did in the shortcut above.

And speaking of returning, Pythonista won’t return automatically. You have to use URL schemes to get back to whatever app you came from. This is not unreasonable if you want to return the Shortcuts. Finishing the script with

webbrowser.open('shortcuts://')

will take you back to the currently running shortcut and it will pick up from where it left off. But if your shortcut is to be used from the Share Sheet3, the webbrowser.open function has to be fed the URL of the app you were in when you brought up the Share Sheet. This makes the Pythonista script less flexible, as we’ll see in a minute.

The rot13.py script called by the Pythonista-based shortcut is this:

python:
 1:  from string import ascii_letters
 2:  import clipboard
 3:  import webbrowser
 4:  
 5:  # Make a ROT13 translation table
 6:  r = 13
 7:  a = ascii_letters
 8:  b = a[r:2*r]+a[:r]+a[3*r:]+a[2*r:3*r]
 9:  t = str.maketrans(a, b)
10:  
11:  # Get the input text, translate it, and put it on the clipboard
12:  s = clipboard.get()
13:  clipboard.set(s.translate(t))
14:  webbrowser.open('tweetbot://')

Again, let’s focus on the code that communicates with Shortcuts. In this case, we’re really communicating with the clipboard (Lines 12-13) and then heading back to the app from which we came (Line 13).

And here you see the lack of flexibility inherent in the webbrowser.open technique. This script works only when reading Twitter in Tweetbot; it won’t work in the official Twitter app, Twitterrific, or Safari. Neither the Scriptable-based shortcut or any of the others have this limitation.4

If there were a way for Shortcuts to know which app was active when it was called from a Share Sheet, we could add code to the Pythonista-based shortcut to build a dictionary of apps and their URL schemes and then pass the appropriate URL scheme to Pythonista along with the tweet text. Sadly, this doesn’t seem possible with Shortcuts now. (Of course, it would be even better to get a Shortcuts-aware update to Pythonista. It’s been about two years since the last release.)

There’s one other deficiency in the Pythonista-based shortcut: after you dismiss the alert with the translated text, you’re left in the Share Sheet, which you have to dismiss by hand. All the other shortcuts dismiss the Share Sheet automatically.

Thanks to mikeymikey for showing me the Pythonista workarounds.

Before leaving this category, I should mention that if you really wanted a ROT13 translator that was based on Pythonista or Scriptable, you probably wouldn’t bother with Shortcuts at all. Both Python and JavaScript themselves have ways to download HTML, extract text, handle HTML entities, and present results. I did these in Shortcuts to explore how the scripting languages can be used within Shortcuts and what the advantages and disadvantages of doing so are.

Finally, we come to doing the ROT13 translation in Shortcuts itself. Conceptually, the approach is the same as what I used in Line 11 of the JavaScript code:

  1. Split the string into a list of characters.
  2. Use a dictionary to replace all ASCII characters with their ROT13 translation. Leave the other characters alone.
  3. Join the list of characters back into a string.

The difference between doing this in Shortcuts and doing it in JavaScript is that making the translation dictionary with 52 entries by hand is a pain in the ass and the split-translate-join steps take more time to write and use up more code space. But it can be done. Here’s the complete shortcut:

ROT13 Internal shortcut

The tedious dictionary creation is in the text block that contains this JSON object

{"A":"N","B":"O","C":"P","D":"Q","E":"R","F":"S",
"G":"T","H":"U","I":"V","J":"W","K":"X","L":"Y",
"M":"Z","N":"A","O":"B","P":"C","Q":"D","R":"E",
"S":"F","T":"G","U":"H","V":"I","W":"J","X":"K",
"Y":"L","Z":"M","a":"n","b":"o","c":"p","d":"q",
"e":"r","f":"s","g":"t","h":"u","i":"v","j":"w",
"k":"x","l":"y","m":"z","n":"a","o":"b","p":"c",
"q":"d","r":"e","s":"f","t":"g","u":"h","v":"i",
"w":"j","x":"k","y":"l","z":"m",".":"."}

and the “Get dictionary” step after that.

To avoid typing in all that nonsense, I edited the JavaScript code above to output the trans variable and pasted that into the text block. The only addition I had to make was the weird period-to-period mapping at the end. For some reason, without that addition, Shortcuts would choke whenever it encountered a period in the input.

The split-translate-join steps are reproduced here:

Split-translate-join steps

Compare this with Line 11 of the JavaScript code and you can understand John Gruber’s distain for Shortcuts. And this JavaScript is clumsy when compared to Perl/Python/Ruby, which don’t need to convert the string to and from a list of characters. But this is a way to avoid any outside dependencies.

Thanks to David Anson and Leon Cowle for their tenacity in working on and sharing their Shortcuts-only ROT13 code.

For me, the “right” solution is the one that uses Text Case. It’s relatively compact and leverages an app I already have. But it’s good sometimes to stretch your scripting muscles and learn new things that aren’t immediately productive. I definitely learned some new things putting these shortcuts together and it sure wasn’t immediately productive.


  1. I’ve lost track of who told me about ESSE. If you read this, please get in touch so I can credit you. 

  2. A little birdie tells me someone else is working on a post covering the ROT13 algorithm as expressed in several languages. 

  3. Apple is moving to call this the Activity View, but the future is not evenly distributed, so I’m sticking with the older terminology here. 

  4. A generic way to end the script would have been webbrowser.open(shortcuts://'), but then the user would have to switch from Shortcuts to their Twitter app to get the shortcut to finish up.