Thursday, October 17, 2019
  • Index
  •  » Code Exchange
  •  » Passing arguments to an AppleScript application at launch time

#1 2019-10-03 11:26:36 am

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 305

Passing arguments to an AppleScript application at launch time

It is sometimes desirable to pass arguments to an AppleScript application at launch time. While there is no direct way to accomplish this, several workarounds have been used.  Here are some options:

     - Save the application as a stay-open application, launch the application, wait until it is finished launching, then call a handler within the application with the argument values as the handler's argument. Drawbacks to this approach include the need for the application to be a stay-open application and the need to wait for the application to finish launching before sending it the argument values.

     - Save the argument values to a temporary file on disk, launch the application, then have the application read then delete the temporary file. A drawback to this approach is the overhead involved in saving the file to disk and then deleting it after it is read.

     - Execute the application via a run script command in the calling script of the form:  run script file ...HFS path to application... with parameters ...argument values..., then capture the argument values in the application with an explicit run handler of the form:  on run myArgs  A significant drawback to this approach is that the application is not actually launched; rather, the code is executed as a script in the context of the run script command.

The following method passes the argument values directly to the application at launch time and avoids the above limitations:

1) In the calling script, serialize the argument value, or list or record of argument values if multiple values are to be passed, into a text representation of the argument.  This is accomplished through the time-tested technique of forcing an error message on the argument value and extracting the text representation from the message.  Then launch the application with an open shell command, and pass the serialized argument value to the application via the open command's --args option.  Place the following code in your calling script:

Applescript:


set myArgs to {1, 2.2, "three", true, missing value, null, path to home folder, current date, {1, 2, 3}, {aa:1, bb:2, cc:3}} -- or whatever value or values you wish to pass to the application
set myAppPath to "/path/to/MyApp.app" -- place the POSIX path to your application here

set serializedValue to my serializeValue(myArgs)

do shell script ("open -a " & myAppPath's quoted form & " --args " & serializedValue's quoted form)

on serializeValue(val)
   -- Returns the text representation of the input value
   set tid to AppleScript's text item delimiters
   try
       || of {val} -- embedding the object in a list allows proper handling of certain object types (e.g., script objects)
   on error m
       try
           set AppleScript's text item delimiters to "{"
           set m to m's text items 2 thru -1 as text
           set AppleScript's text item delimiters to "}"
           set valAsText to m's text items 1 thru -2 as text
       on error
           try
               {||:{val}} as null -- backup solution if "|| of {val}" fails
           on error m
               try
                   set AppleScript's text item delimiters to "{"
                   set m to m's text items 3 thru -1 as text
                   set AppleScript's text item delimiters to "}"
                   set valAsText to m's text items 1 thru -3 as text
               on error
                   set AppleScript's text item delimiters to tid
                   error "Could not get a text representation of the object."
               end try
           end try
       end try
   end try
   set AppleScript's text item delimiters to tid
   return valAsText
end serializeValue

2) In the application, capture the serialized argument that was passed from the calling script using ASObjC's NSProcessInfo class, then deserialize the serialized argument into its original value using a run script command.  Note that the first item of the processInfo's arguments list is the POSIX path to the application itself, whereas the second item is the serialized argument value, which is the item of interest.  Place the following code in your AppleScript application:

Applescript:


use framework "Foundation"
use scripting additions

