Learning & Using AppleScript & JavaScript for Automation (JXA)

As you may be aware, KM macros can call/use scripts like AppleScript and JXA (and others).
In another topic, we had a sub-topic (read off-topic) discussion about how to learn AppleScript, and then JXA.

Since it was off-topic in the other topic, but still, IMO, a very important topic that might be useful to many others, I thought I'd start this topic to focus on Learning & Using AppleScript & JavaScript for Automation (JXA)


TLDR: Quick Start to Learning AppleScript

If you are new to AppleScript and are interested in learning more, then I'd suggest you take a look at:

  1. AppleScript: Beginner's Tutorial
  2. Script Debugger 7 -- a full IDE for developing AppleScript.
    SD7 now comes with a 20-day free trial, and then will gracefully downgrade to the FREE SD7 Lite (which is still much better than Script Editor).

You can get lots of AppleScript help here in this forum, and in the Script Debugger Forum.

Quick Start to learning JXA

JXA Resources


Worthwhile Reading Below

There is much discussion below that you will benefit from.


First, let me point you to the post in the other topic where we began the "Learning..." discussion:

What follows is some great discussion by Chris (ccstone) and ComplexPoint.
All well worth reading.


ComplexPoint suggested to me that it may be better to learn the new JavaScript for Automation (JXA) that became available with Yosemite, rather than continue to butt my head against the AppleScript wall. :smile:
I think I agree with him.

For those of you who might be interested in JXA, you may want to take a look at this article I just read, and found very helpful and interesting:

Getting Started with JavaScript for Automation on Yosemite

I think this article can definitely save you a lot of time in getting started with JXA, as well as help you determine if you want to go that route.

Please post here if you have any suggestions, or questions, about learning JXA or AppleScript.

2 Likes

Here is an excellent reference provided by ComplexPoint in the other thread:

JavaScript for Automation Release Notes

I just ran across a related article on:

Finding, creating, reading and updating KM variables from JXA Javascript

that I thought might be of interest.

And here’s a slightly expanded example:

  1. reading a KM variable which contains some paragraphs of text
  2. making a sorted list (without duplicates) of the vocabulary which it uses
  3. putting the list of words into another KM variable (perhaps for a following FOR EACH action)
function run() {
	// referring to a KM variable by name
	// (creating it if it doesn't yet exist)
	function kmVar(k) {
		var kme = Application("Keyboard Maestro Engine"),
			vs = kme.variables,
			vks = vs.where({
				name: k
			});

		return vks.length ? vks[0] :
			vs.push(kme.Variable({
				'name': k
			})) && vs[k];
	}

	// READ SOME PARAGRAPHS OF TEXT FROM A KM VARIABLE
	var	oKMvar = kmVar('srcText');

	// SPLIT THE TEXT INTO WORDS
	var	lstTokens = oKMvar.value().split(/[^\w]+/);
		
	// BUILD A WORD LIST WITHOUT DUPLICATES
    var	lstLexicon = [],
		i = lstTokens.length,
		strToken, strList;

	// loop through the words backwards
	// (happens to be simple and fast, and we don't need the word order here)
	while (i--) {
		// Read a word and normalise it to lower case,
		strToken = lstTokens[i].toLowerCase();
		
		// add the word to our list if it:
		//	A) is not an empty '' 
		//		(an empty string evaluates to 'false' in a test), and
		//	B) doesn't yet have an index in the existing list
		// 		(an index of -1 means 'not found')
		if (strToken && lstLexicon.indexOf(strToken) === -1) {
			// .push() adds an item to the end of a list/array
			lstLexicon.push(strToken);
		}
	}

	// SORT THE LIST OF WORDS ALPHABETICALLY
	lstLexicon.sort();
	
	// CONVERT IT TO LINES OF TEXT
	strList = lstLexicon.join('\n');

	// AND STORE IT IN A KM VARIABLE
	kmVar('srcLexicon').value = strList;
	
	return strList;
}
1 Like

or a little more briefly, pulling the .reduce() method out of the JavaScript toolbox to:

  1. start with the empty list [],
  2. pass it along the list of words under the name of lstSeen, pushing any previously unseen word into it, and
  3. assign the final result to lstLexicon
