Using version control with AppleScripts
In 42 Astoundingly Useful Scripts and Automations for the Macintosh I devoted one diversion to the importance of version control. One thing I left unmentioned except through omission is that AppleScripts created with Script Editor (as most will be, due to Script Editor’s verification and testing ability) can’t really be tracked in version control. They aren’t text files, so while changes will be noted, what those changes are will not. Change tracking pretty much requires line-oriented text files, and Script Editor files are not text files, at least in any sense meaningful to Mercurial or Git.
I have a lot of AppleScripts in my User Scripts folder for several applications, as well as a handful of AppleScripts in my Finder window toolbars and my Favorites folder. I’ve always been disappointed that I can’t track changes to them in Mercurial. And I’ve always been worried that I don’t have a backup of them in easily-readable text format.
While working on this problem, I noticed that I have a lot of scripts I’ll never read again because the format is no longer valid on macOS.
I’ve considered, and occasionally tried, keeping two copies of every script I write: when I’m done editing a script in Script Editor, copy the text to a text file and save that as a readable, future-proofed version. It always works for a very short period and then I forget, and the two sets of scripts get out of sync. As I was writing this post, I discovered an abandoned .hg
folder in my user scripts folder, last touched on September 20, 2014.
It occurred to me while writing the Save Clipboard script that there are so many commands beginning with osa there must be one for getting the text of an AppleScript out of a .scpt file or .app folder.1 And to ask the question is to answer it: osadecompile
does exactly that. This makes it trivial to write a script that keeps the two locations—the live location and the text repository—in sync. The text backup can then be tracked easily in any version control system, including Mercurial.
Use a text editor to save this Perl script to your ~/bin directory, perhaps using the edit script from 42 Astounding Scripts.
[toggle code]
- #!/usr/bin/perl
- # copy AppleScripts as text to a backup folder and Mercurial repository
- # Jerry Stratton astoundingscripts.com
- $HOME = $ENV{'HOME'};
- #where should the decompiled AppleScripts be stored?
- $repository = "$HOME/Documents/Backups/AppleScripts";
- mkdir $repository if !-d $repository;
- #what folders hold AppleScripts?
-
%scriptFolders = (
- 'User Scripts', "$HOME/Library/Scripts",
- 'Programming', "$HOME/Documents/Programming/Applescripts",
- );
-
while (($section, $folder) = each %scriptFolders) {
- chdir $folder or die("Unable to find $section folder $folder: $!.\n");
- decompileFolder('.', $section);
- }
-
sub decompileFolder {
- my $folder = shift;
- my $section = shift;
- opendir(my $handler, $folder);
- my @files = readdir($handler);
- closedir($handler);
-
foreach my $file (@files) {
- next if $file =~ /^\./;
- my $source = "$folder/$file";
-
if ($file =~ /\.(scpt|app|applescript)$/) {
- my $backupFolder = "$repository/$section";
- my $destination = "$backupFolder/$file";
- #text files shouldn't have the .app extension
- $destination =~ s/\.app$/ (Application).txt/;
-
if ($codeToSave = decompileIfChanged($source, $destination)) {
- print "Backing up $source.\n";
- open (my $destinationHandle, '>', $destination) or die("Cannot open $destination for writing.\n");
- print $destinationHandle $codeToSave;
- close $destinationHandle;
- }
-
} elsif (-d $source) {
- decompileFolder($source, "$section/$file") if $file !~ /^Old /;
-
} else {
- print "UNKNOWN ITEM: $source\n";
- }
- }
- }
- #decompile from source if the source has changed
-
sub decompileIfChanged {
- my $source = shift;
- my $destination = shift;
- #decompile source if the destination doesn't exist or the source is younger
-
if (!-e $destination || -M $source < -M $destination) {
- my $escapedSource = quotemeta($source);
- my $escapedDestination = quotemeta($destination);
- my $sourceCode = `/usr/bin/osadecompile $escapedSource`;
- die("There is no code for $source\n") if $sourceCode eq '';
- #always save if the destination doesn't exist
- return $sourceCode if !-e $destination;
- #otherwise, check to see if the source is different than the destination
- my $destinationCode = `/bin/cat $escapedDestination`;
-
if ($sourceCode ne $destinationCode) {
- return $sourceCode;
- }
- }
- }
I call this “osaBackup”.
The only drawback is that I have to run the script every time I make a change if I want the change to be fresh in my mind. I’m very unlikely to remember to do that. So it goes in crontab and I’ll have to remember what it was I changed or added when I commit the changes in Mercurial later. Usually the next morning when I see a notice from cron that an AppleScript file changed and was copied over. And I remember (if I’m lucky), oh yeah, I changed that file yesterday to handle xyz correctly. But it does mean always having text versions of the scripts2 and tracking changes.
This is a very simple script. It changes the working directory to the folder where my AppleScripts are stored, and then walks that directory for everything that’s changed. If it sees something it doesn’t recognize, it says so. Otherwise, it saves the output of osadecompile
to the destination repository. Except for .app folders, it leaves the extension the same. Script Editor handles text .scpt and .applescript files fine.
It only decompiles an AppleScript if the script file is younger than the destination and the source has changed, or if the destination doesn’t exist. It checks that the source has changed because AppleScript files can change without changing the source code. When a script is run, if that script format can store properties the script will be marked as changed. This is true even if the script doesn’t have any properties—it’s saving more about the state than just the value of properties.
I have two places where I store AppleScripts. The obvious is in the User Scripts folder in ~/Library/Scripts. This is where all of the User Scripts in Script Menu reside. The other is in an AppleScripts folder in ~/Documents/Programming. This script backs up both of those areas; because there are multiple areas, I have a $section variable that gets used for where in the repository this folder of files should go. It’s set to “User Scripts” for ~/Library/Scripts and “Programming” for ~/Documents/Programming/AppleScripts.
You can probably leave the User Scripts line alone, but you’ll need to change the second line to point to your own AppleScripts.3 You will also want to change the $repository
path to where the decompiled scripts are saved.
There are more complicated ways of handling version control for AppleScripts; Git, for example, can set up a pre- and post- processing command by filetype for check-in and check-out. For my purposes, however, I’m the only person editing my scripts, so I don’t need that complication. This technique is simpler and solves two problems at once: tracking changes, and keeping text backups.
AppleScripts saved as applications are stored like any other application on the Mac: they are folders with the .app extension; the folder contains any resources the application needs, such as icons and the actual code.
↑When an application is deleted, its dictionary will no longer be available. So some code will get changed from (for example, since iPhoto no longer exists) “album” to “«class ipal»”. But when this happens, you’ll see it in your version control and be able to revert the change and/or remove the original script so it doesn’t get backed up any more.
↑If you don’t currently store all of your non-User Scripts AppleScripts in a single location, you may want to consider it. Besides being more useful for this particular script, it also makes it a lot easier to find a script when you want to modify it after a few years of use.
↑
- 42 Astoundingly Useful Scripts and Automations for the Macintosh
- MacOS uses Perl, Python, AppleScript, and Automator and you can write scripts in all of these. Build a talking alarm. Roll dice. Preflight your social media comments. Play music and create ASCII art. Get your retro on and bring your Macintosh into the world of tomorrow with 42 Astoundingly Useful Scripts and Automations for the Macintosh!
- Edit (Zsh)
- One of the first scripts in the book is a script to edit scripts. But that elicits a bootstrapping problem. Without the edit script, you can’t use the edit script to edit the edit script!
- Mercurial
- “Mercurial is a free, distributed source control management tool. It efficiently handles projects of any size and offers an easy and intuitive interface.”
- Save clipboard text to the current folder
- Use the Finder toolbar to save text on the current clipboard directly to a file in the folder that Finder window is displaying.
More AppleScript
- Find all parent mailboxes in macOS Mail
- The macOS Mail app seems to want to hide the existence of mailboxes and any sense of hierarchical storage. These two AppleScripts will help you find the full path to a selected message and open the message’s mailbox.
- JXA and AppleScript compared via HyperCard
- How does JXA compare to the AppleScript solution for screenshotting every card in HyperCardPreview?
- Save clipboard text to the current folder
- Use the Finder toolbar to save text on the current clipboard directly to a file in the folder that Finder window is displaying.
- Adding parenthetical asides to photograph titles on macOS
- Use Applescript to append a parenthetical to the titles of all selected photographs in Photos on macOS.
- AppleScript, variables, and dropped filenames in Automator
- Automator is a simple workflow system for Mac OS X. By its nature it is very procedural: one task follows another; workflows don’t loop and they don’t store variables for later. However, this is possible in Automator and while it adds complexities it can also solve problems such as wanting to save dropped filenames for later use.
- 17 more pages with the topic AppleScript, and other related pages
More Astounding Scripts
- 42 Astoundingly Useful Scripts and Automations for the Macintosh
- MacOS uses Perl, Python, AppleScript, and Automator and you can write scripts in all of these. Build a talking alarm. Roll dice. Preflight your social media comments. Play music and create ASCII art. Get your retro on and bring your Macintosh into the world of tomorrow with 42 Astoundingly Useful Scripts and Automations for the Macintosh!
- Play music faster in the Music App
- If you enjoy Marvin and the Chipmunks, you’ll love this AppleScript.
- Catalina: iTunes Library XML
- What does Catalina mean for 42 Astounding Scripts?
- 42 Astounding Scripts is live!
- Ready to get your retro on? Type in programs for your Macintosh and make it play music, roll dice, and talk to you? Customize your services menu? 42 Astoundingly Useful Scripts and Automations for the Macintosh is the book for you!
- About Astounding Scripts
- Because I can!
Now if we could just do the same for Automator scripts!