How Do I Get the Count of All Actions Within a Macro?

How do I get the count of all actions within a macro?

Command+a

right-click

Erg... LOL

I knew that... LOL

Thanx @suliveevil

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.

4 Likes

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:

  1. Count them manually (Yuck!)

  2. Use a recursive AppleScript (or JXA) to run through the entire action tree and count the nodes.

  3. 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)

Display-Macro-Image

-Chris

6 Likes

That’s a great solution Chris and very easy to understand! Nice one!

2 Likes

Awesome, thank you...

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

image

Then thought a little harder -- why use two actions when you can use one?

image

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)

Image

5 Likes

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

1 Like

Try this version!

Count Actions in Selected Macro -- AS Variant.kmmacros (9.0 KB)

Image

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...)

5 Likes

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...  :sunglasses:

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

Keyboard Maestro Export

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

4 Likes

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 :wink:

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 :wink:

3 Likes

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)

Image

3 Likes

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

4 Likes

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)

Display-Macro-Image


Keyboard Maestro Editor – Count Actions in Selected Macro (JXA) v1.01.kmmacros (12 KB)

Display-Macro-Image


Much faster!

Keyboard Maestro Editor – Count Actions in Selected Macro (JXA) v1.02.kmmacros (12 KB)

Display-Macro-Image

2 Likes

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 )

1 Like

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.

2 Likes

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;
})();
2 Likes

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.

1 Like

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();
})();
2 Likes