I have an AppleScript that needs to have a customized version to be run on hundreds of machines, each machine having a different version of the script. My thought was to separate the customized details into one file, and then have a "codebase script" (CodeBaseTest.scpt) file that would be included on them. That way, I could maintain one codebase file more easily.
Because I don't know the version of macOS running (and it may be as far back as 10.13), I need to keep things super simple. Furthermore, because they won't have script libraries installed, I can't use that mechanism for a codebase lib. That also rules out the third party tools, I believe, that make it easier to produce scripts with includes (whether a library, or something like Script Debugger).
So, I've gone ahead and split it to 2 scripts:
first.last.scpt << customized for each machine CodeBaseTest.scpt << is copied to the same folder as first.last.scpt so it's in the same folder
To make this work, and to make it easy to test new versions of code in the code base, I wanted to make the "on run" handler the same in both files -- but this isn't a must, just made it easier to do things.
Also to make it work, I had to remove all the properties and move to globals with declarations and then a set to make the defaults work.
The problem is that the scripts are not seeing variables that are clearly defined as globals.
There are two sets of these two files below -- a short set, and a set that has a ton of debug code in the two files here (which will make the errors very obvious if you run them).
Short set first
first.last-test.scpt here:
on run
global myPath, myName, scriptFileExtension, myLibFilename, myLib
set myPath to (path to me) as text
set myName to ((name of me) as text)
set scriptFileExtension to ".scpt"
set myLibFilename to "CodeBaseTest" & scriptFileExtension
if (myName & scriptFileExtension) is not myLibFilename then
set myLib to load script (text 1 through ((length of myPath) - (length of (myName & scriptFileExtension))) of myPath & myLibFilename) as alias
else if (myName & scriptFileExtension) is myLibFilename then
set myLib to me
else
set fullErrorMsg to "Critical error assigning myLib or loading script. Script terminating. (main on run handler)" & return
log fullErrorMsg
display dialog fullErrorMsg
end if
(* —-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
Assumptions that need to be defined early on… and are done so in initializeGlobals()
So that they can be used from either the user script (e.g., first.last.scpt) or test data
within code base script (i.e., OutlookCodeBase.scpt). First a check which file we're in.
—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
*)
initializeGlobals() of myLib
-- Account to copy to. If empty, defaults to first account in AcctsToAddList.
global theDestAcctParams, theDestAcctParamNames
set theDestAcctParams to {}
set theDestAcctParamNames to {}
(* —-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
Accounts to Add: Custom User Account Parameters -- defined for each user
—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
*)
global AcctsToAddList
set AcctsToAddList to {¬
{"Field1", "Field2", "Field3"}, ¬
{"A", "B", "C"}, ¬
{"D", "E", "F"}, ¬
{EndOfListMarker}}
-- Main Script, and supporting routines, are executed by a routine in the OutlookCodeBase.scpt file
if (myName & scriptFileExtension) is not myLibFilename then
mainScript() of myLib
else
mainScript()
end if
end run
`
and CodeBaseTest.scpt (with debug code) here:
on initializeGlobals()
-- Additional logging takes place with the debugFlag set to true
global debugFlag
set debugFlag to true as boolean
global BlankUserPassword, EndOfListMarker
set BlankUserPassword to "" -- should remain blank and not seen
set EndOfListMarker to "ENDOFLIST"
end initializeGlobals
on mainScript()
(* —-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
Log the time, so we know which run this is...
—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
*)
--if debugFlag then log "This run started at: " & (time string of (current date))
-- if theDestAcctParams is empty, copy first account from AcctsToAddList
if number of items in theDestAcctParams is 0 then
if number of items in AcctsToAddList is greater than 1 then
if debugFlag is true then log "theDestAcctParams is empty. copying 2nd item from AcctsToAddList"
set theDestAcctParams to item 2 of AcctsToAddList
else
log "!!! ERROR: not enough items in AcctsToAddList to copy"
end if
if debugFlag is true then log "theDestAcctParams is not empty."
end if
end mainScript
on run
global myPath, myName, scriptFileExtension, myLibFilename, myLib
set myPath to (path to me) as text
set myName to ((name of me) as text)
set scriptFileExtension to ".scpt"
set myLibFilename to "CodeBaseTest" & scriptFileExtension
if (myName & scriptFileExtension) is not myLibFilename then
set myLib to load script (text 1 through ((length of myPath) - (length of (myName & scriptFileExtension))) of myPath & myLibFilename) as alias
else if (myName & scriptFileExtension) is myLibFilename then
set myLib to me
else
set fullErrorMsg to "Critical error assigning myLib or loading script. Script terminating. (main on run handler)" & return
log fullErrorMsg
display dialog fullErrorMsg
end if
(* —-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
Assumptions that need to be defined early on… and are done so in initializeGlobals()
So that they can be used from either the user script (e.g., first.last.scpt) or test data
within code base script (i.e., OutlookCodeBase.scpt). First a check which file we're in.
—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
*)
initializeGlobals() of myLib
-- Account to copy to. If empty, defaults to first account in AcctsToAddList.
global theDestAcctParams, theDestAcctParamNames
set theDestAcctParams to {}
set theDestAcctParamNames to {}
(* —-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
Accounts to Add: Custom User Account Parameters -- defined for each user
—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
*)
global AcctsToAddList
set AcctsToAddList to {¬
{"Field1", "Field2", "Field3"}, ¬
{"A", "B", "C"}, ¬
{"D", "E", "F"}, ¬
{EndOfListMarker}}
-- Main Script, and supporting routines, are executed by a routine in the OutlookCodeBase.scpt file
if (myName & scriptFileExtension) is not myLibFilename then
mainScript() of myLib
else
mainScript()
end if
end run
Anyone have any idea why this isn't working? I'm sure (and hopeful) that I'm doing something stupid ... after all, I am coding during a migraine ... but I have to get this done today!
Thanks for reading (and even more if you can help)! Neil
================================================================ The below is just the same scripts, but the set with all the extra debug code: ================================================================
first.last-test.scpt here:
on run
global actionsHistory
set actionsHistory to ""
global myPath, myName, scriptFileExtension, myLibFilename, myLib
set myPath to (path to me) as text
set myName to ((name of me) as text)
set scriptFileExtension to ".scpt"
set myLibFilename to "CodeBaseTest" & scriptFileExtension
set actionsHistory to actionsHistory & return & return & ¬
"• " & ("hello world from " & myName)
display dialog ("hello world from " & myName)
if (myName & scriptFileExtension) is not myLibFilename then
set myLib to load script (text 1 through ((length of myPath) - (length of (myName & scriptFileExtension))) of myPath & myLibFilename) as alias
else if (myName & scriptFileExtension) is myLibFilename then
set myLib to me
else
set fullErrorMsg to "Critical error assigning myLib or loading script. Script terminating. (main on run handler)" & return
log fullErrorMsg
display dialog fullErrorMsg
set actionsHistory to actionsHistory & return & return & ¬
"• " & fullErrorMsg
end if
(* —-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
Assumptions that need to be defined early on… and are done so in initializeGlobals()
So that they can be used from either the user script (e.g., first.last.scpt) or test data
within code base script (i.e., OutlookCodeBase.scpt). First a check which file we're in.
—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
*)
try
initializeGlobals() of myLib
(*
if (myName & scriptFileExtension) is not myLibFilename then
initializeGlobals() of myLib
else
initializeGlobals()
end if
*)
on error errorStr number errorNum partial result resultList
set fullErrorMsg to ("Error is: " & errorStr & return & "Error number: " & errorNum & return & "Result List: " & resultList as text) ¬
& return & "Critical error initializing globals. Script terminating. (attempt to call initializeGlobals() in " & myName & ")"
log fullErrorMsg
display dialog fullErrorMsg
set actionsHistory to actionsHistory & return & return & ¬
"• " & fullErrorMsg
end try
-- Account to copy to. If empty, defaults to first account in AcctsToAddList.
global theDestAcctParams, theDestAcctParamNames
set theDestAcctParams to {}
set theDestAcctParamNames to {}
(* —-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
Accounts to Add: Custom User Account Parameters -- defined for each user
—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
*)
global AcctsToAddList
set AcctsToAddList to {¬
{"Field1", "Field2", "Field3"}, ¬
{"A", "B", "C"}, ¬
{"D", "E", "F"}, ¬
{EndOfListMarker}}
-- Main Script, and supporting routines, are executed by a routine in the CodeBaseTest.scpt file
if (myName & scriptFileExtension) is not myLibFilename then
mainScript() of myLib
else
mainScript()
end if
display dialog "Action history is: " & return & return & actionsHistory
end run
and CodeBaseTest.scpt (with debug code) here:
on initializeGlobals()
display dialog ("hello world from initializeGlobals() in CodeBaseTest.scpt")
try
-- Additional logging takes place with the debugFlag set to true
global debugFlag
set debugFlag to true as boolean
global BlankUserPassword, EndOfListMarker
set BlankUserPassword to "" -- should remain blank and not seen
set EndOfListMarker to "ENDOFLIST"
on error errorStr number errorNum partial result resultList from badObject to expectedType
set fullErrorMsg to (("Error is: " & errorStr & return * " Error number: " & errorNum & return ¬
& "in script: " & myName & return ¬
& "Result List: " & resultList as text) & return ¬
& "badObject: " & badObject as text) & return ¬
& "Coercion failure: " & expectedType
log fullErrorMsg
display dialog fullErrorMsg
set actionsHistory to actionsHistory & return & return & ¬
"• " & fullErrorMsg
end try
display dialog ("In routine " & "initializeGlobals" & " (end)" & " in " & myName & " with debugFlag = " & debugFlag)
end initializeGlobals
on mainScript()
display dialog ("hello world from mainScript() in CodeBaseTest.scpt")
(* —-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
Log the time, so we know which run this is...
—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
*)
try
display dialog ("In routine " & "mainScript" & " (start)" & " with debugFlag = " & debugFlag)
if debugFlag then log "This run started at: " & (time string of (current date))
on error errorStr number errorNum partial result resultList from badObject to expectedType
set fullErrorMsg to ("Error is: " & errorStr & return & " Error number: " & errorNum & return ¬
& "Result List: " & (resultList as text) & return ¬
& "badObject: " & (badObject as text) & return ¬
& "Coercion failure: " & expectedType)
log fullErrorMsg
display dialog fullErrorMsg
set actionsHistory to actionsHistory & return & return & ¬
"• " & fullErrorMsg
end try
-- if theDestAcctParams is empty, copy first account from AcctsToAddList
try
if number of items in theDestAcctParams is 0 then
if number of items in AcctsToAddList is greater than 1 then
if debugFlag is true then log "theDestAcctParams is empty. copying 2nd item from AcctsToAddList"
set theDestAcctParams to item 2 of AcctsToAddList
else
log "!!! ERROR: not enough items in AcctsToAddList to copy"
end if
if debugFlag is true then log "theDestAcctParams is not empty."
end if
on error errorStr number errorNum partial result resultList from badObject to expectedType
set fullErrorMsg to ("Error is: " & errorStr & return & " Error number: " & errorNum & return ¬
& "Result List: " & (resultList as text) & return ¬
& "badObject: " & (badObject as text) & return ¬
& "Coercion failure: " & expectedType)
log fullErrorMsg
display dialog fullErrorMsg
set actionsHistory to actionsHistory & return & return & ¬
"• " & fullErrorMsg
end try
end mainScript
on run
global actionsHistory
set actionsHistory to ""
global myPath, myName, scriptFileExtension, myLibFilename, myLib
set myPath to (path to me) as text
set myName to ((name of me) as text)
set scriptFileExtension to ".scpt"
set myLibFilename to "CodeBaseTest" & scriptFileExtension
set actionsHistory to actionsHistory & return & return & ¬
"• " & ("hello world from " & myName)
display dialog ("hello world from " & myName)
if (myName & scriptFileExtension) is not myLibFilename then
set myLib to load script (text 1 through ((length of myPath) - (length of (myName & scriptFileExtension))) of myPath & myLibFilename) as alias
else if (myName & scriptFileExtension) is myLibFilename then
set myLib to me
else
set fullErrorMsg to "Critical error assigning myLib or loading script. Script terminating. (main on run handler)" & return
log fullErrorMsg
display dialog fullErrorMsg
set actionsHistory to actionsHistory & return & return & ¬
"• " & fullErrorMsg
end if
(* —-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
Assumptions that need to be defined early on… and are done so in initializeGlobals()
So that they can be used from either the user script (e.g., first.last.scpt) or test data
within code base script (i.e., OutlookCodeBase.scpt). First a check which file we're in.
—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
*)
try
initializeGlobals() of myLib
(*
if (myName & scriptFileExtension) is not myLibFilename then
initializeGlobals() of myLib
else
initializeGlobals()
end if
*)
on error errorStr number errorNum partial result resultList
set fullErrorMsg to ("Error is: " & errorStr & return & "Error number: " & errorNum & return & "Result List: " & resultList as text) ¬
& return & "Critical error initializing globals. Script terminating. (attempt to call initializeGlobals() in " & myName & ")"
log fullErrorMsg
display dialog fullErrorMsg
set actionsHistory to actionsHistory & return & return & ¬
"• " & fullErrorMsg
end try
-- Account to copy to. If empty, defaults to first account in AcctsToAddList.
global theDestAcctParams, theDestAcctParamNames
set theDestAcctParams to {}
set theDestAcctParamNames to {}
(* —-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
Accounts to Add: Custom User Account Parameters -- defined for each user
—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-
*)
global AcctsToAddList
set AcctsToAddList to {¬
{"Field1", "Field2", "Field3"}, ¬
{"A", "B", "C"}, ¬
{"D", "E", "F"}, ¬
{EndOfListMarker}}
-- Main Script, and supporting routines, are executed by a routine in the CodeBaseTest.scpt file
if (myName & scriptFileExtension) is not myLibFilename then
mainScript() of myLib
else
mainScript()
end if
display dialog "Action history is: " & return & return & actionsHistory
end run
AppleScript’s global
is an absolute menace; one of the language’s (many) design flaws. Avoid. You will tie yourself in knots by using it. Instead, use property
for all non-local variables. Properties are strictly scoped to the script that declares them. If script B needs to access properties and handlers from script A, script B should import script A and assign it to a property: use ALib : script "ALib"
(or property ALib : load script (POSIX file "/path/to/ALib")
if you’re doing it old-style).
You can use AppleScript’s native library support. Here is the documentation. See #1 and #2. Save your customized scripts as either a Script Editor/Script Debugger .app
applet or as a .scptd
file, and put all its libraries inside that bundle. When the main app/script finds a use
statement, it will search for those embedded libraries first. You don’t have to install any libraries in /Library/Script Libraries
or ~/Library/Script Libraries
.
Where you do need to share some properties/handlers between several scripts, put them into their own library and have the other scripts import that. You want to create a nice tree-shaped dependency structure, e.g. shared lib A is imported by libs B and C, which are imported by scripts D, E, and F. You want to avoid a cat’s cradle of dependencies where every script imports every other script (e.g. A imports B and C; B imports A and C; C imports A and B) and each script manipulates the others ad-hoc. That will create an incomprehensible tangle that even you can’t understand, never mind quickly and easily debug. (Especially avoid circular dependencies: e.g. A imports B, B imports C, C imports A again. Those are a nightmare to do right, and are generally a sign that you’ve divided up your program badly.)
…
Anyway, that’s the techie bit summarizing How to structure your code. Read on if you want to understand the Why.
Programming is not about creating complexity. It’s about minimizing and managing that complexity successfully. Handlers and libraries are two of the tools you use for that. Use them well, and you will smash the problem. Use them ineffectively, complexity will smash you.
I’m guessing you’re a self-taught AppleScripter, not a college-trained Computer Science graduate (although plenty of those jokers can’t program for toffee). Either way, your scripts have now reached a level of ambition, size, and complexity where your old “bodge it till it works” AppleScript hacking skills no longer succeed like they used to. Spaghetti hacking skills are great for quickly making simple automations that do the job, but once you get into hundreds and thousands of lines of code those same skills will bury you alive.
You need to go learn yourself a little bit of Computer Science now, because CS ran into and figured out techniques for managing code complexity 50 years ago.
Here’s two words for you to start researching: “coupling” and “cohesion”.
Good code has low coupling and high cohesion; bad code has the opposite.
BTW, I wrote a very brief intro to software design in ch 29 of Learn AppleScript, 3rd edition, which you might or might not find helpful. (Ignore the section about making libraries in the Script Objects chapter though: that’s all obsolete since AS introduced native library support.)
Beyond that, I strongly recommend you buy a cheap copy of Steve McConnell’s Code Complete, 1st edition off eBay. That book covers just about every Do and Don’t of architecting software. However, don’t read CC cover-to-cover (you will never take it in). Instead, put it on your bookshelf until you start struggling with some aspect of your program’s design. When you do, take it down, read through the Table of Contents, and when you find a chapter topic that sounds like it might relate to your problem, go read just that chapter. You may need to hop back and forth a few times between your code and the book, but eventually what it’s saying will start to make sense.
…
One more thing:
The real payoff to learning yourself some CS is not simply that your scripts work reliably and be a relatively fast and pain-free pleasure to code. That is lovely to achieve, but it is not your killer USP.
What mastering CS stuff does is equip you to run circles around professional college-trained programmers who know lots about programming but possess none of your years of expert knowledge, insight, and experience inside your specialist profession (e.g. print industry). Being skilled in both is a force multiplier.
A programmer who understands their problem domain at a deep, expert level—who can talk to users in those users’ own language, and treat them as full peers and collaborators in building their solutions—is like discovering a unicorn amongst a herd of donkeys. (Not all customers will appreciate this but for those who do you can blow their socks off.) Takes a few years to get up to this level, but as you do remember to increase your rates accordingly!