[Tutorial] Scripting Libraries

Mavericks (Mac OS X 10.9) ships with an updated version of AppleScript. One of the new features in AppleScript 2.3 is creating your own script libraries. I would like to explain in a small tutorial how this new feature can be very useful.

Pre 2.3 versions
you can use load script commands to return an instance of the script object stored in a file. This is an useful way to store script objects in files so they can be used by multiple AppleScript scripts or applications. The script object will behave much like a library, so this was one way to create your own pseudo-libraries. However, locating and managing such pseudo-libraries was always a mess and you need to write your own “include” function to make sure the correct library and version are loaded, if users would make the effort. Sharing those pseudo-libraries was always difficult and rarely a success because it was hard for another user to understand the library. At the end these pseudo-libraries were mostly used by scripters locally on their machine, particularly those needing to do a lot of writing involving reuse of AppleScript code.

AppleScript Libraries
Script libraries are in basic form are pretty much the same as loading a script object from a file. In simplest form, the primary difference with loading script objects is that you don’t need to know the path to the file, but the library must be stored in a fixed location. That location for libraries is a folder in one of the Libraries folders with the name “Script Libraries” (which doesn’t exist after installing the OS). You can also use AppleScriptObjC code in a library and use it in a normal script file that doesn’t support AppleScriptObjC. At an even higher level you can use your own AppleScript terminology. With the use of AppleScriptObjC, your own terminology and the new loading mechanism, script libraries are a major improvement over just loading scripts.

Creating your first script library
The first thing to do is create a simple script library. It is a library that functions just like loading an script file only you do not need to load the file by its path, instead you call the script library by its name exactly as you address applications by their name. To get started in the easiest way, start with an handler that is missing in the AppleScript language. I think one of the most missed functions is string replacements. Just like a normal stored script object that we load later with the load script command we create a simple script and save it as script in the Library:


on textReplace(sourceText, searchText, replaceText)
	set {TID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, searchText}
	set textItems to every text item of sourceText
	set AppleScript's text item delimiters to replaceText
	set changedText to textItems as string
	set AppleScript's text item delimiters to TID
	return changedText
end textReplace

Save the script above as a script file, not a bundle, and store it in your “Script Libraries” folder of your home folder with the name “Basic Text Utilities.scpt”. Apple suggests that when you are in the development phase you should store the script library in the Library folder of your user’s home folder. By default the folder “Script Libraries” doesn’t exist in the library folder in Mavericks. To use the script library we no longer need to know it’s path. When we use the new object specifier script AppleScript will look in the script libraries for the given script file name. We can use the script library as follows:


use myLib : script "Basic Text Utilities"

myLib's textReplace("Hxllo World!", "x", "a")

or, using a tell block


tell script "Basic Text Uilities"
	textReplace("Hxllo World!", "x", "a")
end

or


script "Basic Text Utilities"'s textReplace("Hxllo World!", "x", "a")

Apart from the new object specifier script there is also the new “use” statement in AppleScript. The use statement for loading a script library loads an instance of the script file. Because there is no terminology definition in this script library we need to bind that instance to a variable (myLib).

Make use of AppleScriptObjC in Script Libraries
In this section I’m not going to explain how AppleScriptObjC works. If you need that explanation, I recommend that you read Shane Stanley’s ebook “AppleScriptObjC Explored” or “Everyday AppleScriptObjC”

As we know, the power of Cocoa compared to AppleScript is huge. But in the past it was difficult for a normal script to make use of AppleScriptObjC. Having script libraries using cocoa and calling them by simple script files makes the use AppleScriptObjC even more attractive than it already was. For instance missing functions as string replacements, finding index of in arrays, sorting data or get key values of dictionaries (record) are all part of the Cocoa API and which does it all at an amazing speed compared to AppleScript. What we’re going to do is create a library as above but this time use AppleScriptObjC instead of AppleScript’s text item delimiters to replace sub-strings in a string.

Start with a blank script and save it as a script bundle. Save it in the folder containing “Basic Text Utilities.scpt” with the name “ASOC Text Utilities.scptd”. When you store the script as script bundle, toolbar item to show the bundle contents will appear. Click this button and a drawer will appear. Inside the drawer you’ll see the contents of your bundle. Inside this drawer there is a checkbox to enable AppleScriptObjC, which enables the key value OSAAppleScriptObjCEnabled key in the info.plist file. Make sure that the checkbox is checked because by default it is not in the AppleScript Editor. Now add the following code to your script.


