├── Date.scptd └── Contents │ ├── Info.plist │ └── Resources │ ├── Date.sdef │ ├── Scripts │ └── main.scpt │ └── description.rtfd │ └── TXT.rtf ├── File.scptd └── Contents │ ├── Info.plist │ └── Resources │ ├── File.sdef │ ├── Scripts │ └── main.scpt │ └── description.rtfd │ └── TXT.rtf ├── List.scptd └── Contents │ ├── Info.plist │ └── Resources │ ├── List.sdef │ ├── Scripts │ └── main.scpt │ └── description.rtfd │ └── TXT.rtf ├── Number.scptd └── Contents │ ├── Info.plist │ └── Resources │ ├── Number.sdef │ ├── Scripts │ └── main.scpt │ └── description.rtfd │ └── TXT.rtf ├── Objects.scptd └── Contents │ ├── Info.plist │ └── Resources │ ├── Objects.sdef │ ├── Scripts │ └── main.scpt │ └── description.rtfd │ └── TXT.rtf ├── TODOs.txt ├── TestTools.scptd └── Contents │ ├── Info.plist │ └── Resources │ ├── Script Libraries │ └── TestSupport.scpt │ ├── Scripts │ └── main.scpt │ ├── TestTools.sdef │ ├── bin │ └── osatest │ └── description.rtfd │ └── TXT.rtf ├── Text.scptd └── Contents │ ├── Info.plist │ └── Resources │ ├── Scripts │ └── main.scpt │ ├── Text.sdef │ └── description.rtfd │ └── TXT.rtf ├── TypeSupport.scptd └── Contents │ ├── Info.plist │ └── Resources │ ├── Scripts │ └── main.scpt │ └── description.rtfd │ └── TXT.rtf ├── Web.scptd └── Contents │ ├── Info.plist │ └── Resources │ ├── Scripts │ └── main.scpt │ ├── Web.sdef │ └── description.rtfd │ └── TXT.rtf ├── bin └── asdiff └── unittests ├── Date.unittest.scpt ├── File.unittest.scpt ├── List.unittest.scpt ├── Number.unittest.scpt ├── Objects.unittest.scpt ├── Text.unittest.scpt ├── TypeSupport.unittest.scpt └── Web.unittest.scpt /Date.scptd/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.apple.ScriptEditor.id.library.Date 7 | CFBundleName 8 | Date 9 | CFBundleShortVersionString 10 | 1.0 11 | OSAScriptingDefinition 12 | Date.sdef 13 | WindowState 14 | 15 | bundleDividerCollapsed 16 | 17 | bundlePositionOfDivider 18 | 585 19 | dividerCollapsed 20 | 21 | eventLogLevel 22 | 2 23 | name 24 | ScriptWindowState 25 | positionOfDivider 26 | 493 27 | savedFrame 28 | -49 33 884 744 0 0 1280 777 29 | selectedTab 30 | result 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Date.scptd/Contents/Resources/Date.sdef: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | By default, the format date command converts a date object to canonical ISO8601-formatted text, for example:

24 | 25 |
format date (date "Thursday, 7 January 2016 at 01:41:34")
 26 | → "2016-01-07T01:41:34Z"
27 | 28 |

Unlike the date text used by AppleScript’s date TEXT specifier, where the language and format changes according to each user’s current locale and time zone settings, ISO8601-formatted date text has a standard structure that is widely recognized and does not vary, making it highly portable. This allows ISO8601-style date text to be safely stored as plain text, sent to users in other countries, and converted back to a date object anywhere at any time, without risk of errors or incorrect results due to language differences, ambiguous ordering, or unknown time zone.

29 | 30 |

By default, dates are formatted for Universal Time (a.k.a. GMT). If the time zone parameter is given, the date is formatted for that time zone instead:

31 | 32 |
format date (date "Thursday, 7 January 2016 at 01:41:34") time zone (5 * hours) 
 33 | → "2016-01-07T06:41:34+05:00"
 34 | 
 35 | format date (date "Thursday, 7 January 2016 at 01:41:34") time zone "Africa/Addis_Ababa"
 36 | → "2016-01-07T04:41:34+03:00"
37 | 38 |

To format a date for the host machine’s local time zone, use the current time zone command to retrieve the local time zone’s name first:

39 | 40 |
format date (current date) time zone (current time zone)
41 | 42 |

For information on formatting dates using custom formats, see the Customizing Date Formats section below.

43 | ]]> 44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | By default, the parse date command creates a date object from canonical ISO8601-formatted text, for example (assuming the user’s current time zone is GMT):

64 | 65 |
parse date "2016-01-07T00:41:34-0800"
 66 | → date "Thursday, 7 January 2016 at 08:41:34" (note: AS dates always display local time)
67 | 68 |

For information on formatting dates using custom formats, see the Customizing Date Formats section below.

69 | ]]> 70 |
71 | 72 |
73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | For example, to split a date object into a date component record using the current time zone (which for these examples is assumed to be “America/New_York”):

100 | 101 |
split date (date "Friday, 1 January 2016 at 12:00:00")
102 | → {class:date component record, year:2016, month:January, day:1, hours:0, minutes:0, seconds:0, timezone:"America/New_York"}
103 | 104 |

To split the date into a record that describes the same date and time information using Greenwich Mean Time (which is 5 hours ahead of America/New_York):

105 | 106 |
split date (date "Friday, 1 January 2016 at 12:00:00") time zone "GMT"
107 | → {class:date component record, year:2016, month:1, day:1, hours:5, minutes:0, seconds:0, timezone:"GMT"}
108 | 109 | ]]> 110 |
111 |
112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | The following command creates a date object describing 7:45am, New York time, on June 15, 1990: 124 | 125 |
join date {year:1990, month:June, day:15, hours:7, minutes:45, seconds:0, timezone:"America/New_York"}
126 | → date "Friday, 15 June 1990 at 07:45:00" (assuming the current time zone is GMT+5)
127 | 128 |

All record properties are optional, thus the following creates a date object describing midnight local time on January 1, 2016:

129 | 130 |
join date {year:2016} 
131 | → date "Friday, 1 January 2016 at 00:00:00"
132 | 133 |

If the date components record does not contain a timezone property then it is interpreted as local time by default. The optional time zone parameter can be used to specify a different default time zone if necessary. The following example creates a date object describing midnight in New York on January 1, 2016:

134 | 135 |
join date {year:2016} time zone "America/New_York"
136 | 137 |

If the local time zone is “GMT”, AppleScript will display the resulting date object as follows:

138 | 139 |
→ date "Friday, 1 January 2016 at 05:00:00" (GMT is 5 hours ahead of New York)
140 | 141 |

Or, if the local time zone is “America/Los_Angeles”:

142 | 143 |
→ date "Thursday, 31 December 2015 at 17:00:00" (Los Angeles is 3 hours behind New York)
144 | 145 | ]]> 146 |
147 |
148 | 149 | 150 | 151 | 152 | { class : date component record, 154 | year : integer, 155 | month : integer or constant, 156 | day : integer, 157 | hours : integer, 158 | minutes : integer, 159 | seconds : integer 160 | timezone: text or integer } 161 | 162 |

All properties are optional. The month property can be a month constant (January, February, etc) or integer. The timezone property should be a time zone ID or offset to GMT in seconds. Other properties must be integers.

163 | ]]> 164 |
165 |
166 | 167 |
168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | locale info for "en_GB" in language "de_DE" 211 | → { localeIdentifier:"en_GB", 212 | localeName:"Englisch (Vereinigtes Königreich)", 213 | languageCode:"en", 214 | countryCode:"GB"} 215 | ]]> 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | time zone info for "GMT" in language "de_DE" 241 | → { timeZoneID:"Europe/London", 242 | secondsToGMT:0, 243 | standardName:"Mittlere Greenwich-Zeit", 244 | standardAbbreviation:"GMT", 245 | daylightSavingName:"Britische Sommerzeit", 246 | daylightSavingAbbreviation:"BST", 247 | isDaylightSaving:false, 248 | daylightSavingOffset:0, 249 | nextDaylightSavingTransition:date "Sunday, 27 March 2016 at 02:00:00" } 250 | ]]> 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | The time zone offset command returns different results depending on whether none, one, or both parameters are given:

271 | 272 |
    273 |
  • If no parameters are given (i.e. both are missing value), the result is the number of seconds between the current time zone and GMT (equivalent to time zone offset to "GMT"):

    274 | 275 |
    (time zone offset) / hours → 3.0
  • 276 | 277 |
  • If one parameter is given, the result is the number of seconds between that time zone and the current time zone. For example, if the current time zone is “Africa/Addis_Ababa”:

    278 | 279 |
    (time zone offset from "Asia/Shanghai") / hours → 5.0
    280 | 
    281 | (time zone offset from "America/New_York") / hours → -8.0
    282 | 
    283 | (time zone offset to "America/New_York") / hours → 8.0
  • 284 | 285 |
  • If both parameters are given, the result is the number of seconds between the two time zones:

    286 | 287 |
    (time zone offset from "Africa/Addis_Ababa" to "GMT") / hours 
    288 | → 3.0 (Addis Ababa is 3 hours, or 10800 seconds, ahead of Greenwich Mean Time)
    289 | 
    290 | (time zone offset from "America/New_York" to "Africa/Addis_Ababa") / hours
    291 | → -8.0 (New York is 8 hours behind Addis Ababa)
  • 292 |
293 | 294 |

Time zones can also be given as offsets to GMT in seconds (e.g. Central Europe is 1 hour, or 3600 seconds, ahead of GMT):

295 | 296 |
(time zone offset from (1 * hours) to "America/New_York") / hours → 6.0
297 | 298 | ]]> 299 |
300 |
301 | 302 |
303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | In addition to parsing and formatting ISO8601-style date text, the parse date and format date commands allow custom formats to be specified via their optional using parameters. These parameters accept either a text value containing special character-based date format patterns, or one or a pair of predefined constants representing commonly used formats.

312 | 313 |

For example, the default ISO8601 date format is defined by the pattern “yyyy-MM-dd'T'HH:mm:ssZZ”, where each letter sequence describes a particular date field, except for the “T” letter which is wrapped in single quotes (the escape character) so appears unchanged.

314 | 315 |

For a complete list of supported patterns, see Appendix F: Date Format Patterns of Unicode Technical Standard #35. Commonly used patterns are listed below for convenience.

316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 |
ValueFormatPatternExampleNotes
year number y 2016
year 2-digit number yy 16
year 4-digit number yyyy 2016
month number M 9
month 2-digit number MM 09
month abbreviated name MMM Sept [1]
month full name MMMM September [1]
day number d 6
day 2-digit number dd 06
weekday abbreviated name eee Tues [2]
weekday full name eeee Tuesday [2]
hour number (1-12) h 7
hour 2-digit number (01-12) hh 07
hour 2-digit number (00-23) HH 07
minute number m 5
minute 2-digit number mm 05
second number s 0
second 2-digit number ss 00
period AM or PM a AM
timezone number ZZ -0800
timezone full name zzzz Pacific Daylight Time [2]
345 | 346 |

[1] If the parse date or format date command’s for locale parameter is “none” (the default), the “MMM” and “MMMM” patterns will use a generic “Mnn” representation instead of the month’s actual name, e.g. “M09” instead of “September”. To parse/format the month name using a particular language, pass the appropriate locale ID via the for locale parameter. For example, pass “current” to use the language specified by the user’s current locale, “en_US” for US English, “fr_FR” for French, and so on.

347 | 348 |

[2] As with month names, to parse and format weekday and time zone names using a particular language, use the for locale parameter to specify the appropriate locale.

349 | 350 |

The parse date and format date commands also accept the following predefined constants which are convenient when presenting date information to users:

351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 |
ConstantBasic Pattern [3]Example
canonical date format yyyy-MM-dd'T'HH:mm:ssZZ 2016-09-06T070109-0800
short date format dd/MM/y 06/09/2016
medium date format d MMM y 6 Sept 2016
long date format d MMMM y 6 September 2016
full date format EEEE, d MMMM y Tuesday, 6 September 2016
short time format HH:mm 07:05
medium time format HH:mm:ss 07:05:00
long time format HH:mm:ss z 07:05:00 PT
full time format HH:mm:ss zzzz 07:05:00 Pacific Daylight Time
368 | 369 |

[3] Except for canonical date format, which is locale- and language-independent, the exact pattern and language used automatically adjusts for the specified locale. For example, if the locale is “none” (the default), the short date format uses the pattern “yyyy-MM-dd” (e.g. 2016-09-06); if the locale is “en_US” then it uses a US-style pattern, “M/d/y” (e.g. 9/6/16); if it is “en_GB” then “dd/MM/y” is used (as shown above); and so on.

370 | 371 | 372 | [TO DO: examples of use] 373 | 374 | ]]> 375 |
376 | 377 |
378 | 379 | 380 | 381 | 382 |
383 | -------------------------------------------------------------------------------- /Date.scptd/Contents/Resources/Scripts/main.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alldritt/applescript-stdlib/fe693369d826a25c02570dd00cbacdc7299c847c/Date.scptd/Contents/Resources/Scripts/main.scpt -------------------------------------------------------------------------------- /Date.scptd/Contents/Resources/description.rtfd/TXT.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf460 2 | {\fonttbl} 3 | {\colortbl;\red255\green255\blue255;} 4 | } -------------------------------------------------------------------------------- /File.scptd/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.apple.ScriptEditor.id.library.File 7 | CFBundleName 8 | File 9 | CFBundleShortVersionString 10 | 1.0 11 | OSAScriptingDefinition 12 | File.sdef 13 | WindowState 14 | 15 | bundleDividerCollapsed 16 | 17 | bundlePositionOfDivider 18 | 616 19 | dividerCollapsed 20 | 21 | eventLogLevel 22 | 0 23 | name 24 | ScriptWindowState 25 | positionOfDivider 26 | 823 27 | savedFrame 28 | 142 33 865 1024 0 0 1920 1057 29 | selectedTab 30 | log 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /File.scptd/Contents/Resources/File.sdef: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Examples:

22 | 23 |
convert path "/Users/jsmith/User Guide.txt" to HFS path format
 24 | → "Macintosh HD:Users:jsmith:User Guide.txt"
 25 | 
 26 | convert path "Macintosh HD:Users:jsmith:User Guide.txt" from HFS path format
 27 | → "/Users/jsmith/User Guide.txt"
 28 | 
 29 | convert path "/Users/jsmith/User Guide.txt" to alias file object
 30 | → alias "Macintosh HD:Users:jsmith:User Guide.txt"
 31 | 
 32 | set theFile to POSIX file "/Users/jsmith/User Guide.txt"
 33 | convert path theFile to file URL format -- (a ‘from’ parameter isn't needed here)
 34 | → "file:///Users/jsmith/User%20Guide.txt"
35 | ]]> 36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 62 | 63 | 64 | 65 | Examples:

67 | 68 |
normalize path "/Users/jsmith/Pictures//./../Movies/"
 69 | → "/Users/jsmith/Movies"
 70 | 
 71 | normalize path "~/Movies/" with tilde expansion
 72 | → "/Users/jsmith/Movies"
 73 | 
 74 | normalize path "Applications" with absolute expansion
 75 | → "/Applications" (the exact result depends on the current working directory)
 76 | 
 77 | normalize path "./Music/iTunes/"
 78 | → "Music/iTunes"
79 | 80 |

This command normalizes a path by performing the following steps as needed:

81 | 82 |
    83 |
  • expanding the initial tilde expression, if tilde expansion is true
  • 84 | 85 |
  • expanding a relative path to an absolute path relative to the current working directory, if absolute expansion is true
  • 86 | 87 |
  • removing the initial component of “/private/var/automount”, “/var/automount”, or “/private” from the path, if the result still indicates an existing file or folder (checked by consulting the file system)
  • 88 | 89 |
  • reducing empty components and references to the current folder (that is, the sequences “//” and “/./”) to single path separators
  • 90 | 91 |
  • removing the trailing slash from the last component.
  • 92 |
93 | 94 |

For absolute paths, it also resolves references to the parent folder (that is, the component “..”) to the real parent folder if possible. For relative paths, references to the parent folder are left in place.

95 | 96 |

If a relative path is given and absolute expansion is true but the current working directory is unknown, an error (-1728) will occur.

97 | ]]> 98 |
99 |
100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | Examples:

112 | 113 |
join path {"/", "Users", "jsmith", "Desktop"} 
114 | → "/Users/jsmith/Desktop"
115 | 
116 | join path {"Documents", "ReadMe"} using file extension "txt" 
117 | → "Documents/ReadMe.txt"
118 | 
119 | join path {POSIX file "/Users/jsmith", "Documents/ReadMe.txt"} 
120 | → "/Users/jsmith/Documents/ReadMe.txt"
121 | 122 |

To construct an absolute path, the first item should be/start with "/" (the first item may also be an alias or file object); the remaining items are always treated as relative paths.

123 | 124 |

If the using file extension parameter is not empty, it will be added to the end of the last path component, ignoring any trailing slashes. A period separator (.) will be inserted automatically; do not supply it yourself.

125 | ]]> 126 |
127 |
128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | Examples:

139 |
set {folderPath, fileName} to split path "/Users/jsmith/Documents/ReadMe.txt"
140 | → {"/Users/jsmith/Documents", "ReadMe.txt"}
141 | 
142 | split path (POSIX file "/Users/jsmith/Documents/ReadMe.txt") at all components 
143 | → {"/", "Users", "jsmith", "Documents", "ReadMe.txt"}
144 | 
145 | split path "/Users/jsmith/Documents/ReadMe.txt" at file extension 
146 | → {"/Users/jsmith/Documents/ReadMe", "txt"}
147 | ]]> 148 |
149 |
150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 |
160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | For example, to read a UTF8-encoded plain text file:

173 | 174 |
read from file (alias "Macintosh HD:Users:jsmith:Documents:welcome.txt")
175 | → "Hello!"
176 | 177 |

For convenience, the read from file command also accepts a POSIX path string as its direct parameter:

178 | 179 |
read from file "/Users/jsmith/Documents/welcome.txt"
180 | 181 |

While nowadays UTF8 is the preferred encoding for plain text files, the read from file command can also read plain text files written in a variety of older legacy encodings. For example, to read a plain text file that was written in the legacy MacOSRoman encoding:

182 | 183 |
read from file myFile using MacOSRoman encoding
184 | 185 |

Or to read the file using the host machine's default legacy encoding (equivalent to Standard Additions' read myFile as string): 186 | 187 |

read from file myFile using primary encoding
188 | 189 |

The read from file command can also be used to read AppleScript values previously written to file using write to file or Standard Additions' write command. For example, to read a data file containing an AppleScript record:

190 | 191 |
read from file "/Users/jsmith/script.data" as record
192 | → {name: "Bob", age:42}
193 | ]]> 194 |
195 |
196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | For example, to write a UTF8-encoded plain text file:

208 | 209 |
write to file (alias "Macintosh HD:Users:jsmith:Documents:welcome.txt") data "Hello!"
210 | 211 |

For convenience, the write to file command also accepts a POSIX path string as its direct parameter:

212 | 213 |
write to file "/Users/jsmith/Documents/welcome.txt" data "Hello!"
214 | 215 |

The read from file command can also write plain text files in a variety of legacy text encodings. For example, to write a plain text file in the legacy MacOSRoman encoding:

216 | 217 |
write file myFile data theText using MacOSRoman encoding
218 | 219 |

Or to write the file using the host machine's default legacy encoding (equivalent to Standard Additions' write theText to myFile as string): 220 | 221 |

write to file myFile data theText using primary encoding
222 | 223 |

The write to file command can also be used as a convenient shortcut for Standard Additions' write command to write AppleScript values to file. For example, to write a data file containing an AppleScript record:

224 | 225 |
write to file "/Users/jsmith/script.data" data {name: "Bob", age:42} as record
226 | 227 | ]]> 228 |
229 |
230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 |
268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | Useful in AppleScript-based shell scripts to obtain the shell's current working directory. For example, the following shell script processes one or more file paths supplied as arguments or, if no arguments are given, the current working directory’s path instead:

279 | 280 |
#!/usr/bin/osascript
281 | 
282 | use script "File"
283 | 
284 | to run pathsList
285 |   if pathsList is {} then set pathsList to {(current working directory)'s POSIX path}
286 |   repeat with pathRef in pathsList
287 |     set aFile to (normalize path pathRef's contents with ¬
288 |         tilde expansion, absolute expansion and link expansion) as POSIX file
289 |     -- process the file...
290 |   end repeat
291 | end run
292 | 293 |

Throws error number -1728 if the current working directory is unknown.

294 | ]]> 295 |
296 | 297 |
298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | If the user interaction option is false, the command will wait until the previous process has finished writing to the script’s stdin before returning. If it is true, it will return a line of text (minus the trailing linefeed) as soon as the Return key is pressed, or ‘missing value’ if there is no more text to be read.

315 | ]]> 316 |
317 |
318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | Here is a simple demonstration of an interactive shell script, written in AppleScript:

327 | 328 |
#!/usr/bin/osascript
329 | 
330 | use script "File"
331 | 
332 | write to standard output "What is your name?"
333 | set userName to read from standard input with user interaction
334 | if userName is "" then set userName to "stranger"
335 | write to standard output "Hello, " & userName & ". How are you feeling today?"
336 | set userMood to read from standard input with user interaction
337 | if userMood contains "happy" or userMood contains "good" then
338 |   write to standard output "I’m delighted to hear that."
339 | else
340 |   write to standard output "C’est la vie."
341 | end if
342 | ]]> 343 |
344 |
345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | The following basic example demonstrates how parse command line arguments works:

363 | 364 |
set argv to {"-f", "plain", "-r", "/Users/jsmith/file1.rtf", "~/file2.rtf"}
365 | 
366 | parse command line arguments argv ¬
367 |     options {¬
368 |       {propertyName:"fileFormat", shortName:"f"}, ¬
369 |       {propertyName:"outputFolder", shortName:"o", valueType:file, defaultValue:missing value}, ¬
370 |       {propertyName:"replacesExistingFiles", shortName:"r", valueType:boolean}} ¬
371 |     arguments {¬
372 |       {propertyName:"fileList", valueType:file, isList:true}}
373 | 374 |

Given a list of shell command arguments (argv), parse command line arguments reads each item in the list, initially checking it against a list of supported options (in this case -f, -o, and -r), then against a list of supported arguments once all of the given options have been processed. On completion, the result is a record of all supported options and arguments, with the supplied values converted to the correct types and default values provided when optional options/arguments are omitted:

375 | 376 |
{ fileFormat:"plain", 
377 |   outputFolder:missing value,
378 |   replacesExistingFiles:true, 
379 |   fileList:{file "Macintosh HD:Users:jsmith:file1.rtf",
380 |             file "Macintosh HD:Users:jsmith:file2.rtf"}, 
381 |   |help|:false, 
382 |   |version|:false}
383 | ]]> 384 |
385 |
386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | Each option definition consists of two or more the following record properties:

411 | 412 |
{ propertyName : text,
413 |   shortName : text,
414 |   longName : text,
415 |   valueType : type or list of text,
416 |   isList : boolean,
417 |   defaultValue : any,
418 |   valuePlaceholder : type,
419 |   valueDescription : text }
420 | 421 |

The record must contain a propertyName property, plus a shortName and/or longName property. All other properties are optional.

422 | 423 |
424 |
propertyName
425 |
The name of the property that will appear in the generated options record. Property names must be unique.
426 |
shortName
427 |
A single character to be used as the option’s short name. Do not include a leading hyphen as this will be added automatically. Short names must be unique.
428 |
longName
429 |
A single word to be used as the option’s long name (camelCase words and hypenated-words are also acceptable). Do not include leading hyphens as these will be added automatically. Long names must be unique.
430 |
valueType
431 |
If the option is given, the type of its value (default: text). This should be one of: boolean, integer, real, number, text, alias, file. If boolean is used, the option does not take a value but instead changes the option’s value from false to true (or the opposite of defaultValue, if given). Alternatively, if a list of text values is given (e.g. {"play", "pause", "skip"}), this is treated as an enumeration of allowed values, in which case the given option must be one of the values in this list.
432 |
isList
433 |
If true, the option may appear any number of times and will appear in the options record as a list of the specified type. If false, a user error will be reported if the same option appears more than once. (default: false)
434 |
defaultValue
435 |
The default value that will be added to the generated options record if the option is not given; this may be missing value or any of the supported value types. If omitted, and the option is not a boolean flag, a user error will be reported indicating the value is required. (default: error)
436 |
valuePlaceholder
437 |
If given, a single all-uppercase word that will appear in the option description generated by format command line help. If omitted, a default placeholder is chosen according to valueType (INT, NUM, TEXT, FILE). Used by format command line help only.
438 |
valueDescription
439 |
If given, a single paragraph of text that will appear in the option description generated by format command line help. Used by format command line help only.
440 |
441 | 442 |

If the options parameter doesn’t include -h/--help and/or -v/--version definitions, default definitions will be added automatically.

443 | ]]> 444 |
445 |
446 | 447 | 448 | 449 | 450 | Each argument definition consists of one or more of the following record properties:

452 | 453 |
{ propertyName : text,
454 |   valueType : type or list of text,
455 |   isList : boolean,
456 |   defaultValue : any,
457 |   valuePlaceholder : type,
458 |   valueDescription : text }
459 | 460 |

Argument definition properties have the same meaning as those in option definition records, except that they apply to any arguments that remain after the options have been parsed. The propertyName property is required, and should be different to the property names used by the option definitions. If the last argument definition record has an isList:true property, a list of all remaining arguments is assigned to that property.

461 | ]]> 462 |
463 |
464 | 465 | 466 |
467 | 468 | 469 | 470 | 471 | 472 | How an AppleScript-based shell script works 475 | 476 |

An AppleScript-based shell script is normally saved as an uncompiled plain text file without an .applescript extension, made executable by running a chmod +x SCRIPT on it, and placed into a directory that appears on the user’s $PATH (e.g. /usr/local/bin) so that it can be executed as a shell command directly from Terminal.

477 | 478 |

For convenience, here is a standard template for creating AppleScript-based shell scripts:

479 | 480 |
#!/usr/bin/osascript
481 | 
482 | use script "File"
483 | 
484 | property _version : "xx.xx.xx" -- the script’s version (e.g. "1.2.0")
485 | property _summary : "a one-line summary of the command’s purpose"
486 | property _description : "detailed instructions for use, examples, etc."
487 | 
488 | property _optionDefinitions : { ... }
489 | 
490 | property _argumentDefinitions : { ... }
491 | 
492 | on run argv -- called by osascript
493 |   -- argv : list of text -- any command-line arguments supplied by user
494 |   set parameterRecord to parse command line arguments argv ¬
495 |       options _optionDefinitions arguments _argumentDefinitions
496 |   if parameterRecord's help then -- output help text
497 |     return format command line help ¬
498 |         summary _summary description _description ¬
499 |         options _optionDefinitions arguments _argumentDefinitions 
500 |   else if parameterRecord's |version| then -- output version text
501 |     return _version
502 |   else -- perform the command and output its result, if any
503 |     return runCommand(parameterRecord)
504 |   end if
505 | end run
506 | 
507 | to runCommand(params)
508 |   -- params : record -- the processed option and argument values, ready to use
509 |   -- your code goes here...
510 | end runCommand
511 | 512 |

The _version property should contain a text value representing the script's current version, typically written as "majorUpdate.minorUpdate.bugFix". (Version numbers written this way are easily compared using a considering punctuation and numeric strings ... end considering block.) The version number is automatically written to standard output when the shell script is executed with the -v (--version) flag.

513 | 514 |

The _summary property contains a one-line summary that appears at the top of the help text generated when the -h (--help) option is given, which the _description property can contain any additional information to append to the help text.

515 | 516 |

The _optionDefinitions and _argumentDefinitions properties describe any options and/or arguments that can be passed to the command, and are used both to convert the values passed by the shell to their AppleScript equivalents (text, numbers, booleans, etc) and to generate the corresponding help text when the -h (--help) option is used.

517 | 518 |

The run handler requires no modification, as all it does is receive the list of raw arguments supplied by the shell command, converts it into a record of ready-to-use AppleScript objects, then deals with it automatically if a -h (--help) option or the -v (--version) option was given or else passes it to the runCommand handler for your own code to process normally.

519 | 520 | 521 |

Example shell script: itunes-remote

522 | 523 |

The following AppleScript-based shell script allows basic control of the iTunes application from Terminal:

524 | 525 |
#!/usr/bin/osascript
526 | 
527 | use script "File"
528 | use script "Text"
529 | 
530 | property _version : "1.0.0"
531 | property _summary : "Control iTunes from the command line."
532 | property _description : "Examples of use:
533 | 
534 |   $ itunes-remote -v
535 |   1.0.0
536 |   
537 |   $ itunes-remote --add 'some file.mp3' --add 'another file.mp3'
538 |   
539 |   $ itunes-remote play
540 |   
541 |   $ itunes-remote -i
542 |   >> skip
543 |   >> pause
544 |   >> info
545 |   Not playing.
546 |   >> exit
547 |   $
548 |   
549 | Interactive mode recognizes the same actions as non-interactive mode 
550 | (“play”, “pause”, etc.) plus “exit” to end the interactive session."
551 | 
552 | property _optionDefinitions : {¬
553 |   {propertyName:"filesToAdd", shortName:"a", longName:"add", ¬
554 |     valueType:alias, isList:true, defaultValue:{}, ¬
555 |     valueDescription:"Import a media file into iTunes."}, ¬
556 |   {propertyName:"isInteractive", shortName:"i", valueType:boolean, ¬
557 |     valueDescription:"Run in interactive mode."}}
558 | 
559 | property _argumentDefinitions : {¬
560 |   {propertyName:"actionName", ¬
561 |     valueType:{"play", "pause", "skip", "info"}, defaultValue:"info", ¬
562 |     valuePlaceholder:"ACTION", valueDescription:"The action to perform."}}
563 | 
564 | on run argv
565 |   set parameterRecord to parse command line arguments argv ¬
566 |       options _optionDefinitions arguments _argumentDefinitions
567 |   if parameterRecord's help then -- output help text
568 |     return format command line help ¬
569 |         summary _summary description _description ¬
570 |         options _optionDefinitions arguments _argumentDefinitions 
571 |   else if parameterRecord's |version| then -- output version text
572 |     return _version
573 |   else -- perform the command and output its result, if any
574 |     return runCommand(parameterRecord)
575 |   end if
576 | end run
577 | 
578 | --
579 | 
580 | to runCommand(params)
581 |   if (length of filesToAdd of params) > 0 then -- process any -a options
582 |     tell application "iTunes" to add (filesToAdd of params)
583 |   end if
584 |   doItunesAction(actionName of params) -- process the given/default argument
585 |   if isInteractive of params then -- was the -i option given?
586 |     repeat -- run the interactive loop
587 |       set actionName to read from standard input with user interaction
588 |       if actionName is "exit" then exit repeat
589 |       doItunesAction(actionName)
590 |     end repeat
591 |   end if
592 | end runCommand
593 | 
594 | to doItunesAction(actionName)
595 |   if actionName is "play" then
596 |     tell application "iTunes" to play
597 |   else if actionName is "pause" then
598 |     tell application "iTunes" to pause
599 |   else if actionName is "skip" then
600 |     tell application "iTunes" to next track
601 |   else if actionName is "info" then
602 |     tell application "iTunes"
603 |       if player state is playing then
604 |         set trackInfo to {name, artist, time} of current track
605 |         write to standard output ¬
606 |             (format text "Now playing “\\1” by \\2 (\\3)." using trackInfo)
607 |       else
608 |         write to standard output "Not playing."
609 |       end if
610 |     end tell
611 |   else
612 |     write to standard output "Unknown action."
613 |   end if
614 | end doItunesAction
615 | 616 |

To use this script, save it as an uncompiled text file named itunes-remote, make it executable (chmod +x itunes-remote), and place it into a directory on the shell's search path (e.g. /usr/local/bin.

