Debugging an AppleScript Handler in VBA
Script Debugger vs Apple’s Script Editor
AppleScript is designed to help automate simple tasks that you need to do often, without the need for learning a high level language like Objective C. While it’s been billed as closer to “natural language,” I find it a bit quirky, but I’m slowly getting used to it. Apple’s Script Editor, however, leaves something to be desired. Debugging scripts in Script Editor forces the developer to use lots of log messages which can only be comfortably reviewed after the script is complete. Anoher alternative is Display Dialog, which is widely discouraged for debugging.
Enter the Script Debugger. I’ve used lots of interactive symbolic debuggers in my time and Script Debugger replicates the best features of most of them, particularly single-stepping and variable examination and editing. When I met Script Debugger, I gave a huge sigh of relief. At last I was able to see what was going on as the script progressed. When it failed, I could identify the problem with little guesswork. I can watch what happens one step at a time and can make quick fixes and then re-run.
Overview
I initially started to write a simple how-to for debugging a VBA-to-Applescript connection. It turned out to be more complicated than I thought, due to differences in calling sequences between Excel versions. So this is a longer, but more complete coverage of the concept.
This article addresses How To:
- Debug your AppleScript scripts when called from Microsoft Excel VBA (Visual Basic for Applications)
- Pass parameters from VBA to AppleScript handlers
- Separate code for two different versions of Excel
- Produce code that can handle both versions of Excel
Quick Links to Topics in this article
- Debugging AppleScript
- Excel 2011
- Excel 2016/2019
- Coding VBA for Both Versions
- Causes of Run-Time Error 5
- Recommended AppleScript Web Sites
Debugging AppleScript
Normally, to use Script Debugger, you select or double-click an AppleScript file and it opens up, ready to go to work. You can debug most AppleScripts this way just fine. I call this Direct debugging.
Sometimes invoking a script, already debugged using the direct method, from another application like VBA, can cause unexpected problems. That’s when we need what I term Indirect debugging.
Both are described in detail in the following sections.
Direct debugging
For most scripts, all you have to do is turn on the debugger and single-step through, or set breakpoint(s) and run till they pause the execution. Easy as pie.
Debugging a handler (Apple’s term for what the rest of us might call a subroutine) is comfortably handled by Script Debugger. Here’s an example of a handler that splits a string based on a delimiter.
# main line
set SplitArray to split("String 1|String 2|String 3", "|")
# the handler
# Ref: https://erikslab.com/2007/08/31/applescript-how-to-split-a-string/
on split(theString, theDelimiter)
local oldDelimiters, theArray – allows local variables to be seen by the debugger
set oldDelimiters to AppleScript's text item delimiters -- save delimiters to restore on completion
set AppleScript's text item delimiters to theDelimiter -- set delimiters to delimiter to be used
set theArray to every text item of theString -- create the array
set AppleScript's text item delimiters to oldDelimiters -- restore the original setting
return theArray -- return the result
end split
A handler is not run unless invoked by mainline. When this is executed, the mainline code runs and calls the handler, just like other languages call subroutines. While debugging in Script Debugger this way, logged messages are visible in the log pane, and other valuable information can be displayed in other panes.
A handler can also be called by an external program such as VBA.
Indirect Debugging from VBA
VBA is a macro programming language underlying the apps in Microsoft Office: Excel, Word, Access, Outlook and others. It works in Windows remarkably well and I have used it since the 90’s for more than one client. Under MacOS, it appears to be a bit handicapped. For example, the Mac’s long file names cause errors, and it can’t do timed dialog boxes. AppleScript to the rescue; it can do many of the things that MS left out of the Mac version of Office, although at the expense of a bit slower execution time.
But herein lies a problem. You can debug your individual routines/handlers using the direct method but adding VBA to the mix introduces a new level of complexity. When you call an already debugged AppleScript script from VBA, you are likely to see this:
You can’t tell from looking at that what the problem is. Just a vague error message that something unspecified went wrong. (One of my complaints about VBA has always been some of the generic non-specific error messages, but that’s another story.)
So how can you debug a handler invoked from VBA? While you might have logging as part of the script, that log disappears once the script completes and you don’t get to see it. Display Dialogs are poor substitutes.
The answer: use Script Debugger to call itself. It took me a while for that to sink in. It turns out that the Script Debugger developers included an incredible list of scriptable features in Script Debugger. I won’t go into detail, but go to Menu > Window > Dictionary and pick the little red ladybug. You’ll see quite an array of properties and methods that should give you lots of ideas.
I’m going to show you a set of simple VBA to Script Debugger scripts that should give you enough to get started with your own. First, though, I have to tell you: Because of security restructuring, it is different for Excel 2011 and Excel 2016/2019, so we need separate solutions for each. For more information on the reasons and the methods, visit Ron de Bruin’s excellent web page at https://www.rondebruin.nl/mac/applescripttask.htm
.
In fact his whole site is full of good stuff, much of it focused on VBA for Mac.
Excel 2011
Method 1 – VBA-Generated AppleScript
This is for simple scripts that are not hard to debug visually. Write an Excel VBA procedure that creates an AppleScript script and pass it to the MacScript statement to be executed. For example, the following creates a dialog box with a timeout, so if the user doesn’t respond within a specified time frame, the default button is used and the box closes.
For this version, we will need only one piece of code:
- VBA code to create the AppleScript handler being debugged
VBA Code
Create a new workbook, open the VB editor and insert a new module.
Sub Process_Copmlete()
Dim myScript As String
Dim Response As String
myScript = "set ProcNameMsg to ""The Purge process is complete""" & vbCr
myScript = myScript & "Beep" & vbCr
myScript = myScript & "set {gave up:gaveUp} to display alert ProcNameMsg " & _
"buttons {""Stop"", ""Continue""} default button ""Continue"" giving up after 5" & vbCr
myScript = myScript & "If gaveUp Then" & vbCr
myScript = myScript & " set Response to ""Continue""" & vbCr
myScript = myScript & "Else" & vbCr
myScript = myScript & " set Response to button returned of result"
myScript = myScript & "End If"
Response = MacScript(myScript) ' invoke Applescript
End Sub ' Process_Copmlete
Copy this procedure to VBA and run it. Surprise! You get the dreaded Run-time error 5. With no information to go on, you’ll have to scan the code in VBA. Clearly the VBA code obfuscates the AppleScript code. Here’s the answer: set a VBA breakpoint at the MacScript line and run it again. When it pauses, go to the Immediate Window and type Print myScript. What you see is:
set ProcNameMsg to "The Purge process is complete"
beep
set {gave up:gaveUp} to display alert ProcNameMsg buttons {"Stop", "Continue"} default button "Continue" giving up after 5
if gaveUp Then
set Response to "Continue"
else
set Response to button returned of resultEnd If
This is exactly what the AppleScript process sees. It looks okay all the way to the bottom line. Oops, the problem is resultEnd
. VBA wasn’t told to put a carriage return at the end of the previous line. Modify that line as follows:
myScript = myScript & "Else" & vbCr
myScript = myScript & " set Response to button returned of result" & vbCr
myScript = myScript & "End If"
Now hit the Reset button and try running it again. This time you get the intended dialog box:
This has demonstrated the easiest way to debug your VBA to AppleScript: Print the script and scan for errors. You can test that the buttons work correctly and that if you do nothing, the box closes itself after 5 seconds.
Next, if you still can’t see any error(s) in the AppleScript, you can copy the results from Debug Print and paste it into Script Debugger and see if it compiles. If not, the error message will help you determine what the problem is. If it compiles, try stepping through it till it fails, then identify and fix that problem.
The worst case occurs when the same code fails when run from VBA and works fine when run from Script Debugger. That is a nightmare I have encountered occasionally and, to be honest, I don’t know why it happens; perhaps some quirk in the communications between the two. Fortunately, it doesn’t happen often. If it does, then you may have to design a workaround within AppleScript, but that is well beyond the scope of this article. I’ve included a list of the most common problems I’ve encountered, further along.
So, Method 1 has shown you how to copy the code back and forth between VBA and Script Debugger. If you get your AppleScript code working in Script Debugger, then copy it to VBA and form it into a string for MacScript. If it doesn’t work from VBA, copy it to Script Debugger and try to run it.
Method 2 – External AppleScript Script
When your AppleScript code gets complex and lengthy, examining the VBA code that generates it can get pretty dicey. This is where we utilize Script Debugger’s ability to invoke itself. To summarize the process: VBA code generates an AppleScript that invokes Script Debugger, which in turn invokes itself to debug the desired AppleScript script.
For this version, we will need two pieces of code:
- VBA code to create the AppleScript debug handler which will call the handler to be tested/debugged
- The actual AppleScript handler being debugged
VBA Code
Option Explicit
Sub Test_Handler()
' Debugging AppleScript from VBA
Dim myScript As String
myScript = "tell application ""Script Debugger""" & vbCr
myScript = myScript & " tell document ""Testing.scpt""" & vbCr
myScript = myScript & " Debug_Handler(""Counting Tags"", ""Keep on Testing!"", ""5"")" & vbCr
myScript = myScript & " end tell" & vbCr
myScript = myScript & "end tell" & vbCr
MacScript myScript
End Sub ‘ Test_Handler
Create a new workbook, open the VB editor and insert a new module. Copy this code into the new module. Save it as any name you want. I used the descriptive title of Test.xlsm. This represents the VBA script you need to invoke the AppleScript task. Be sure you don’t have two copies of the first line (Option Explicit).
AppleScript Code
on Debug_Handler(P1, P2, P3)
local Parms
set Parms to P1 & return -- set breakpoint here
set Parms to Parms & P2 & return
set Parms to Parms & P3 & return
display dialog "This is a message from the handler" & return & ¬
Parms buttons {"OK"} default button "OK"
end Debug_Handler
Open Script Debugger, create a new script and paste this code into it. set a breakpoint on the first set Params
line then save it, in debug mode, as Testing.scpt, which you’ll notice is the name of the document in the VBA script. Also notice that the name of the handler (Debug_Handler) is the same as the name inside the tell document block above. This represents the script you want to debug.
Now, to debug this, make sure that Testing.scpt is open in Script Debugger. If you closed it, reopen it and make sure it’s in debug mode. Open your workbook to the VB editor and run the sub Test_Handler. The Script Debugger window should activate, with the script paused at the line with the breakpoint. At this point you can examine variables, log messages, and single-step through the rest of the script. Since this one is so simple, there isn’t much to see besides Parms. The dialog box pops up and shows the message and parameters.
After debugging is complete , you have two choices.
-
Implement the debugged AppleScript code using inline code to pass to MacScript, similar to what we did in an earlier VBA procedure. It might look like:
Sub Test_Handler2() Dim myScript As String myScript = "set Parms to ""Counting Tags"" & return" & vbCr myScript = myScript & "set Parms to Parms & ""Keep on Testing!"" & return" & vbCr myScript = myScript & "set Parms to Parms & ""5"" & return" & vbCr myScript = myScript & "display dialog ""This is a message from the handler"" & return & Parms" & " buttons {""OK""} default button ""OK""" & vbCr MacScript myScript End Sub ' Test_Handler2
A couple of things to note: you don’t include the handler’s on (a “Begin” construct) and end. You don’t pass parameters; you just include them in the code. And you may still run into the dreaded Run-time error ‘5’, the elimination of which was the whole point of this exercise. That’s most likely because you had to change the code to plug it into MacScript.
-
Invoke the unmodified debugged handler script. This is desirable, since you don’t have to touch the AppleScript code that you know works. But it does involve a bit more twiddling with VBA. Here’s an example:
Sub Call_Handler() ' Script path under user's home folder - change these to match your folder structure Const YourUserName As String = "Chuck" ' MacOS user name Const HandlerScriptPath As String = "/Users/" & YourUserName & _ "/Desktop/Development/Debugging VBA and AppleScript/2011/" Const HandlerScriptName As String = "Testing.scpt" ' script file name Const HandlerName As String = "Debug_Handler" ' name of the handler Const P1 As String = "Counting Tags" Const P2 As String = "Keep on Testing!" Const P3 As String = "5" Dim Response As String Dim InvokeHandlerScript As String InvokeHandlerScript = "Set HandlerScript to load script (""" & HandlerScriptPath & _ HandlerScriptName & """)" & vbCr InvokeHandlerScript = InvokeHandlerScript + "HandlerScript's " & HandlerName & _ "(""" & P1 & """, """ & P2 & """, """ & P3 & """)" & vbCr Response = MacScript(InvokeHandlerScript) ' Call the AppleScript routine from Excel 2011 End Sub ' Call_Handler
Copy this VBA code into a new module and run it. It should bring up the dialog box you already saw in the debugging step.
Some things to mention about this code:
-
The VBA procedure name can be anything you want. It is not significant to the process.
-
Define your user name and the full path to the script. It is widely recommended that you store all your AppleScripts in a single location for convenience and consistency.
-
If you change script file name and/or handler name, be sure to change them here, or VBA won’t find them and you’ll get the dreaded error 5 message.
-
You can pass any number of parameters to the VBA routine to pass along to the AppleScript handler.
-
If you print the InvokeHandlerScript variable, you’ll see:
Set HandlerScript to load script ("/Users/Chuck/Desktop/Development/Debugging VBA and AppleScript/2011/Testing.scpt") HandlerScript's Debug_Handler("Counting Tags", "Keep on Testing!", "5")
This is an AppleScript method of calling a handler in another script file. I’m going to describe it a little more detail because it’s employed in another script further on.
The first line defines a variable named HandlerScript, to hold the full path name of the handler script.
The second line invokes the handler from that script file and passes the defined parameters to it. You can use it in your own scripts if, for example, you have a number of handlers dedicated to different purposes and have gathered them by function into separate script files. So a routine in one script file can call a handler in another script file, just as if they were in the same file.
-
Now that you see how it works, you should be able to use this method on your own scripts.
Excel 2016/2019
For security reasons, this version of Excel doesn’t allow you to create scripts on the fly and execute them from VBA. It requires that the handler, which will do the work, must already exist in a shared library folder dedicated to Excel. You must then invoke that existing script from VBA. If it seems convoluted, you’re right. Go read Ron de Bruin’s explanation.
The process for this version is nearly identical to Method 2 above. The three differences are:
- Script files invoked by Excel VBA must be stored in Excel’s shared library folder.
- MacScript has been replaced by AppleScriptTask
- AppleScriptTask permits only one parameter to be passed to the handler.
For this version, we will need three pieces of code:
- VBA code to call the AppleScript debug handler
- The AppleScript debug handler which will call the handler to be tested/debugged
- The actual AppleScript handler being debugged
VBA Code
This code sets up the call to a AppleScript handler, which will in turn call Script Debugger to allow interactive debugging of the handler under development.
Option Explicit
Sub Test_Handler_2016()
Const HandlerScriptName As String = "Testing Handler2016.scpt" ' name of the script file
Const HandlerName As String = "Debug_Handler2016" ' name of the handler
Dim Response As String
Dim ParameterList As String
ParameterList = "Counting Tags|Keep on Testing!|5" ' | is the parm delimiter
Response = AppleScriptTask(HandlerScriptName, HandlerName, ParameterList)
End Sub ' Test_Handler_2016
Copy this to a new module in the workbook you’ve been using.
Some things to mention about this code:
- Of course, you must be running Excel 2016 or 2019 for this to work, since MacScript was deprecated after version 2011.
- Likewise, AppleScriptTask will not run under 2011.
- Like an earlier procedure, the VBA procedure name can be anything you desire. It is not significant to the process.
- You can only pass one parameter to the handler. This could be a significant barrier to serious development, except that you can concatenate multiple parameters in a single string, separated by a delimiter. The handler must then split them into their separate pieces (remember the Split handler shown earlier?). I generally use the gravé (the backwards apostrophe also known as the “back-tick,” above the tab key on the main keyboard), because it never otherwise appears in my scripts.
- Note: I’m using the vertical bar in this document because it is more recognizable and easier to see. You are free to use whatever you prefer as long as it does not conflict with characters in your parameters.
- If you change script file name and/or handler name, be sure to change them here, or VBA won’t find them and you’ll get the dreaded error message.
- You don’t have to provide a path to the script file – Excel assumes you have placed it in a specific folder dedicated to the purpose. For Excel, the path is
/Users/YourUserName/Library/Application Scripts/com.microsoft.Excel/
If the folder does not exist, you must create it – you can use mkdir for that. Be sure to spell it EXACTLY as shown – any difference in case or spelling will prevent Excel from finding it.
AppleScript Code (debug handler)
This code is strictly for debugging. It will not go into production.
on Debug_Handler2016(ParameterList)
tell application "Script Debugger"
tell document "Testing 2016.scpt"
Debug_Handler2016(ParameterList)
end tell
end tell
end Debug_Handler2016
Create an AppleScript file and copy the above into it. Save it to the Excel shared library folder (identified above) with the name Testing Handler2016.scpt
.
AppleScript Code (handler being debugged)
This code is what will, when debugging is complete, become the production routine, to be called by VBA, without the Testing Handler2016.scpt
handler in between.
on Debug_Handler2016(ParameterList)
local Parms, myArray
set myArray to Split(ParameterList, "|") -- set breakpoint here
set Parms to item 1 of myArray & return
set Parms to Parms & item 2 of myArray & return
set Parms to Parms & item 3 of myArray & return
display dialog "This is a message from the handler" & return & ¬
Parms buttons {"OK"} default button "OK"
end Debug_Handler2016
on Split(theString, theDelimiter)
local theArray
set oldDelimiters to AppleScript's text item delimiters -- save default delimiters
set AppleScript's text item delimiters to theDelimiter -- note delimiter to be used
set theArray to every text item of theString -- create the array
set AppleScript's text item delimiters to oldDelimiters -- restore original delimiters
return theArray -- return the result
end Split
Set the breakpoint at the set myArray
line, turn debugging on, and save the file in the Excel shared library folder (identified above) with the name Testing 2016.scpt
. Keep it open for the test. If it is not open, you can expect the dreaded Run-time 5 error. My experience has shown that it can crash Excel, so make sure the script is open in Script Debugger.
Run the VBA routine and watch Testing 2016.scpt
pause at the line with the breakpoint. Single step through and watch the array and variables get established, and the result from the dialog box. You could fix any bugs you discovered and repeat until you were satisfied it was running correctly.
After debugging is complete, turn off debugging in the script file Testing 2016.scpt
, save it as a compiled script, then close it. Return to the VBA code and change the name of the script file to Testing 2016.scpt
. You can then run the VBA code again and the dialog box pops up without Script Debugger showing itself. You can delete or retain the script file Testing Handler2016.scpt
.
That’s it. You have debugged the AppleScript file and VBA code at the same time.
Coding VBA for Both Versions
As mentioned earlier, the means of calling AppleScript for Excel VBA 2011 differs from that of 2016/2019. 2011 uses the MacScript call while 2016/2019 uses AppleScriptTask. This means that code designed for 2011 will not work in 2016/2019, and vice versa. In fact it won’t even compile - it will give an error message and refuse to execute.
If you are only writing for one version, then you can skip this section and use code for only the your version. Otherwise:
One solution is to maintain two code bases, one for each version. But there is an alternative: conditional compilation, which means that you use the same code for all bases, but build a wrapper around each version’s code that compiles only if that one is executing. The result is not necessarily prettier code, but it does simplify things quite a bit. It uses Excel’s internal constants to determine which version and compile/execute only the code designed for it. It can also distinguish between Windows and MacOS platforms.
Again, I refer to Ron de Bruin – I learned a lot from him. He showed me how to determine which platform (Window, MacOS, Linux) your code was operating under and which version of Excel was running. I am providing an example here and you should be able to use it for your own purposes. Assuming you have to produce code that will work under multiple conditions. If not, consider yourself lucky.
Now, like so much else in this area, there are two ways you can implement this:
-
Include the complete routines for both versions and put them inside wrappers for their version. This is best for small routines that are pretty simple and don’t have much in common. A simple example follows:
#If Mac Then ‘ MacOS only code #If MAC_OFFICE_VERSION < 16 Then ' call this routine from Excel 2011 Function test() Debug.Print "Test - for 2011" End Function #Else ' Call this routine from Excel 2016 and 2019 Function test() Debug.Print "Test - for 2016 and 2019" End Function #End If #Else ‘ Windows only code ' Windows does not use appleScript, so this will never happen. #End If
Note that the routines have the same name (“Test”). Without the #If - #End If wrappers, this would throw an error: “Ambiguous name detected: Test”, but conditional compilation keeps them from interfering with each other.
-
Consolidate the routines into a single routine and place the wrappers around segments dedicated to each version. This is best for large, complicated routines. What follows is a merged version of the routines for Excel 2011 and Excel 2016/2019:
Sub Test_Handler() ' these would be passed parameters Const P1 As String = "Counting Tags" Const P2 As String = "Keep on Testing!" Const P3 As String = "5" Dim HandlerScriptName As String ' script file name Dim HandlerName As String ' name of the handler Dim Response As String #If Mac Then #If MAC_OFFICE_VERSION < 16 Then ' call this routine from Excel 2011 Const YourUserName As String = "Chuck" ' MacOS user name Const HandlerScriptPath As String = "/Users/" & YourUserName & _ "/Desktop/Development/Debugging VBA and AppleScript/2011/" Dim InvokeHandlerScript As String HandlerScriptName = "Testing.scpt" HandlerName = "Debug_Handler" InvokeHandlerScript = "Set HandlerScript to load script (""" & HandlerScriptPath & _ HandlerScriptName & """)" & vbCrLf InvokeHandlerScript = InvokeHandlerScript + "HandlerScript's " & HandlerName & _ "(""" & P1 & """, """ & P2 & """, """ & P3 & """)" & vbCrLf Response = MacScript(InvokeHandlerScript) ' Call AppleScript routine from Excel 2011 #Else ' Call the AppleScript routine from Excel 2016 and 2019 ' Script path under user's home folder - change these to match your folder path Dim ParameterList As String HandlerScriptName = "Testing 2016.scpt" HandlerName = "Debug_Handler2016" ParameterList = P1 & "|" & P2 & "|" & P3 ' | is the parm delimiter Response = AppleScriptTask(HandlerScriptName, HandlerName, ParameterList) #End If #End If End Sub ' Test_Handler
Note a few things about this routine:
- Code that is common to both versions can be kept outside the conditional compilation wrappers. That would include testing the Response variable if you chose.
- They use different methods of parameter passing. One passes parameters separately and the other combines them to be separated by the handler. This of course implies that each uses their own handler. One could revise the MacScript parameter list to combine them as done with AppleScriptTask, and then they could use the same handler. You would also have to force MacScript to look in the Excel shared library folder as AppleScriptTask does, and make sure you copy the handler there.
- If you try to place a breakpoint in the code for the other version, it will not be accepted, because the code does not get compiled. So don’t even try.
These are left as an exercise for the Very Dedicated. Have fun with it. There are lots more things you can do.
Causes of Run-Time Error 5
During your debugging, you’re likely to encounter many times the dreaded Run-Time Error 5. Working your way through that and other errors is going to take time and patience. But then, as a developer, you probably already know that. Here are some of the problems that caused me that error:
- Wrong script file name or file missing. If you change the name one place, the process can’t find the file. Same if you have forgotten to place the file in the expected location. Just imagine, you’re looking through the phone directory for “Sarah Connor” and she’s moved.
- Wrong handler name or handler is not in the file you specified. Just like the file name, if the handler name can’t be found, things will fail.
- Wrong number of parameters. VBA has optional parameters, but AppleScript does not. The number of parameters must match. Good news: AppleScript converts all parameters to strings and deals with that at execution time, so parameter type is irrelevant.
- AppleScript was saved in debug mode by Script Debugger. Re-open the script, take Script Debugger out of debug mode and save it back.
- File path specification error: HFS instead of POSIX or vice versa: Study up on how to specify the path of a file or folder. It matters because 2011 does it differently than 2016/2019, and Finder does it differently than everybody else. Sorry.
- Untrapped error in the script: Use the Try-Catch construct to allow you to trap errors and return problem status to the calling routine.
Recommended AppleScript Web Sites
Jerry Krinock
https://gist.github.com/jerrykrinock
This guy is amazing. He has developed an incredible number of things AppleScript can do for you.
Ron de Bruin
https://www.rondebruin.nl/index.htm
I’ve mentioned his site a couple times here. His tips for Mac are enlightening.
Alvin C – unforswearing
https://github.com/unforswearing/applescript
A substantial number of useful scripts, helpfully categorized. Also lists other AppleScript resources.