on textReplace(sourceText, searchText, replaceText)
	set cocoaString to current application's NSMutableString's stringWithString:sourceText
	cocoaString's replaceOccurrencesOfString:(searchText as string) withString:(replaceText as string) options:(current application's NSCaseInsensitiveSearch) range:{0, (cocoaString's |length|())}
	return cocoaString as unicode text
end textReplace

Save the script again and its use is identical to our AppleScript example:


use myLib : script "ASOC Text Utilities"

myLib's textReplace("Hxllo World!", "x", "a")


tell script "ASOC Text Uilities"
	textReplace("Hxllo World!", "x", "a")
end


script "ASOC Text Utilities"'s textReplace("Hxllo World!", "x", "a")

Script Library with our own terminology
By adding our own terminology in our script library we can make script libraries behave like scripting additions. The advantage is that we can use features similar to Scripting Additions without using the complex languages of C/C++, using instead AppleScript(ObjC) code. Of course, there is a lot more you can do in a Scripting Addition, things like optional parameters which aren’t supported in a scripting library with it’s own terminology. There are also classes and events you can create in a scripting addition as well. But apart from those shortcomings, these work from your (calling) script pretty much the same as Scripting Additions. First let’s take a look at how to define our own terminology.

Scripting definition files are files that are used by AppleScript to know the terminology (syntax) of applications or scripting addition in certain contexts. Real AppleEvents that are used to communicate through the system for interprocess communications are defined by a four letter code. These codes would make reading the AppleScript source code unreadable. Sometimes we use these when we want an UTF-8 encoded string for example. We use the class with a four letter code: «class utf8». Because AppleScript doesn’t have a definition for this class, we’re forced to use the raw class name. We’re going to use a definition file for the same purpose but instead of defining a class for script libraries we define commands and types. types are enumerators which can be useful for parameters with predefined values.

A valid but empty sdef file looks like the file below. It’s an XML file that contains a dictionary element.

But first we’re going to add a suite. A suite is more or less a separator to divide different commands in different groups. For example, you can split all your commands into different suites like “Text Suite”, “List Suite”, “Record Suite” and “Date Suite” for examples of different categories. For now we’re going to use “Text Suite”. Then we need to add a code attribute to the suite element. Here you choose a four-letter code. Apple says that they use only lower case letters and that lower case codes should only be used by Apple, or at least not in scripting definitions used in script libraries. So you must have at least 1 character uppercase or it will conflict with Apple’s own coding and rules. What I will do is using the odd position capital for commands, suite and parameter codes and using the even positions capital for enumerators. But it’s all up to you how you’re going to use them in the future, as long as at least one character is uppercase.

For all following XML elements we are going to a description attribute. This is because this description will be shown when the dictionary is opened by AppleScript Editor. So the description attributes are good for documentation about the script library for your own use but especially if you ever want to distribute it.

Once we have created a suite we can add commands and types to that suite. For now, our command is an AppleScript handler, so commands needs to be defined in an named parameter style. If we are not using restricted parameter names and restricted handler names, then we’re free to use whatever we want as long as the handler and named parameters match those in the script. For this example, I want a command named “replace in string”. I add an element “command” with an attribute name. The attribute “name” is the name of the handler we’re going to use in the script later. We also need code here for this command but this time it must be 8 characters long with the first 4 characters are the code that’s been used for the suite. So our code ends with “StRe”.

After adding the command we want to add parameters to the command. There is a direct-parameter element which is the parameter that’s directly behind the command. First example, the string after a do shell script command is called a direct-parameter. The direct-parameter contains a type and description. The direct-parameter can only be defined once and unlike named parameters it doesn’t need a code. The type can be a self-defined enumeration type or standard AppleScript types (classes) like integer, text, and record for example.

Our command needs more than one parameter so we extend our command by adding named parameters. According to the sdef manual, named parameters support the attribute “optional”, because script libraries doesn’t support it I’m not going to use those attributes here. The named parameters has a name, code, type and description. The name attribute of can contain also almost any word, but it’s better not to use confusing names. with or without are usually to set the boolean value of one ore more named parameters in one parameter. So a logical name for our second parameter to use something like “search for” and “replace with” as our third named parameter. To show how enumrations works we add another parameter: how we search the string byte search is text search. With enumerations we need to fill in our own type. This type filled here we’re going to define later.

We also need to define our enumerator. We fill in numerations directly in the suite element next to the commands. As I said, for enumeration I use another capital type (even positions are capitalized) to avoid conflicts. And the attribute name of the enumeration element needs to be excactly the same as the value of the type we have used for “using search option”. So first we need to create a container for every enumerator later:

The last part we need to do to finish out sdef file is defining the enumerators. The enumerators have a name and code attribute as well. The names will work as labels or constants.

Now save the file somewhere and give and name it “ASOC Text Utilities.sdef”, and remember were you save it. You can test if the file is correctly saved by openening the file with AppleScript Editor. If the file is not a proper sdef format it will say “there is nothing to show”.

Now we need to add the scripting definition file to the scripting bundle. We’re going to re-use the script library “ASOC Text Utilities.scptd” and open it with AppleScript-editor. Open the drawer in to show it’s bundle contents (blue-white icon in your toolbar) and drop the “ASOC Text Utilities.sdef” in the file table. Fill in the Scripting Definition text field “ASOC Text Utilities”, the name of the sdef file without extensions. This the library itself, when editing, as software that will understand the terminology,

replace the code in the script library with the following code and save it:


on replace in string sourceText search for searchText replace with replaceText using search option searchOption
	if searchOption = text search then
		set searchOption to current application's NSCaseInsensitiveSearch
	else
		set searchOption to current application's NSLiteralSearch
	end if
	set cocoaString to current application's NSMutableString's stringWithString:sourceText
	cocoaString's replaceOccurrencesOfString:(searchText as string) withString:(replaceText as string) options:searchOption range:{0, (cocoaString's |length|())}
	return cocoaString as Unicode text
