LearningAppleScript

From HerzbubeWiki
Jump to navigation Jump to search

I'm not really keen on learning AppleScript in any depth, it's just that on rare occasions I find myself in a position where an AppleScript program would help me to perform a certain job, and I keep forgetting how the darn thing works. So here are is another learning page that I quickly threw together from reading a couple of sources, most notably the Wikipedia page.


References

  • AppleScript Language Guide. The official Apple documentation. The document has the following prominent disclaimer at the top: "This document is no longer being updated." I have been unable to find newer versions of the document, so I wonder whether Apple is discontinuing AppleScript.
  • Wikipedia


Glossary

AEOM
AppleEvent Object Model. Specifies the objects any particular application "knows".


Tools

  • The Script Editor app that comes with macOS (located in /Applications/Utilities)


AppleScript as a Unix executable

Create a new file with this shebang:

#!/usr/bin/osascript

Give the file the executable bit:

chmod +x /path/to/script


Script structuring

Comments

-- Comment until end of line
# Comment until end of line

(* A
multi-line
comment *)


Multi-line statements

The character ¬, produced by typing Option+Enter in the Script Editor, makes the current statement continue to the next line, just like the backslash character does in a shell script:

log ¬
  "number of elements in aListVariable = " & ¬
  (count of aListVariable)


Variables and constants

Variables

Variables do not need to be declared. They also do not have a type, meaning that the same variable can first hold a value of one type, then set later to hold a value of a different type.


Variables are assigned to either with the set or the copy keyword. Examples:

set aVariable to "foo"
set {aVariable, anotherVariable} to aThirdVariable
copy 1.2345 to aVariable


Constants

TODO: How do I define a constant?

Useful built-in constants:

  • true, false
  • missing value. Can be used as a placeholder for missing or uninitialized information, similar to null in other programming langues. However, a variable that has not been used before does not exist and does not contain the missing value constant - you have to explictly initialize a variable with the constant. To test for the constant, use this: if aVariable is equal to missing value then [...].
  • pi
  • result. When a statement is executed, AppleScript stores the resulting value, if any, in the predefined property result.
  • space, tab, return, linefeed, quote


Control statements

Conditionals

if <condition> then [...]

if <condition> then
  [...]
else if <condition> then
  [...]
else
  [...]
end if


Loops

AppleScript does not have an equivalent of a continue statement.

# Endless loop
repeat
  [...]

  # Terminate the loop. "exit repeat" can be simplified to just "exit".
  if <condition> then exit repeat
end repeat

# Pre-determined number of iterations
repeat 42 times
  [...]
end repeat

# Iterate over a list
repeat with aLoopVariable in aListVariable
  [...]
end repeat

# Conditional loops
repeat while <condition>
  [...]
end repeat
repeat until <condition>
  [...]
end repeat

# for-loop
repeat with aVariable from 0 to 42
  [...]
end repeat
repeat with aVariable from 0 to 42 by 3
  [...]
end repeat


Data types

Booleans

AppleScript knows the usual boolean values true and false.

Booleans have class boolean.


Numbers

AppleScript has integer and real number data types. Examples:

set aVariable to 1
set aVariable to -42
set aVariable to 5.678
set aVariable to 5.678E+10

Numbers have either class integer or real.


Strings

Strings consist of Unicode characters. Literals are enclosed in doublequotes. Strings are concatenated with the & operator. Examples:

set aVariable to "foo"
set aVariable to "foo" & "bar"
set aVariable to "the answer is " & 42   # if left operand is a string, the right operand is coerced into a string

Strings have class text.


Lists

Lists have class list.


Basic literals:

set aListVariable to {}
set aListVariable to {1, 2, "3", 4, 5.6789}
set aListVariable to {1, 2, {3, 4}, 5, 6}


Changing a list:

set beginning of aListVariable to "foo"   # add to the beginning
set end of aListVariable to "foo"         # add to the end


Accessing items:

set aVariable to item 42 of aListVariable


Records

A record is an unordered collection of key/value pairs where the key is a string-like label and the value can have any type. In other programming languages similar concepts are "associative arrays" or dictionaries where the key has the type string.

Note that in the following example the label is not enclosed in doublequotes to form a string.

set aRecord to { foo: "bar", baz: 1.234 }


Records have class record. Records can be concatenated using the & operator.


Operators

