Get Automator app result in external Applescript? - applescript

Is there a way to retrieve result of an Automator app script in an external Applescript app (not the Applescript lines in Automator)?
Something like:
tell application "My_Automator_App"
-- suppose My_Automator_App checks the Calendar to see if there some events today
-- "Show Result" in Automator will display a list
get the_Result -- list returned by Automator
end tell

I looked into this a little bit and didn't find a natural means by which AppleScript and Automator applets can communicate, although this doesn't mean one definitely doesn't exist.
In the meantime, you could implement one of a couple of workarounds/hacks that, although a little unseemly in their methods, do achieve the desired result without creating any side issues that would affect the functionality of an applet itself.
1. Use The Clipboard
Append a Copy to Clipboard action at the end of the applet's workflow, or following the action whose result you would wish to be reported.
Retrieving the clipboard from AppleScript is simple:
get the clipboard
This will probably suit return values that are simple text strings or a number. Passing an array of items from an Automator action to the clipboard isn't very reliable, sometimes only allowing access to the first item. However, this can be resolved with a small AppleScript within the workflow to process results arrays properly and convert them into an accessible format, e.g. a comma-delimited string.
However, the clipboard is also capable of storing image data, file references, and other data types, so it will be possible (if not always straightforward) to send those to be retrieved in an AppleScript.
Where possible, strings and numbers are the safest storage types.
2. Write Out To A Temporary File
To avoid using the clipboard as an intermediary, or if you wish the applet to report multiple variables without too much work, then writing the data to a temporary file is a fairly common practice, such as is done in shell scripts when persistant values are needed between multiple executions of the same script.
There's actually a special directory that gets periodically purged so that temporary data files don't accumulate: /tmp. It's hidden in Finder, but you can still create files and delete them as you would any other directory. Files that aren't access for 3 days get purged by the system.
There is a New Text File action that can write text to a file:
Specifying the /tmp directory is most easily done by creating a variable whose value is "/tmp" (without the quotes), and dragging that variable onto the appropriate field.
But my inclination would be to insert an AppleScript, or more suitably, a shell script into the workflow, with which file manipulation becomes easy and more capable.
Calendar Events Example
Using a similar example to the scenario you described, a simple applet that retrieves calendar events might have a workflow that looks like this:
where you can calibrate the first action to isolate the events you want, such as today's events. That action returns a type of object that isn't easily processed by AppleScript, but the second action extracts the relevant data in text format, summarising the list of events that the first action returned.
This is where a temporary file is useful to write out the data to a text file, which can then be retrieved in an AppleScript.
Given this Automator applet saved under the named "CalEvents", this AppleScript makes use of that applet and its result:
property tidEvents : [linefeed, linefeed, "EVENT", space] as text
property tidDetails : {tab, " to "}
property tid : a reference to my text item delimiters
run application id "com.apple.automator.CalEvents"
set tid's contents to tidEvents
set EventsSummary to read POSIX file "/tmp/EventsSummary.txt"
set EventsList to the EventsSummary's text items
set [[n], EventsList] to [it, rest] of EventsList
set n to n's last word as number
EventsList -- The final list of events from first to last
Upon its first run, the applet requires consent to access your calendar information, which only needs to be done once and will make the above script appear to fail. Once authorised, you can run the script as often as you like to get the most up-to-date content of the /tmp/EventsSummary.txt file.
Each item in the list variable EventsList is a block of text that looks like this (asterisks are my redactions for privacy, as are the address items in curly braces):
4 OF 8
Summary: GP Appointment
Status: none
Date: 07/12/2017 to 07/12/2017
Time: 14:45:00 to 15:45:00
Location: ******** Medical Centre
{Address Line 1}
{Address Line 2}
{County}
{Post Code}
United Kingdom
Notes: 01*** *****9
Each value is separated from the preceding colon by a tab character, which won't be obvious here. Also, as you can tell from the date format and address, these are British-formatted values, but yours will, of course, be whatever they are set as in Calendar.
But since each list item is much the same, extracting details for a particular event will be simple in AppleScript, first by splitting a particular event item into paragraphs, and then splitting a particular paragraph by either a tab or space character (or both) or some preposition that naturally delimits useful bits of text:
set |Event| to some item in the EventsList
set tid's contents to tidDetails
set EventDetails to {title:text item 2 of paragraph 2 ¬
, startTime:text item 2 of paragraph 5 ¬
, EndTime:text item 3 of paragraph 5} of the |Event|
which places the important event details, such as its name and start/end times, in an AppleScript record:
{title:"GP Appointment", startTime:"15:45:00", EndTime:"16:00:00"}