617 | ]]> 618 |
619 |
620 | 621 |
-------------------------------------------------------------------------------- /File.scptd/Contents/Resources/Scripts/main.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alldritt/applescript-stdlib/fe693369d826a25c02570dd00cbacdc7299c847c/File.scptd/Contents/Resources/Scripts/main.scpt -------------------------------------------------------------------------------- /File.scptd/Contents/Resources/description.rtfd/TXT.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf460 2 | {\fonttbl} 3 | {\colortbl;\red255\green255\blue255;} 4 | } -------------------------------------------------------------------------------- /List.scptd/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.apple.ScriptEditor.id.library.List 7 | CFBundleName 8 | List 9 | CFBundleShortVersionString 10 | 1.0 11 | OSAScriptingDefinition 12 | List.sdef 13 | WindowState 14 | 15 | bundleDividerCollapsed 16 | 17 | bundlePositionOfDivider 18 | 568 19 | dividerCollapsed 20 | 21 | eventLogLevel 22 | 2 23 | name 24 | ScriptWindowState 25 | positionOfDivider 26 | 523 27 | savedFrame 28 | 249 33 867 744 0 0 1280 777 29 | selectedTab 30 | log 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /List.scptd/Contents/Resources/List.sdef: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | To insert a value (or values) at a specific point in the list, either the before item or after item parameter should be given. For example:

17 | 18 |
insert into list {1, 2, 3} value 8 before item 2 → {1, 8, 2, 3}
 19 | 
 20 | insert into list {1, 2, 3} value 8 after item -3 → {1, 8, 2, 3}
 21 | 
 22 | insert into list {1, 2, 3} value {7, 8, 9} after item 2 → {1, 2, {7, 8, 9}, 3}
 23 | 
 24 | insert into list {1, 2, 3} value {7, 8, 9} after item 2 with concatenation → {1, 2, 7, 8, 9, 3}
25 | 26 |

If neither before item nor after item parameters are given, the insert into list automatically appends the new value(s) to the end of the list:

27 | 28 |
insert into list {1, 2, 3} value 4 → {1, 2, 3, 4}
29 | 30 |

The before item or after item parameter must be a valid index, with the following special exceptions:

31 | 32 |
    33 |
  • 34 |

    To make the after item parameter insert the new value(s) before the first item in the list, use after item 0 or after item -N where N = the number of items in list + 1:

    35 | 36 |
    insert into list {1, 2, 3} value 8 after item 0 → {8, 1, 2, 3}
     37 | insert into list {1, 2, 3} value 8 after item -4 → {8, 1, 2, 3}
    38 |
  • 39 | 40 |
  • 41 |

    To make the before item parameter insert the new value(s) after the last item in the list, use before item 0 or after item N where N = the number of items in list + 1:

    42 | 43 |
    insert into list {1, 2, 3} value 8 before item 0 → {1, 2, 3, 8}
     44 | insert into list {1, 2, 3} value 8 before item 4 → {1, 2, 3, 8}
    45 |
  • 46 |
47 | 48 |

Any other out-of-range index will result in error -1728.

49 | ]]> 50 |
51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | To delete a single item from the list, pass its index via the item parameter:

63 | 64 |
delete from list {1, 2, 3, 4, 5} item 1 → {2, 3, 4, 5}
 65 | 
 66 | delete from list {1, 2, 3, 4, 5} item -2 → {1, 2, 3, 5}
67 | 68 |

(Tip: You can also use get rest of theList to remove the first item from a list.)

69 | 70 |

To delete a range of items from the list, use the from item and/or to item parameters:

71 | 72 |
delete from list {1, 2, 3, 4, 5} from item 2 to item 4 → {1, 5}
 73 | 
 74 | delete from list {1, 2, 3, 4, 5} to item 3 → {4, 5}
75 | 76 |

If no index is specified, the last item in the list is automatically removed:

77 | 78 |
delete from list {1, 2, 3, 4, 5} → {1, 2, 3, 4}
79 | 80 |

If the start index is greater than the end index, the list is copied but no items are removed:

81 | 82 |
delete from list {1, 2, 3, 4, 5} from item 4 to item 2 → {1, 2, 3, 4, 5}
83 | 84 |

If the list is empty or an index is out of range, error -1728 will occur.

85 | ]]> 86 |
87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | The using parameter's script object must implement a handler named mapItem that takes a value as its input and returns a new value as its output. The following example demonstrates how to map a list of numbers to create a new list where each item is the square of its original value:

97 | 98 |
script SquareNumbers
 99 |   to mapItem(aValue)
100 |     return aValue ^ 2
101 |   end mapItem
102 | end script
103 | 
104 | map list {1, 2, 3, 4, 5} using SquareNumbers → {1, 4, 9, 16, 25}
105 | ]]> 106 |
107 |
108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | The using parameter's script object must implement a handler named reduceItem that takes two values as its input and returns a new value as its output. The following example demonstrates how to reduce a list of numbers to the sum of all its items:

117 | 118 |
script SumNumbers
119 |   to reduceItem(partialResult, aValue)
120 |     return partialResult + aValue
121 |   end reduceItem
122 | end script
123 | 
124 | reduce list {5, 1, 9, 3} using SumNumbers → 18
125 | ]]> 126 |
127 |
128 | 129 | 130 | 131 | 132 | 133 | 134 | For example:

136 | 137 |
remove duplicates from list {"A", "b", "c", B", "E", "b"} → {"A", "b", "c", "E"}
138 | 139 |

Be aware that this command uses AppleScript’s standard is in operator to check if each item has previously appeared in the list. When working with lists of text, for example, you may want to wrap the remove duplicates from list command in an appropriate considering/ignoring block to ensure predictable behavior whenever that code is executed. For example, wrapping the previous command in a considering case block alters the way in which text items are compared:

140 | 141 |
considering case
142 |   remove duplicates from list {"A", "b", "c", B", "E", "b"} → {"A", "b", "c", "B", "E"}
143 | end considering
144 | 145 |

(Similarly, when working with lists of real numbers, be aware that two fractional numbers that appear identical can sometimes compare as false due to the limited accuracy of that data type. For example, 0.7 * 0.7 ≠ 0.49(!) due to tiny imprecisions in the CPU’s floating point math calculations.)

146 | ]]> 147 |
148 |
149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | This command works much like get items i thru j of theList, except that it returns an empty list if the start index is greater than the end index, and doesn’t throw an error if an index is out of range. (Index 0 still raises error -1728 though as it is not a valid AppleScript list index.) For example:

159 | 160 |
slice list {"a", "b", "c", "d", "e"} from item 4 → {"d", "e"}
161 | 
162 | slice list {"a", "b", "c", "d", "e"} from item 2 to item -2 → {"b", "c", "d"}
163 | 
164 | slice list {"a", "b", "c", "d", "e"} from item 3 to item 3 → {"c"}
165 | 
166 | slice list {"a", "b", "c", "d", "e"} to item 2 → {"a", "b"}
167 | 
168 | slice list {"a", "b", "c", "d", "e"} from item 10 to item 15 → {}
169 | 
170 | slice list {"a", "b", "c", "d", "e"} from item 4 to item 3 → {}
171 | ]]> 172 |
173 |
174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | The transpose list command treats a list of lists as a 2D matrix, rearranging it so that rows become columns and columns become rows.

188 | 189 |
transpose list {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}} → {{1, 4, 7}, {2, 5, 8}, {3, 6, 9}}
190 | 191 |

This command is particularly useful when getting large amounts of data from scriptable applications, as it is much quicker to ask the application to get property of every element than to get every element, then iterate over the returned list of references asking for each element’s properties one at a time. For example, to obtain name, album, and artist information for every track in iTunes, then rearrange it into an easier-to-use form:

192 | 193 |
tell application "iTunes"
194 |   tell every track
195 |     set allNames to its name
196 |     set allAlbums to its album
197 |     set allArtists to its artist
198 |   end tell
199 | end tell
200 | set trackInfo to {allNames, allAlbum, allArtists}
201 | -- trackInfo is a list of form {{name, name, ...}, {album, album, ...}, {artist, artist, ...}}
202 | 
203 | set trackInfo to transpose list trackInfo
204 | -- trackInfo is now a list of form {{name, album, artist}, {name, album, artist}, ...}
205 | ]]> 206 |
207 |
208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | For example, to create a list containing numbers 1 to 100 in random order:

223 | 224 |
set orderedList to {}
225 | repeat with i from 1 to 100
226 |   set end of orderedList to i
227 | end repeat
228 | 
229 | set unorderedList to unsort list orderedList
230 | ]]> 231 |
232 |
233 | 234 |
235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | The using parameter's script object must implement a handler named filterItem that takes a value as its input and returns true or false indicating whether or not the item should appear in the output list. The following example demonstrates how to filter a list of numbers to obtain a new list containing only the items that are greater than 18:

247 | 248 |
script IsOverEighteen
249 |   to filterItem(aValue)
250 |     return aValue > 18
251 |   end filterItem
252 | end script
253 | 
254 | filter list {12, 23, 17, 22, 14} using IsOverEighteen → {23, 22}
255 | ]]> 256 |
257 |
258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | find in list {"A", "b", "c", B", "E", "b"} value "b" → {2, 4, 6} 271 | 272 | find in list {"A", "b", "c", B", "E", "b"} value "b" returning first occurrence → {2} 273 | 274 | find in list {"A", "b", "c", B", "E", "b"} value "b" returning first occurrence → {2} 275 | 276 |

The value parameter uses AppleScript’s standard is equal to operator to compare each list item to the specified value, adding the item’s index to the result list if true. As with remove duplicates from list, remember that AppleScript’s is equal to operation is subject to additional rules and restrictions when comparing certain types. For example, wrapping the previous command in a considering case block alters the way in which text items are compared:

277 | 278 |
considering case
279 |   find in list {"A", "b", "c", B", "E", "b"} value "b" → {2, 6}
280 | end considering
281 | 282 |

For more complex tests than simple comparison, the using parameter can be used to supply a custom matching handler. Like the filter list command’s using parameter, this takes a script object containing a filterItem handler that takes the item to check as its sole parameter and returns true or false to indicate a match or non-match. For example:

283 | 284 |
script IsOverEighteen
285 |   to filterItem(aValue)
286 |     return aValue > 18
287 |   end filterItem
288 | end script
289 | 
290 | find in list {12, 23, 17, 22, 14} using IsOverEighteen returning last occurrence → {4}
291 | ]]> 292 |
293 |
294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 |
302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | By default, the sort list command sorts a list of numbers, text, or dates and returns a new list of values in ascending order; for example:

314 | 315 |
sort list {2, 7, 4, 1, 9, 4} → {1, 2, 4, 4, 7, 9}
316 | 
317 | sort list {"Mary White", "Bob Green", "Sue Black"} → {"Bob Green", "Mary White", "Sue Black"}
318 | 319 |

The sort list command’s sorting behavior can be customized by supplying a ‘sort comparator’ script object as its using parameter. A sort comparator must define the following handlers:

320 | 321 |
322 | 323 |
makeKey(anItem)
324 |

Given an item from the list to be sorted, this handler should convert it into a form suitable for use in the object’s compareKeys handler, and return the result. Called once for each item in the list.

325 |
    326 |
  • anItem : anything
  • 327 |
  • Result: anything
  • 328 |
329 |
330 | 331 |
compareKeys(key1, key2)
332 |

Given any two keys previously generated by the makeKey handler above, compare them against each other as appropriate and return a negative, zero, or positive number that indicates the order in which they should appear. The corresponding items in the original list are then reordered the same way.

333 |
    334 |
  • key1 : anything
  • 335 |
  • key2 : anything
  • 336 |
  • Result: integer – return -1 (or other negative number) to indicate key 1 comes before key 2, +1 (or other positive number) to indicate key 1 comes after key 2, or 0 to indicate both keys are equivalent
  • 337 |
338 |
339 |
340 | 341 |

The List library includes commands for constructing a variety of basic sort comparator objects, or you can define your own as needed. For example, to sort a list of playing card values, aces (“A”) low:

342 | 343 |
use script "List"
344 | use script "Number"
345 | 
346 | script PlayingCardComparator
347 | 	
348 |   property _rank : {"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"}
349 |   
350 |   to makeKey(anItem)
351 |     -- return a number indicating the given card’s rank
352 |     return find in list _rank value (anItem as text) returning first occurrence
353 |   end makeKey
354 |   
355 |   to compareKeys(key1, key2)
356 |     -- use the  Number library’s ‘cmp’ command to compare any two card rank numbers
357 |     return cmp {key1, key2} -- returns -1, 0, or +1 to indicate ordering
358 |   end compareKeys
359 | end script
360 | 
361 | 
362 | sort list {"3", "Q", "10", "K", "2", "4", "J", "A"} using PlayingCardComparator
363 | 
364 | → {"A", "2", "3", "4", "10", "J", "Q", "K"}
365 | ]]> 366 |
367 |
368 | 369 | 370 | 371 | 372 | 373 | When sorting a list of values, it is essential that all values are compared against each other in the same way as the rules by which AppleScript, say, compares two text values are very different to the rules by which it compares two numbers.

375 | 376 | 377 | [TO DO: finish documenting behavior: default comparator used by `sort list` and usually sufficient for simple sorting ops; given a list of all number/all text/all date items, it sorts them using standard < and > operators: text will be ordered according to standard Unicode ordering rules, subject to current considering/ignoring options; numbers and dates from lowest to highest; mixed type lists are rejected (as there's no way to know e.g. if a number and a text value should both be compared as numbers or as sequences of characters) - to sort those as a specific type, use one of the others] 378 | 379 |

When creating a default comparator in your own code, avoid reusing it across multiple sorts, as once it is used to sort a list of one type (e.g. list of text), it cannot be used to sort a list of another (e.g. list of number).

380 | ]]> 381 |
382 |
383 | 384 | 385 | 386 | 387 | Sort a list that contains date objects.

389 | 390 | [TO DO: finish] 391 | ]]> 392 |
393 |
394 | 395 | 396 | 397 | 398 | For example:

400 | 401 |
sort list {7.4, -25, 0.03, 912, 0, -0.01} using (number comparator)
402 | → {-25, -0.01, 0, 0.03, 7.4, 912}
403 | 404 |

When given a list of numerical text values, the number comparator coerces each item to a number before comparing them, ensuring that the resulting list is ordered numerically rather than by character order: 405 | 406 |

sort list {"22", "5", "17", "-6", "-9"} -- sort by comparing characters
407 | → {"-6", "-9", "17", "22", "5"}
408 | 
409 | sort list {"22", "5", "17", "-6", "-9"} using (number comparator) -- sort by comparing numeric values
410 | → {"-9", "-6", "5", "17", "22"}
411 | 412 |

Be aware that when given a list of decimal text values to sort, e.g. {"3.14", "-1.2", "0.09", ...}, AppleScript coerces each text item to a number according to the current user’s localization settings. For example, a US-localized machine expects numerical text to use periods as decimal separators, e.g. "3.14", whereas a German-localized machine uses commas, e.g. "3,14", so will fail if given "3.14" instead. If you need precise control of text-to-number conversions, use the Number library’s parse number command, for example, to sort a list of German-style numerical text:

413 | 414 |
use script "List"
415 | use script "Number"
416 | 
417 | to makeNumericalTextComparator(decimalSeparator)
418 |   script NumericalTextComparator
419 |     property parent : number comparator
420 |     
421 |     to makeKey(anItem)
422 |       if anItem's class is text then
423 |         -- use Number library to parse real numbers consistently
424 |         return parse number anItem using ¬
425 |             {basicFormat:decimal format, decimalSeparator:decimalSeparator}
426 |       else
427 |         return continue makeKey(anItem)
428 |       end if
429 |     end makeKey
430 |   end script
431 | end makeNumericalTextComparator
432 | 
433 | 
434 | sort list {"3,14", "99", "4,2", "-0,01"} using my makeNumericalTextComparator(",")
435 | → {"-0,01", "3,14", "4,2", "99"}
436 | ]]> 437 |
438 |
439 | 440 | 441 | 442 | 443 | 444 | This command returns an object that controls how the ‘sort list’ command sorts a list of text values. This object performs two operations when ‘sort list’ is called:

446 |
    447 |
  1. It coerces each item in the list to text. If any item cannot be coerced to text, the `sort list` command will report a coercion error (-1700). This value is only used to determine sorting order; it will not appear in the final list.
  2. 448 |
  3. It compares a pair of text keys to determine which item should come first, and rearranges the items in the original list as needed.
  4. 449 |