A selection of the most useful operators:

  • and, or, not
  • &. Concatenation of strings, records or lists. For records, the properties in the left-hand operand take precedence. Operands that are neither strings, records nor lists are placed into a temporary list with that operand as the single item, then concatenation occurs.
  • =, is equal, equals, equal to, is equal to. Equality. Operands of different type are not coerced but generally treated as not equal. A certain leeway is given to scalar operands such as booleans, integers and reals. For lists the order of items must match. For records the order in which properties appear is not relevant.
  • , is not equal, is not equal to, is not, isn't, isn't equal, isn't equal to, doesn't, does not equal. Inequality.
  • >, >=, <, <=, and similar natural language constructs as shown above for equality/inequality - just try it out.
  • starts with, ends with, and other similar forms. Used to compare the beginning/end of a string or list.
  • contains, is in, does not contain, is not in, and other similar forms. Used to check for containment/non-containment within a string, list or record.
  • mod, div. Modulus and integral division.
  • ^. Exponentiation.
  • as. Coerces its left-hand operand to the class specified on the right-hand side.


Coercion

The as operator can be used to explicitly coerce a value from one type to another. Example:

set aVariable to 42 as text


Implicit coercion occurs when an object is of a different class than was expected.


Locale:

  • When coercing text strings to values of class integer, number, or real, or vice versa, AppleScript uses the current Numbers settings in the Formats pane in International preferences to determine what separators to use in the string.
  • When coercing strings to values of class date or vice versa, AppleScript uses the current Dates settings in the Formats pane.


Functions (aka handlers)

AppleScript calls a function a handler. The following examples show the difference between functions with positional and labelled parameters. Not shown here:

  • There are additional ways to define functions with labelled parameters, with the goal to be able to write code that looks like natural language, but these are too weird/exotic to write down here - refer to Apple's documentation for details.
  • There are also "functions with interleaved parameters", which apparently are useful for bridging into Objective-C since they naturally resemble Objective-C syntax. This is also something not discussed here.
  • Last but not least, a script can have special handlers called "run handlers" (executed when the script is launched), "open handlers" (executed when the script is launched with the "open" command), "idle handlers" (periodically executed when the script is a "stay-open application" and is not executing any other code) and "quit handlers" (executed when the script is a "stay-open application" and is terminated by the user). These are useful for writing AppleScripts that can be considered applications in their own right. I'm not showing these things here because they are out of my scope.


# Declare a function with positional parameters
on aFunctionWithPositionalParameters(parameter1, parameter2, [...])
  [...]

  # Exits the handler and returns the specified value.
  # Specifying a value is optional. TODO: What does this return?
  # If there is no return statement, AppleScript returns the
  # value returned by the last statement.
  return <expression>
end aFunctionWithPositionalParameters

# Declare a function with labelled parameters
on aFunctionWithLabelledParameters given label1:param1, label2:param2, [...]
  [...]
end aFunctionWithLabelledParameters


# Invoke the functions
aFunctionWithPositionalParameters("foo", 1.2345)
aFunctionWithLabelledParameters given label2:"bar", label1:"foo"

# To invoke a function from within a "tell" block, you must use
# the "my" or "of me" keywords
tell application "foo"
  my aFunction()
  aFunction() of me
end tell


Error handling

try
  [...]
on error
  [...]
end try


AppleEvent Object Model

Summary

AppleEvent Object Model = AEOM.


The AEOM allows an application to define human-readable terms for two types of things, so that scripts can be written similarly to natural language:

  • Objects. Examples: document, paragraph, photo, album
  • Actions. Examples: cut, close, select, start slideshow


AEOM in general also defines ways to refer to properties of objects in natural language. For instance, in iPhoto one can refer to the "fourth photo of the events album".


Application dictionaries

The objects and actions supported by an application together make up a dictionary that can be used to script the application. To view the dictionary of an application in Script Editor:

  • Select "File > Open Dictionary"
  • Select the application that you want to script


Keyword "tell"

The keyword tell is used to send an AppleEvent to an object. Less technically, one could say that we want an object to perform a given action. Example:

tell <object> "foo" to <action>

When an object should execute several actions in a row, a tell block can be used as shown in the following example. Within the block, the default target of events/actions is changed and does not need to be specified.

tell <object> "foo"
  <action>
end tell


Keywords "it" and "me"

The keyword it refers to default target of events/actions. This means that a tell block change the value of this keyword.


The keyword me refers to the AppleScript that is currently running. Outside of any tell block, the two keywords refer to the same thing.