set myArgs to run script ((current application's NSProcessInfo's processInfo's arguments as list)'s second item)

-- myArgs now contains the original argument values

One limitation to this approach (and to the other approaches listed above) is that only native AppleScript values may be passed.  Complex objects (e.g., script objects) and non-AppleScript application objects generally cannot be passed.

Addendum:  I jumped the gun with the last paragraph.  It is possible to pass some non-AppleScript application objects.  See the next post for a description of how to accomplish this.

Edit note:  I made a slight cosmetic change from my original post to the calling script code above to make it a bit clearer to read.

Last edited by bmose (2019-10-08 11:10:20 pm)

Offline

 

#2 2019-10-04 07:01:54 am

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 305

Re: Passing arguments to an AppleScript application at launch time

I must amend the last statement in my original post.  It is indeed possible to pass at least some non-AppleScript objects as arguments.  The key is to wrap the argument in a tell block for the application from which the argument value came.  This applies to both the serialized version of the argument in the calling script and the deserialized version of the argument in the application.  (Note: It is sometimes unnecessary to wrap the deserialized version of the argument if the application can resolve the object's terminology.)

Here is an example of passing a "Finder" application object.  Place the following code in your calling script:

Applescript:


tell application "Finder" to set myArgs to (get properties of (path to applications folder))
set myAppPath to "/path/to/MyApp.app" -- place the POSIX path to your application here

set serializedValue to "tell application \"Finder\" to " & my serializeValue(myArgs) -- fails if "tell application \"Finder\" to " is omitted

do shell script ("open -a " & myAppPath's quoted form & " --args " & serializedValue's quoted form)

on serializeValue(val)
   ### INCLUDE THE PREVIOUSLY DESCRIBED HANDLER CODE HERE ###
end serializeValue

and place the following code in your AppleScript application:

Applescript:


use framework "Foundation"
use scripting additions

set myArgs to run script ((current application's NSProcessInfo's processInfo's arguments as list)'s second item)

tell application "Finder"
   myArgs's displayed name --> "Applications"
   myArgs's everyones privileges --> read only
   -- etc.
end tell

And here is an example of passing a "Safari" application object.  Place the following code in your calling script:

Applescript:


tell application "Safari" to set myArgs to (get properties of window 1)
set myAppPath to "/path/to/MyApp.app" -- place the POSIX path to your application here

set serializedValue to "tell application \"Safari\" to " & my serializeValue(myArgs) -- fails if "tell application \"Safari\" to " is omitted

do shell script ("open -a " & myAppPath's quoted form & " --args " & serializedValue's quoted form)

on serializeValue(val)
   ### INCLUDE THE PREVIOUSLY DESCRIBED HANDLER CODE HERE ###
end serializeValue

and place the following code in your AppleScript application:

Applescript:


use framework "Foundation"
use scripting additions

set myArgs to run script ((current application's NSProcessInfo's processInfo's arguments as list)'s second item)

tell application "Safari"
   myArgs's zoomable --> true
   myArgs's id --> 28795
   -- etc.
end tell

Offline

 

#3 2019-10-06 04:50:48 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5064

Re: Passing arguments to an AppleScript application at launch time

Hi bmose. That's pretty clever.  cool

For me personally, the only reasons to have a script as an application in the first place are if it's to be a stay-open applet, a droplet, or a log-in item. But many people seem to run scripts as applets as a matter of course, so some means of passing parameters may occasionally be useful.

You mention the overhead incurred when using a file to pass the data, but your own method involves recovering from an error, calling 'do shell script' (which itself involves opening a shell and running an executable stored on disk), and using 'run script' with text — three traditionally un-fast processes. Not yet having timed the two methods myself, I'd guess there's little to chose between them when the time taken to launch the applet is added in.

With the file method, neither script needs to delete the file if it's written to a temporary items folder or if it's in a location only used by the second script. It can be created and read either with the StandardAdditions File Read/Write commands …

Applescript:

(* Calling script. *)
set myArgs to {1, 2.2, "three", true, missing value, null, path to home folder, current date, {1, 2, 3}, {aa:1, bb:2, cc:3}} -- or whatever value or values you wish to pass to the application
set myAppPath to (path to desktop as text) & "MyApp.app" -- place the HFS path to your application here

-- Use File Read/Write commands to write the list myArgs to a file in the User temporary items folder.
set tempPath to (path to temporary items folder from user domain as text) & "myArgs.dat"
set fRef to (open for access file tempPath with write permission)
try
   set eof fRef to 0
   write myArgs to fRef
   close access fRef
on error msg
   close access fRef
   display dialog msg buttons {"Cancel"} cancel button 1 default button 1
end try

-- Open the file in the AppleScript application (which must have an 'open' handler to deal with it).
-- The file will eventually be disposed of by the system.
tell application "Finder" to open {file tempPath} using application file myAppPath

Applescript:

(* Script applet . *)
on open theseItems
   set myArgs to (read item 1 of theseItems as list)
   -- Rest of script.
end open

… or as a script file, using 'store script' and 'load script':

Applescript:

(* Calling script. *)
set myArgs to {1, 2.2, "three", true, missing value, null, path to home folder, current date, {1, 2, 3}, {aa:1, bb:2, cc:3}} -- or whatever value or values you wish to pass to the application
set myAppPath to (path to desktop as text) & "MyApp 2.app" -- place the HFS path to your application here

-- Create a script containing the arguments as a property (or as individual properties if preferred).
script
   property myArgs : missing value
end script
set argsScript to result
set argsScript's myArgs to myArgs

-- Store the script in the User temporary items folder.
set tempPath to (path to temporary items folder from user domain as text) & "myArgs.scpt"
store script argsScript in file tempPath replacing yes

-- Open the file in the AppleScript application (which must have an 'open' handler to deal with it).
-- The file will eventually be disposed of by the system.
tell application "Finder" to open {file tempPath} using application file myAppPath

Applescript:

(* Script applet. *)
on open theseItems
   set myArgs to myArgs of (load script (item 1 of theseItems))
   -- Rest of script.
end open

A variation on the latter idea would be to store the script file in a library location and have the applet activate and 'use' it instead of being told by the Finder to 'open' it. But this isn't as straightforward to set up and may have security implications.


NG

Online

 

#4 2019-10-06 07:16:48 am

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 5959

Re: Passing arguments to an AppleScript application at launch time

You could also combine a bit of A and a bit of B. Save the data in a file using one of the techniques Nigel suggests, and use ASObjC to pass the path as an argument:

Applescript:

use framework "Foundation"
use framework "AppKit"
use scripting additions

-- save parameters to tempPath

set ws to current application's NSWorkspace's sharedWorkspace()
set appURL to current application's NSURL's fileURLWithPath:myAppPath
-- or more flexibly:
-- set appURL to ws's URLForApplicationWithBundleIdentifier:"com.foo.whatever"
set argsDict to current application's NSDictionary's dictionaryWithObject:{tempPath} forKey:(current application's NSWorkspaceLaunchConfigurationArguments)
ws's launchApplicationAtURL:appURL options:0 configuration: argsDict |error|:(missing value)

And then follow up with:

Applescript:

use framework "Foundation"
use scripting additions

set tempPath to ((current application's NSProcessInfo's processInfo()'s arguments() as list)'s second item)
-- load stuff from tempPath

Last edited by Shane Stanley (2019-10-06 05:12:34 pm)


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/
latenightsw.com

Online

 

#5 2019-10-06 01:22:28 pm

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5064

Re: Passing arguments to an AppleScript application at launch time

Shane Stanley wrote:

Save the data in a file using one of the techniques Nigel suggests, and use ASObjC to pass the path as an argument:


Interestingly, I can't get 'store script' to work at all with the 'use' statements at the top of the first script, even after changing 'file tempPath' to 'tempPath as «class furl».

error "Can’t make «script» into type script." number -1700 from «script» to script


To get it to work, the 'use' statements have to be banished to a script object in a handler below that point the script. (Also, in the last line of Shane's first script, ws's sharedWorkspace()'s should just be ws's.)

Applescript:

set myArgs to {1, 2.2, "three", true, missing value, null, path to home folder, current date, {1, 2, 3}, {aa:1, bb:2, cc:3}} -- or whatever value or values you wish to pass to the application
set myAppPath to (path to desktop as text) & "MyApp.app" -- place the HFS path to your application here
-- save parameters to tempPath
-- Create a script containing the arguments as a property (or as individual properties if preferred).
script
   property myArgs : missing value
end script
set argsScript to result
set argsScript's myArgs to myArgs

-- Store the script in the User temporary items folder.
set tempPath to (path to temporary items folder from user domain as text) & "myArgs.scpt"
store script argsScript in (tempPath as «class furl») replacing yes

doTheRest(tempPath, myAppPath)

on doTheRest(tempPath, myAppPath)
   script o
       use framework "Foundation"
       use framework "AppKit"
       use scripting additions
       
       on theRest(tempPath, myAppPath)
           set ws to current application's NSWorkspace's sharedWorkspace()
           set appURL to current application's NSURL's fileURLWithPath:(POSIX path of myAppPath)
           -- or more flexibly:
           -- set appURL to ws's URLForApplicationWithBundleIdentifier:"com.foo.whatever"
           set argsDict to current application's NSDictionary's dictionaryWithObject:{POSIX path of tempPath} forKey:(current application's NSWorkspaceLaunchConfigurationArguments)
           ws's launchApplicationAtURL:appURL options:0 configuration:argsDict |error|:(missing value)
       end theRest
   end script
   
   o's theRest(tempPath, myAppPath)
end doTheRest


NG

Online

 

#6 2019-10-06 03:46:10 pm

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 305

Re: Passing arguments to an AppleScript application at launch time

Nigel Garvey wrote:

...the file method...

Nigel, thank you for your kind comment.  I would consider the two temporary-file methods that you describe to be the currently preferred conventional approach to passing arguments to an AppleScript application at launch time.  With respect to the temporary-file-less technique that I describe, I find that serialization takes about one order of magnitude less time than either the do shell script or run script command (for instance, 0.001 to 0.002 seconds to serialize the sample argument value in my original post).  Thus, the limiting factor with my technique would be the do shell script and run script commands in most situations.  As you mentioned, and without doing the actual testing, I suspect that the overall execution time of the temporary-file and temporary-file-less methods would be of the same order of magnitude.

Shane Stanley wrote:

You could also combine a bit of A and a bit of B. Save the data in a file using one of the techniques Nigel suggests, and use ASObjC to pass the path as an argument

Shane, thank you for pointing out NSWorkSpace's launchApplicationAtURL:options:configuration:error: method. I was unaware of it but like it very much. It accomplishes what the shell's open command does but without the overhead of the do shell script command. (It should also be added to the ever-growing list under the heading, "Things that AppleScript and the shell can do that ASObjC can do, and do better!") I wish the ...configuration:... parameter value could be an NSDictionary whose value for key NSWorkspaceLaunchConfigurationArguments was not restricted to an NSString only; otherwise, it might be possible to pass arbitrary arguments directly without the need for serialization or the creation of a temporary file. But since that's not the case, and since a string argument is permitted, it does raise yet another possible hybrid solution, namely passing the serialized argument as described in my original post (which is a text string) as the value for key NSWorkspaceLaunchConfigurationArguments. That would eliminate the do shell script "open..." command in the calling script and thus save execution time. As a fringe benefit, it would also enable the passing of non-AppleScript application objects using the method I described in my second post above. Here is my original temporary-file-less solution modified to incorporate your NSWorkSpace technique:

1) Place the following code in your calling script:

Applescript:


use framework "Foundation"
use scripting additions

set myArgs to {1, 2.2, "three", true, missing value, null, path to home folder, current date, {1, 2, 3}, {aa:1, bb:2, cc:3}} -- or whatever value or values you wish to pass to the application
set myAppPath to "/path/to/MyApp.app" -- place the POSIX path to your application here

set serializedValue to my serializeValue(myArgs)

set appURL to current application's |NSURL|'s fileURLWithPath:myAppPath
set argsDict to current application's NSDictionary's dictionaryWithObject:{serializedValue} forKey:(current application's NSWorkspaceLaunchConfigurationArguments)
current application's NSWorkspace's sharedWorkspace()'s launchApplicationAtURL:appURL options:0 configuration:argsDict |error|:(missing value)

on serializeValue(val)
   -- Returns the text representation of the input value
   set tid to AppleScript's text item delimiters
   try
       || of {val} -- embedding the object in a list allows proper handling of certain object types (e.g., script objects)
   on error m
       try
           set AppleScript's text item delimiters to "{"
           set m to m's text items 2 thru -1 as text
           set AppleScript's text item delimiters to "}"
           set valAsText to m's text items 1 thru -2 as text
       on error
           try
               {||:{val}} as null -- backup solution if "|| of {val}" fails
           on error m
               try
                   set AppleScript's text item delimiters to "{"
                   set m to m's text items 3 thru -1 as text
                   set AppleScript's text item delimiters to "}"
                   set valAsText to m's text items 1 thru -3 as text
               on error
                   set AppleScript's text item delimiters to tid
                   error "Could not get a text representation of the object."
               end try
           end try
       end try
   end try
   set AppleScript's text item delimiters to tid
   return valAsText
end serializeValue

2) Place the following code in your AppleScript application:

Applescript:


use framework "Foundation"
use scripting additions

set myArgs to run script ((current application's NSProcessInfo's processInfo's arguments as list)'s second item)

-- myArgs now contains the original argument values

P.S. Since launchApplicationAtURL:options:configuration:error: is listed in the Cocoa documents as deprecated, is it correct to think that the method to use going forward will be openApplicationAtURL:configuration:completionHandler:? Also, is there any concern regarding the use of NSWorkspaceLaunchConfigurationArguments, whose documentation includes the statement, This constant is not available to sandboxed apps. (Please excuse my limited knowledge of matters related to sandboxing.)

Last edited by bmose (2019-10-07 05:14:59 am)

Offline

 

#7 2019-10-06 05:12:06 pm

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 5959

Re: Passing arguments to an AppleScript application at launch time

bmose wrote:

I wish the ...configuration:... parameter value could be an NSDictionary whose value for key NSWorkspaceLaunchConfigurationArguments was not restricted to an NSString only



You're talking about one of the fundamental rules of the C language, and probably most others as well. Launch arguments are always strings.

it does raise yet another possible hybrid solution, namely passing the serialized argument as described in my original post (which is a text string) as the value for key NSWorkspaceLaunchConfigurationArguments.



It doe, but I have to say I'm always a bit uncomfortable about using error-trapping like that when there's an alternative. And sending potentially very long strings as launch arguments rings minor alarm bells. It may be fine, but there just doesn't seem to be enough advantage to warrant any risk.

P.S. Since launchApplicationAtURL:options:configuration:error: is listed in the Cocoa documents as deprecated, is it correct to think that the method to use going forward will be openApplicationAtURL:configuration:completionHandler:?



Only if you're writing scripts that will never be run in a version before 10.15. It's a "soft" deprecation, pointing to a better approach for the future. it's also problematic for AppleScript if you want to launch an app synchronously.

Also, is there any concern regarding the use of NSWorkspaceLaunchConfigurationArguments, whose documentation includes the statement, This constant is not available to sandboxed apps.



That means using it from sandboxed apps. Nothing to worry about.


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/
latenightsw.com

Online

 

#8 2019-10-06 05:15:17 pm

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 5959

Re: Passing arguments to an AppleScript application at launch time

Nigel Garvey wrote:

Interestingly, I can't get 'store script' to work at all with the 'use' statements at the top of the first script, even after changing 'file tempPath' to 'tempPath as «class furl».



Yes, I forgot to mention that. I'm not sure if it's a parent issue or what.

(Also, in the last line of Shane's first script, ws's sharedWorkspace()'s should just be ws's.)



Thanks -- fixed. Editing in the page again...


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/
latenightsw.com

Online

 

#9 2019-10-06 05:19:23 pm

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 5959

Re: Passing arguments to an AppleScript application at launch time

Just to round things out a bit more, arguments that begin with a hyphen are treated differently. For example, if you take my script above and change the argsDict line to this:

Applescript:

set argsDict to current application's NSDictionary's dictionaryWithObject:{"-filepath", tempPath} forKey:(current application's NSWorkspaceLaunchConfigurationArguments)

the value will be available to user defaults, like so:

Applescript:

set tempPath to (current application's NSUserDefaults's standardUserDefaults()'s objectForKey:"filepath") as text

Which is a good argument for being careful with key names generally.


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/
latenightsw.com

Online

 

#10 2019-10-06 05:20:19 pm

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 305

Re: Passing arguments to an AppleScript application at launch time

Shane, thanks for the helpful information. Can you please clarify what alarm bells might be raised by passing a very long string?

Offline

 

#11 2019-10-06 09:48:27 pm

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 5959

Re: Passing arguments to an AppleScript application at launch time

For starters, I can't find the size limit. I think it's about 260KB on Macs, and that includes any environment variables also passed by the system. But I'm just generally nervous about abusing such a fundamental function in a way I can't see any examples of anyone else doing.


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/
latenightsw.com

Online

 

#12 2019-10-06 10:36:02 pm

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 305

Re: Passing arguments to an AppleScript application at launch time

If you don't mind my pursuing this a bit further, isn't the purpose of the configuration parameter to pass parameter values to an application in the form of strings, analogous to the way parameters are passed to C and shell executables? That is exactly what I'm doing, differing only in that I'm passing a single string containing the serialized form of one or multiple values rather than multiple strings. As far as the forced error message method of getting a value's text representation goes, it is not an officially documented technique, but I have found that it has worked flawlessly on native AppleScript values over a period of 15 years and thousands of varied scripting situations. It has proven the test of time for me (for whatever that's worth!)

Offline

 

#13 2019-10-06 11:51:50 pm

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 5959

Re: Passing arguments to an AppleScript application at launch time

It's not "analagous to the way" -- it's exactly the way. Under the hood the string is being passed to a C main(int argc, const char * argv[]) function.

It should be fine, as long as you don't exceed the size limit. I'm just innately conservative when I don't see any particular advantage.


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/
latenightsw.com

Online

 

#14 2019-10-07 05:01:11 am

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 305

Re: Passing arguments to an AppleScript application at launch time

Thank you very much again for the further insights. Your knowledge of ASObjC is of enormous benefit to us scripting laity out here.

Offline

 
  • Index
  •  » Code Exchange
  •  » Passing arguments to an AppleScript application at launch time

Board footer

Powered by FluxBB

RSS (new topics) RSS (active topics)