function run() {
	// referring to a KM variable by name
	// (creating it if it doesn't yet exist)
	function kmVar(k) {
		var kme = Application("Keyboard Maestro Engine"),
			vs = kme.variables,
			vks = vs.where({
				name: k
			});

		return vks.length ? vks[0] :
			vs.push(kme.Variable({
				'name': k
			})) && vs[k];
	}

	// SPLIT SOME PARAGRAPHS OF TEXT FROM A KM VARIABLE INTO WORDS
	// AND PRUNE ANY DUPLICATES OUT OF THE LIST
	
	var lstLexicon = kmVar(
			'srcText'
		).value().split(/[^\w]+/).reduce(
			function (lstSeen, strToken) {
				var strWord = strToken.toLowerCase();
				
				if (strWord &&
					lstSeen.indexOf(strWord) === -1) {
					
					lstSeen.push(strWord);
				}
				return lstSeen;
			}, []
		);
		
	lstLexicon.sort();
	 	
	return (
		kmVar('srcLexicon').value = lstLexicon.join('\n')
	);	
}
1 Like

There is an overview of OS X JavaScript for Automation at:

https://macosxautomation.com/yosemite/index.html

with a video presentation:

https://macosxautomation.com/yosemite/mov/JXA-Overview-720P.m4v

The OSA bridge is a new layer on top of JavaScriptCore. (An ObjC wrapper around the (WebKit) JavaScript engine that is used by Safari).

2 Likes

Wow! Thanks for the great examples and references, Rob. :smile:

1 Like

To experiment with the flexibility of JavaScript records (or ‘dictionaries’), we could tweak the example slightly to count the number of occurrences of each word:

function run() {
'use strict';
	// referring to a KM variable by name
	// (creating it if it doesn't yet exist)
	function kmVar(k) {
		var kme = Application("Keyboard Maestro Engine"),
			vs = kme.variables,
			vks = vs.where({
				name: k
			});

		return vks.length ? vks[0] :
			vs.push(kme.Variable({
				'name': k
			})) && vs[k];
	}

	// SPLIT SOME PARAGRAPHS OF TEXT FROM A KM VARIABLE INTO WORDS
	// AND PRUNE ANY DUPLICATES OUT OF THE LIST

	var dctWordCount = kmVar(
		'srcText'
	).value().split(/[^\w]+/).reduce(
		function (dctSeen, strToken) {
			var strWord = strToken.toLowerCase(),
				maybeCount = strWord ? dctSeen[strWord] : null;

			if (strWord) {
				dctSeen[strWord] = maybeCount ? maybeCount + 1 : 1;
			}
			return dctSeen;
		}, {}
	);

	// Ask the dictionary for a list of all its keys
	var lstWords = Object.keys(dctWordCount);

	// derive a list of {word:w, count:n} dictionaries
	var lstWordCount = lstWords.map(
		function (w) {
			return {
				word: w,
				count: dctWordCount[w]
			};
		}
	);


	// Sort by descending count 
	// (and A-Z within equally frequent words)
	lstWordCount.sort(
		function (a, b) {

			// -1 sorts earlier, 0 the same, and +1 later
			var d = (b.count - a.count);
			if (d) return d;
			else {
				if (a.word < b.word) return -1;
				else return (a.word !== b.word) ? 1 : 0;
			}
		}
	);


	// Store sorted (frequency, word) pairs in a KM variable
	// as a sequence of text lines
	return (
		kmVar('srcWordCounts').value = lstWordCount.map(
			function (dct) {
				return dct.count.toString() + '\t\t' + dct.word;
			}
		).join('\n')
	);
}

Thanks Rob. You are one prolific fellow!

BTW, I don’t know how you guys keep track of stuff, but I make extensive use of Evernote. I can easily send to Evernote all of the KM emails I get, or do a web clip into Evernote using the EN clipper for Chrome (or Firefox, Safari).

I have EN tags for KM, JXA, AppleScript, etc that make it very easy to find what I need.

@ComplexPoint, what is the best way to get started, or maybe I should say re-started, using JavaScript, both JXA and traditional JS for web?

As i mentioned in another thread, I have done extensive JS coding, but it was years ago using Windows and IE. So I need to know how to get started developing, testing, and debugging JS on a Mac.

