Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, Swift, BASIC, and whatever else I happen to feel like hacking at.

42 Astoundingly Useful Scripts and Automations for the Macintosh

Work faster and more reliably. Add actions to the services menu and the menu bar, create drag-and-drop apps to make your Macintosh play music, roll dice, and talk. Create ASCII art from photos. There’s a script for that in 42 Astounding Scripts for the Macintosh.

Catalina vs. Mojave for Scripters

Jerry Stratton, September 16, 2020

Swift conditional at end

As the author of a book filled with scripts for the Macintosh, I was worried about the differences between scripting for Mojave and scripting for Catalina. When testing the Catalina update of the book, it turned out there wasn’t much difference. For the most part, what worked in Mojave worked in Catalina.

The most obvious changes, of course, were those having to do with iTunes. Catalina broke the iTunes app into several apps. The new app for music is called Music. This means that AppleScript scripts need to change from:

  • tell application "iTunes"

to

  • tell application "Music"

The application id has also changed, from “com.apple.iTunes” to “com.apple.Music”.

The application id is useful if, for example, you want to do some stuff if the Music app is running, but skip out if the Music app is not running. The normal means of talking to an application in AppleScript will start the application up, which means it can’t be used to check whether the application is running: it always will be, after using “tell” on it. The application id can check the status of the app without opening the app:

[toggle code]

  • property iTunes : "com.apple.Music"
  • if application id iTunes is running then
    • display dialog "Music is running"
  • end if

The other new iTunes successor apps—Podcasts and TV—don’t seem to be scriptable via AppleScript.

NSSound.currentTime

Within other scripting languages, there’ve been more interesting, if obscure, changes. In 42 Astounding Scripts I have a Swift script for playing alert sounds.

[toggle code]

  • //wait until the sound is done before going on
  • while sound.currentTime < sound.duration {
    • usleep(100000)
  • }

Waiting is necessary because if the script exits before the sound is finished, the sound will stop. The obvious method of checking if the sound is still playing—NSSound.isPlaying—doesn’t work for alert sounds as it does for audio files. It remains true until calling NSSound.stop(), which makes it useless for deciding when to call NSSound.stop().

But in Catalina, the currentTime property no longer counts up to the duration property and stays there. It counts up to the duration property and then drops to zero, which means that unless you’re very lucky and hit the top of the while loop exactly when the two properties are equal, that loop will never end.

The problem was compounded by the fact that the currentTime property is not immediately updated after starting the sound. When I changed the while condition to NSSound.currentTime > 0 the loop never executed. The first time through, currentTime was zero.

This necessitated using one of my favorite constructs, but one that is, sadly, almost never necessary: a loop with its condition at the end.

[toggle code]

  • //wait until the sound is done before going on
  • repeat {
    • usleep(100000)
  • } while sound.currentTime > 0

The contents of the loop are executed, and only after the first execution is the condition checked.

This loop form is so rarely needed that some scripting languages don’t even include it. Python doesn’t have one.1 AppleScript doesn’t have one.

But Swift does, and this is the perfect time to use it. It ensures that the sleep is executed at least once before checking whether currentTime has dropped back to zero.

osascript and Contacts

More serious was that in a JavaScript osascript that calls the Contacts app, the data started coming back with strange characters around some field titles:

  • $ contacts wayne
  • full name: Bruce Wayne
  • birthday: 2/19/1982
  • _$!<Home>!$_ address: 1007 Mountain Drive, Gotham, NJ, 12345, USA
  • _$!<Work>!$_ address: 380 S. San Rafael Ave, Pasadena, CA, 91105, USA
  • _$!<Mobile>!$_ phone: 735-185-7301

As far as I can tell, _$!<LABEL>!$_ is the actual text returned by Contacts.2 I fixed it by creating a getLabel function that removes those characters if they appear.

  • var labelText = record.label() + " " + fieldName;

and

  • var addressLabel = address.label() + " address"

become:

  • var labelText = getLabel(record) + " " + fieldName;

