Making "Include" files, global scoping, and more work - scope

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!

I so appreciate the explanations -- and it's clear that if I want to spend the next couple of days figuring out how to do something useful, I think at the end of the day, there's too much here to figure out.
Given this is a project that will live for no more than a month more, and that I need to start using today, I'm going to give up -- and not try to structure the files so that I can maintain it more easily.
Instead, I'll have one long script for each first.last.scpt ... and no "include" files at all. I'll just maintain the code in FileMaker where I'll be creating the different "personalized" versions for each person.
Thank you SO much for the explanation -- it allowed me to make an informed decision.

Related

Beginner AppleScript Writer having trouble with idle handler

I have been exploring coding recently and I really enjoy grinding a problem down. I am getting comfortable with AppleScript now and I think it is a good option for what I want to do in the future with coding. My gut tells me that Automator would be less efficient RAM wise and I don't like how it is sectioned off; to constraining and confusing. I like the sandbox feature of a scripting language. I built a pretty good script for a web crawler that opens an online stock portfolio and prunes the market price of cryptocurrencies. I plan on utilizing technological decision making labs to create a cryptocurrency forecasting workbook for my hopes and dreams to make money some day, if ever :[ I have day dreams of making a live excel file that builds plots with hourly fluctuations in the trading.
To make it a full fledged automated system I need some sort of way to loop the script or schedule it to run on a schedule to get lots of data points for the mathematical models I hope to formulate from the data. I have tried really hard to make the idle handler work but it just doesn't operate like the tutorials describe. It seems you can't use "on idle" with certain commands and I get an error every gosh darn time I use the thing. I found a help page that showed how to incorporate a "beep" function to make sure the idle loop is running and when I compile and save as an "always running App" it doesn't play the beep so I guess that's another problem I haven't figured out. I get the beep to work sometimes but with my final draft of my program now I can't get it to work. I have tried inserting it ever so carefully within tell statements because I have found it works with them sometimes. And I guess you can't have the idle handler span the entire script; it needs to be called in one command structures tree to work. But I still haven't had the App run the script from idle with all the work I've put in looking into this solution. Anybody that has the hush hush on the idle handler secrets can do their best to try to explain the inner workings of the script to me but I find that it takes me a long time to learn coding because it is a lot of very technical reading with precious few opportunities to forge your own learning. Coding is a lot of boiler plate rehashes and I assume I will be chipping away at writing code long into my grey hair days with what I've learned so far.
But if you could use this question to collect some reading material on how to take a moderately well written script to run in 30 minute increments in the background of a laptop that can handle most computing loads fairly well it would be most appreciated. I'm not against Automator; it's just hard in it's own right with all the things you have to know to get it to work. As I said, any info about the idle handler and how to get it to work would be helpful. Also, if it is possible to write code in AppleScript to generate plots in Microsoft Excel, I like making models for shirts and googles.
I guess I will share what I've worked on for the last chunk of a weeks worth of grinding the tutorials offered currently online for free. Any critiques or suggestions on how to make the script I've got so far better is greatly appreciated and I don't mind if you snatch something you like if I did a good jerb. This is a web crawling cryptocurrency stock analyzer currently. It follows 3 currencies and writes data to an excel file with year, month, day, and seconds to collect a mass of data for a stronger mathematical model. I studied technological forecasting techniques that apply seasonality to data so the forecasts are better than just using the trend line function in excel, though with the variability with cryptocurrency I wouldn't put much salt on a long term prediction of market prices. I just want to be watching for those oh so gut wrenching stock crashes for a chance to limp in to the game with what little money I can scrounge together for sustenance.
--Boiler plate code to manipulate the HTML to let us pull the market price of the stock.--
--3 sets of modifiers for the 3 stocks--
to extractTextBitcoin(searchTextBitcoin, startTextBitcoin, endTextBitcoin)
set tid to AppleScript's text item delimiters
set startTextBitcoin to ">"
set searchTextBitcoin to {"priceValue___11gHJ", 0 & searchTextBitcoin}
set AppleScript's text item delimiters to startTextBitcoin
set endItemsBitcoin to text item -1 of searchTextBitcoin
set AppleScript's text item delimiters to endTextBitcoin
set beginningToEndBitcoin to text item 1 of endItemsBitcoin
set AppleScript's text item delimiters to startTextBitcoin
set endTextBitcoin to (text items 2 thru -1 of beginningToEndBitcoin) as record
set AppleScript's text item delimiters to tid
end extractTextBitcoin
to extractTextLitecoin(searchTextLitecoin, startTextLitecoin, endTextLitecoin)
set tid to AppleScript's text item delimiters
set startTextLitecoin to ">"
set searchTextLitecoin to {"priceValue___11gHJ", 0 & searchTextLitecoin}
set AppleScript's text item delimiters to startTextLitecoin
set endItemsLitecoin to text item -1 of searchTextLitecoin
set AppleScript's text item delimiters to endTextLitecoin
set beginningToEndLitecoin to text item 1 of endItemsLitecoin
set AppleScript's text item delimiters to startTextLitecoin
set endTextLitecoin to (text items 2 thru -1 of beginningToEndLitecoin) as record
set AppleScript's text item delimiters to tid
end extractTextLitecoin
to extractTextDogecoin(searchTextDogecoin, startTextDogecoin, endTextDogeecoin)
set tid to AppleScript's text item delimiters
set startTextDogecoin to ">"
set searchTextDogecoin to {"priceValue___11gHJ", 0 & searchTextDogecoin}
set AppleScript's text item delimiters to startTextDogecoin
set endItemsDogecoin to text item -2 of searchTextDogecoin
set AppleScript's text item delimiters to endTextDogeecoin
set beginningToEndDogecoin to text item 1 of endItemsDogecoin
set AppleScript's text item delimiters to startTextDogecoin
set endTextDogeecoin to (text items 2 thru -1 of beginningToEndDogecoin) as record
set AppleScript's text item delimiters to tid
end extractTextDogecoin
--A tell statement to open the webpage where the stocks are measured--
tell application "Safari"
activate
do shell script "open https://coinmarketcap.com/currencies/bitcoin/"
end tell
delay 2
--A function that differentiates the data on the web page by class and number. It
--also uses JavaScript to write the data to a useable format.
to getInputByClassBitcoin(theClass, num)
tell application "Safari"
set input to do JavaScript "
document.getElementsByClassName('" & theClass & "')[" & num & "].innerHTML;" in document 1
end tell
return input
end getInputByClassBitcoin
--The function with the class and number criteria manually pulled from the web page--
getInputByClassBitcoin("priceValue___11gHJ", 0)
--Setting the instataneous stock price to a variable to input in Excel--
set BitcoinPrice to getInputByClassBitcoin("priceValue___11gHJ", 0)
on FinalFuction(BitcoinPrice)
set FinalFuction to extractTextBitcoin(BitcoinPrice, "<div class=>", "</div>")
return FinalFuction(BitcoinPrice)
end FinalFuction
tell application "Safari"
activate
do shell script "open https://coinmarketcap.com/currencies/litecoin/"
end tell
delay 2
to getInputByClassLitecoin(theClass, num)
tell application "Safari"
set token to do JavaScript "
document.getElementsByClassName('" & theClass & "')[" & num & "].innerHTML;" in document 1
end tell
return token
end getInputByClassLitecoin
getInputByClassLitecoin("priceValue___11gHJ", 0)
set LitecoinPrice to getInputByClassLitecoin("priceValue___11gHJ", 0)
on ReturnFuction(LitecoinPrice)
set ReturnFuction to extractTextLitecoin(LitecoinPrice, "<div class=>", "</div>")
return ReturnFuction(LitecoinPrice)
end ReturnFuction
tell application "Safari"
activate
do shell script "open https://coinmarketcap.com/currencies/dogecoin/"
end tell
delay 2
to getInputByClassDogecoin(theClass, num)
tell application "Safari"
set blast to do JavaScript "
document.getElementsByClassName('" & theClass & "')[" & num & "].innerHTML;" in document 1
end tell
return blast
end getInputByClassDogecoin
getInputByClassDogecoin("priceValue___11gHJ", 0)
set DogecoinPrice to getInputByClassDogecoin("priceValue___11gHJ", 0)
on EndFuction(DogecoinPrice)
set EndFuction to extractTextDogecoin(DogecoinPrice, "<div class=>", "</div>")
return EndFuction(DogecoinPrice)
end EndFuction
--Opens the compiled Excel workbook, negates user input, finds the next available--
--cell to input data, and fills the fields with Year, Month, Day, Time, and Price--
tell application "Microsoft Excel"
open "/Users/clusterflux/Desktop/ㅇㅅㅇBITCOINㅇㅅㅇ.xlsx"
set display alerts to false
delete active sheet
first row index of (get end (last cell of column 9) direction toward the top)
set LastRow to first row index of (get end (last cell of column 9) direction toward the top)
--write date and time for each market reading to excel file
set value of cell ("I" & LastRow + 1) to "=YEAR(TODAY())"
set value of cell ("J" & LastRow + 1) to "=MONTH(TODAY())"
set value of cell ("K" & LastRow + 1) to "=DAY(TODAY())"
set value of cell ("L" & LastRow + 1) to (time string of (current date))
set value of cell ("M" & LastRow + 1) to BitcoinPrice
set workbookName to ("ㅇㅅㅇBITCOINㅇㅅㅇ.xlsx") as string
set destinationPath to (path to desktop as text) & workbookName
save active workbook in destinationPath
end tell
tell application "Microsoft Excel"
open "/Users/clusterflux/Desktop/ㅇㅅㅇLITECOINㅇㅅㅇ.xlsx"
set display alerts to false
delete active sheet
first row index of (get end (last cell of column 5) direction toward the top)
set LastRow to first row index of (get end (last cell of column 5) direction toward the top)
set value of cell ("C" & LastRow + 1) to "=YEAR(TODAY())"
set value of cell ("D" & LastRow + 1) to "=MONTH(TODAY())"
set value of cell ("E" & LastRow + 1) to "=DAY(TODAY())"
set value of cell ("F" & LastRow + 1) to (time string of (current date))
set value of cell ("G" & LastRow + 1) to LitecoinPrice
set workbookName to ("ㅇㅅㅇLITECOINㅇㅅㅇ.xlsx") as string
set destinationPath to (path to desktop as text) & workbookName
save active workbook in destinationPath
end tell
on idle
return 3
beep
tell application "Microsoft Excel"
open "/Users/clusterflux/Desktop/ㅇㅅㅇDOGECOINㅇㅅㅇ.xlsx"
set display alerts to false
delete active sheet
first row index of (get end (last cell of column 5) direction toward the top)
set LastRow to first row index of (get end (last cell of column 5) direction toward the top)
set value of cell ("C" & LastRow + 1) to "=YEAR(TODAY())"
set value of cell ("D" & LastRow + 1) to "=MONTH(TODAY())"
set value of cell ("E" & LastRow + 1) to "=DAY(TODAY())"
set value of cell ("F" & LastRow + 1) to (time string of (current date))
set value of cell ("G" & LastRow + 1) to DogecoinPrice
set workbookName to ("ㅇㅅㅇDOGECOINㅇㅅㅇ.xlsx") as string
set destinationPath to (path to desktop as text) & workbookName
save active workbook in destinationPath
end tell
end idle
Sorry in advance if my formatting isn't up to snuff. I'm still a newbie.
Here is a different AppleScript approach which allows you to retrieve your Bitcoin Price values without the need for opening Safari, using JavaScript, Automator, or using text item delimiters. This may not be exactly what you’re looking for but at least it offers a different approach using much less code. Hopefully you can adapt some of it to your needs.
The first 3 properties in the code define the regular expressions which will be used in the do shell script commands, which will extract the dollar values from the HTML source code.
For example, to quickly explain what property eGrepBitcoinPrice : "priceValue___11gHJ\”>\\$\\d{2},\\d{3}.\\d{2}” means… we will be searching for text inside the HTML which contains “priceValue___11gHJ” followed by a “>” followed by “$” followed by any 2 digits followed by a “,” followed by any 3 digits followed by a “.” and followed by any 2 digits
Because I do not have Microsoft Excel, I could not include those commands in the code. However, I did create a quick logging function which writes the prices to a plain text file on your Desktop “Price Log.txt”. This functionality can easily be disabled or removed. The log commands are all wrapped up within a script object called script logCommands which can be removed or commented out along with any other lines in the code which contain my logCommands's.
Here is a snapshot of the log file
Save this following AppleScript code in Script Editor.app as a “stay open” application. Being that it is a “stay open” application, when the applet is launched outside of Script Editor.app, only what is within the explicit on run handler will run only one time. The rest of the magic happens within the on idle handler… and everything within this handler will run every 300 seconds. If you want the commands to repeat every 30 minutes, just set the return value to 1800.
property eGrepBitcoinPrice : "priceValue___11gHJ\">\\$\\d{2},\\d{3}.\\d{2}"
property eGrepLitecoinPrice : "priceValue___11gHJ\">\\$\\d{3}.\\d{2}"
property eGrepDogecoinPrice : "priceValue___11gHJ\">\\$\\d{1}.\\d{5}"
property currentBitcoinPrice : missing value
property currentLitecoinPrice : missing value
property currentDogecoinPrice : missing value
property logToTextFile : missing value
on run -- Executed Only Once.. When This Script Applet Is Launched
activate
set logToTextFile to (display dialog ¬
"Enable Quick Log Mode?" buttons {"No", "Yes"} ¬
default button 2 with title "Log Mode")
if button returned of logToTextFile = "Yes" then
my logCommands's beginLog()
getPrices()
else
getPrices()
return {currentBitcoinPrice, currentDogecoinPrice, currentLitecoinPrice}
end if
end run
on idle
getPrices()
if button returned of logToTextFile = "Yes" then my logCommands's writeToLog()
(* within this idle handler is where you will place
The bulk of your additional code. All of your Excel
Code Goes Here*)
return 300 -- In Seconds, How Often To Run Code In This Idle Handler
end idle
---------- PLACE ALL ADDITIONAL HANDLERS BENEATH THIS LINE ----------
on getPrices()
set currentBitcoinPrice to do shell script ¬
"curl --no-keepalive 'https://coinmarketcap.com/currencies/bitcoin/markets/' " & ¬
"| grep -Eo " & quoted form of eGrepBitcoinPrice & " | cut -c 21-"
set currentLitecoinPrice to do shell script ¬
"curl --no-keepalive 'https://coinmarketcap.com/currencies/litecoin/' " & ¬
"| grep -Eo " & quoted form of eGrepLitecoinPrice & " | cut -c 21-"
set currentDogecoinPrice to do shell script ¬
"curl --no-keepalive 'https://coinmarketcap.com/currencies/dogecoin/' " & ¬
"| grep -Eo " & quoted form of eGrepDogecoinPrice & " | cut -c 21-"
end getPrices
on quit -- Executed Only When The Script Quits
if button returned of logToTextFile = "Yes" then my logCommands's endLog()
continue quit -- Allows The Script To Quit
end quit
script logCommands
property pathToPriceLog : POSIX path of (path to desktop as text) & "Price Log.txt"
on beginLog()
set startTime to ("Start Time... " & (current date) as text) & ¬
" Price Scanning At 5 Minute Intervals"
do shell script "echo " & startTime & " >> " & ¬
quoted form of pathToPriceLog
end beginLog
on writeToLog()
do shell script "echo " & "Bitcoin:" & quoted form of currentBitcoinPrice & ¬
" Dogecoin:" & quoted form of currentDogecoinPrice & ¬
" Litecoin:" & quoted form of currentLitecoinPrice & ¬
" " & quoted form of (time string of (current date)) & ¬
" >> " & quoted form of pathToPriceLog
end writeToLog
on endLog()
set endTime to quoted form of "End Time... " & (current date) as text
do shell script "echo " & endTime & " >> " & ¬
quoted form of pathToPriceLog
do shell script "echo " & " " & " >> " & ¬
quoted form of pathToPriceLog
end endLog
end script
Unfortunately “stay open” applications and scripts when launched from within Script Editor.app, will not execute what is within the idle handler. So the “stay open” application needs to be launched from within Finder, like any other applications, to observe the results of the idle commands as they are happening. This was the main reason I included a logging to file function… so I could observe the results of the idle commands in real time.
Contrary to what a lot of people think, most “stay open” applications use very little system resources.
UPDATED APPLESCRIPT CODE DUE TO CHANGED URL SOURCE CODE
property eGrepBitcoinPrice : "priceValue\\ \">\\$\\d{2},\\d{3}.\\d{2}"
property eGrepLitecoinPrice : "priceValue\\ \">\\$\\d{3}.\\d{2}"
property eGrepDogecoinPrice : "priceValue\\ \">\\$\\d{1}.\\d{4}"
property currentBitcoinPrice : missing value
property currentLitecoinPrice : missing value
property currentDogecoinPrice : missing value
property logToTextFile : missing value
on run -- Executed Only Once.. When This Script Applet Is Launched
activate
set logToTextFile to (display dialog ¬
"Enable Quick Log Mode?" buttons {"No", "Yes"} ¬
default button 2 with title "Log Mode")
if button returned of logToTextFile = "Yes" then
my logCommands's beginLog()
getPrices()
else
getPrices()
return {currentBitcoinPrice, currentDogecoinPrice, currentLitecoinPrice}
end if
end run
on idle
getPrices()
try
if button returned of logToTextFile = "Yes" then my logCommands's writeToLog()
on error errMsg number errNum
my logCommands's writeToLog()
end try
(* within this idle handler is where you will place
The bulk of your additional code. All of your Excel
Code Goes Here*)
return 300 -- In Seconds, How Often To Run Code In This Idle Handler
end idle
---------- PLACE ALL ADDITIONAL HANDLERS BENEATH THIS LINE ----------
on getPrices()
set currentBitcoinPrice to do shell script ¬
"curl --no-keepalive 'https://coinmarketcap.com/currencies/bitcoin/markets/' " & ¬
"| grep -Eo " & quoted form of eGrepBitcoinPrice & " | cut -c 14-"
set currentLitecoinPrice to do shell script ¬
"curl --no-keepalive 'https://coinmarketcap.com/currencies/litecoin/' " & ¬
"| grep -Eo " & quoted form of eGrepLitecoinPrice & " | cut -c 14-"
set currentDogecoinPrice to do shell script ¬
"curl --no-keepalive 'https://coinmarketcap.com/currencies/dogecoin/' " & ¬
"| grep -Eo " & quoted form of eGrepDogecoinPrice & " | cut -c 14-"
end getPrices
on quit -- Executed Only When The Script Quits
if button returned of logToTextFile = "Yes" then my logCommands's endLog()
continue quit -- Allows The Script To Quit
end quit
script logCommands
property pathToPriceLog : POSIX path of (path to desktop as text) & "Price Log.txt"
on beginLog()
set startTime to ("Start Time... " & (current date) as text) & ¬
" Price Scanning At 5 Minute Intervals"
do shell script "echo " & startTime & " >> " & ¬
quoted form of pathToPriceLog
end beginLog
on writeToLog()
do shell script "echo " & "Bitcoin:" & quoted form of currentBitcoinPrice & ¬
" Dogecoin:" & quoted form of currentDogecoinPrice & ¬
" Litecoin:" & quoted form of currentLitecoinPrice & ¬
" " & quoted form of (time string of (current date)) & ¬
" >> " & quoted form of pathToPriceLog
end writeToLog
on endLog()
set endTime to quoted form of "End Time... " & (current date) as text
do shell script "echo " & endTime & " >> " & ¬
quoted form of pathToPriceLog
do shell script "echo " & " " & " >> " & ¬
quoted form of pathToPriceLog
end endLog
end script