450 | 451 |

The optional for parameter can customize text comparison behavior as follows:

452 |
    453 |
  • exact ordering – Case, diacriticals, hyphens, punctuation, and white space are all considered, while numeric strings are ignored (i.e. matched character for character).
  • 454 |
  • case insensitive ordering – Case and numeric strings are ignored, while all other attributes are considered.
  • 455 |
  • current ordering – Text is sorted using whatever considering/ignoring settings are currently applied when sort list is called. (By default, AppleScript ignores case and numeric strings, and considers diacriticals, hyphens, punctuation, and white space. To alter these settings – or to guarantee predictable sorting at all times – wrap your sort list command in the appropriate considering and/or ignoring block.)
  • 456 |
457 | 458 |

For example:

459 | 460 |
sort list {"Fu", "Foo", "FOO", "foo", "fOO", "fu", "FU"} ¬
461 |     using (text comparator for exact ordering)
462 | → {"foo", "fOO", "Foo", "FOO", "fu", "Fu", "FU"}
463 | 464 |

(Be aware that text values are ordered intelligently according to standard Unicode rules; thus text items that are identical except for case are grouped together, with lowercase characters coming first.)

465 | 466 |

Be aware that when AppleScript coerces numbers and dates to text, the results can vary according to the current user’s localization settings; coercing lists to text likewise produces different results depending on AppleScript’s current text item delimiters. This may affect sorting order when sorting lists of mixed types, e.g. sort list {"3.1.4", 3.2} using text comparator will return {3.2, "3.1.4"} on systems that use commas as decimal separators, as 3.2 coerces to "3,2", and comma characters are ordered before periods. If this is a concern, define a custom comparator object whose makeKey handler converts non-text values to text in a consistent format, for example:

467 | 468 |
script MixedTextAndNumberComparator
469 |   property parent : text comparator
470 |   
471 |   to makeKey(anItem)
472 |     if anItem's class is real then
473 |       -- use Number library to format real numbers consistently
474 |       return format number anItem using decimal format
475 |     else
476 |       return continue makeKey(anItem)
477 |     end if
478 |   end makeKey
479 | end script
480 | ]]> 481 |
482 | 483 |
484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | For example, to sort a list of lists of uniform type, in this case numbers:

503 | 504 |
sort list {{3}, {1, 1, 5}, {}, {2, 1}, {2}, {1, 3}, {1, 2}} ¬
505 |     using (list comparator for number comparator)
506 | → {{}, {1, 1, 5}, {1, 2}, {1, 3}, {2}, {2, 1}, {3}}
507 | 508 |

It is also possible to sort a list of lists of mixed types by specifying exactly which items to sort on and how each one should be compared. For example, consider the following list where each sublist is of form {name, married, age}:

509 | 510 |
set myList to { ¬
511 |     {"Bob", false, 33}, ¬
512 |     {"Jane", true, 25}, ¬
513 |     {"Andi", true, 33}}
514 | 515 | To sort this first by comparing item 3 of each sublist numerically, then by comparing item 1 of each sublist as text:

516 | 517 |
sort list myList using (list comparator for { ¬
518 |     {itemIndex:3, itemComparator: number comparator}, ¬
519 |     {itemIndex:1, itemComparator: text comparator}})
520 | 521 |

The resulting list of lists is ordered first by ages then by names:

522 | 523 |
→ {{"Jane", true, 25}, {"Andi", true, 33}, {"Bob", false, 33}}
524 | 525 |

A ListComparator object can also be used to sort a list of records by extending its standard makeKey handler to first extract the property values on which to sort into a list:

526 | 527 |
script AgeAndNameComparator
528 |   (* Comparator object for sorting a list of records of form: 
529 |   
530 |        {{name:TEXT, ..., age:NUMBER}, ...},
531 |      
532 |      Records are ordered first by age, then by name. 
533 |      
534 |      Any other properties are ignored.
535 |   *)
536 | 
537 |   -- Create a list comparator that orders on a 2-item key list of form {NUMBER, TEXT}...
538 |   property parent : list comparator for {number comparator, text comparator}
539 |     
540 |   to makeKey(aRecord)
541 |     -- ... then extend its existing `makeKey` handler to extract each  record’s
542 |     -- age and name properties into the corresponding {NUMBER, TEXT} key list:
543 |     return continue makeKey({age, name} of aRecord)
544 |   end makeKey
545 |     
546 | end script
547 | 
548 | 
549 | set myList to { ¬
550 |     {name:"Bob", married:false, age:33}, ¬
551 |     {name:"Jane", married:true, age:25}, ¬
552 |     {name:"Andi", married:true, age:33}}
553 | 
554 | sort list myList using AgeAndNameComparator
555 | 
556 | → {{name:"Jane", married:true, age:25}, 
557 |     {name:"Andi", married:true, age:33}, 
558 |     {name:"Bob", married:false, age:33}}
559 | ]]> 560 |
561 |
562 | 563 | 564 | 565 | 566 | { itemIndex : integer, 568 | itemComparator: script } 569 | ]]> 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | For example, to sort a list of numbers in descending order:

580 | 581 |
sort list {2, 7, 4, 1, 9, 4} using (reverse comparator for number comparator)
582 | → {9, 7, 4, 4, 2, 1}
583 | ]]> 584 |
585 |
586 | 587 |
588 | 589 |
-------------------------------------------------------------------------------- /List.scptd/Contents/Resources/Scripts/main.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alldritt/applescript-stdlib/fe693369d826a25c02570dd00cbacdc7299c847c/List.scptd/Contents/Resources/Scripts/main.scpt -------------------------------------------------------------------------------- /List.scptd/Contents/Resources/description.rtfd/TXT.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf460 2 | {\fonttbl} 3 | {\colortbl;\red255\green255\blue255;} 4 | } -------------------------------------------------------------------------------- /Number.scptd/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.apple.ScriptEditor.id.library.Number 7 | CFBundleName 8 | Number 9 | CFBundleShortVersionString 10 | 1.0 11 | OSAScriptingDefinition 12 | Number.sdef 13 | WindowState 14 | 15 | bundleDividerCollapsed 16 | 17 | bundlePositionOfDivider 18 | 0.0 19 | dividerCollapsed 20 | 21 | eventLogLevel 22 | 0 23 | name 24 | ScriptWindowState 25 | positionOfDivider 26 | 574 27 | savedFrame 28 | 222 33 698 744 0 0 1280 777 29 | selectedTab 30 | log 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Number.scptd/Contents/Resources/Number.sdef: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | By default, the format number command converts an integer/real number to numeric text in canonical format. Unlike coercing an integer or real value to text, which formats the text according to the current user’s localization settings, format number always uses the same numerical syntax as the AppleScript language, unless additional formatting and/or locale settings are explicitly given.

22 | 23 |

For example, on a US-localized system, coercing 3.14 to text produces "3.14":

24 | 25 |
3.14 as text → "3.14" (localized conversion)
 26 | 
 27 | format number 3.14 → "3.14" (canonical conversion)
28 | 29 |

On a German system, however, the same number-to-text coercion uses a comma instead of a period as the decimal separator:

30 | 31 |
3.14 as text → "3,14" (localized conversion)
 32 | 
 33 | format number 3.14 → "3.14" (canonical conversion)
34 | 35 |

Using the format number command instead of coercing the number to text ensures a consistent result, no matter where the script is run.

36 | 37 |

For information on formatting numbers using custom formats, see the Customizing Number Formats section below.

38 | ]]> 39 |
40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | By default, the parse number command converts numeric text in canonical format to an integer/real number. Unlike coercing a text value to number, which parses the text according to the current user’s localization settings, format number always uses the same numerical syntax as the AppleScript language, unless additional formatting and/or locale settings are explicitly given.

56 | 57 |

For example, on a US-localized system, coercing "3.14" to a number produces 3.14:

58 | 59 |
"3.14" as number → 3.14 (localized conversion)
 60 | 
 61 | parse number "3.14" → 3.14 (canonical conversion)
62 | 63 |

On a German system, however, the same text-to-number coercion requires the decimal separator to be a comma, not a period:

64 | 65 |
"3.14" as number → Error: Can’t make "3.14" into type number.
 66 | 
 67 | "3,14" as number → 3.14 (localized conversion)
 68 | 
 69 | parse number "3.14" → 3.14 (canonical conversion)
70 | 71 |

Using the parse number command instead of coercing the number to text ensures a consistent result, no matter where the script is run.

72 | 73 |

For information on parsing numbers using custom formats, see the Customizing Number Formats section below.

74 | ]]> 75 |
76 |
77 | 78 | 79 | 80 | 81 | { class : number format record, 83 | basicFormat : constant or text, 84 | minimumDecimalPlaces : integer, 85 | maximumDecimalPlaces : integer, 86 | minimumSignificantDigits : integer, 87 | maximumSignificantDigits : integer, 88 | decimalSeparator : text, 89 | groupingSeparator : text, 90 | roundingBehavior : constant } 91 | 92 |

The basicFormat property is required and should be one of the format constants accepted by the format number and parse number commands (canonical number format, integer format, etc) or format text. All other properties are optional. The roundingBehavior property accepts the same rounding constants used in the round number command (rounding up, rounding down, etc).

93 | 94 | [TO DO: examples?] 95 | 96 | ]]> 97 |
98 |
99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | For example:

124 | 125 |
format hex 526 width 4 with prefix → "0x020E"
126 | 
127 | format hex {1, 11, 6, 13, 0, 8} width 2 → "010B060D0008"
128 | 129 |

If the ‘width’ parameter is given, the hexadecimal value will be padded to that number of digits (not including sign or prefix) unless the number is too large to represent within that number of hexadecimal digits, in which case an error is raised instead:

130 | 131 |
format hex 526 width 2 with prefix
132 | → error: Number is too large to represent as 2-digit hexadecimal text 
133 |    (not between -256 and 255).
134 | ]]> 135 |
136 |
137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | For example:

150 | 151 |
parse hex "-0x020E" → -526
152 | 
153 | parse hex "010B060D0008" width 2 → {1, 11, 6, 13, 0, 8}
154 | 155 |

The parse text will return a non-fractional real number if a hexadecimal value is too large to represent using AppleScript’s native 30-bit integer type, normally raising an error if it can’t be accurately represented as a real number either (although this may be overridden if some loss of precision is acceptable):

156 | 157 |
parse hex "0xFFFF" -- 2^16-1
158 | → 65535
159 | 
160 | parse hex "0xFFFFFFFF" -- 2^32-1 
161 | →- 4.294967295E+9
162 | 
163 | parse hex "0xFFFFFFFFFFFFFFFF" -- 2^64-1 
164 | → error: Hexadecimal text is too large to convert to number without losing precision.
165 | 
166 | parse hex "0xFFFFFFFFFFFFFFFF" with precision loss 
167 | → 1.84467440737096E+19 (approximate only)
168 | 169 | ]]> 170 |
171 |
172 | 173 |
174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | Examples:

197 |
abs 3.1 → 3.1
198 | 
199 | abs -3.1 → 3.1
200 | 201 | ]]> 202 |
203 |
204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | Unlike AppleScript's ‘=’ operator, which compares two numbers for exact equality, the cmp command allows a small margin of error (±1.0e-9), so ignores any slight differences due to the limited precision of real (a.k.a. floating point) numbers. For example:

214 | 215 |
(0.7 * 0.7) = 0.49 → false (probably not what you wanted!)
216 | 
217 | cmp {(0.7 * 0.7), 0.49} → 0 (i.e. the numbers are "equal")
218 | 219 | ]]> 220 |
221 |
222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | max {5, 1, 3, -8, 5, 4, -2} → 5 232 | ]]> 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | min {5, 1, 3, -8, 5, 4, -2} → -8 245 | ]]> 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | round number 4.145 to places 2 → 4.14 259 | 260 | round number -8.51 to places 1 → -8.6 by rounding away from zero 261 | 262 | round number 7949.0 to places -2 → 7900 263 | ]]> 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 |
279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | In addition to parsing and formatting numeric text in canonical AppleScript-style format, the parse number and format number commands allow custom formats to be specified via their optional using parameters. These parameters accept either a text value containing special character-based number format patterns, a predefined constant representing a commonly used format, or a number format record containing a predefined format constant plus additional customizations.

361 | 362 |

For a full explanation of pattern syntax, see Part 3: Numbers of Unicode Technical Standard #35. Recognized patterns are listed below for convenience.

363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 |
Value/PositionFormatPatternNotes
number any digit 0
number digit; zero is omitted #
number digit with rounding 1–9
number significant digit @[1]
number decimal separator .
number grouping separator ,
number minus sign -
numberseparates mantissa and exponent in scientific notationE[2]
exponentprefix positive exponents with localized plus sign+[2]
prefix or suffixmultiply by 100 and show as percentage%
prefix or suffixmultiply by 1000 and show as per mille(\u2030)
prefix or suffixcurrency symbol¤(\u00A4) [3]
prefix or suffixquote-escape special characters'e.g. “'#'”, “o''clock”
prefix or suffix boundaryprecedes a pad character*e.g. “* ###0”
subpattern boundaryseparates positive and negative subpatterns;
425 | 426 | 427 |

[1] For example, “@@” will display a number to 2-significant digits, e.g. “12345” → “12000”. When given, the “0” and “.” patterns are not allowed.

428 | 429 |

[2] The “E” and “+” characters do need not be quote-escaped when they appear in a prefix or suffix.

430 | 431 |

[3] Use “¤” for currency symbol, “¤¤” for international currency symbol, or “¤¤¤” for the long form of the decimal symbol. If given, monetary decimal and grouping separators (if available) are used instead of the numeric ones.

432 | 433 | 434 | [TO DO: include summary of template text syntax plus examples] 435 | ]]> 436 |
437 | 438 |
439 | 440 |
-------------------------------------------------------------------------------- /Number.scptd/Contents/Resources/Scripts/main.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alldritt/applescript-stdlib/fe693369d826a25c02570dd00cbacdc7299c847c/Number.scptd/Contents/Resources/Scripts/main.scpt -------------------------------------------------------------------------------- /Number.scptd/Contents/Resources/description.rtfd/TXT.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf460 2 | {\fonttbl} 3 | {\colortbl;\red255\green255\blue255;} 4 | } -------------------------------------------------------------------------------- /Objects.scptd/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.apple.ScriptEditor.id.library.Objects 7 | CFBundleName 8 | Objects 9 | CFBundleShortVersionString 10 | 1.0 11 | OSAScriptingDefinition 12 | Objects.sdef 13 | WindowState 14 | 15 | bundleDividerCollapsed 16 | 17 | bundlePositionOfDivider 18 | 684 19 | dividerCollapsed 20 | 21 | eventLogLevel 22 | 2 23 | name 24 | ScriptWindowState 25 | positionOfDivider 26 | 800 27 | savedFrame 28 | 538 34 982 1023 0 0 1920 1057 29 | selectedTab 30 | log 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Objects.scptd/Contents/Resources/Objects.sdef: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Unlike AppleScript records, which are predefined groups of properties whose values are identified by keyword or identifier, dictionary objects are dynamic collections of key-value pairs that can be added and removed at any time. Dictionary keys can be text, numbers, dates, and/or type and constant symbols.

15 | 16 | 17 |

Examples

18 | 19 |

The following script uses a dictionary to store and retrieve RGB color values by name:

20 | 21 |
use script "Objects"
 22 | 
 23 | -- create a new dictionary
 24 | set obj to dictionary collection
 25 | 
 26 | -- add some key-value pairs
 27 | obj's addItem("red", {255, 0, 0})
 28 | obj's addItem("yellow", {255, 255, 0})
 29 | obj's addItem("green", {0, 255, 0})
 30 | obj's addItem("blue", {0, 0, 255})
 31 | 
 32 | -- get the number of items in the collection
 33 | log obj's countItems() --> 4
 34 | 
 35 | -- get the value that is currently stored under the key "green"
 36 | log obj's getItem("green") --> {0, 255, 0}
 37 | 
 38 | -- attempting to get or remove a non-existent item raises an error
 39 | log obj's getItem("banana") -- Error -1728: "Item not found."