Related

Can Mac's Automator scan a folders contents & lists the files that are over a certain length?

I work in media production at a university, we work on Mac systems, but our servers are windows based.
Illegal characters & long file names are causing us problems when transferring our production files to the server.
To prevent file transfers failing & being sent to a holding pen in our DAM system i'm looking to create a simple Automator App that can be used by the production team to do the following;
Accept source folder as input for the app.
Scan contents & replace the following characters ()\/[]"*?<>|+ with an underscore.
Scan contents & for file names longer than 100 characters
Log / report on the affected files for our producers to amend.
Within Automator I have had success with replacing the illegal characters using a find & replace rule for each, but I'm not sure of the apple script that would be required to check the file name lengths & reporting all changes.
I'll be eternally grateful if anyone would be able to suggest a route forwards!
Obviously, I have no clue what you might be passing along, nor how you might be replacing the text in filenames, nor exactly what you would like to report, as you don't really provide any of those details. However, here is a straightforward way to test for filenames longer than a given length within automator.
To provide the initial file list to test, I begin with three actions:
Get Selected Finder Items
Get Folder Contents ('repeat for each subfolder found' is checked)
Filter Finder Items ('kind is document')
This will pass along a list of alias file objects to the fourth 'run applescript' action, as input.
on run {input, parameters}
set fList to input
set nList to {} -- becomes list of filenames
set cList to {} -- becomes list of filename lengths
tell application "Finder"
repeat with ff in fList -- list of file aliases
set nn to name of ff
set end of nList to nn
set end of cList to length of nn
end repeat
end tell
set longList to {}
repeat with cc from 1 to length of cList
if item cc of cList is greater than 100 then
set end of longList to item cc of nList -- names with more than 100 characters
end if
end repeat
return longList
end run
This should be run when a folder is selected in the Finder.
Essentially, what this does is take the input (i.e. list of file aliases) and create corresponding lists of filenames and filename lengths. The script then loops through the list of lengths looking for items > 100 and and returns a list of matching filenames. If instead, you set end of longList… to items from fList then it will return a list of file aliases.
If this isn't what you're looking for, please advise. The above works under Sierra.

AppleScript error: "Mail got an error: Can’t set text item delimiters to {"+", "#"}."