end replace in string

Now our script that will use this library has something new. Because we’re using a scripting definition it means that we have our own terminology and have extended the syntax of AppleScript. Because the library’s syntax is added to the AppleScript interpreter that it using the library you can also directly use the library without a tell block.

use script "ASOC Text Utilities"

replace in string "Hxllo World!" search for "X" replace with "e" using search option text search

or you can still use a safer tell block:


tell script "ASOC Text Utilities"
	replace in string "Hxllo World!" search for "X" replace with "e" using search option binary search
end

For safety it’s better to use tell script block when you’re in the context of another application. When you’re in the context of the current application, I think, it’s pretty safe to call the command without the tell block around it.

Sharing libraries
Script libraries is a great improvement for AppleScript but on the other hand maybe less for MacScripter. First as you may have noticed my custom AppleScript syntax isn’t supported by the AppleScript markup on MacScripter. Also having your own library can be confusing when you seek for help on MacScripter because not everybody is using the same libraries. So I would already warn everyone for this.

Software I’ve used
In the past I used Sdef Editor.app from Shadow Lab but they stopped supporting this application in 2007. It still works in Mavericks and works correctly (and it’s free). The sdef editor was developed for easily writing sdef files for applications and scripting additions. This editor has full support of the sdef file while script libraries only use a small part of it. So it can be a bit confusing. For the week before this was written I have been using Shane’s ASObjC Explorer for Mavericks. This version of OSObjC Explorer has a built-in sdef editor that makes creating script libraries even easier, especially when using your own terminology. Also the sdef editor in Shane’s application will only show you the options that are needed.

using the return element in sdef
For documentation purpose I use the return element in the sdef file. Here you define the return type of a command.

I hope this tutorial is useful and I welcome any comments about this tutorial.

  • Shane has also a great video tutorial on his website for changing the case of a string object on Mac OS X Automation using AppleScriptObjC.

revision a: 29-10-13 Changed the named parameter with into by because with is normally used for boolean true values (thanks to Shane for pointing it out).
revision b: 01-11-13 Rearranged the order of topics in a more obvious way, thanks to McUsr
revision c: 04-11-13 Rewrote the tutorial and added some more information to it, writing 3 different kinds of libraries, extensively discudding the scripting definition file and put as much suggestion in it as possible.

Some Light editing done by A.C. Bell

2 Likes

Hello.