My immediate concern is how to setup a JS development environment on my Mac. I know you mentioned just using Safari, but I would like to know the optimum setup.

I’ll stop here, and just ask if you have any links/references on getting started with developing JS on a Mac. If you have any recommended tools, please share.

Thanks.

Well, beyond the Crockford ‘Best parts’ book, I would just say:

  1. Choose your text editor (I use Atom), and install a linter and beautifier (linters are useful teaching tools)
  2. see what catches your attention on JXA Cookbook wiki,
  3. and choose your preferred development debugger (Chrome or Safari - I happen to prefer the former), and get to know its console, debugging and timing views really well.

Another book I found quite useful for a broad and sane overview was:

http://cisdal.com/publishing.html

Finally, of course, whenever you look up a DOM function, skip past all the ‘w3schools’ stuff and go straight to the much more helpful and reliable http://developer.mozilla.org pages.

Thanks, Rob.

I found this web site that has link to free D/L, in case others might be interested:
JavaScript: The Good Parts

Got it! Kindle ver < $5

Crockford is worth buying too, I think – a good read and a widely shared reference.

Yes, the link I provided is to the Crockford book. :smile:

My point is that I’m not sure that the download isn’t a copyright violation.

Worth reading of course – what I mean is that it’s worth buying : -)

( the supply of good books seems likely to dry up if their price evaporates … )

1 Like

Hey guys, I’ve got an AppleScript question that I figure both of you probably know the answer off the top of your head. I’ve done a lot of searching, but I’m stuck here.

###Objective: Search the /Users folder (and all subfolders) to find a specific file.

I’ve got this far:

set strFileToSearchFor to "Green Check Mark in Sphere.icns"
set lstFiles to do shell script "mdfind -name " & strFileToSearchFor & " -onlyin /Users"

This seems to work very well, and very fast. It returns 2 files:

"/Users/Shared/Dropbox/SW/DEV/KM/Macros/Green Check Mark in Sphere.icns
/Users/Shared/Dropbox/SW/DEV/AppleScript/How To Scripts/Green Check Mark in Sphere.icns"

How do I get just the first or second file?
I thought it would return a list or record, but it seems like just one big string.
When I do a

count lstFiles

it returns 158.

TIA.

Haven’t looked at this particular case but generally if strCmd contains a bash incantation which generates several lines of output (one path per line, for example)

--  lines → list
set lst to paragraphs of (do shell script strCmd)

Thanks, Rob. That did the trick!

set lstFiles to paragraphs of (do shell script "mdfind -name " & strFileToSearchFor & " -onlyin /Users")
set strFile to item 1 of lstFiles
1 Like

and in JavaScript for Applications, perhaps something like:

var a = Application.currentApplication(),
    sa = (a.includeStandardAdditions = true && a),
	 
    lst = sa.doShellScript('env').split(/[\n\r]+/);
	
lst;

Hey JM,

Since you're counting characters in a string that's not too surprising.   :smile:

Keep in mind that when you shell-out from AppleScript what you get back will be a string.

------------------------------------------------------------
set fileToSearchForStr to "Typinator Tutorial.rtfd"

set shCMD to "mdfind -onlyin /Users/ 'kMDItemFSName == \"" & fileToSearchForStr & "\"c'"
set fileList to do shell script shCMD

if fileList ≠ "" then
  set fileList to paragraphs of fileList
  repeat with i in fileList
    set contents of i to alias POSIX file (contents of i)
  end repeat
  
  fileList
  
end if
------------------------------------------------------------

I always write shell commands as a string to a variable (usually shCMD) before running the do shell script command on it, because the shell command string often needs to be debugged.

I do not always convert posix-paths to aliases, but I usually do if I'm working with the files in the Finder.

Now let's try extrapolating Rob's code to do that in JXA and make it reasonably user-friendly:

var apl      = Application.currentApplication(),
    stdAdns  = (apl.includeStandardAdditions = true && apl),

    queryStr = "Typinator Tutorial.rtfd",

    slQuery  = "mdfind -onlyin /Users/ 'kMDItemFSName == \"" + queryStr + "\"c'",

    pathList = stdAdns.doShellScript(slQuery).split(/[\n\r]+/);
  
pathList;

-Chris

4 Likes