├── .gitignore ├── History.md ├── LICENSE ├── README.md ├── lib ├── applescript-parser.js └── applescript.js ├── package.json └── samples ├── execFile.js ├── execString.js ├── fail-case.applescript └── locationCurrentTrack.applescript /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 1.0.0 / 2015-01-27 3 | ================== 4 | 5 | * README: use single quotes for `require()` call 6 | * package: remove "lib" and "engines" fields 7 | * Merge pull request #9 from linclark/fix-arg-order 8 | * fix arg order 9 | * Merge pull request #6 from Resseguie/no-callback 10 | * allow for no callback 11 | * Merge pull request #1 from cnandreu/patch-1 12 | * Fixed example in README.md 13 | 14 | 0.2.1 / 2010-09-03 15 | ================== 16 | 17 | * If an error is returned from the AppleScript, then the offending code is attached to the Error object as `err.appleScript` 18 | 19 | 0.2.0 / 2010-07-27 20 | ================== 21 | 22 | * Exposed the underlying parser Functions via the exported `Parsers` property 23 | * Add MIT license 24 | 25 | 0.1.0 / 2010-07-21 26 | ================== 27 | 28 | * Adding a "package.json" file for "npm". Preparing for v0.1.0 tag 29 | * Added a simple script to easily execute *.applescript test script 30 | * Added a new test case to ensure the 'fail' case issue is (and remains) fixed 31 | * Fixed the case where a zero-length response would cause an infinite loop in the result parser 32 | * Now converting `«data »` results into native Buffer instances. Horray! 33 | * Fixed StringParser with escaping control characters 34 | * Rewrote parsing logic to use Strings and substring 35 | * Replaced the RegExp based result parsing logic to actual char-by-char based parsing logic 36 | * Write to stdin when 'execString' is called, instead of passing the String as an argument (potentially dangerous) 37 | * Rename 'node-applescript.js' to 'applescript.js' 38 | * Initial commit 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Nathan Rajlich 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-applescript 2 | ================ 3 | 4 | A high-level way to execute AppleScript code through Node.js, and retrieve 5 | the result as a native JavaScript object. Underneath the hood, this 6 | module is just a simple wrapper around the macOS `osascript` command. 7 | 8 | ### Why? 9 | AppleScripts are the only way to communicate and interact with certain 10 | external macOS processes, for example [iTunes](http://www.itunes.com). 11 | 12 | Easy Install 13 | ------------ 14 | 15 | ``` bash 16 | $ npm install applescript 17 | ``` 18 | 19 | Requirements 20 | ------------ 21 | 22 | * Mac (or Hackintosh) running [macOS](https://www.apple.com/macos) (tested with High Sierra) 23 | * [Node.js](https://nodejs.org) (v0.2.0 or newer) 24 | 25 | Usage 26 | ----- 27 | 28 | The `node-applescript` module provides `execString` and `execFile` functions 29 | to easily execute AppleScript commands and buffer the output into a calback. 30 | 31 | ``` js 32 | const applescript = require('applescript'); 33 | 34 | // Very basic AppleScript command. Returns the song name of each 35 | // currently selected track in iTunes as an 'Array' of 'String's. 36 | const script = 'tell application "iTunes" to get name of selection'; 37 | 38 | applescript.execString(script, (err, rtn) => { 39 | if (err) { 40 | // Something went wrong! 41 | } 42 | if (Array.isArray(rtn)) { 43 | for (const songName of rtn) { 44 | console.log(songName); 45 | } 46 | } 47 | }); 48 | ``` 49 | 50 | `execFile` works the exact same way, except you pass the _path_ of the AppleScript 51 | (`*.applescript`) file as the first argument instead of the command itself, and you 52 | may pass an optional Array of String arguments to send to the applescript file. 53 | 54 | Licence 55 | ------- 56 | 57 | The `node-applescript` module is licensed under the MIT license, of course! 58 | -------------------------------------------------------------------------------- /lib/applescript-parser.js: -------------------------------------------------------------------------------- 1 | 2 | // 'parse' accepts a string that is expected to be the stdout stream of an 3 | // osascript invocation. It reads the fist char of the string to determine 4 | // the data-type of the result, and creates the appropriate type parser. 5 | exports.parse = function(str) { 6 | if (str.length == 0) { 7 | return; 8 | } 9 | 10 | var rtn = parseFromFirstRemaining.call({ 11 | value: str, 12 | index: 0 13 | }); 14 | return rtn; 15 | } 16 | 17 | // Attemps to determine the data type of the next part of the String to 18 | // parse. The 'this' value has a Object with 'value' as the AppleScript 19 | // string to parse, and 'index' as the pointer to the current position 20 | // of parsing in the String. This Function does not need to be exported??? 21 | function parseFromFirstRemaining() { 22 | var cur = this.value[this.index]; 23 | switch(cur) { 24 | case '{': 25 | return exports.ArrayParser.call(this); 26 | break; 27 | case '"': 28 | return exports.StringParser.call(this); 29 | break; 30 | case 'a': 31 | if (this.value.substring(this.index, this.index+5) == 'alias') { 32 | return exports.AliasParser.call(this); 33 | } 34 | break; 35 | case 'd': 36 | if (this.value.substring(this.index, this.index+4) == 'date') { 37 | return exports.DateParser.call(this); 38 | } 39 | break; 40 | case '«': 41 | if (this.value.substring(this.index, this.index+5) == '«data') { 42 | return exports.DataParser.call(this); 43 | } 44 | break; 45 | } 46 | if (!isNaN(cur)) { 47 | return exports.NumberParser.call(this); 48 | } 49 | return exports.UndefinedParser.call(this); 50 | } 51 | 52 | // Parses an AppleScript "alias", which is really just a reference to a 53 | // location on the filesystem, but formatted kinda weirdly. 54 | exports.AliasParser = function() { 55 | this.index += 6; 56 | return "/Volumes/" + exports.StringParser.call(this).replace(/:/g, "/"); 57 | } 58 | 59 | // Parses an AppleScript date into a native JavaScript Date instance. 60 | exports.DateParser = function() { 61 | this.index += 5; 62 | return new Date(exports.StringParser.call(this).replace(' at', ',')); 63 | } 64 | 65 | // Parses an AppleScript Array. Which looks like {}, instead of JavaScript's []. 66 | exports.ArrayParser = function() { 67 | var rtn = [], 68 | cur = this.value[++this.index]; 69 | while (cur != '}') { 70 | rtn.push(parseFromFirstRemaining.call(this)); 71 | if (this.value[this.index] == ',') this.index += 2; 72 | cur = this.value[this.index]; 73 | } 74 | this.index++; 75 | return rtn; 76 | } 77 | 78 | // Parses «data » results into native Buffer instances. 79 | exports.DataParser = function() { 80 | var body = exports.UndefinedParser.call(this); 81 | body = body.substring(6, body.length-1); 82 | var type = body.substring(0,4); 83 | body = body.substring(4, body.length); 84 | var buf = new Buffer(body.length/2); 85 | var count = 0; 86 | for (var i=0, l=body.length; i", 6 | "repository": "TooTallNate/node-applescript", 7 | "keywords": [ 8 | "applescript", 9 | "osascript", 10 | "macos", 11 | "mac", 12 | "osx" 13 | ], 14 | "main": "./lib/applescript", 15 | "license": "MIT", 16 | "os": ["darwin"] 17 | } 18 | -------------------------------------------------------------------------------- /samples/execFile.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var path = require("path"); 3 | var applescript = require("../lib/applescript"); 4 | 5 | if (process.argv.length <= 2) { 6 | console.error("USAGE: " + path.basename(process.execPath) + " " + path.basename(__filename) + " something.applescript"); 7 | console.error(" Try one of:"); 8 | fs.readdirSync(__dirname).filter(function(file) { 9 | return path.extname(file) == '.applescript'; 10 | }).forEach(function(file) { 11 | console.error(" " + file); 12 | }); 13 | } else { 14 | console.error('Executing "' + process.argv[2] + '"'); 15 | applescript.execFile(process.argv[2], process.argv.slice(3), function(err, rtn) { 16 | console.error(' DONE!\n'); 17 | if (err) { 18 | // Something went wrong! 19 | console.error("EXIT CODE: " + err.exitCode); 20 | console.error(err); 21 | } else { 22 | // If we got here, then there's probably some worthy content. 23 | console.error(rtn); 24 | } 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /samples/execString.js: -------------------------------------------------------------------------------- 1 | var applescript = require("../lib/applescript"); 2 | 3 | // Very basic AppleScript command. Returns the song name of each 4 | // currently selected track in iTunes as an 'Array' of 'String's. 5 | var script = 'tell application "iTunes" to get name of selection'; 6 | 7 | applescript.execString(script, function(err, rtn) { 8 | if (err) { 9 | // Something went wrong! 10 | throw err; 11 | } 12 | 13 | if (Array.isArray(rtn) && rtn.length > 0) { 14 | console.log("Currently selected tracks in \"iTunes\":"); 15 | rtn.forEach(function(songName) { 16 | console.log("\t" + songName); 17 | }); 18 | } else { 19 | console.log("No tracks are selected in \"iTunes\"...") 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /samples/fail-case.applescript: -------------------------------------------------------------------------------- 1 | tell application "iTunes" 2 | return source 100 3 | end tell 4 | -------------------------------------------------------------------------------- /samples/locationCurrentTrack.applescript: -------------------------------------------------------------------------------- 1 | tell application "iTunes" 2 | return location of current track 3 | end tell 4 | --------------------------------------------------------------------------------