The problem with the simple alert I demonstrated last time is that it only hangs around for a second or two (much less than a Folder Action alert, which takes a couple of minutes to time out). In this updated function, it now also writes a list of the file changes to a (by default) file on the Desktop. The write is an append: if the file doesn’t exist it will create it before writing; if it does exist, it will append the latest changes and date to the file. This way, even if you miss the alert, you’ll always have a record of what files have been added, deleted or modified in your watched folder.
In this example, the folder being watched is ~/Library/LaunchAgents since we want to be aware of any adware, malware or other unsavoury processes being surreptitiously added by, for example, apps we download from the internet. Although there are, of course, many legitimate reasons for apps placing items in here, this folder is prime real estate for attackers as it is one of the locations that can launch processes at log in time without the user’s interaction (or knowledge).
Here’s the code, also available from my pastebin here. A code walkthrough follows.
function writeStringToFileWithMode(aString, aPath, aMode)
this_file = io.open(aPath, aMode)
local str = "Launch Agents folder was modified on " .. os.date() .. " :\n\t"
local this_path = os.getenv("HOME") .. "/Desktop/LaunchFolderModified.txt"
local ignore = "DS_Store"
local count = 0
for _,file in pairs (files) do
count = count + 1
i = string.find(file, ignore)
if not i then
str = str .. file .. "\n\t"
if count == 1 then
str = "\n"
str = str .. "\n"
writeStringToFileWithMode(str, this_path, "a")
if string.len(str) > 2 then
hs.alert.show("Launch Agents folder was modified.")
local aWatcher = hs.pathwatcher.new(os.getenv("HOME") .. "/Library/LaunchAgents/", myFolderWatch):start()
The first function, ‘writeStringToFileWithMode’ is just a convenience function. Hopefully the clue is in the name. The ‘aMode’ parameter is either “w” for write(over) or “a” for write(append).
The myFolderWatch function starts off by declaring some local variables.
‘str’ includes the initial line that we want to write to our output file and interpolates the time of the change by calling os.date().
‘this_path’ defines the path that we want to write our list of file names too.
The ‘ .. ‘ in both these assignments is a string concatenator (i.e., like ‘&’ in AppleScript or ‘stringByAppendingString’ in Obj-C).
‘ignore’ is a string that will help us to exclude .DS_Store files from triggering the alert or appearing in the list of file changes.
The ‘count’ variable is an internal var we need in order to eliminate .DS_Store changes when it’s the only change. Lua doesn’t have an easy way to count entries in tables, so we bascially iterate a variable each time through the loop to achieve the same effect.
After that, we have the ‘for’ loop. For loops in Lua are weird (at least for me), as you’ll see that they have this structure for
a,b in pair (aPair). I won’t go into why, other than to say its a result of Lua’s table data structure. The
'_' here is just a dummy variable for the first parameter. The ‘files’ in parentheses are the list of file names (not file specifiers, you AppleScripters!) that were added, deleted, or modified in the watched folder.
The loop begins by incrementing the count, then checks for the ignore file (.DS_Store). If the ignore file is not found, we then append the filename to the str variable.
If it is found, we check the count. If the count is ‘1’ (i.e., the only change was .DS_Store) we discard the entire str var and replace it with new line character. If the count is more than 1 we don’t do anything to ‘str’. We just ignore adding anything to it all.
At the end of the for loop we add another new line to the string just so our outputted text file looks nice and neat.
Then we call the write function mentioned above, passing ‘a’ (append) for the mode.
Finally, we fire the UI alert on the condition that the string has got more than 2 characters in it (if it didn’t it was just the “\n\n” string from ignoring the DS.Store file).
After the function definition, the aWatcher variable sets up the watcher on the path we want to observe and tells the watcher to start monitoring. It tells the watcher to call our myFolderWatch function when anything happens.
Deploying the code
After editing the config file, remember there’s two steps: i. save the file and ii. choose ‘Reload Config’ from the Hammerspoon menu.
More details are available about Hammerspoon from the official site here.
My beginners guide with the original simple watcher function is here.
Is it me, or is AppleScript experiencing something of an Indian Summer? It seems everywhere I go, people are talking more about macOS automation, AppleScript and even Apple’s curious hybrid syntax AppleScriptObjC (ASObjC).
Of course, some people have suffered miserably at the hands of AppleScript in the past, and even though the thought of scripting with access to Cocoa APIs through Objective-C is tempting, they fear the AppleScript side of it.
If that’s you, bear in mind that AppleScriptObjC isn’t really “AppleScript + Objective-C” at all. It is actually just a dialect of Objective-C that will be accepted in the (Apple)Script Editor and can be run by an instance of the AppleScript component. In plainer English, you can use Objective-C in an AppleScript without any AppleScript whatsoever!
The point of doing so would be that one could package Objective-C code in a .scpt file (or scptd bundle or AppleScript .app), and also mix whatever scripting language you prefer with calls to Cocoa’s APIs.*
The problem that using ASObjC presents anyone familiar with Objective-C is how to translate ‘pure’ Objective-C into the dialect that Script Editor (and other applescript runners like FastScripts, Keyboard Maestro, Automator, etc) can understand. If you use LateNight Software’s Script Debugger for scripting, you’ll already know that the work is done for you by the app’s built-in code completion. If you’re battling on in Apple’s default Script Editor, you’ll need to do the translation manually.
By way of example, then, here’s some original Objective-C, and below it, a translation that would work in Script Editor:
NSString *aString = @"hello";
NSString *bString = @" world";
aString = [aString stringByAppendingString:bString];
NSUserNotification *notif = [[NSUserNotification alloc] init];
notif.informativeText = aString;
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notif];
set aString to NSString's stringWithString:"hello"
set bString to NSString's stringWithString:" world"
set aString to aString's stringByAppendingString:bString
set notif to NSUserNotification's alloc's init
set notif's informativeText to aString
NSUserNotificationCenter's defaultUserNotificationCenter()'s deliverNotification:notif
As you can see, there’s a direct 1-to-1 correspondence, with the 6 statements in Objective-C paralleled by the 6 statements in AppleScriptObjC.
The main peculiarity is the use of possessive word forms and that variable attribution is done by using
"set X to Y" rather than
"X = Y". Type declaration is done via the idiom
'set <var> to <NSObject>'s <class init method>', which returns an instance of the object just as it would normally. You call instance methods by putting the instance in front of the method just as you would in regular Objective-C (e.g, see line 3 of the examples).
As you can see in the screenshot below showing Xcode and Script Editor, they work in the same way. You’ll notice in Script Editor there is a
'use' statement (equivalent to Objective-C’s ‘import’), and there’s also a whole load of property statements. These latter are peculiar to the ASObjC translation, and don’t have a counterpart in pure Objective-C. All you need to know about these is for each kind of Objective-C object you want to use (NSString, NSArray, whatever*), you’ll want a property statement for it at the beginning of the script. The statement always has the same form:
property <NSObject> : a reference to current application's < NSObject>
I think the best way to think of ASObjC was recently summed up by Sal Saghoian, when he said that ASObjC is “…the ultimate duct tape. You can do anything you want with ASObjC. You own the computer.”
*not all Cocoa frameworks nor all Objective-C objects can be bridged to, but pretty much all the most useful ones are available.
I recently discovered a neat little extra automation tool on top of the familiar ones of AppleScript, Automator, and script runners like FastScripts and Keyboard Maestro. Meet Hammerspoon, which differs significantly in not using Apple Events to do many of its automation tasks. Instead, Hammerspoon bridges directly to Apple APIs using the lua scripting language, and that allows you to do some interesting things.
Here’s a good example. One of the ‘danger zones’ on your mac – by which I mean one of the favourite places for adware, malware and other assorted badwares to infect – is your LaunchAgents folders. Apps like my DetectX and FastTasks 2 keep an eye on these areas by design, warning you in the Changes and History logs when files have been added or removed from them – but Hammerspoon can add an extra little ‘canary’ warning for you too. With only a few lines of code in Hammerspoon’s config file, you can set up an alert that will fire whenever the LaunchAgents folder is modified.
It has been possible to rig up something similar for a long time with Apple’s built-in Folder Actions, but there’s a couple of reasons why I prefer Hammerspoon for this task. One, despite Apple’s attempt to improve Folder Actions’ reliability, I’m still not convinced. I get inconsistent reports when asking System Events to check whether Folder Actions is enabled even on 10.11 and 10.12. Second, Folder Actions is limited in what it will respond to. By default, only if an item is added. With a bit of effort, you can rig it up to watch for items deleted, too, but that’s pretty much it. With Hammerspoon, it’ll alert you whenever the folder or its contents are modified in any way whatsoever. The final reason for preferring Hammerspoon for this particular task is ease of use. It really is as simple as pasting this code into the config file:
hs.alert.show("Launch Agents folder was modified")
hs.alert.show("Canary folder was modified")
local aWatcher = hs.pathwatcher.new(os.getenv("HOME") .. "/Library/LaunchAgents/", myFolderWatch):start()
local bWatcher = hs.pathwatcher.new(os.getenv("HOME") .. "/_Acanary/", canaryFolderWatch):start()
And what’s the config file you ask? Nothing complicated! Just launch Hammerspoon, click its icon and choose ‘open config’.
That will launch your default text editor, and all you do is paste your code into there, save it (no need to tell it where, Hammerspoon already knows) and then go back to the Hammerspoon icon and click ‘Reload config’. That’s it. Less than a minute’s work!
There’s a lot, lot more you can do with Hammerspoon, so if you’re interested head off and check out the Getting Started guide. One of the nice things is that you can even call AppleScripts with it, so you really do have a world of automation options to choose from!
Here’s a short video showing some of the differences between Apple’s own Script Editor and my DisplayDroid.
If you haven’t got 5 minutes, the highlights include:
DisplayDroid shows result of each line of the script
DisplayDroid offers more informative error messages
DisplayDroid allows you to set a breakpoint on any line in your script
DisplayDroid lets you step through the script line by line