40 | 41 | 42 |

Object Commands

43 | 44 |

DictionaryCollection script objects recognize the following commands:

45 | 46 |
47 | 48 |
countItems()
49 |

Count the number of key-value pairs in the collection

50 |
    51 |
  • Result: integer 52 |
53 |
54 | 55 |
containsItem(theKey)
56 |

Does the collection contain a key-value pair with the specified key?

57 |
    58 |
  • theKey : text, number, date, type or constant
  • 59 |
  • Result: boolean
  • 60 |
61 |
62 | 63 |
addItem(theKey, theValue)
64 |

Add a key-value pair to the collection

65 |
    66 |
  • theKey : text, number, date, type or constant
  • 67 |
  • theValue : anything
  • 68 |
69 |
70 | 71 |
removeItem(theKey)
72 |

Remove a key-value pair from the collection

73 |
    74 |
  • theKey : text, number, date, type or constant
  • 75 |
  • Result: anything – the removed value
  • 76 |
77 |
78 | 79 |
getItem(theKey)
80 |

Get the value for the given key from the collection

81 |
    82 |
  • theKey : text, number, date, type or constant
  • 83 |
  • Result: anything – the removed value
  • 84 |
85 |
86 | 87 | 88 |
addDictionary(theDictionary)
89 |

Add another DictionaryObject's key-value pairs to the collection

90 |
    91 |
  • theDictionary : script – the DictionaryObject whose keys and values are to be added
  • 92 |
93 |
94 | 95 |
addKeysAndValues(keyValueList)
96 |

Add a list of key-value pairs to the collection

97 |
    98 |
  • keyValueList : list of list – a list of form: {{KEY, VALUE},...}
  • 99 |
100 |
101 | 102 |
getKeysAndValues()
103 |

Get a list of key-value pairs from the collection

104 |
    105 |
  • Result: list of list – a list of form: {{KEY, VALUE},...}
  • 106 |
107 |
108 | 109 |
getKeys()
110 |

Get a list of keys from the collection

111 |
    112 |
  • Result: list of anything
  • 113 |
114 |
115 | 116 |
getValues()
117 |

Get a list of values from the collection

118 |
    119 |
  • Result: list of anything
  • 120 |
121 |
122 | 123 | 124 |
deleteAllItems() 125 |

Delete all key-value pairs from the collection

126 | 127 | 128 |
copyObject()
129 |

Returns a shallow copy of the object

130 |
    131 |
  • Result: script – a new DictionaryObject containing the same keys and values
  • 132 |
133 |
134 | 135 |
objectDescription()
136 |

Returns a brief description of the object

137 |
    138 |
  • Result: text
  • 139 |
140 |
141 | 142 |
143 | 144 |

Notes

145 | 146 |

Numeric keys are compared for numeric equality (e.g. 1 and 1.0 are the same). Other types of keys, including text-based keys, are compared exactly (e.g. "foo" and "Foo" are different).

147 | 148 | ]]> 149 |
150 |
151 | 152 | 153 | 154 | 155 | 156 | 157 | Unlike a list, where any item can be accessed at any time, a queue is an ordered sequence of items where items can only be added (“pushed”) to the back of the queue and retrieved/removed (“pulled”) from the front – i.e. items are returned in the exact same order as they were added.

160 | 161 | 162 |

Examples

163 | 164 |
use script "Objects"
165 | 
166 | set obj to queue collection
167 | 
168 | obj's addItem("a")
169 | obj's addItem("b")
170 | obj's addItem("c")
171 | log obj's removeItem() --> "a"
172 | 
173 | obj's addItem("d")
174 | log obj's removeItem() --> "b"
175 | log obj's removeItem() --> "c"
176 | log obj's removeItem() --> "d"
177 | 
178 | log obj's countItems() --> 0
179 | log obj's removeItem() -- Error -1728: "Queue is empty."
180 | 181 | 182 |

Object Commands

183 | 184 |

QueueCollection script objects recognize the following commands:

185 | 186 |
187 | 188 |
countItems()
189 |

Count the number of items in the collection

190 |
    191 |
  • Result: integer 192 |
193 |
194 | 195 |
addItem(theValue)
196 |

Push an item onto the back of the queue

197 |
    198 |
  • theValue : anything
  • 199 |
200 |
201 | 202 |
removeItem()
203 |

Pull an item from the front of the queue

204 |
    205 |
  • Result: anything
  • 206 |
207 |
208 | 209 |
getItem()
210 |

Return the item at the front of the queue without removing it

211 |
    212 |
  • Result: anything
  • 213 |
214 |
215 | 216 |
deleteAllItems() 217 |

Delete all items from the collection

218 | 219 | 220 |
copyObject()
221 |

Returns a shallow copy of the object

222 |
    223 |
  • Result: script
  • 224 |
225 |
226 | 227 |
objectDescription()
228 |

Returns a brief description of the object

229 |
    230 |
  • Result: text
  • 231 |
232 |
233 | 234 |
235 | ]]> 236 |
237 |
238 | 239 | 240 | 241 | 242 | 243 | 244 | Unlike a list, where any item can be accessed at any time, a stack is an ordered sequence of items where items can only be added (“pushed”) and retrieved/removed (“popped”) from the top of the stack – i.e. the most recently added item is returned first, and the oldest added item returned last.

247 | 248 | 249 |

Examples

250 | 251 |
use script "Objects"
252 | 
253 | set obj to stack collection
254 | 
255 | obj's addItem("a")
256 | obj's addItem("b")
257 | obj's addItem("c")
258 | log obj's removeItem() --> "c"
259 | 
260 | obj's addItem("d")
261 | log obj's removeItem() --> "d"
262 | log obj's removeItem() --> "b"
263 | log obj's removeItem() --> "a"
264 | 
265 | log obj's countItems() --> 0
266 | log obj's removeItem() -- Error -1728: "Stack is empty."
267 | 268 | 269 |

Object Commands

270 | 271 |

StackCollection script objects recognize the following commands:

272 | 273 |
274 | 275 |
countItems()
276 |

Count the number of items in the collection

277 |
    278 |
  • Result: integer 279 |
280 |
281 | 282 |
addItem(theValue)
283 |

Push an item onto the top of the stack

284 |
    285 |
  • theValue : anything
  • 286 |
287 |
288 | 289 |
removeItem()
290 |

Pop an item from the top of the stack

291 |
    292 |
  • Result: anything
  • 293 |
294 |
295 | 296 |
getItem()
297 |

Return the item at the top of the stack without removing it

298 |
    299 |
  • Result: anything
  • 300 |
301 |
302 | 303 |
deleteAllItems() 304 |

Delete all items from the collection

305 | 306 | 307 |
copyObject()
308 |

Returns a shallow copy of the object

309 |
    310 |
  • Result: script
  • 311 |
312 |
313 | 314 |
objectDescription()
315 |

Returns a brief description of the object

316 |
    317 |
  • Result: text
  • 318 |
319 |
320 | 321 |
322 | ]]> 323 |
324 |
325 | 326 |
327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | Examples 341 | 342 |

The following script uses a timer object to measure the time it takes to create 1000 random numbers:

343 | 344 |
use script "Objects"
345 | use scripting additions
346 | 
347 | set theTimer to timer object
348 | 
349 | set n to 1000
350 | theTimer's startTimer()
351 | repeat n times
352 | 	random number from -99999 to 99999
353 | end repeat
354 | theTimer's stopTimer()
355 | 
356 | set millisecs to theTimer's totalTime() * 1000 div 1
357 | display alert "Created " & n & " random numbers in " & millisecs & "ms." 
358 | 359 | 360 |

Object Commands

361 | 362 |

TimerObject script objects recognize the following commands:

363 | 364 |
365 | 366 |
timerName()
367 |

the timer name, if given

368 |
    369 |
  • Result: text
  • 370 |
371 |
372 | 373 |
startTimer()
374 |

[re]start the timer (this does nothing if the timer is currently running)

375 |
    376 |
  • Result: script – the TimerObject returns itself, allowing timer creation and start commands to be chained for convenience, e.g. (timer object)'s startTimer()
  • 377 |
378 |
379 | 380 |
stopTimer()
381 |

stop the timer (this does nothing if the timer is already stopped)

382 |
    383 |
  • Result: real – the number of seconds elapsed since timer was last started
  • 384 |
385 |
386 | 387 |
elapsedTime()
388 |

returns the number of seconds since running timer was last started

389 |
    390 |
  • Result: real
  • 391 |
392 |
393 | 394 |
totalTime()
395 |

returns the total number of seconds the timer has been running

396 |
    397 |
  • Result: real
  • 398 |
399 |
400 | 401 | 402 |
copyObject()
403 |

Returns a shallow copy of the object

404 |
    405 |
  • Result: script
  • 406 |
407 |
408 | 409 |
objectDescription()
410 |

Returns a brief description of the object

411 |
    412 |
  • Result: text
  • 413 |