I have written an AppleScript that is activated by a mail rule whenever an email comes in that contains "+".
Why? I host my own mail server that allows for address tagging. What this means is that for example when I'm at a store and they ask for my email address, so they can email the receipt, I can give them my email address like this: whatmyemailnormallyis+nameofstore#domain.com. The applescript should then get the string between the "+" and "#" character, create a mailbox called "nameofstore" and move the message to it. Everything works fine except for I'm getting the following error:
"Mail got an error: Can’t set text item delimiters to {"+", "#"}."
This is my script:
tell application "Mail"
set unreadmessages to the first message of mailbox "INBOX" of account "Account"
set theEmail to extract address from sender of item 1 of unreadmessages
set mystring to theEmail
set text item delimiters to {"+", "#"}
set textlist to text items of mystring
set mylist to {}
repeat with i from 2 to count of textlist by 2
set end of mylist to item i of textlist
end repeat
get mylist
set mailboxName to mylist
set messageAccount to account of (mailbox of item 1 of unreadmessages)
set newMailbox to make new mailbox at (end of mailboxes of messageAccount) with properties {name:mailboxName}
repeat with eachMessage in unreadmessages
set mailbox of eachMessage to newMailbox
end repeat
end tell
When I run only the text extract portion of the script it works fine:
set mystring to "whatmyemailnormallyis+nameofstore#domain.com"
set text item delimiters to {"+", "#"}
set textlist to text items of mystring
set mylist to {}
repeat with i from 2 to count of textlist by 2
set end of mylist to item i of textlist
end repeat
get mylist
result:
{"nameofstore"}
Any help would be greatly appreciated. If anyone with better AppleScript skills than me can improve the script in other areas that would also be greatly appreciated.
This is an inheritance problem. The property (text item delimiters) is a property of the current application (AppleScript) instance, but it's being referenced unqualified inside the tell application block that directs commands to and enumerates properties from the target application, in this case Mail.
The temptation might be to set the text item delimiters outside the tell block, by moving it from its current line position to just before the block declaration. That's reasonable, but I think you've positioned it perfectly, as it's important (and good practice) to keep track of this property to ensure it's always appropriately set and, more significantly, never inappropriately not set. The most reliable way to do this, which also makes it easier to follow for other people, is to do as you've done, which is to set the property immediately prior to any statement where it exerts influence (namely, any time a list is coerced to text, or text is split into text items).
So, to avoid shuffling lines of code around, we need to be able to make it clear to the compiler that the property that we're referencing doesn't belong to application "Mail", but to the top-level scripting object, which will most typically be the current application. The three ways to do this are:
set AppleScript's text item delimiters to ...
set the current application's text item delimiters to ...
set my text item delimiters to ...
Stylistically, I favour the last option. However, my is not a synonym for current application nor for AppleScript, but rather a reference to the parent of your script. Unless the parent property is specifically declared in your script, then it will default to current application. However, there are reasons one might choose to assign a different value, in which case only the first or second of the above three options will be viable.
Here's a slight reworking of your script, which I'm afraid you'll have to test in lieu of my purchase of a new MacBook. I noticed some oddities in yours:
You obtain the first message in the inbox, but on the next line, reference item 1... of, what I imagine you thought would be a list of messages, but would in fact be a single message class object.
This single message object is the only message your script utilises to process the sender's email address. However, later on, you loop through, again, what you expect to be a collection of inbox messages, which, if it were, may not all have the same + tag.
You loopp through the text items generated by splitting the email address, and quite smartly start at index 2, and skip over every other item in the list. However, this list will only have three items in it, so there's never any looping to be done, and you can simply make use of text item 2.
In creating a new mailbox, you didn't first check to see if the mailbox already exists. I'm not sure whether Mail would throw an error, or silently ignore this. But I've redrafted the line to check first, and create if necessary.
Lastly, the final repeat loop is presently not necessary given unreadmessages is not a list (so might actually throw an error). So I removed the loop construct, but otherwise kept the line as it was. I'm not sure whether the mailbox property of a message is one that can be set, i.e. it might be read-only. If this is the case, that will throw an error, and you'll have to invoke the move command in order to move a message to a new mailbox. I may be wrong, though, and it may work just the way you intended.
tell application "Mail"
set firstInboxMessage to the first message in the inbox
set theEmail to extract address from sender of the firstInboxMessage
set my text item delimiters to {"+", "#"}
-- Since the script will trigger only when an email address
-- contains "+", we know text item 2 will always exist and
-- will always represent the slice of text we're after
set mailboxName to text item 2 of theEmail
set messageAccount to account of mailbox of the firstInboxMessage
tell the messageAccount to if the name of its mailboxes does not contain ¬
the mailboxName then make new mailbox at the end of its mailboxes ¬
with properties {name:mailboxName}
set newMailbox to the mailbox named mailboxName in the messageAccount
set the mailbox of the firstInboxMessage to the newMailbox
end tell
In reality, if you're invoking this script as a mail rule, you'll probably want to enclose this within the special handler that you can look up in the Mail scripting dictionary, called something like on receiving messages <messages> for mailbox rule <rule>

