How do I get the count of all actions within a macro?
Command+a
right-click
Unfortunately that may not give you what you want.
For example, if you have a Group
action that contains say 5 other actions, the above method would only count 1 as opposed to 6...
The same goes for other "enclosing" actions like IF
etc.
Hey Guys,
@tiffle is quite right.
Keyboard Maestro will only count the top-level of actions at any given level, so any embedded sub-actions will be missed.
There are three ways to go about counting all actions in a macro:
-
Count them manually (Yuck!)
-
Use a recursive AppleScript (or JXA) to run through the entire action tree and count the nodes.
-
Parse the macro XML (which is way easier for most people than writing a recursive script).
- Keyboard Maestro Editor β Edit > Copy as > Copy as XML
- Parsing for:
<key>ActionUID</key>
Here's a reasonably complete macro that does the parsing for you:
Keyboard Maestro Editor β Count Actions in Selected Macro v1.00.kmmacros (14 KB)
-Chris
Thatβs a great solution Chris and very easy to understand! Nice one!
For funsies and practice, I tried treating the clipboard as an array, delimited by <key>ActionUID</key>
, by replacing the entire "For Each..." action with
Then thought a little harder -- why use two actions when you can use one?
While now looking like an entry in the "Obfuscated Macro" category, it does seem to process longer macros faster -- but it might be that speed with my tester is be atypical (the same block of actions repeatedly pasted). Would love to hear about some real-world results!
Count Actions in Selected Macro -- Array Variant.kmmacros (11.8 KB)
Ok you brainiacs!!! Wow... all good... =)
So the macro that had 235 actions took about a minute in the first macro posted and about a second in the second macro posted.
A macro that has 31 actions took about 6 seconds using the first macro and a second using the second macro.
I appreciate all you folks and your talents and kindness to help me and all the others that you do as well.
Troy
Try this version!
Count Actions in Selected Macro -- AS Variant.kmmacros (9.0 KB)
Still using @ccstone's "parse the XML" idea, but doing it via AppleScript. No UI interaction means no screen flicker and no pause required, and the extra speed more than compensates for the overhead of spawning an AS instance (on longer macros, at least).
That's my last go, I promise! (Unless I finally get round to learning some Swift scripting. Hmmm...)
Hey @Nige_S,
That's really good. Light, fast, and elegant.
The only thing I'd do different is to work with selected macros
instead of the selection
. That way you don't have to monkey with the class of the selection.
You have to manage the possibility of zero or more than one macros being selected, but that's easy enough.
I was trying to avoid scripting in my first macro for the benefit of the non-scripters on the forum, and then you went and stole the glory of the AppleScript solution...Β
So just for fun I had a go with Perl, since I haven't touched it in a while β and I added a timer too.
Keyboard Maestro Editor β Count Actions in Selected Macro (Perl) v2.00.kmmacros (12 KB)
Display-Macro-Image
It's not quite as fast as the pure AppleScript solution, but it's not too far off.
If I get bored I'll have a go at a JXA version.
-Chris
Pro-tip! Thanks...
How about returning a JSON array of records {name:"blah blah", actionCount: 1}
that KM could iterate through? Then multiple selected macros is a feature, not a problem!
If that's even possible -- just typing "JSON" is pushing hard against the limits of my JavaScript knowledge
Side note:
Over the last few months I've really come to appreciate KM's "There's more than one way to do it" (to, again, borrow from Perl). Being able to quickly put together an easily understood (and debuggable!) proof of concept, then go through it and optimise certain steps while remaining "KM native", then another round of "what if we farm that bit out to AppleScript/JXA/KM sub-routine" has not only helped me learn about KM but also opened my eyes as to how it can be so much more than something that "just" automates repetitive UI tasks.
But I'm also aware that posting a dozen variants of the same solution can get a tad confusing and, to tie into a recent thread, make finding answers on the Forum even more difficult. So please, please, shoot me down when I get carried away
Well, that was a learning experience! Not going to claim to have become an expert in JSON, but I can see that it's a more reliable way of returning multiple values to KM from a script.
So... a macro to display the name(s) and action count(s) of one or more selected macros:
Count Actions in Selected Macro -- JSON Variant.kmmacros (8.6 KB)
I tend to use the command line for most anything like this.
In the current example:
- Select the macro of interest in a KM editor window
- Copy the XML for the macro with KM β Copy as β Copy as XML
- On a Terminal command line run
pbpaste | grep "<key>ActionUID</key>" | wc -l
The result is the number of lines containing <key>ActionUID</key>
in the clipboard, which is what you are looking for. Hit return and you have the answer instantly.
(Thanks @ccstone for the right XML tag to use.)
If you want a macro that will do it all with a hotkey:
Here is a fancier single-hotkey solution that gets the relevant information for the currently selected KM macro. "Currently selected" here means highlighted in blue in the KM Macros pane.
AppleScript queries application "Keyboard Maestro" for the name, class, and XML of the Keyboard Maestro selection. If the class is not "macro", then the AppleScript returns an error code of 1.
Otherwise the AppleScript runs the shell script given above to find the number of actions in the selection (along with an extra bit to strip leading spaces). The return value is a comma-delimited string containing the macro name and number of actions. KM will see this as a two-element array containing those values.
I'm sure that everyone contributing to this thread knows this, but others looking for solutions might not. A Mac is really a UNIX box with a bunch of very nice windowing and other capabilities layered on top. But when you open the Terminal application or use an Execute a Shell Script
action you are in a UNIX shell with the world at your fingertips. That brings with it a host of capabilities that augment/complement KM.
In the present context, UNIX has a suite of very powerful command line tools for text manipulation. These tools can get very complex. A couple are programming languages in their own right, as is the shell itself. (Note that the default Mac shell is zsh, not bash.) But most (with the possible exception of awk) offer a great deal of power even in their simplest forms. Most of these tools also make use of the power of Regular Expressions.
It is also worth noting that when KM spawns a shell it makes all of its variables, including local variables, available as shell variables. So, for example, if you have a KM variable called myVar
, you can access the contents of that variable from a spawned shell with $KMVAR_myVar
. Debugging is fairly straightforward because you can get code working in Terminal before importing it into KM.
For those unfamiliar with UNIX command-line text manipulation, here is a good primer:
Introduction to text manipulation on UNIX-based systems
I didn't get bored, but I did take it on as a challenge.
Iβm pleased with the general brevity and clarity of the code, but I'm quite surprised to find the JXA is slower than the pure AppleScript and even my AS-Perl hybrid code...
@ComplexPoint β care to weigh in on that?
-Chris
Keyboard Maestro Editor β Count Actions in Selected Macro (JXA) v1.00.kmmacros (12 KB)
Keyboard Maestro Editor β Count Actions in Selected Macro (JXA) v1.01.kmmacros (12 KB)
Much faster!
Keyboard Maestro Editor β Count Actions in Selected Macro (JXA) v1.02.kmmacros (12 KB)
Not sure β the only thing that jumps to the eye is that compound .whose
condition.
Does reducing that to kmEditor.macros.whose({selected: true})
make a detectable difference ?
(the enclosing {"=": }
, which I notice that used at some point above, is actually redundant)
( Probably also worth cacheing selectedMacros.length
,
rather than deriving that value twice over an Apple Events interface )
Thank you.
I posted the 1.01 version above with the changes you recommended, and it runs perhaps a couple tenths faster β at least enough that I notice.
The other thing, FWIW, is that we don't actually need that .whose
clause:
Expand disclosure triangle to view JS source
(() => {
"use strict";
const
kmEditor = Application("Keyboard Maestro"),
selectedMacros = kmEditor.selectedMacros();
return selectedMacros.length;
})();
That's the main reason I looked at JSON in the version above -- what if the selected macro has a comma (or whatever separator you use) in its name? While, in this case at least, you could pull element [-1]
to get the count and then loop through [1]
to [-2]
to rebuild the name, but it's still a bit of a faff.
The action count can also be defined as a count of the XPath matches for:
//key[text()='ActionUID']
Expand disclosure triangle to view JS source
(() => {
"use strict";
// Count of actions in selected KM Macro
// (Expressed in terms of XPath)
// Rob Trew @2022
const uw = ObjC.unwrap;
// MAIN ---
const main = () => {
const
kmEditor = Application("Keyboard Maestro"),
selectedMacros = kmEditor.selectedMacros(),
nMacros = selectedMacros.length;
return bindLR(
1 === nMacros ? (
Right(selectedMacros[0])
) : Left(
0 === nMacros ? (
"No macros selected."
) : "More than one macro selected."
)
)(
macro => either(
alert("Count of actions in selected macro")
)(
n => `${n} actions in:\t\n"${macro.name()}."`
)(
bindLR(
xPathLR("//key[text()='ActionUID']")(
macro.xml()
)
)(
matches => Right(matches.length)
)
)
);
};
// ---------------------- XPATH ----------------------
// xPathLR :: String ->
// String -> Either String [NSXMLElement]
const xPathLR = xpath =>
xml => {
const
error = $(),
xmlDoc = $.NSXMLDocument.alloc
.initWithXMLStringOptionsError(
xml, 0, error
);
return bindLR(
Boolean(xmlDoc.isNil()) ? (
Left(uw(error.localizedDescription))
) : Right(xmlDoc)
)(doc => {
const
e = $(),
matches = (
doc.documentContentKind = (
$.NSXMLDocumentXMLKind
),
doc.nodesForXPathError(
xpath, e
)
);
return matches.isNil() ? (
Left(uw(e.localizedDescription))
) : Right(
uw(matches)
);
});
};
// ----------------------- JXA -----------------------
// alert :: String => String -> IO String
const alert = title =>
s => {
const sa = Object.assign(
Application("System Events"), {
includeStandardAdditions: true
});
return (
sa.activate(),
sa.displayDialog(s, {
withTitle: title,
buttons: ["OK"],
defaultButton: "OK"
}),
s
);
};
// --------------------- GENERIC ---------------------
// Left :: a -> Either a b
const Left = x => ({
type: "Either",
Left: x
});
// Right :: b -> Either a b
const Right = x => ({
type: "Either",
Right: x
});
// bindLR (>>=) :: Either a ->
// (a -> Either b) -> Either b
const bindLR = m =>
mf => m.Left ? (
m
) : mf(m.Right);
// either :: (a -> c) -> (b -> c) -> Either a b -> c
const either = fl =>
// Application of the function fl to the
// contents of any Left value in e, or
// the application of fr to its Right value.
fr => e => e.Left ? (
fl(e.Left)
) : fr(e.Right);
// MAIN ---
return main();
})();