414 |
415 | 416 |
417 | ]]> 418 |
419 |
420 | 421 |
422 | 423 | 424 |
-------------------------------------------------------------------------------- /Objects.scptd/Contents/Resources/Scripts/main.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alldritt/applescript-stdlib/fe693369d826a25c02570dd00cbacdc7299c847c/Objects.scptd/Contents/Resources/Scripts/main.scpt -------------------------------------------------------------------------------- /Objects.scptd/Contents/Resources/description.rtfd/TXT.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf460 2 | {\fonttbl} 3 | {\colortbl;\red255\green255\blue255;} 4 | } -------------------------------------------------------------------------------- /TODOs.txt: -------------------------------------------------------------------------------- 1 | TO DO: 2 | 3 | - TestTools contains various TODOs, mostly enhancements, but since that library is currently somewhat hamstrung by AS compiler bug those should be tabled for now 4 | 5 | 6 | - TypeSupport contains various TODOs - enhancements, safety improvements, etc: 7 | 8 | - coercing a record to `any[thing]` throws error -1700(!) due to AS stupidity; need to go through handlers below and check they coerce to `any` where appropriate (i.e. to ensure ASOC objects are converted to their AS equivalents before use), with workarounds as necessary (e.g. in `asList`, to handle the case where the item is a record or specifier) 9 | 10 | - note: make sure `asTYPEParameter` handlers will always accept ASOC objects, coercing to corresponding AS type (i.e. don't add any checks that'd cause them to reject 'ocid' specifiers), e.g. `asNumberParameter` will probably currently fail on ASOC object as it doesn't first coerce to `any`; might be best if all `asTYPEParameter` handlers first coerce to `any` before coercing to actual required type (the only exception would be an `asReferenceParameter` handler, as that needs to typecheck only, as coercing a specifier dereferences it) -- OTOH, coercing a specifier to list is semantically ambiguous (should it dereference in hope of receiving a list?), while AS fails when coercing a record to any 11 | 12 | - should asTextParameter() always throw an error if value is a list? (i.e. avoids inconsistent concatenation results due to TIDs); another option would be to whitelist some or all known 'safe' types (integer/real, text, date, alias/file/furl, etc) and reject everything else; this should ensure stable predictable behavior - even where additional custom coercion handlers are installed (users can still use other types of values, of course; they just have to explicitly coerce them first using `as` operator) 13 | 14 | - `missing value` and other type/constant symbols are currently accepted by asTextParameter, though may coerce to either keyword name text or raw chevron text, which may be a reason for `asTextParameter` to disallow them 15 | 16 | - add more AS-to-ASOC handlers? also include error checking, and provide both `asNSCLASS` and `asNSCLASSParameter` variants 17 | 18 | 19 | - various TODOs in Web 20 | 21 | 22 | - finish tests, documentation 23 | 24 | 25 | ====================================================================== 26 | 27 | The following TODOs are deferred/ignored/rejected: 28 | 29 | 30 | - ideally, these libraries would be owned by Apple and always use all-lowercase four-char codes to avoid any conflicts with 3rd-party (mixed-case) codes; however, changing their dictionaries this way would only be practical if Apple took over these libraries before they were widely distributed, as once they're in the wild it'll be a giant PITA to change their current four-char codes without breaking users' existing scripts) 31 | 32 | 33 | ---------------------------------------------------------------------- 34 | Date 35 | ---------------------------------------------------------------------- 36 | 37 | 38 | - what about NSDateComponentsFormatter, NSDateIntervalFormatter? (e.g. handy for formatting time intervals, although it is possible to kludge that already using date arithmetic) 39 | 40 | - see also NSFormattingContext... constants in NSFormatter.h for fine-tuning capitalization for standalone/start-of-sentence/in-sentence use (TBH, any user who needs this level of control would be better using Cocoa classes directly) 41 | 42 | - what additional info should appear in `locale info` record? (see NSLocale Component Key constants) 43 | 44 | 45 | 46 | ---------------------------------------------------------------------- 47 | File 48 | ---------------------------------------------------------------------- 49 | 50 | 51 | - add support for enumeration type in `parse command line arguments`, where valueType is a list of allowed text values, e.g. {"yes", "no", "maybe"}; allowed chars would be same as for long option names (Q. should it be case sensitive?) 52 | 53 | - Standard Additions' `read` and `write` commands don't always provide good explanatory error messages (e.g. passing wrong `as` parameter just gives 'Parameter error.' -50); Cocoa error messages often aren't very helpful either. Not sure there's anything can be done about this. 54 | 55 | - is there any benefit to adding `with UTF8 byte order mark` option to `write to file`? (NSString automatically adds UTF16/32 BOMs, but doesn't add UTF8 BOM for compatibility's sake; however, users might want to add UTF8 BOM to improve portability of files they intend to distribute) 56 | 57 | - commands for safely creating temp file/dir? (challenge here is doing it securely, via `do shell script`; see: http://developer.apple.com/library/etc/redirect/xcode/mac/1153/documentation/OpenSource/Conceptual/ShellScripting/ShellScriptSecurity/ShellScriptSecurity.html) 58 | 59 | - is there a Cocoa API to fuzzily convert IANA charset names to NSString encodings? if so, would be better than hardcoded list of encoding constants. A. Doesn't appear to be; also, NSStringEncodings aren't a directly interchangeable subset of CFStringEncodings, and since CFString APIs aren't accessible via ASOC there isn't any way to access the additional CF encodings anyway 60 | 61 | - implement Windows path support in `convert path`? (TBH, CFURLCreateWithFileSystemPath and CFURLCopyFileSystemPath don't do a convincing job of this on OS X, so probably best forget about this) 62 | 63 | - any way to determine (via Cocoa) if stdout/stderr is connected to a terminal? if so, get rid of `terminal styles` parameter in `format command line help`? or provide separate `outputs to terminal` command that returns true/false (isStyled should only be true if help text will be displayed in Terminal.app or other VT100 terminal emulator) - A. probably not: isatty() is part of C stdlib, which ASOC can't import AFAIK, and NSFileHandle doesn't provide an equivalent method 64 | 65 | - if isStyled, text should be auto-wrapped to terminal width; Q. any way to get current terminal width, if available? (suspect that's a C call only [e.g. see osatest's terminalColumns() function]; anything in NSUserDefaults?) 66 | 67 | - in `read from file` allow `unknown encoding` to be passed, in which case use NSString's stringWithContentsOfFile:usedEncoding:error:, which tries to determine file's text encoding in various ways (e.g. by checking for BOM or "com.apple.TextEncoding" extended file attribute)? downside of that approach is that it really ought to return the encoding along with text for caller's information, which would make the command's return type inconsistent; another option might be to cheat it and implement a separate `detect encoding` and/or `guess encoding` command that returns just the encoding (ideally along with a value that indicates whether it's a reliable determination based on BOM or extended attribute or a heuristic guess) 68 | 69 | - what is status of alias and bookmark («class bmrk») objects in AS? (the former is deprecated everywhere else in OS X; bmrk objects are poorly supported, with crashing bugs, and rarely appears); what can/should/shouldn't be done to support these? 70 | 71 | 72 | ---------------------------------------------------------------------- 73 | List 74 | ---------------------------------------------------------------------- 75 | 76 | 77 | - implement `ordered comparator for {enum_1, enum_2, ..., enum_N}` that works for any enumeration, including boolean 78 | 79 | 80 | - in _sort(), if resultListObject's _list_'s length>someLargeThreshold then use median of 3 items? (TBH, picking a good midrange pivot is probably least of its problems performance-wise) 81 | 82 | - stable sorting? A. currently out of scope: a non-stable sort is still better than no sort at all, although `sort list` documentation does need to state that it isn't stable sort so items that are 'equal' aren't guaranteed to maintain their original order, e.g. sorting {{name:"Bob", age:33}, {name:"Jan", age:33}} on age only can return either {{name:"Bob", age:33}, {name:"Jan", age:33}} or {{name:"Jan", age:33}, {name:"Bob", age:33}} 83 | 84 | 85 | - what about lists containing lots of duplicates, e.g. when sorting a large list containing only numbers from 0 to 10, or only true/false? basic quicksort gets pathological on those cases, so a three-way quicksort or a mergesort would work much better there 86 | 87 | 88 | - might consider using NSMutableArray when sorting simple uniform lists of text or number without a user-supplied comparator (would need to compare costs of ASOC bridging vs native sorting to see which performs better); not sure about dates given that they're mutable (ASOC will perform deep copy of list, so resulting list will contain copies of the original date objects, whereas native sort preserves the original date objects); complex values (e.g. list of records) will still be sorted natively, of course (though even there it might be possible to use NSArray to sort an array of [key,valueIndex] and then iterate the result to reposition values in output list, making the [slow] AS part of the code O(2n)) 89 | 90 | 91 | - would it be worth implementing an ArrayCollection object that encapsulates list lookup kludges, and just encouraging users to use that for manipulating lists (in which case some/most of the below handlers might be as well made into methods on that); A. out of scope for stdlib: since it's an AS flaw it should be fixed at source, though would be useful as a 3rd-partly library 92 | 93 | 94 | - in `search text`, when matching with TIDs, optionally accept a list of multiple text values to match? (note:TIDs can do that for free, so it'd just be a case of relaxing restriction on 'for' parameter's type when pattern matching is false to accept a list of text as well); also optionally accept a corresponding list of replacement values for doing mapping? (note that map will need to be O(n) associative list in order to support considering/ignoring, although NSDictionary should be usable when matching case-sensitively) 95 | 96 | 97 | ---------------------------------------------------------------------- 98 | Number 99 | ---------------------------------------------------------------------- 100 | 101 | 102 | - what about NSByteCountFormatter, NSEnergyFormatter, NSMassFormatter, NSLengthFormatter, MKDistanceFormatter? Also, is it worth trying to support AS's largely neglected and awkwardly incomplete unit types, or is it better just to leave those to do their own thing (i.e. limited range unit conversions) and deal solely with plain numbers? If the latter, would it be worth provided more comprehensive `convert [length/mass/volume/etc] NUMBER from UNIT to UNIT` commands here, eliminating the need for users to use AS's unit types completely? 103 | 104 | 105 | - what else needs implemented? e.g. atan2? (note that trivial operations such as `hypotenuse`, `square` and `square root` are not implemented as those are simple to do using AS's existing `^` operator (e.g. sqrt(n)=n^0.5), while `floor`, `ceil`, etc. are already covered by `round number` 106 | 107 | 108 | - support `NSNumberFormatterOrdinalStyle`, `NSNumberFormatterCurrencyISOCodeStyle `, etc (10.11+) in `parse/format number`? 109 | 110 | - in `format number`, any way to include "+" sign for exponent, same as AS? (e.g. "1.0E+8" rather than "1.0E8") 111 | 112 | 113 | ---------------------------------------------------------------------- 114 | Objects 115 | ---------------------------------------------------------------------- 116 | 117 | 118 | - worth adding 'maximum size' option to stack and queue constructors? (simplest way to implement it is to define a script object can inherit either StackCollection or QueueCollection [caveat this will obscure original object's name, which users should be able to see when logging/displaying a script object, e.g. for troubleshooting purposes], overriding addItem() to check `my _count` before continuing) 119 | 120 | 121 | - any other data structures worth adding? e.g. PriorityQueueCollection? 122 | 123 | 124 | - decide naming convention for constructors; e.g. `make dictionary object` vs `make dictionary` vs `dictionary object` vs `new dictionary`? (`new ...` might be safest, as 'new' isn't an existing AS keyword, whereas `make ...` will try to to compile as `[make] [identifier]` if the full command isn't found) (note: if sticking to verb-noun format, `create ...` might be an option: while 'create' keyword was used as a synonym for 'make' in some early apps, e.g. FMP, 'create' isn't a keyword in AS dictionary itself); would quite like to get rid of 'object' suffix (which is really only included to make the keyword more 'unique' and less likely to conflict/compile incorrectly) as it isn't used consistently and smacks of restating the obvious; another consideration: comparator constructors use noun-only names, e.g. `text comparator`, which suggests `dictionary collection`, `unique collection`, `queue sequence`, `stack sequence` might be better names for constructors 125 | 126 | 127 | - decide naming convention for object methods: traditional positional parameters or ObjC-style syntax? e.g. `setItem(k, v)` vs `setValue: v forKey: k` (while the former doesn't explicitly describe each parameter, the latter is visually ambiguous and often needs parentheses to avoid being mis-read by both AS and users) 128 | 129 | 130 | 131 | - Is it worth implementing a growable bucket list? or would a balanced B-tree be more efficient? (it would eliminate the significant impact of dynamically growing bucket list, though lookups will be O(log n) rather than ameliorated O(1) [degrades as collisions increase] and inserts will likely be slower due to rebalancing [which requires each traversed node's key to be re-compared]); TBH, the simplest and most efficient option would probably be to use a default size (e.g. 1024) suitable for light-to-medium use, but allow a different size to be optionally passed to constructor command if user needs a particularly large or small collection; however, that isn't very user-friendly, in which case a better option may be to grow by fixed sizes only (e.g. 128->2048->32768) 132 | 133 | 134 | - Is it worth implementing a case-insensitive Dictionary? (client code traditionally normalizes keys (e.g. by lowercasing) prior to adding items, but it may be more user-friendly to do it automatically) 135 | 136 | 137 | - option to specify no. of buckets? (cheaper than growing dynamically, but also less user-friendly); if so, would probably make more sense to use enum rather than int, e.g. `small`/`default`/`large`; however, probably best to try a conservative growing algorithm first (e.g. starts at 256 and grows 4x/8x each time, with additional tweak to pre-grow dictionary before adding a large list of items) and see how well that works in practice (i.e. if auto-growing proves to be 'good enough' then it's best to keep things simple for users and not complicate its API with technical housekeeping crud); alternatively, maybe just stick with 1024-item bucket list as a reasonable compromise (users who need more speed and are only working with basic types can still use NSMutableDictionary if they wish) 138 | 139 | 140 | - if implementing variable size bucket lists, addDictionary() will be faster if records include hashNum, avoiding need to fully re-hash keys 141 | 142 | 143 | - removed `unique collection` for now as it's a quick-n-dirty implementation (just a wrapper around `dictionary collection`) and currently lacks most set functionality (union, intersection, etc). Furthermore, there is a good argument to be made for using NSMutableSet, either directly or in a script object wrapper, as unlike `dictionary collection` it only has to support a few basic AS types - integer, real, text, date - which are all fully bridged and safely round-trippable. (Ideally, type and constant names would also be supported, but NSAppleEventDescriptors aren't hashable and there's probably not much need for it in practice.) The downside of using NSMutableSet is that it breaks autosave and serialization, something these libraries aim to avoid, so might be best left to a third-party library. 144 | 145 | 146 | ---------------------------------------------------------------------- 147 | TestTools 148 | ---------------------------------------------------------------------- 149 | 150 | 151 | - implement line-wrapping in TestSupport.scpt to improve readability when displaying reports in Terminal 152 | 153 | 154 | 155 | ---------------------------------------------------------------------- 156 | Text 157 | ---------------------------------------------------------------------- 158 | 159 | 160 | - extended backreference syntax should really allow backslash-escaped “}” and “\” characters to appear within braces (currently these two characters are disallowed by _tokens pattern) 161 | 162 | 163 | - fix inconsistency: `search text`'s `for` parameter doesn't allow list of text but `split text`'s `using` parameter does, even though both commands are supposed to support same matching options for consistency 164 | 165 | 166 | - should line break normalization (in `normalize text` and `join paragraphs`) also recognize `Unicode line break`, `Unicode paragraph break` constants? 167 | 168 | - need to decide exactly what constitutes a line break in `transform text`, and make sure both paragraph element and pattern based splitting are consistent (e.g. what about form feeds and Unicode line/paragraph breaks?) 169 | 170 | 171 | - 10.11 provides -[NSString stringByApplyingTransform:reverse:]; currently this is only used to convert Unicode codepoint escapes (e.g. "\u1234"), but in future could be expanded on, e.g. `transform text` could add `smart punctuation` and `ASCII punctuation` options, options to convert text between Asian and Latin representations, etc. 172 | 173 | 174 | - add `matching first item only` boolean option to `search text` (this allows users to perform incremental matching fairly efficiently without having to use an Iterator API)? Currently inclined to reject this, as `search text` command is already fairly complex so am reluctant to add any more parameters unless a convincing use case is identified first. 175 | 176 | 177 | - would it be worth implementing a `compare text` command that allows considering/ignoring options to be supplied as `considering`/`ignoring` parameters (considering/ignoring blocks can't be parameterized as they require hardcoded constants) as this would allow comparisons to be safely performed without having to futz with considering/ignoring blocks all the time (c.f. Number library's `compare number`); for extra flexibility, the comparator constructor should also be exposed as a public command, and the returned object implement the same `makeKey`+`compareItems` methods as List library's sort comparators, allowing them to be used interchangeably (one could even argue for putting all comparators into their own lib, which other libraries and user scripts can import whenever they need to parameterize comparison behavior). Currently inclined to reject this: while reliably comparing text in AS is a PITA, don't think having such a command will do much to improve things (end users will likely continue to use standard operators in their own code, and library developers will be better off adding considering/ignoring blocks as required.) 178 | 179 | 180 | - in `_findText`, is it worth switching to a more efficient algorithim when hypens, punctuation, and white space are all considered and numeric strings ignored (the default)? i.e. given a fixed-length match, the endIndex of a match can be determined using `forText's length + startIndex - 1` instead of measuring the length of all remaining text after `text item i`; will need to implement both approaches and profile them to determine if it makes any significant difference to speed 181 | 182 | 183 | - `literal representation` - problem with this is that while it's straightforward enough to use OSAKit running in a background process to format AS values (just wrap the values in a script object and send that to the process via Apple event), ASOC's ocid specifiers can't be sent out of process so any lists/records containing those will cause a serialization error when packed into an AE. While lists could (laboriously) be recursively formatted one item at a time to avoid this, there's no way to iterate records natively (passing them to ASOC won't help as its record-to-NSDictionary conversion screws up a lot of record contents). Add the extra problems of formatting application references and type/constant names (which will appear as raw chevron syntax unless app terminology is explicitly loaded at runtime), and really the only satisfactory solution is for AS to implement this feature as a built-in command. (Alternatively, being able to call OSAKit methods directly on ASOC-hosted script objects might work better with ocids as it'd avoid going out of process, but ASOC doesn't expose public APIs for manipulating those script objects as OSAScript instances.) 184 | 185 | 186 | - `insert into text`, `delete from text` for inserting/replacing/deleting ranges of characters (c.f. `insert into list`, `delete from list` in List library) 187 | 188 | 189 | 190 | ---------------------------------------------------------------------- 191 | TypeSupport 192 | ---------------------------------------------------------------------- 193 | 194 | 195 | - should asPOSIXPath[Parameter] include options for expanding/normalizing path when a text value is given rather than an alias/file/POSIX file? given that AS isn't too clever when passed relative paths, this might be wise (although there is the question of what to do about tildes, as those should only be expanded automatically when processing shell script arguments; Cocoa's stringByStandardizingPaths is excessively aggressive here) 196 | 197 | 198 | - add `isValueListOfType` for checking if value is a *list* of the specified type 199 | 200 | (Note that checking if value is a record of specific form, e.g. `{name:text, isDone:boolean}`, is impractical due to the lack of AS introspection and the inability of ASOC's record-to-NSDictionary conversion to preserve keyword based names, therefore it's not worth trying to implement that.) 201 | 202 | (Be aware that while the `count` command's `each` parameter claims to accept a list of type classes, this is no good for actually determining if all list items are one of those types. It looks like when a list is given it actually counts all the values that can be _coerced_ one of specified types (as per `as` operator enhancement in 10.10), not whether the value is actually one of those types. Thus when checking a list for multiple types, it's necessary to check each item against each type O(nm), as it's possible for two type names to match the same item (e.g. counting each `integer`, then each `real`, then each `text` and summing the result should be ok, but counting each `integer`, then each `number`, then each `real` would result in all integer and real items being counted twice), resulting in an incorrect count when the subtotals for each time are finally added up. While AS's collection type names is quite stable, making it tempting to hack in special-case checks to prevent such overcounts, it's impossible to guarantee new names won't be added in future, so that isn't a safe solution.) 203 | 204 | 205 | - add `canValueCoerceToType` handler that checks if value can [safely?] be coerced to the specified type (Q. does `count {theValue} each {theType}` work same as `theValue as {theType}` by first testing for exact type then testing for coercibility?) 206 | 207 | 208 | - add `isValueOfObjCClass` handler that checks if value is ocid specifier and its class name is specified string? (note: it'd be better to use standard Cocoa methods to check it's a [sub]class of a specific class - checking string-based class names is hardly robust) 209 | 210 | 211 | ---------------------------------------------------------------------- 212 | Web 213 | ---------------------------------------------------------------------- 214 | 215 | 216 | - how to split path components from parameter strings? RFC2396 allows parameters on all path portions (NSURL only allows parameters after final path portion), so probably also need to rework/rewrite `split/join URL` handlers to support this newer form as standard; see also Python's urllib.parse.urlsplit(), which unlike its older NSURL-like urlparse() function doesn't split parameters from path (although Python doesn't provide a parse function for such paths either, presumably leaving it to users to deal with themselves as needed). 217 | 218 | 3.3. Path Component 219 | 220 | The path component contains data, specific to the authority (or the 221 | scheme if there is no authority component), identifying the resource 222 | within the scope of that scheme and authority. 223 | 224 | --path = [ abs_path | opaque_part ] 225 | 226 | --path_segments = segment *( "/" segment ) 227 | --segment = *pchar *( ";" param ) 228 | --param = *pchar 229 | 230 | --pchar = unreserved | escaped | ":" | "@" | "&" | "=" | "+" | "$" | "," 231 | 232 | The path may consist of a sequence of path segments separated by a 233 | single slash "/" character. Within a path segment, the characters 234 | "/", ";", "=", and "?" are reserved. Each path segment may include a 235 | sequence of parameters, indicated by the semicolon ";" character. 236 | The parameters are not significant to the parsing of relative 237 | references. 238 | 239 | 240 | - NSURL's component properties don't appear to support generic resource locators, e.g. given "mailto:foo@example.org", NSURL.path returns nil; may be the case that a vanilla parser implemented according to RFC3986 would be a better solution than using NSURL 241 | 242 | 243 | - in `send HTTP request`: 244 | 245 | -how to support authentication, e.g. tie into keychain? (might be best to leave that for now) 246 | 247 | - requestBodyData/responseBodyType could also be file object/`file`, in which case upload/download tasks could be used (Q. will NSSession supply Content-Length automatically when a file is given? also, will it supply Content-Type automatically, or is there a way to get file's MIME type via Cocoa APIs?) 248 | 249 | - if responseBodyType is `text` and requestHeaders doesn't include "Accept*" headers, add appropriate content negotiation header (e.g. "Accept: text/*") automatically? (what about common application/… headers, e.g. application/json, application/xml? TBH, it'd be a shot in the dark; probably best to leave it entirely to user) 250 | 251 | 252 | 253 | - add `normalize URL` handler? see NSURL's standardizedURL(); need to decide if it's worth putting in for users to call directly (note that `split URL` and anything else that uses TypeSupport's asNSURLParameter() already normalizes URLs automatically; OTOH, `join URL` does not) 254 | 255 | 256 | - what about `split/join URL parameter string`? (see also below) 257 | 258 | 259 | - commands for converting HTML entities? (&/</>/"); what about '? what about non-ASCII entities? (decode command would need to handle all entities; encode command could probably just do required entities which is sufficient for use in Unicode [UTF8] encoded documents, possibly providing a Boolean option to encode all non-ASCII entities should users have to produce non-Unicode documents [though this shouldn't be encouraged]) 260 | 261 | - OTOH, one might argue that if users are dealing with HTML content they should use a proper library that understands and processes all HTML data correctly, and providing commands here for encoding/decoding HTML entities is just encouraging them to hack it (something a stdlib really shouldn't do); given what a mess of complexity it is, might be wisest to leave HTML processing for other libraries to deal with 262 | 263 | - for numeric entities only, see NSString's stringByApplyingTransform:reverse:, using "Any-Hex/XML;Any-Hex/XML10" to convert "􏿿" and "􏿿" (Q. what about HTML entity names? Cocoa's existing ICU transforms aren't much use if they can't convert those too) 264 | 265 | 266 | 267 | ---------------------------------------------------------------------- 268 | Additional libraries 269 | ---------------------------------------------------------------------- 270 | 271 | 272 | - Logging -- commands for logging messages and info at various levels (debug, info, warning, error, fatal), plus configurable, composable objects for filtering+writing logged messages to various destinations (file, email, screen, etc), c.f. Python's `logging` module. 273 | 274 | Note: Big problem with this is that it really needs to be stateful, with a Logging module instance being configured and shared across main script and all its libraries. However, AS's library loader doesn't respect separation between unrelated scripts (it requires applications to instantiate new CIs for every unrelated user script, even though apps - and even NSAppleScript - have spent the last 20 years happily sharing the same CI). Thus while a Logging library would be usable within standalone AS applets and ASOC apps, or with apps that use NSUserAppleScriptTask to run scripts, it would be a usability disaster used within scripts run within attachable apps that run OSA scripts in-process. This alone rules it out for stdlib inclusion, though it might still be of use as a third-party module with its limitations clearly described. 275 | 276 | - Defaults -- commands for interacting with NSUserDefaults. Very problematic for stdlib as retaining ASOC objects between commands breaks script serialization and SE autosave. Plus NSUserDefaults' native API is fairly low level and not very AS-friendly. Plus all data needs to be converted to and from ASOC, with all the quirks, limitations, and silent corruptions that involves (e.g. records lose keyword-based properties, app object specifiers lose target app identifier, etc). Plus live changes to defaults data structures won't be stored automatically. One alternative would be to add simple `read preferences` and `write preferences` to File module that uses StdAdditions read/write commands to read and write records of prefs data explicitly; crude, but might be adequate for AS users' needs. 277 | 278 | - Errors -- get descriptions of Carbon/Cocoa/etc error codes 279 | 280 | 281 | 282 | ---------------------------------------------------------------------- 283 | Other issues 284 | ---------------------------------------------------------------------- 285 | 286 | 287 | - Should (and can?) ALL ObjC class, method, enum, etc names always be enclosed in pipes? e.g. The File library was accidentally recompiled and resaved after AS converted 'NSURL' identifiers to 'nsurl', causing handlers that used it to break as ASOC, unlike AS, is case-sensitive. 288 | 289 | Normalizing identifier case in not just current script but all imported scripts too is a fundamental AS flaw that will continue to break users' ASOC code until fixed at source, but these libraries need to be as robust as possible, even if that means crudding up the ASOC parts with pipes. 290 | 291 | The big annoyance is that unless identical names but with different case are also defined without pipes, AS will remove the pipes from names that do have them on compilation, whereupon the library code is right back where it started, at risk of breaking in future use. 292 | 293 | 294 | 295 | -------------------------------------------------------------------------------- /TestTools.scptd/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.apple.ScriptEditor.id.TestTools 7 | CFBundleName 8 | TestTools 9 | CFBundleShortVersionString 10 | 1.0 11 | OSAScriptingDefinition 12 | TestTools.sdef 13 | WindowState 14 | 15 | bundleDividerCollapsed 16 | 17 | bundlePositionOfDivider 18 | 674 19 | dividerCollapsed 20 | 21 | eventLogLevel 22 | 0 23 | name 24 | ScriptWindowState 25 | positionOfDivider 26 | 557 27 | savedFrame 28 | 8 33 923 744 0 0 1280 777 29 | selectedTab 30 | log 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /TestTools.scptd/Contents/Resources/Script Libraries/TestSupport.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alldritt/applescript-stdlib/fe693369d826a25c02570dd00cbacdc7299c847c/TestTools.scptd/Contents/Resources/Script Libraries/TestSupport.scpt -------------------------------------------------------------------------------- /TestTools.scptd/Contents/Resources/Scripts/main.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alldritt/applescript-stdlib/fe693369d826a25c02570dd00cbacdc7299c847c/TestTools.scptd/Contents/Resources/Scripts/main.scpt -------------------------------------------------------------------------------- /TestTools.scptd/Contents/Resources/TestTools.sdef: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | For example:

21 | 22 |
to test_uppercaseText
 23 |   assert test result for (uppercase text "foøbår") is "FOØBÅR"
 24 | end test_uppercaseText
25 | 26 |

If the note parameter is given, its text is included in the generate test report. For example, an assert test result command could use this parameter to describe the type of bugs that particular test is designed to detect.

27 | ]]> 28 |
29 | 30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | For example:

41 | 42 |
to test_uppercaseText
 43 |   assert test error for {a:"foo"} is {errorNumber:-1703} ¬
 44 |       note "Check unsuitable value types are rejected."
 45 | end test_uppercaseText
 46 | 
 47 | to call_uppercaseText(usingParam)
 48 |   uppercase text usingParam
 49 | end call_uppercaseText
 50 | 
51 | ]]> 52 |
53 |
54 | 55 | 56 | 57 | 58 | { errorNumber : integer, 60 | errorMessage : text, 61 | fromValue : any, 62 | toType : type, 63 | partialResult : any } 64 | 65 |

The errorNumber property is required; other properties are recommended where appropriate.

66 | ]]> 67 |
68 |
69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | While assert test result is the preferred choice for checking whether or not a result value is correct, assert test passed and assert test failed may occasionally be used in situations that require complex checking logic beyond the capabilities of a standard ‘check’ object.

78 | ]]> 79 |
80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | For example, the following assertion would normally fail due to the calculation 0.7 * 0.7 returning a real (a.k.a. floating point) number that is very nearly but not precisely 0.49:

104 | 105 |
assert test result for (0.7 * 0.7) is 0.49 -- fails even though it shouldn’t
106 | 107 |

To allow for the inherent imprecision of real numbers, pass a NumericEqualityCheck object as the assert test result command’s using parameter:

108 | 109 |
assert test result for (0.7 * 0.7) is 0.49 using (numeric equality check) -- passes
110 | ]]> 111 |
112 |
113 | 114 | 115 | 116 | 117 | 118 | 119 | The assert test result command’s is parameter should be a two-number list of form {MIN, MAX}. For example:

121 | 122 |
assert test result for aResult is {0.49, 0.51} -- result must be 0.5±0.01
123 | ]]> 124 |
125 |
126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | By default, each error property specified in the assert test error command’s is property is checked for exact equality with the corresponding property in the actual error. Occasionally, it may be necessary to customize the way in which a particular error property is compared, in which case the an alternate check object may be specfied to check that property.

140 | 141 |

To illustrate, the following assert test error command makes two checks: 1. that the error number is exactly 3000, and 2. that the error value is 0.49 allowing for any slight differences due to real numbers’ limited precision:

142 | 143 |
assert test error is {errorNumber:3000 fromValue:0.49} ¬
144 |         using {fromValue:numeric equality check}
145 | 146 |

Thus, if the error value was produced by the calculation 0.7 * 0.7 – which returns a real number that is very nearly but not precisely 0.49 – this test will pass as expected.

147 | ]]> 148 |
149 |
150 | 151 |
152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | To run a unit test script directly in Script Editor, include the following handler in the script:

167 | 168 |
on run
169 |   run unit tests (path to me)
170 | end run
171 | 172 | ]]> 173 |
174 | 175 |
176 | 177 |
178 | 179 | 180 | 181 | 182 | 183 | Test Script Structure 187 | 188 |

A unit test script is saved as a compiled TESTNAME.unittest.scptfile and has the following basic structure:

189 | 190 |
use script "TestTools"
191 | 
192 | use script "FILENAME" -- the script being tested
193 | 
194 | script suite_SUITE -- a group of related tests
195 | 
196 |   to test_HANDLER()
197 |     -- assertions to test a command
198 |   end
199 | 
200 |   to call_HANDLER(PARAM)
201 |     -- the command being tested (error checking only)
202 |   end
203 | 
204 | end
205 | 206 |

A test script must contain one or more suite_NAME script objects, each of which contains one or more test_NAME handlers. As a rule of thumb, each test handler should thoroughly test a single command, asserting it returns the correct result/error responses for one or more sets of good/bad inputs.

207 | 208 |

Each suite object’s name must have a suite_ prefix; each test handler’s name must have a test_ prefix. The rest of each name can be anything: short descriptions of the purpose of the suite and the command being tested are strongly recommended as these will appear in the generated test report. When the osatest test runner loads the script, it searches for these suite_ and test_ prefixes and will invoke each test handler automatically; the script should not call test handlers itself.

209 | 210 |

To minimize the risk of unintended interactions between tests, prior to calling each test handler, osatest creates a new component instance and loads a fresh copy of the test script into it. This ensures text item delimiters, loaded libraries, etc. are not shared between tests. If the code being tested (or the tests themselves) also interact with external data – shared files, scriptable applications, etc. — it is the test script’s responsibility to ensure that these external data sources are put into a known state prior to running each test.

211 | 212 | 213 | 214 |

Writing Test Handlers

215 | 216 |

The goal of unit testing is to confirm that each handler in your own script returns the correct results and error responses for a representative range of normal and abnormal inputs, covering both general and corner cases. (For example, in addition to testing what happens when correct values are given, it's essential to check that failures are dealt with appropriately too: for instance, what if a handler expects a list of values to process but receives an empty list instead, or needs to access a data file but the file is missing or has the wrong permissions?)

217 | 218 |

Testing for a correct result is done using TestTools’ assert test result command, while testing for an expected error is done using its assert test error command. Each test_NAME handler can contain any number of assert... commands. For example, to check that a squareNumber handler’s actual result exactly matches the expected return value:

219 | 220 |
to test_squareNumber()
221 |   assert test result for (squareNumber(4)) is (16)
222 |   assert test result for (squareNumber(0)) is (0)
223 |   assert test result for (squareNumber(1.0)) is (1.0)
224 |   assert test result for (squareNumber(-12345)) is (152399025)
225 |   ...
226 | end test_squareNumber
227 | 228 |

(Note that you may have to parenthesize the code being tested to avoid AppleScript misreading the assert command’s is parameter as an is operator.)

229 | 230 |

Similarly, testing for an expected error is done using the assert test error command:

231 | 232 |
to test_squareRoot()
233 |   assert test result for (squareRoot(64)) is (8.0)
234 |   assert test result for (squareRoot(0)) is (0.0)
235 |   ...
236 |   assert test error for ({a:1}) is ¬
237 |       {errorNumber:-1700, errorMessage:"Bad parameter: not a number."}
238 |   assert test error for (-1) is ¬
239 |       {errorNumber:-1703, errorMessage:"Bad parameter: negative numbers not allowed."}
240 | end test_squareRoot
241 | 242 |

This time, however, the assert test error command’s for parameter should contain only data needed to perform the test. Since the goal is to generate a deliberate error, the command being tested must be wrapped in a separate call_NAME handler which assert test error then calls automatically:

243 | 244 |
to call_squareRoot(aValue)
245 |   squareRoot(aValue)
246 | end call_squareRoot
247 | 248 |

If the assert test error command includes a for parameter, the value is passed to the call_HANDLER handler as its sole parameter, otherwise the handler receives no parameters. To pass multiple parameters, use a record or list:

249 | 250 |
assert test error for {param1, param2, param3} is ...
251 | 
252 | to call_foo({param1, param2, param3})
253 |   ...
254 | end call_foo
255 | 256 |

TestTools catches the error thrown in the call_NAME handler and compares it against the assert test error command’s is record parameter. This expected error information record must contain an errorNumber property, plus any additional properties to be checked – errorMessage, fromValue, toType and/or partialResult — corresponding to an AppleScript error’s number, message, from, to and/or partial result attributes.

257 | 258 |

If all assertions within a test handler succeed, TestTools will report that test as passed once the handler returns:

259 | 260 |
    ModifyText's normalizeText: OK (performed 34 assertions)
261 | 262 |

However, if the code being tested returns an incorrect result or throws an unexpected or incorrect error, TestTools will report that test as failed, along with a description of the problem:

263 | 264 |
    JoinSplitDate's joinDate: FAILED on assertion 32 in ‘assert test result’ received incorrect result:
265 |     • actual result: date "Thursday, 30 April 1959 at 18:11:01"
266 |     • expected result: date "Thursday, 30 April 1959 at 14:11:01"
267 | 268 |

Once the cause of the problem is identified in the code being tested (assuming it’s not a bug in the test script itself), it can be corrected and the test script re-run — and the cycle repeated until all tests finally pass.

269 | 270 | 271 |

While test handlers can include any code, not just assert commands, it’s a good idea to keep such code to a minimum so that any bugs in the test script are not confused for defects in code being tested. For example, rather than writing code to connect to a live database to obtain test data, it is often simpler to hardcode a mockup object of known values within the test script, and to use that. In addition, rather than preparing all this test data within the test handler itself, it is possible to use a separate set-up handler to prepare this test data and store it in a property of the suite object, prior to the test handler being called. The next section describes the additional support handlers that may be included in suite objects.

272 | 273 | 274 |

Optional Suite Handlers

275 | 276 |

In addition to test_NAME and associated call_NAME handlers, a suite object may also contain zero or more of the following optional handlers:

277 | 278 |

configure_setUp()

279 | 280 |

Called by TestTools before it calls a test_NAME handler in a suite. Use this handler to perform any pre-test preparation of common test data; for example, to create AppleScript objects, temporary files, database connections, etc. that will be used by each test in that suite. If a set-up handler throws an error (e.g. if a permissions error prevents a temp file being written) the rest of the test is aborted, including any post-test cleanup, in which case the set-up handler should perform its own cleanup of any partially created test resources.

281 | 282 | 283 |

configure_tearDown()

284 | 285 |

Called by TestTools after it calls a test_NAME handler in a suite. Use this handler to perform any post-test cleanup; for example, to delete temporary files, close database connections, etc. This handler is called regardless of whether the test handler succeeds or fails, so should be designed to clean up any partially consumed or unused test data left by a failed test, as well as to clean up after a successful test.

286 | 287 | 288 |

configure_skipTests()

289 | 290 |

Called before running each suite. Use this handler to temporarily disable selected test handlers (e.g. if a test should only run on newer OS versions), or even the entire suite:

291 | 292 |
    293 |
  • If the handler returns missing value, all tests are run.
  • 294 |
  • If the handler returns a text value, no tests are run. The text should explain why the tests were skipped; this will appear in the test report.
  • 295 |
  • If the handler contains a record of form {test_NAME: text or missing value, ...}, the named handlers will either be skipped or run according to the property value. Test handlers not named in the record will run as normal.

  • 296 |
297 | 298 |

For example, to skip the test handler named test_Foo if the OS version is older than 10.9 while allowing all other tests in the suite to run normally:

299 | 300 |
use scripting additions
301 | 
302 | to configure_skipTests()
303 |   considering numeric strings
304 |     return {test_Foo:(system info)'s system version < "10.9"}
305 |   end considering
306 | end configure_doTest
307 | 308 | 309 |

configure_doTest(testObject)

310 | 311 |

By default, TestTools calls each test_NAME directly, as soon as the configure_setUp() (if any) returns. Some configuration options – for example, considering/ignoring blocks – cannot be applied by set-up and tear-down handlers, but must instead be applied directly to the test_NAME call itself. To do this, add a configure_doTest handler to the suite that takes a script object as its sole parameter. The script object contains a doTest handler, which takes no parameter and returns no result. The configure_doTest handler should perform any additional configuration, then send the script object a doTest command to run the test itself. For example, to run each test in a suite using custom considering/ignoring options:

312 | 313 |
to configure_doTest(testObject)
314 |   considering case, diacriticals and numeric strings ¬
315 |       but ignoring hyphens, punctuation and white space
316 |     testObject's doTest() -- perform the test
317 |   end considering
318 | end configure_doTest
319 | 320 |

When the doTest command returns, the configure_doTest handler may perform any related clean up and/or additional assertion checks. If the doTest command raises an error, the configure_doTest may temporaily handle it if necessary, but must re-throw that original error when done:

321 | 322 |
to configure_doTest(testObject)
323 |   -- do normal preparation here
324 |   try
325 |     testObject's doTest()
326 |   on error eText number eNumber from eValue to eType
327 |     -- do essential clean up here...
328 |     error eText number eNumber from eValue to eType -- ...then rethrow original error
329 |   end try
330 |   -- do normal clean up here
331 | end configure_doTest
332 | 333 | 334 | 335 |

Customizing Assertions

336 | 337 |

By default, assert test result compares values exactly, including text case and object type (e.g. if 3 is expected then 3.0 will be reported as incorrect). This can be sometimes be too precise; for example, when comparing two real numbers, a small amount of leeway may be necessary to allow for tiny rounding errors that are an unavoidable consequence of the limited precision of floating-point CPU math. For instance, 0.7 * 0.7 returns a real number that displays as 0.49 but is actually fractionally less:

338 | 339 |
to squareNumber(n)
340 |   return n ^ 2
341 | end squareNumber
342 | 
343 | squareNumber(0.7) -- i.e. '0.7 * 0.7'
344 | result = 0.49 → false(!)
345 | 346 |
assert test result for (squareNumber(0.7)) is (0.49) -- assertion fails!
347 | 348 |

To modify the way in which assert test result compares expected vs actual results, pass a custom ‘result comparator’ script object as its using parameter. Several custom comparators are included as standard in TestTools. For example, to allow for a small amount of imprecision when comparing reals, use a NumericEqualityCheck object:

349 | 350 |
assert test result for (squareNumber(0.7)) is (0.49) using (numeric equality check) -- passes
351 | 352 |

TestTools also provides basic assert test passed and assert test failed commands for use in situations where even greater flexibility is needed, but for most testing tasks the standard assert commands are simple, reliable, and produce the most detailed test results.

353 | 354 | [TO DO: the interface for test comparator objects has yet to be finalized (currently it only returns true or false, but really needs a way to return detailed failure information as well for inclusion in test reports); once that's done, might be an idea to document here, or perhaps in TestTools.scptd code comments, how to implement additional comparator objects as the included comparators are still not comprehensive] 355 | 356 | ]]> 357 |
358 |
359 | 360 |
361 | 362 | -------------------------------------------------------------------------------- /TestTools.scptd/Contents/Resources/bin/osatest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alldritt/applescript-stdlib/fe693369d826a25c02570dd00cbacdc7299c847c/TestTools.scptd/Contents/Resources/bin/osatest -------------------------------------------------------------------------------- /TestTools.scptd/Contents/Resources/description.rtfd/TXT.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf460 2 | {\fonttbl} 3 | {\colortbl;\red255\green255\blue255;} 4 | } -------------------------------------------------------------------------------- /Text.scptd/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.apple.ScriptEditor.id.library.Text 7 | CFBundleName 8 | Text 9 | CFBundleShortVersionString 10 | 1.0 11 | OSAScriptingDefinition 12 | Text.sdef 13 | WindowState 14 | 15 | bundleDividerCollapsed 16 | 17 | bundlePositionOfDivider 18 | 774 19 | dividerCollapsed 20 | 21 | eventLogLevel 22 | 0 23 | name 24 | ScriptWindowState 25 | positionOfDivider 26 | 492 27 | savedFrame 28 | 223 33 1023 744 0 0 1280 777 29 | selectedTab 30 | log 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Text.scptd/Contents/Resources/Scripts/main.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alldritt/applescript-stdlib/fe693369d826a25c02570dd00cbacdc7299c847c/Text.scptd/Contents/Resources/Scripts/main.scpt -------------------------------------------------------------------------------- /Text.scptd/Contents/Resources/description.rtfd/TXT.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf460 2 | {\fonttbl} 3 | {\colortbl;\red255\green255\blue255;} 4 | } -------------------------------------------------------------------------------- /TypeSupport.scptd/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.apple.ScriptEditor.id.library.TypeSupport 7 | CFBundleName 8 | TypeSupport 9 | CFBundleShortVersionString 10 | 1.0 11 | WindowState 12 | 13 | bundleDividerCollapsed 14 | 15 | bundlePositionOfDivider 16 | 663 17 | dividerCollapsed 18 | 19 | eventLogLevel 20 | 2 21 | name 22 | ScriptWindowState 23 | positionOfDivider 24 | 701 25 | savedFrame 26 | 776 109 912 948 0 0 1920 1057 27 | selectedTab 28 | result 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /TypeSupport.scptd/Contents/Resources/Scripts/main.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alldritt/applescript-stdlib/fe693369d826a25c02570dd00cbacdc7299c847c/TypeSupport.scptd/Contents/Resources/Scripts/main.scpt -------------------------------------------------------------------------------- /TypeSupport.scptd/Contents/Resources/description.rtfd/TXT.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf460 2 | {\fonttbl} 3 | {\colortbl;\red255\green255\blue255;} 4 | } -------------------------------------------------------------------------------- /Web.scptd/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.apple.ScriptEditor.id.library.Web 7 | CFBundleName 8 | Web 9 | CFBundleShortVersionString 10 | 1.0 11 | OSAScriptingDefinition 12 | Web.sdef 13 | WindowState 14 | 15 | bundleDividerCollapsed 16 | 17 | bundlePositionOfDivider 18 | 660 19 | dividerCollapsed 20 | 21 | eventLogLevel 22 | 2 23 | name 24 | ScriptWindowState 25 | positionOfDivider 26 | 497 27 | savedFrame 28 | 124 33 909 744 0 0 1280 777 29 | selectedTab 30 | result 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Web.scptd/Contents/Resources/Scripts/main.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alldritt/applescript-stdlib/fe693369d826a25c02570dd00cbacdc7299c847c/Web.scptd/Contents/Resources/Scripts/main.scpt -------------------------------------------------------------------------------- /Web.scptd/Contents/Resources/Web.sdef: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | For example:

17 | 18 |
split url "http://jsmith@example.com/some/path?x=1&y=2"
 19 | 
 20 | → { urlScheme:"http", 
 21 |     networkLocation:"jsmith@example.com", 
 22 |     resourcePath:"/some/path", 
 23 |     parameterString:"", 
 24 |     queryString:"x=1&y=2", fragmentIdentifier:"" }
25 | ]]> 26 |
27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | For example:

38 | 39 |
join url { urlScheme:"http", ¬
 40 |     networkLocation:"jsmith@example.com", ¬
 41 |     resourcePath:"/some/path", ¬
 42 |     queryString:"x=1&y=2" }
 43 | 
 44 | → "http://jsmith@example.com/some/path?x=1&y=2"
45 | ]]> 46 |
47 |
48 | 49 | 50 | 51 | 52 | { urlScheme : text, 54 | networkLocation : text or network location record, 55 | resourcePath : text, 56 | parameterString : text, 57 | queryString : text, 58 | fragmentIdentifier : text } 59 | ]]> 60 | 61 | 62 | 63 | 64 | 65 | 66 | { userName : text, 68 | userPassword : text, 69 | hostName : text, 70 | portNumber : text } 71 | ]]> 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | By default, this converts all characters except A-Za-z0-9_.- to UTF8-based %XX escape codes. For example, to escape a resource path, preserving path separators:

83 | 84 |
encode URL characters "/foo bar/ø.txt" preserving "/" → "/foo%20bar/%C3%B8.txt"
85 | ]]> 86 |
87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | All UTF8-based %XX escape codes will be automatically replaced with the corresponding characters. If an escape sequence does not represent a valid UTF8 codepoint, an error is raised instead. For example:

96 | 97 |
decode URL characters "/foo%20bar/%C3%B8.txt" → "/foo bar/ø.txt"
98 | ]]> 99 |
100 |
101 | 102 | 103 | 104 | 105 | 106 | 107 | Keys and values will be text values. All UTF8-based %XX escape codes will be automatically replaced with the corresponding characters. For example:

109 | 110 |
split URL query string "age=23&name=Jan+M%C3%BCller"
111 | → {{"age", "23"}, {"name", "Jan Müller"}}
112 | ]]> 113 |
114 |
115 | 116 | 117 | 118 | 119 | 120 | Keys and values must be text values (e.g. numbers should be converted to numeric text using Number library's ‘format number’ command to avoid localization issues). All characters except A-Za-z0-9_.- will be automatically replaced with UTF8-based %XX escape codes. For example:

122 | 123 |
join URL query string {{"age", "23"}, {"name", "Jan Müller"}}
124 | → "age=23&name=Jan+M%C3%BCller"
125 | ]]> 126 |
127 |
128 | 129 |
130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | For example, to request a document and automatically decode it to text (assuming the HTTP response contains a recognized Content-Type header):

181 | 182 |
send HTTP request to "http://apple.com/index.html"
183 | 184 |

To download the document to file without any automatic decoding:

185 | 186 |
set filePath to "/path/to/file.html"
187 | 
188 | set httpResponse to send HTTP request to "http://apple.com/index.html" returning data
189 | 
190 | -- on success, the returned record's `responseBody` property contains an NSData object
191 | if httpResponse's statusCode div 100 = 2 then -- 2xx status indicates success (e.g. 200)
192 |   set {didSucceed, asocError} to httpResponse's responseBody's writeToFile:filePath ¬
193 |       options:(current application's NSDataWritingAtomic) |error|:(specifier)
194 |   -- confirm file was written successfully
195 |   if not didSucceed then error (asocError's localizedDescription()) number (asocError's code())
196 | else
197 |   -- deal with other status codes (e.g. 3xx, 4xx, 5xx) here...
198 | end if
199 | 200 |

HTTP headers can be added to the HTTP request via the send HTTP request command's headers parameter. For example, the following list of HTTP header records tells the web server that your script wants either HTML (preferred) or plain text, encoded as UTF8 (preferred) or Latin1, in response:

201 | 202 |
{{headerName:"Accept", headerValue:"text/html, text/plain;q=0.5"}, 
203 |  {headerName:"Accept-Charset", headerValue:"utf-8, iso-8859-1;q=0.5"}}
204 | 205 |

By default, send HTTP request sends a GET request to the web server. Other HTTP methods (POST, PUT, DELETE etc.) can be specified via the method parameter. For example, to send a POST request with UTF8-encoded JSON data as the request body and ask for UTF8-encoded JSON data in response:

206 | 207 |
set requestData to {someKey:101, anotherKey:"hello"}
208 | 
209 | set httpResponse to send HTTP request to "http://example.org/some-path" ¬
210 |     method "POST" ¬
211 |     headers {{headerName:"Accept", headerValue:"application/json"}, ¬
212 |              {headerName:"Accept-Charset", headerValue:"utf-8"}, ¬
213 |              {headerName:"Content-Type", headerValue:"application/json;charset=utf-8"}} ¬
214 |     body (encode JSON requestData)
215 | 
216 | if (httpResponse's statusCode) div 100 ≠ 2 then -- non-2xx status = failure (e.g. 403)
217 |   error (HTTP status name httpResponse's statusCode) number httpResponse's statusCode
218 | end if
219 | 
220 | set responseData to decode JSON httpResponse's responseBody
221 | 222 | ]]> 223 |
224 |
225 | 226 | 227 | 228 | 229 | { headerName:text, 231 | headerValue:text } 232 | 233 |

For example:

234 | 235 |
{headerName:"Content-Type", headerValue:"application/json; charset=utf-8"}
236 | ]]> 237 |
238 |
239 | 240 | 241 | 242 | 243 | { statusCode:integer, 245 | responseHeaders:list of HTTP header record, 246 | responseBody:text, NSData, or missing value } 247 | ]]> 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | For example:

258 | 259 |
HTTP status name 404 → “not found”
260 | ]]> 261 |
262 |
263 | 264 |
265 | 266 |
267 | -------------------------------------------------------------------------------- /Web.scptd/Contents/Resources/description.rtfd/TXT.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf460 2 | {\fonttbl} 3 | {\colortbl;\red255\green255\blue255;} 4 | } -------------------------------------------------------------------------------- /bin/asdiff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/osascript 2 | 3 | (* Compare two compiled AppleScript (.scpt) files for differences. 4 | 5 | Usage: asdiff FILE1 FILE2 6 | 7 | Diff tools typically operate on plain text files only, so this shell script uses `osadecompile` to convert .scpt files to temporary .applescript files before comparing them. (Other file types are not affected.) 8 | 9 | Be aware that `osadecompile` may not have access to all the required terminology (e.g. script library SDEFs) when decompiling code, in which case raw chevron syntax will be used in place of unknown keywords. This is a limitation of AppleScript. 10 | 11 | Uses TextWrangler's `twdiff` tool to display scripts in TextWrangler . You can modify _difftool property to use a different tool, e.g. `opendiff`, if preferred. 12 | 13 | For basic .applescript code coloring, BBEdit/TextWrangler codeless language module: 14 | 15 | https://github.com/ewancarr/BBEdit/blob/master/Language%20Modules/AppleScript.plist 16 | *) 17 | 18 | property _difftool : "/usr/local/bin/twdiff" 19 | 20 | use script "File" 21 | use scripting additions 22 | 23 | on run {arg1, arg2} 24 | if arg1 ends with ".scpt" and arg2 ends with ".scpt" then 25 | set tempFolder to do shell script "mktemp -dt CTFD" 26 | set arg1 to exportAppleScript(arg1, ".1", tempFolder) 27 | set arg2 to exportAppleScript(arg2, ".2", tempFolder) 28 | end if 29 | do shell script quoted form of _difftool & " " & quoted form of arg1 & space & quoted form of arg2 & " &>/dev/null &" 30 | end run 31 | 32 | to exportAppleScript(scptFilePath, aLabel, tempFolder) 33 | set scptFilePath to normalize path scptFilePath with tilde expansion and absolute expansion 34 | set tempFilePath to join path {tempFolder, last item of (split path scptFilePath) & aLabel & ".applescript"} 35 | do shell script "osadecompile " & quoted form of scptFilePath & " >" & quoted form of tempFilePath 36 | return tempFilePath 37 | end exportAppleScript 38 | 39 | -------------------------------------------------------------------------------- /unittests/Date.unittest.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alldritt/applescript-stdlib/fe693369d826a25c02570dd00cbacdc7299c847c/unittests/Date.unittest.scpt -------------------------------------------------------------------------------- /unittests/File.unittest.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alldritt/applescript-stdlib/fe693369d826a25c02570dd00cbacdc7299c847c/unittests/File.unittest.scpt -------------------------------------------------------------------------------- /unittests/List.unittest.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alldritt/applescript-stdlib/fe693369d826a25c02570dd00cbacdc7299c847c/unittests/List.unittest.scpt -------------------------------------------------------------------------------- /unittests/Number.unittest.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alldritt/applescript-stdlib/fe693369d826a25c02570dd00cbacdc7299c847c/unittests/Number.unittest.scpt -------------------------------------------------------------------------------- /unittests/Objects.unittest.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alldritt/applescript-stdlib/fe693369d826a25c02570dd00cbacdc7299c847c/unittests/Objects.unittest.scpt -------------------------------------------------------------------------------- /unittests/Text.unittest.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alldritt/applescript-stdlib/fe693369d826a25c02570dd00cbacdc7299c847c/unittests/Text.unittest.scpt -------------------------------------------------------------------------------- /unittests/TypeSupport.unittest.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alldritt/applescript-stdlib/fe693369d826a25c02570dd00cbacdc7299c847c/unittests/TypeSupport.unittest.scpt -------------------------------------------------------------------------------- /unittests/Web.unittest.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alldritt/applescript-stdlib/fe693369d826a25c02570dd00cbacdc7299c847c/unittests/Web.unittest.scpt --------------------------------------------------------------------------------