py-appscript

3. A quick tutorial

The following tutorial provides a practical taste of application scripting with appscript. Later chapters cover the technical details of appscript usage that are mostly skimmed over here.

'Hello World' tutorial

This tutorial uses appscript, TextEdit and the interactive command line python interpreter to perform a simple 'Hello World' exercise.

Caution: It is recommended that you do not have any other documents open in TextEdit during this tutorial, as accidental modifications are easy to make and changes to existing documents are not undoable.

To begin, launch Terminal.app and type python followed by a newline to launch the python interpreter:

brian% python
Python 3.7.2 (v3.7.2:9a3ffc0492, Dec 24 2018, 02:44:43) 
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 

Target TextEdit

The first step is to import the appscript module. Appscript exports only a few public attributes - app, k, con, its, CommandError and ApplicationNotFoundError - so it's common practice to import these into the top-level namespace for convenience:

>>> from appscript import *
>>> 

Once appscript is imported, create new app object, identifying the application to be manipulated, and assign it to a variable, te, for easy reuse:

>>> te = app('TextEdit')
>>> 

The application may be identified by name, path, bundle ID, or, if running remotely, URL. If the application is local and is not already running, it will be launched automatically for you.

Create a new document

First, create a new TextEdit document by making a new document object. This is done using the make command, passing it a single parameter, new=k.document, indicating the type of object to create:

>>> te.make(new=k.document)
app('/System/Applications/TextEdit.app').documents[1]
>>> 

Because document objects are always elements of the root application class, applications such as TextEdit can usually infer the location at which the new document object should appear. At other times, you need to supply an at parameter that indicates the desired location.

As you can see, the make command returns a reference identifying the newly-created object. This reference can be assigned to a variable for easy reuse. Use the make command to create another document, this time assigning its result to a variable, doc:

>>> doc = te.make(new=k.document)
>>> 

Set the document's content

The next step is to set the document's content to the string "Hello World". Every TextEdit document has a property, text, that represents the entire text of the document. This property is both readable and writeable, allowing you to retrieve and/or modify the document's textual content as unstyled unicode text.

Setting a property's value is done using the set() command. The set() command is exposed as a method of the root application class and has two parameters: a direct (positional) parameter containing reference to the property (or properties) to be modified, and a keyword parameter, to, containing the new value. In this case, the direct parameter is a reference to the new document's text property, doc.text, and the to parameter is the string "Hello World":

>>> te.set(doc.text, to='Hello World')
>>> 

The front TextEdit document should now contain the text 'Hello World'.

Because the above expression is a bit unwieldy to write, appscript allows it to be written in a more elegant OO-like format as a special case, where the set() command is called upon the reference and the to keyword is omitted:

doc.text.set('Hello World')

Appscript converts this second form to the first form internally, so the end result is exactly the same. Appscript supports several such special cases, and these are described in the chapter on Application Commands. Using these special cases produces more elegant, readable source code, and is recommended.

Get the document's content

Retrieving the document's text is done using the get() command:

>>> doc.text.get()
'Hello World'
>>> 

This may seem counter-intuitive if you're used to dealing with AppleScript or object-oriented Python references, where evaluating a literal reference returns the value identified by that reference. However, always remember that appscript 'references' are really first-class query objects: while the syntax may look familiar, any similarity is purely superficial. For example, when evaluating the literal reference:

te.documents[1].text

the result is another reference, app('/System/Applications/TextEdit.app').documents[1].text, not the value being referenced ('Hello World'). To get the value being referenced, you have to pass the reference as the direct argument to TextEdit's get() command:

>>> te.get(doc.text)
'Hello World!'
>>> 

As usual, appscript provides alternative convenience forms that allow this to be written as:

doc.text.get()

or even:

doc.text()

Depending on what sort of attribute(s) the reference identifies, get() may return a primitive value (number, string, list, dict, etc.), or it may return another reference, or list of references, e.g.:

>>> doc.text.get()
'Hello World!'
>>> te.documents[1].get()
app('/System/Applications/TextEdit.app').documents[1]
>>> te.documents.get()
[app('/System/Applications/TextEdit.app').documents[1], 
    app('/System/Applications/TextEdit.app').documents[2]]
>>> te.documents.text.get()
['Hello World', '']
>>> 

More on make()

The above exercise uses two commands to create a new TextEdit document containing the text 'Hello World'. It is also possible to perform both operations using the make() command alone by passing the value for the new document's text property via the make() command's optional with_properties parameter:

>>> te.make(new=k.document, with_properties={k.text: 'Hello World'})
app('/System/Applications/TextEdit.app').documents[1]
>>> 

Incidentally, you might note that every time the make() command is used, it returns a reference to document 1. TextEdit identifies document objects according to the stacking order of their windows, with document 1 being frontmost. When the window stacking order changes, whether as a result of a script command or GUI-based interaction, so does the order of their corresponding document objects. This means that a previously created reference such as app('/System/Applications/TextEdit.app').documents[1] may now identify a different document object to before! Some applications prefer to return references that identify objects by name or unique ID rather than index to reduce or eliminate the potential for confusion, but it's an issue you should be aware of, particularly with long-running scripts where there is greater opportunity for unexpected third-party interactions to throw a spanner in the works.

More on manipulating text

In addition to getting and setting a document's entire text by applying get() and set() commands to text property, it's also possible to manipulate selected sections of a document's text directly. TextEdit's text property contains a text object, which in turn has character, word and paragraph elements, all of which can be manipulated using a variety of commands - get(), set(), make(), move, delete, etc. For example, to set the size of the first character of every paragraph of the front document to 24pt:

te.documents[1].text.paragraphs.size.set(24)

Or to insert a new paragraph at the end of the document:

te.make(
        new=k.paragraph,
        with_data='Hello Again, World\n',
        at=te.documents[1].text.paragraphs.end)