I had a slight problem with compiling the code, which I pinned down to being having Script Debugger 4.5 set up as the default editor. Not so understandable, as it is pretty pre Applescript 2.6.

Quitting the editors after having set AppleScript Editor to the default AppleScript Editor, and then restarting both of them solved all issues, at least as long as I compile the new code in AppleScript Editor.

Having read this excellent description rather carefully, though I haven’t thoroughly tested it yet, I have decided to include it with our other tutorials in unScripted. Well done.

McUsrII’s comment that this tutorial must be tested in the Script Editor rather than in Script Debugger arises from the the differences in way SE handles dictionaries compared to SD. I have no doubt that a fix for SD will make this right eventually.

Hello.

I want to add that ScriptLibraries included by the new “use” keyword, that are without an sdef file, works flawlessly with my ScriptDebugger 4.5.7 that I bought originally for Snow Leopard. -Now, that is value for money! Thank you Late Night Software!

Hi,
I had watch the Developers video on script libraries and have written a library. Similar to this one actually. Seems everyone wants to replace text. :slight_smile:

In my search for an answer to one problem I have, I came across you well constructed Tutorial. But I do not think you cover what i am trying to solve.

The same as you I have some options. My options are ‘using any match’ or ‘using exact match’

But what I want is to be able to write the command with or with out the ‘using…’ options. Similar to where you can use: ‘replacing’ or not in the save command.

My sdef does contain the optional=yes in the parameter.


But this does not seem to make it optional and I still get the error.

I already employ error controls in the script libraries handler to catch invalid classes being used as arguments. But they are inside the handler. I cannot find a way to catch an error in the parameters though.

Do you or any one have any ideas

Many thanks

You can’t designate parameters as optional – AppleScript has never supported optional parameters in its handlers, and terminology is just repackaging handlers.

Ah I think I see,. Although not sure how would go about it.

A a test I added

to my sdef inside the command.

This allowed the replacing to be placed on the end or not.

But is the code working?

AppleScript has always allowed this sort of thing:

on addUp(x, y, z)
	return x + y + z
end addUp
addUp(5, 6, 7, 8)
--> 18

where it just ignores the extra argument, but it won’t handle this type of thing:

on addUp(x, y, z)
	return x + y + z
end addUp
addUp(5, 6)
--> {5, 6} doesn't match the parameters {x, y, z} for addUp.

When you add terminology, all you are doing is effectively wrapping a new interface around ordinary AppleScript handlers; you’re not changing the underlying rules.

That’s not to say it couldn’t be made to work, though, so feel free to log a request for it.

Hello.

I have been wondering if it is possible to write an sdef that would make handlers take parenthesis.
Is that possible, -to write and sdef file for a handler like addUp(x,y,z)?

Thanks. (A “no” will suffice.)

Which I think is what it is doing.
Thanks

No. (Or commas.)

While writing this tutorial I’ve tested the optional parameter and found out that you can add this attribute to the parameter element but it’s value is ignored. As for applications and scripting additions the binding process is completely different and therefore optional values can be used. The most important reason is script libraries, using it’s own terminology or not, is an instantiated script object and not an C library that’s dynamically loaded directly into the AppleScript Interpreter. That means, like Shane also said, it’s limited to the rules of AppleScript objects and handlers.

Because script libraries are instantiated AppleScript objects the optional parameters should be made in the AppleScript language. I like the idea of optional parameters but how would that work since AppleScript handlers supports both labeled parameters and positional parameters? If you have an handler for uppercasing you would have something like:

on makeUpper(theString, mode:1) --optional without own terminology
end makeUpper

on makeUpper theString case style:sentence case --option with terminology using enumeration
end makeUpper

@Mark Hunte:

While that’s true, there’s an important exception: if you are using the same term as Apple, you should use the same code. Mark’s example uses the code standard additions uses for the replacing parameter (in store script).

I thought so to until I watched video 416 of the wwdc (Introducing AppleScript Libraries). There it say “The use of codes, comprised of all lower case letter, is reserved by Apple.” and as “Sal Soghoian” says that you need to use at least 1 upper case character in your code, not saying anything about re-using codes by Apple. Am I missing something here, please tell me then I can change that in my tutorial above as well.

Thanks for the prompt reply Shane.