and

  • var addressLabel = getLabel(address) + " address"

The function is:

[toggle code]

  • //remove extraneous text from labels
  • function getLabel(field) {
    • var label = field.label()
    • if (label.substring(0,4) == '_$!<') {
      • label = label.substring(4, label.length-4)
    • }
    • return label
  • }

This is obviously not satisfactory, but it does the job for now.

zsh

Catalina changes the default shell for Terminal. It uses zsh instead of bash. The two shells are similar. I was able to alter most of my bash scripts simply by changing the shebang from bash to zsh.

The main difference is in using history and tab completion. I added case insensitive tab completion while writing 42 Astounding Scripts thinking it would be helpful. But the benefits have been minimal, and in fact it’s annoyed me more often than it’s been useful. Since making zsh case insensitive is more complicated than making bash case insensitive, I’ve removed that from the book.

If you want case insensitive tab completion and you want to use zsh, you can do a web search on zsh case insensitive. If you don’t particularly need zsh, you can switch to bash, where you can add bind "set completion-ignore-case on” to your .bash_profile.

You can change your default shell (the one that you use when you open a new Terminal window) using:

  • chsh -s /bin/zsh

or

  • chsh -s /bin/bash

The first switches your account to use zsh in Terminal (the default for Catalina or later) and the second switches to bash (the default for Mojave or earlier). Your default doesn’t change if you upgrade. So if you’ve been using Mojave you’ll still have bash as your shell in Catalina, unless you change it yourself.

As I wrote in the book, I don’t really care which shell I use, so I switched to zsh. I did this mainly because that’s the shell that new readers of 42 Astounding Scripts will have. If you’ve been using the Terminal since before Catalina and you’re happy with bash I don’t see any reason to change.

The main difference is that the default prompt ends in % rather than in $.

If you want to get rid of duplicates in your shell’s history, zsh uses setopt instead of export. If you want to combine your histories from multiple open Terminal windows, zsh uses setopt instead of shopt.

  • export HISTCONTROL=erasedups
  • shopt -s histappend

becomes:

  • setopt HIST_IGNORE_ALL_DUPS
  • setopt APPEND_HISTORY

Perhaps most annoyingly, where in bash you typed “history” to get all previous commands, in zsh you must type “history 1” or you’ll get only the most recent commands. I grep ancient history far more often than I look at recent history.

cron and launchd

The deprecation of cron continues; while I recommend that you use a launchd app such as Lingon X instead of cron, it is advice I don’t take myself. Adding a script to cron remains much easier than adding it to launchd.

In Catalina, cron needs permission to run programs. The easiest way to do this is to give cron “Full Disk Access” in your Security & Privacy System Preferences.

  1. In the Terminal, type open /usr/sbin to open the folder that contains cron.
  2. Under the Apple Menu, open System Preferences and go into Security & Privacy.
  3. In the Privacy tab, look in the list on the left for Full Disk Access and choose it.
  4. Click the “+” button and drag cron from the /usr/sbin folder into the File Chooser.
  5. Click the Open button.

Cron will now be listed in the files with full disk access, with a checkmark next to it.

This is not something I particularly recommend. But if you’re using cron and don’t want to start using launchd, it appears to be necessary.

If you decide to use launchd, you’ll probably want to avoid calling scripts directly. That requires giving the scripting language—Perl, for example—full disk access. Instead, make an AppleScript or Automator app. The first time the app runs, it will ask for permission to access your disk. So set it to run in the next minute, give it permission, and then change it to run when you really want it to run.

iCalBuddy

The indispensable iCalBuddy doesn’t work in Catalina. But there’s an update by David Kaluta that does.

  1. Python would have trouble implementing conditional-at-the-end loops due to the format of Python blocks. They can’t have conditions at the end of a block because blocks are defined by indentation following a block start. But if the need was serious, they would have found a way.

  2. With LABEL replaced by the label name, of course.

  1. <- Play Music Faster