The keyword my is a synonym for of me, and the keyword its is a synonym for of it.


Object hierarchy

At the top of an application's object hierarchy is the application itself. These are all the same:

tell application "foo" to quit

tell application "foo"
  quit
end tell

# Special notation that is available only for events in the "Core Suite".
# These are the events: activate, open, reopen, close, print, and quit.
quit application "foo"


Objects further down in the hierarchy can be used either via nested tell blocks, or (as Wikipedia puts it) using nested prepositional phrases. Some of the following examples are stolen from Wikipedia.

tell application "QuarkXPress"
  tell document 1
    tell page 2
      tell text box 1
        set word 5 to "Apple"
      end tell
    end tell
  end tell
end tell

pixel 7 of row 3 of TIFF image "my bitmap"

# Similarly you can use the 's notation instead of the "of" keyword:
tell application "foo"
  set aVariable to first window's name
end tell


Recipes

Diagnostics

Use the log keyword to send output to the logging window of Script Editor. This is a useful diagnostics/debugging technique

Example:

log "it currently is " & (name of it)

Here's a reusable handler that outputs a log message with a time stamp:

# --------------------------------------------------------------------------------
# Logs the specified message using the "log" statement. Prepends the message with
# a time stamp so that it is possible to see how much time has elapsed since the
# previous message.
# --------------------------------------------------------------------------------
on logMessage(message)
	set currentDate to current date
	log ¬
		"" & (short date string of currentDate) ¬
		& ¬
		" " & (time string of currentDate) ¬
		& ¬
		" " & message
end logMessage


Simulate keystrokes

This StackOverflow answer has many examples and also lists many key codes for non-printable characters. The SO answer mentions that the key codes are listed in a file named Events.h, but for the moment I don't know where this file can be found.

Here are some of the most importants examples:

activate application "foo"
tell application "System Events" to keystroke "a"
tell application "System Events" to keystroke "a" using command down
tell application "System Events" to keystroke "a" using {control down, command down}
tell application "System Events" to key code 48


Dialogs, Alerts, Selections

All examples stolen from Wikipedia:

# Dialog
set dialogReply to display dialog "Dialog Text" ¬
	default answer "Text Answer" ¬
	hidden answer false ¬
	buttons {"Skip", "Okay", "Cancel"} ¬
	default button "Okay" ¬
	cancel button "Skip" ¬
	with title "Dialog Window Title" ¬
	with icon note ¬
	giving up after 15

# Alert
set resultAlertReply to display alert "Alert Text" ¬
	as warning ¬
	buttons {"Skip", "Okay", "Cancel"} ¬
	default button 2 ¬
	cancel button 1 ¬
	giving up after 2

# Choose from list
set chosenListItem to choose from list {"A", "B", "3"} ¬
	with title "List Title" ¬
	with prompt "Prompt Text" ¬
	default items "B" ¬
	OK button name "Looks Good!" ¬
	cancel button name "Nope, try again" ¬
	multiple selections allowed false ¬
	with empty selection allowed

# Get result from an interaction method (regardless of whether
# it was a dialog, alert or selection
display alert "Hello, world!" buttons {"Rudely decline", "Happily accept"}
set theAnswer to button returned of the result
if theAnswer is "Happily accept" then
	beep 5
else
	say "Piffle!"
end if


Working with files and folders

Basics:

  • The classes file and POSIX file are references to files and folders. The difference is that file uses HFS paths and POSIX file uses POSIX paths.
  • The class alias can also be used, but the file/folder must exist.
  • The advantage of an alias is that it tracks the file/folder even when it is renamed or moved to a new location, whereas file and POSIX file are a reference by path
  • There are AppleScript built-in commands that can be used to open files for reading or writing. Currently I'm not writing anything about these here.
  • Other filesystem manipulations are generally done using the Finder application


Some code snippets:

createFolder("foo", "/path/to/parent-folder")

# --------------------------------------------------------------------------------
# Creates a new folder with the specified name in the specified parent folder.
# The parent folder path must specified as a POSIX path. The parent folder must
# exist. Spaces or other special characters must not be escaped. Shell'isms such
# as the "~" character (which you might expect to refer to the user's home
# directory) do not work.
# --------------------------------------------------------------------------------
on createFolder(folderName, parentFolderPosixPath)
  # This check exists purely so that we can log a useful message.
  # Without the log message, the script aborts with a useless
  # error message.
  if not my pathExists(parentFolderPosixPath) then
    logMessage("Parent folder does not exist: " & parentFolderPosixPath)
  end if

  # Must appear outside of the tell block, otherwise there's an error.
  # I don't know why, though...
  set parentFolder to POSIX file parentFolderPosixPath
	
  tell application "Finder"
    # If the folder already exists the script aborts with a useful
    # error message
    make new folder at parentFolder with properties {name:folderName}
  end tell