With regards to the codes reserved by Apple, the same exception as Shane stated above pertains to those. Just writing this for completion. There is a long list of the codes for the return types in: Cocoa Scripting Guide table 1-1.

I can’t hurt to also read: TN 2106 Scripting Interface Guidelines.

I found the second link now, while looking for the first one, but it looks like a good read up. :slight_smile:

Edit

There is “of course” also a new AppleScript Language Guide that ships with this fine Operating System!

Not as sexy, but it’s possible to have pseudo-optional parameters in AppleScript by passing them in a list or a record. You still have to pass something (ie. the list or the record), but it doesn’t have to be fully populated:

on addUp(aList)
	set |sum| to 0
	repeat with thisValue in aList
		set |sum| to |sum| + thisValue
	end repeat
	
	return |sum|
end addUp

addUp({5, 6, 7})
--> 18

addUp({})
--> 0
on addUp(aRecord)
	set defaults to {x:0, y:0, z:0}
	set aRecord to aRecord & defaults
	
	tell aRecord to return (its x) + (its y) + (its z)
end addUp

addUp({x:5, y:6})
--> 11

addUp({})
--> 0

Thanks.I Know about the code thing and my own codes comply. As Shane say I used apple code here. But only because I was doing a test. Not to be used in my real code.

@Nigel Garvey

Thanks for the idea.

I don’t know if you want to use this in the Tutorial. But I was looking to dynamically choose which CharacterSet to use from NSCharacterSet.

So for example if I ran my script to remove characters and set the CharacterSet with a string:

 set thisSet to "alphanumericCharacterSet"
set changedText to remove characters in "<%p>%s: object_: %@ name:- %@  899userInfo:.,) %@" not in thisSet adding characters " @-_.)" replacing with "-"

I would need to coerce the string into a class method name.

What I came up with was inside the Script Libraries handler I use the NSSelectorFromString function.
Then have NSMutableCharacterSet class perform the selector.

set s to current application's NSSelectorFromString(current application's NSString's stringWithString:thisSet)
	set characterSet to (current application's NSMutableCharacterSet's performSelector:s)
	set charString to current application's NSString's stringWithString:charText
characterSet's addCharactersInString:charString

This returns which ever NSCharacterSet I need. And saves me having to use a load of if statements of a repeat loop to work out what NSCharacterSet is needed.

Note: I am using a NSMutableCharacterSet instead of NSCharacterSet because I will be adding characters to the set later on.

I think Sal was trying to simplify what is actually a fairly tricky business. The commands themselves should be unique both in the term used and the code. But for parameters, and enumerations, the rules are different. For example, if you are going to have an enumeration with yes/no/ask enumerators, using an exact copy of the one used in standard additions is perfectly fine, and makes sense in terms of the pool of available codes – just don’t mess with it. Importantly, it pretty much guarantees that you will not conflict with another definition – and avoiding conflicts is what the guidelines are all about. The same goes for parameters, although it is not compulsory that they be the same.

Apple still have some defined suites, and although they haven’t been updated for a long time, the whole idea behind them was that developers would use consistent terms and codes.

From tech note TN2106:

The quote was aimed at app developers under the pre-use regime. With the use command, the terminology is being pooled, so you can’t just rely on your terms trumping scripting addition terms.

Three things:

The use command, and especially the ability to avoid scripting additions, gives us control over how conflicts are handled, so we possibly don’t have to be so rigid about it all. The fact that conflicts are called out at compile time is a good thing. But it doesn’t hurt to try to avoid them in the first place, to avoid the extra work of including extra tell or using terms from blocks.

The best way to limit conflict with the command part is to try to use multi-word terms. Scripting additions with very large dictionaries can be a serious cause of conflict. (If third-party scripting additions disappeared, this would all be a lot simpler.)

Finally, the all-lowercase rule of thumb is pretty rough. There are quite a few case where Apple has used other characters. (Most of the parameters to the say command, for instance, are all-uppercase.)

(And in case you are wondering: if you write your dictionary using ASObjC Explorer, the red colouring that comes up when you enter a code that conflicts is based on my building a list from trawling through Apple’s headers, so it should be reasonably accurate. But it just means you should not use it for a command, or unless you also use the same name.)

Save yourself some effort – just pass the string and let the bridge convert it for you:

set s to current application's NSSelectorFromString(thisSet)