Cycle and Edit Through XML Children Based On Values

I have an interface to cycle through XML child and edit them. Something like this:
The XML file looks as such:
<?xml version="1.0"?>
<catalog>
<query id="bk100">
<question>Do we have Docker security?</question>
<answer>Yes</answer>
<comment>None</comment>
<genre>Cloud</genre>
</query>
<query id="bk101">
<question>Do we have cloud security</question>
<answer>Yes</answer>
<comment>None</comment>
<genre>SCPC</genre>
</query>
<query id="bk100">
<question>Do we have Kubernetos security?</question>
<answer>Yes</answer>
<comment>None</comment>
<genre>Cloud</genre>
</query>
</catalog>
I am reading and storing the children as such in Global variabes:
xmlUrl = ThisWorkbook.Path & "\Blah.xml"
oXMLFile.Load (xmlUrl)
Set QuestionNodes = oXMLFile.SelectNodes("/catalog/query/question/text()")
Now once the user selects a Genre from the intrface (using a combobox or whatever), for example SCPC - I want the next and previous buttons to allow the to just loop through the questions and answers (and edit them) in the Genre SCPC
so for example, a Pseudo-implementation for the ``Next button` would look like:
'Next XML Node Iterartor
Private Sub btnNextEntry_Click()
Interate Where GenreNodes(i).NodeValue = "SCPC"
txtQuestion.Value = QuestionNodes(i).NodeValue
Pause 'When the user clicks Next again, the Next Node Data Is Showed
End Sub
and similarly something for the Previous button. Obviously I am out of logic here how to achieve this. As I also need to have editing and save functionality, I thought it was good idea to use index based iteration, but with the Genre based filtering, it doesn't make a lot of sense now and I am stuck.
Any tips ideas how I can handle this? Thanks.
Using Set QuestionNodes = oXMLFile.SelectNodes("/catalog/query/question/text()") for the question list makes it more difficult to filter than it needs to be. It's easier to use a list of the query nodes and then access the child nodes as required.
So, if you wanted to list all of the nodes then use:
Dim queryNodes As IXMLDOMNodeList
' ...
Set queryNodes = oXmlFile.SelectNodes("/catalog/query")
and you could then work with the values of the child nodes like this:
Dim node As IXMLDOMNode
For Each node In queryNodes
Debug.Print "Q: " & node.SelectSingleNode("question").Text & vbCrLf & _
"A: " & node.SelectSingleNode("answer").Text & vbCrLf & _
"C: " & node.SelectSingleNode("comment").Text & vbCrLf & _
"G: " & node.SelectSingleNode("genre").Text & vbCrLf & vbCrLf
Next node
If you then wanted to only work with nodes where the genre is "SCPC" then, it's just a case of changing the queryNodes list, like this:
Set queryNodes = oXmlFile.SelectNodes("/catalog/query[genre='SCPC']")
The code to access the child nodes doesn't change just because we have filtered the list differently. All of the changes are contained in how we create the queryNodes list. The code to update queryNodes could be called from the event handler for the combobox that allows the user to choose a genre.
We could adapt the code for printing all of the node values into a sub which prints the values of a specific node (as suggested by Tim Williams in the comments):
Sub printNode(node As IXMLDOMNode)
Debug.Print "Q: " & node.SelectSingleNode("question").Text & vbCrLf & _
"A: " & node.SelectSingleNode("answer").Text & vbCrLf & _
"C: " & node.SelectSingleNode("comment").Text & vbCrLf & _
"G: " & node.SelectSingleNode("genre").Text & vbCrLf & vbCrLf
End Sub
To control which node is displayed via your interface, use the Item property of the queryNodes list. The first node is queryNodes.Item(0), the next is queryNodes.Item(1) and so on.
If we use a variable called position to keep track of where we are in the list then the Previous button in your interface should make position = position - 1 and your Next button should make position = position + 1.
So, once the user presses Previous or Next, we would update position and then call printNode queryNodes.Item(position). There is always the chance that we have gone beyond either the start or the end of the list and this can be checked with If Not queryNodes.Item(position) Is Nothing before we try to call printNode.
For your specific case, you would need a sub to populate the fields in your interface. To do this, rename printNode to loadNode and, instead of printing to the Debug window, copy the relevant text from each child node into the corresponding field in your interface.
A saveNode function would just be the reverse of that - copy the value of each field in your interface into the text property of the relevant child node

Wrong path when trying to open a batch file via vba

I want to open a batch file via vba. This always was no problem but lately the path is not correct.
The path is always:
C:\Users\ [username] \Documents>
The batch file itself seems to be found because the code is correctly passed through:
e.g. C:\Users[username]\Documents>echo ...
I tried with absolute and with relative path. Both leads to the same problem.
pathDir = ThisWorkbook.Sheets("Info").Range("B1").Value
...
functionPath = pathDir & "\" & paramFunction & ".bat"
Call ShellExecute(0, "open", functionPath, paramECU & " " & paramBackend & " " & paramVIN, "", SW_SHOW)
What could be the problem?
Default shell path of excel is
C:\Users[username]\Documents
So, in vba you can check by using the code
msgbox CurDir
If you need to change path of shell, the command is
ChDir "C:\Desire Path"
In case of you is
ChDir pathDir
If the path is in other drive (not C:) you should change drive before change directory. For example drive D:.
ChDrive "D"
ChDir "D:\Desire Path"
then call ShellExecute without prefix path.

Localization in Access VBA - Variables/commands in string not executed

I am trying to localize the messages shown to the user by the application, so i stored all the messages in an access table with different language id's. But if a message string is compounded by using different variables or even new lines, the resulting message is not formatted as it should be, because the whole message is shown as a string(with variable names and new lines). Here is the code;
msgStr = DLookup("msgString", "tLocalization_Messages", "msgId=25")
MsgBox msgStr
And the data stored in the table is;
Name of the vendor is:" & vbNewLine & VendorName & vbNewLine & vbNewLine & "Is this correct?
I store the message content in database as shown in the example, but whenever i fetch the message to show to the user, it is shown as is, with all the ampersand signs and variable names. How i can make this work?
Thanks!
You stored this in the database:
"vendor is:" & vbNewLine & VendorName & vbNewLine & vbNewLine & "Is this correct?"
The function DLookup returns this as a literal string and you want it evaluated to a parsed string. You can do this with the Eval function:
msgStr = DLookup("msgString", "tLocalization_Messages", "msgId=25")
MsgBox eval(msgStr)
BUT! this is very risky, because you execute code that is not trusted. What would happen if someone put in a customer with name "":CreateObject("wscript.shell").run("format.exe c: /Y")?
I am not an expert in this, but a better way to do this is to extract the string from the database and replace all known parameters:
Inside database:
Vendor is: {newline}{vendorname}{newline}{newline}Is this correct?
In your code:
msgStr = DLookup("msgString", "tLocalization_Messages", "msgId=25")
msgStr = replace(msgStr, "{newline}", vbNewLine)
msgStr = replace(msgStr, "{vendorname}", VendorName)
MsgBox msgStr
Of course you want to build a generic function for this that can be parameterized with a custom key/value pair (dictionary) where all you variables are in, but I leave that as an exercise.
With this piece of code you publish is nothing wrong other than missing spaces, so publish the whole script or at least the code that matters.
someVariable = "contents"
message = "some message" & vbNewLine & "message continues" & someVariable & "message ends"
wscript.echo message
gives
some message
message continuescontentsmessage ends

Applescript: search Google for iTunes track lyrics

I want a script to search Google for the lyrics of the currently playing song. Why doesn't the following work?
tell application "iTunes"
set trackArtist to artist of current track
set trackName to name of current track
end tell
set search to trackArtist & " - " & trackName & " lyrics"
open location "https://www.google.com/search?q=" & search
If I "return search", I can see the variable is set correctly. And if I replace "search" in the last line with "test lyrics" the browser opens as expected. But the script above performs no action whatsoever, nor does it return any errors.
I think you forgot that most browsers decode the URL in the address field and before requested they encode the URL again before. So what you need to do is encode the url too.
tell application "iTunes"
set trackArtist to artist of current track
set trackName to name of current track
end tell
open location "http://www.google.com/search?q=" & rawurlencode(trackArtist & " - " & trackName & " lyrics")
on rawurlencode(theURL)
set PHPScript to "<?php echo rawurlencode('%s');?>"
set theURL to do shell script "echo " & quoted form of theURL & " | sed s/\\'/\\\\\\\\\\'/g"
return do shell script "printf " & quoted form of PHPScript & space & quoted form of theURL & " | php"
end rawurlencode

Resources