end createFolder

# --------------------------------------------------------------------------------
# Returns true if the specified POSIX path exists. Returns false if the specified
# POSIX path does not exist.
# --------------------------------------------------------------------------------
on pathExists(posixPath)
  tell application "System Events"
    try
      if exists folder posixPath then
        return true
      else
        return false
      end if
    on error
      # The only reason why we would get an error is because
      # the specified path exists but is not a folder, which causes an
      # error when we try to create a "folder" object from
      # the path. Since we don't care about the path's type,
      # we happily return true here.
      return true
    end try
  end tell
end pathExists


Notes on scriptable applications

iPhoto

Events are effectively inaccessible via AppleScript. Some notes on events:

  • If the user selects an event in the events album, then the selection that can be obtained via AppleScript is a list of photos - not an event as one might expect!
  • The children property of the events album is an empty list
  • The first photo in the events album is not necessarily in the top-left event. Probably the first photo is the oldest photo.
  • Programmatically selecting a photo in the events album via AppleScript causes the event that contains the photo to become selected in the UI


Some code snippets:

tell application "iPhoto"
  # Display nothing
  set eventsAlbum to get events album
  set eventsAlbumChildren to children of eventsAlbum
  repeat with anEventsAlbumChild in eventsAlbumChildren
    set childName to name of anEventsAlbumChild
    display dialog childName
  end repeat

  # Includes the events album and some of the other special albums
  set photoLibraryAlbum to get photo library album
  set photoLibraryAlbumChildren to children of photoLibraryAlbum
  display dialog (name of photoLibraryAlbum) & ", count = " & (count of photoLibraryAlbumChildren)

  # Unsuccessful attempt to understand what "local root albums" are
  set localRootAlbums to get local root albums
  log "number of localRootAlbums = " & (count of localRootAlbums)
  repeat with localRootAlbum in localRootAlbums
    log "name of localRootAlbum = " & name of localRootAlbum
  end repeat

  set allAlbums to get every album
  log "number of allAlbums = " & (count of allAlbums)

  set theSelection to get selection
  log "class of theSelection = " & class of item 1 of theSelection
  log "number of items in theSelection = " & (count of theSelection)
end tell

# Play around with changing the selection via simulating keystrokes
activate application "iPhoto"
repeat 10 times
  tell application "System Events" to key code 124 # RightArrow
  tell application "System Events" to keystroke code 124 # RightArrow
  tell application "iPhoto"
    set theSelection to get selection
    log "number of items in the selection = " & (count of theSelection)
    log "class of item 1 of theSelection = " & class of item 1 of theSelection
  end tell
end repeat


# Incomplete and failed attempt to simulate user keystrokes to select the
# first event in the events album, regardless of where the selection
# currently is.
tell application "iPhoto"
  # Makes sure to select the events album if it is not currently selected
  set eventsAlbum to get events album
  select eventsAlbum

  set theSelection to get selection
  # The selection is always a list, so we need to examine one of its items
  set itemOneOfTheSelection to item 1 of theSelection

  if (itemOneOfTheSelection = eventsAlbum) then
    # If the selection still consists of the events album, then
    # no event is selected in the detail view
    activate application "iPhoto"

    # Send Tab key to set focus on detail view
    # The problem is that the focus may already be in the detail view - in that case
    # sending the Tab key sets the focus to the album tree on the left-hand side
    tell application "System Events" to key code 48  # Tab
    # This works!
    tell application "System Events" to key code 124 # send RightArrow key to select the first event
  else
    # If the selection consists of a photo, then an
    # event is selected in the detail view
    activate application "iPhoto"

    # Send Home key to select the first event
    # This does not work! Sending Home has no effect. What might possibly
    # work is to send ArrowUp key codes until the selection no longer changes
    # (in which case we are at the top row of events), then ArrowLeft key codes
    # until the selection no longer changes (in which case we are at the top-left
    # event).
    activate application "iPhoto"
    tell application "System Events" to key code 115  # Home
end tell