Referencing Two-Word Variables in Applescript?

I'm attempting to get some data from an app called "Timing", which is local to my computer, and post it to a URL to notify a webhook, from which some process automation will occur.
According to the Applescript integration with Timing,
There is a time summary object that's returned from a command which I've successfully executed. When displayed as an alert, that data looks like this:
Can't get |times per project| of {id:5C6CD8C8-357F-4EE7-890C-5946DC03BBB9", overall total:1.18092493622303E+4, times per project:{Maintenance:81.091759443283, Youtube:4820.38001298904, |self improvement effors|:876.930474758148, Homework:2383.20326805115, |(no project)|:3647.64384698868}, overall total without tasks:1.18092493622303E+4, productivity score:0.388005592511, times per project without tasks:{Maintenance:81.091759443283, Youtube:4820.38001298904, |self improvement efforts|:876.930474758148, Homework:2383.20326805115, |(no project)|:3647.64384698868}, class:time summary}.
As you can see, (above), there is a property called productivity score, which is two words.
When attempting to get this datapoint from the object (which I will use to notify the webhook:)
set newnewVar to productivity score of newvar
display alert newvar
Obviously this wont work, because the variable name is two words. I've tried surrounding the name in quotes and surround it with other characters, but nothing seems to work, and the documentation for getting specific properties only has examples with variables with one word.
What's the solution to this problem?
In AppleScript, user-defined variables cannot generally have spaces. Typically they start with a letter or underscore, and then can contain only letters, numbers, or underscores. A user-defined variable can only contain spaces if it is contained within vertical pipes. So all of the following are valid variable forms: alphaUnit, slideRow3, _tempItem, |my variable|, left_hand_vector
However, any application or script that creates and uses a scripting definition can create commands and classes and properties that have multi-word names. For instance, if you look at the System Events app, you'll see that the Disk-Folder-File Suite has a class named disk item with properties like creation date. The reason this works is that these multi-word names are actually represented by a numeric (four-char) code: disk item is actually 'ditm' and creation date is 'ascd'. You often see these codes pop up in error strings like so:
"cannot make class ≪ditm≫ into..."
Make sure you have the scope right to invoke the dictionary — i.e. be within a tell block for the app or script that invokes the scripting dictionary — and the multi-word names should 'just work'. After compiling, you'll see them highlighted in a purple color that's just a bit different from the red of uncompiled text. You do not need to enclose dictionary terms in vertical pipes; if you do, they will be treated as user-defined variables and lose their special scripting purposes.

Interpret "Find action" results on Safari

I am trying to create an AppleScript that can find text on a webpage and tell me the amount of matches I received(Command + F).
I already know how to do the "Find" part:
tell application "System Events"
delay 0.5
keystroke "f" using {command down}
end tell
However, I do not know how to interpret these results, such as tell me whether there is a match, or how many matches I have.
Is there any way to do this?(If it seems a bit vague, I can be more specific)
Thanks!
I agree with #user3439894 and his sentiments about using UI scripting (that is—in this case—getting System Events to issue mouse clicks and keypresses on your behalf). Although it has its uses in other areas, it's by far and away my personal least favourite method to achieve a goal, and one of last resort.
Two very quick reasons why it can be a fragile implementation is: 1) the CmdF shortcut used to initiate the Find... menu command could change, either by your own doing, or if it were to be overridden by a systemwide shortcut that supersedes Safari's claim to it (in fact, for this reason, I would personally trigger the Find... command via the menu bar, which System Events can click on your behalf. Menu items tend not to change like shortcuts, unless regional language settings do); and 2) if Safari loses focus during the time the keypresses are issued and the search is initiated, it messes up the whole command flow in your script, and will at best give you no results, but more likely, throw an error in a later part of the script.
I'm going to demonstrate two alternative methods of searching a Safari webpage for a piece of text, and obtaining the number of times it matches throughout the whole document.
1. Safari's do JavaScript AppleScript command
Most modern web browsers have the ability to run JavaScript code inside their tabs, and Safari can too. AppleScript can take command of this very useful function, provided you give it permission to do so by going into the Develop menu of Safari and ticking Allow JavaScript from Apple Events and Allow Remote Automation (the latter of which will already be on, I'm guessing). There's another menu item called Allow JavaScript from Smart Search Field—I would advise you keep this one off, otherwise it could potentially allow naughty websites to issue commands to your computer and cause mischief.
use A : application "Safari"
set D to the front document of A
set s to "AppleScript" -- the search string
try
set n to (do JavaScript ¬
"document" & ¬
".body" & ¬
".innerText" & ¬
".match(/" & s & "/ig)" & ¬
".length;" in D) as integer -- the number of matches
on error
set n to 0
end try
To break this down: s is the search string that you would otherwise be typing into the search box. It is fed into a JavaScript command, that has the following components:
document: a reference to the Safari webpage document;
body: the body of the document, as opposed to, say, the header or the footer. It's the main bulk of the webpage that users see in front of them;
innerText: the text contained within the body of the document, free of any HTML formatting, but preserving whitespace;
match(): a method or function in JavaScript where the search string s is used to perform a search within the innerText and return an array listing all of the matches;
length: a property of the array returned by match() that reports how many elements is contains, and this equates to the number of matches found during the search.
It's all one command, which, written in full on a single line, looks like this (using the search string "AppleScript"):
document.body.innerText.match(/AppleScript/ig).length;
It returns a number, which is stored in the variable n, and that's it.
This is my favourite method that I would elect to use myself, as it's unlikely to break, and it's nice and fast.
I should point out that match() actually searches and matches using a Regular Expression. I won't go into them right now, but it means that the search string s will need to be a little careful if using any special characters:
\ [ ] { } ^ $ . | ? * + ( )
All you need to be aware of is that, if your search string uses any of these characters, you should precede it with a double-backslash \\. So, for example, if I wanted to search for "matches I received(Command + F)" (which uses (, ) and +), then I would declare my variable s as:
set s to "matches I received\\(Command \\+ F\\)"
2. Chop & Measure
This method is useful if you don't wish to enable Remote JavaScript in your browser, or simply want something that's straightforward to remember and implement off the top of your head next time.
It's simple text manipulation, using AppleScript's text item delimiters and a bit of counting:
use A : application "Safari"
set D to the front document of A
set s to "AppleScript" -- the search string
set T to text of D -- the webpage text content
set l to {initialValue:length of T, finalValue:missing value}
set the text item delimiters to s
set T to text items of T
set the text item delimiters to ""
set T to T as text
set l's finalValue to length of T
set |𝚫l| to (l's initialValue) - (l's finalValue)
set n to |𝚫l| / (length of s)
Safari has a useful AppleScript property called text, which refers to the text content of the specified document or tab (it also has another property called source that contains the HTML source of the document or tab).
Here's the breakdown:
The value of Safari's text property—which is the text content of the webpage—is stored in a variable, T;
The length of T is read and stored. This equates to the number of characters on the whole webpage;
The text item delimiters are set to the search string, s, (which does not need to worry about special characters, so don't insert unnecessary backslashes in this one). The text item delimiters basically erase all occurrences of s from within T;
Then the length of T is read again. If s found any matches in T, it means that the length of T—the number of characters—will have reduced;
It will have reduced by the number of characters in s for each match that occurred. Therefore, turning the equation round a bit, the number of matches, n, is equal to the change in length of T divided by the length of s.
There are other ways to search a webpage with AppleScript, JavaScript, bash, etc., but I think these two serve as reasonable examples of how to achieve the same goal using very different methods. I refer to them as examples, because you might need to make small adjustments to the script to cater for your own needs, such as inserting backslashes where necessary in the first example, or considering in the second how you'd handle the situation if you set s to be an empty string "" (it will throw an error, but this is easily managed).
They also both return real values for n, i.e. 11.0. It's easy to see why in the second example, but I assume it's just a type conversion between JavaScript and AppleScript in the first example (I don't know). Therefore, purely for neatness, I would then coerce the returned value into an integer as I did in the first one, so it reads 11 instead of 11.0:
set n to (...) as integer
but you don't have to.
First of all I must say that UI Scripting can be messy and unreliable. I'd suggest you find a different way to accomplish whatever the real goal is.
That said, using Safari in macOS High Sierra 10.13.3 set to this web page, the following example AppleScript code will set the variable theSearchResult to the result of the search for the word "vague":
tell application "Safari" to activate
delay 0.5
tell application "System Events"
keystroke "f" using command down
delay 0.2
keystroke "vague"
delay 0.2
set theSearchResult to (value of static text 1 of group 2 of tab group 1 of splitter group 1 of window 1 of application process "Safari")
delay 0.2
key code 53 -- # Esc
end tell
return theSearchResult
Result:
"1 match"
Note that the value of the delay commands may need to be adjusted for your system, and or additional delay commands may or may not be needed. Adjust values of and or add/remove the delay commands as appropriate.
The search result can be one of the following, Not found or an integer followed by the word match, e.g. 1 match, and possibly something else, not sure as I've not done extensive testing.
How you want to interpret the result is up to you. You could use a if statement on the theSearchResult, e.g.:
if theSearchResult contains "Not found" then
-- # Do something.
-- # Your code goes here.
else
-- # Do something else.
-- # Your code goes here
end if
Another factor to consider is how is it being searched, i.e. Starts With or Contains. I believe the default in for Safari in macOS High Sierra 10.13.3 is Starts With.
Note: The example AppleScript code is just that and does not employ any error handling and is meant only to show one of many ways to accomplish a task. The onus is always upon the User to add/use appropriate error handling as needed/wanted.

Automator: Choose from list and getting the corresponding variable value

I'm new to AppleScript but I do have some basic knowledge of Automator. Here is what I want to achieve:
There is a simple list of URLs that I often use throughout my work. Like this:
Product datasheet: URL1
Licensing: URL2
Price list: URL3
...
etc.
In most cases I use these URLs when sending a mail message to a customer, but sometimes I also use them in other applications, like Safari. So the service should be global.
I need the service to prompt me with a list of items where each item is the name of a link (like {"Datasheet", "Price list", "Licensing", etc} ).
When I select an item and click OK, the service must fetch the URL that corresponds to that link name and then put it in the clipboard, so that I can paste it whenever I need to use it.
I followed recommendations in this q&a Automator: How do I use the Choose from List action? and created the first action (Run AppleScript). Also, i created a number of variables and specified names and URLs. I.e. I'm going to store the actual list ("database") in the service. There are somewhere between 30 and 50 links/records in total so I don't probably need an external Excel file or something.
What I can't figure out is how to get/fetch the URL from the variable. The Get Value of Variable doesn't work for me because it requires a constant variable name as input. However, I need an action to receive input from the Run AppleScript action, lookup the variables list and fetch the URL and then pass the resulting URL onto the Clipboard action.
Thank you for your help.
Make an automator service workflow with a single Run AppleScript action, containing the following code:
set x to item 1 of (choose from list {"Product datasheet", "Licensing", "Price list"})
if x is "Product datasheet" then
set the clipboard to "URL"
else if x is "Licensing" then
set the clipboard to "Another URL"
else if x is "Price list" then
set the clipboard to "Yet Another URL"
end if
To add to this, add to the list of items on the first line, copy the else if line and edit it to check for the newly added item, then copy the set the clipboard line and edit it so that it contains the new URL.
Also, make sure that you set the workflow to work in "any application" and you should also set it to receive "no input".

Resources