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

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

Aha!

I was trying to get that to work, but it was only returning the UUID of the selected macros.

Subtleties of syntax...

   selectedMacros

   vs

   selectedmacros

That made the difference. Whose clauses are notoriously slow in AppleScript.

Version 1.02 posted above and is the fastest yet.

Your code in post #20 works well, but is just a trifle slower.

I'm still flummoxed by XPath and need to learn how to use it...

1 Like

slow, slower, fastest

The single source of most bugs and system failures :slight_smile:

( The endemic and inordinately costly use of speed as a proxy for quality : - )

True. The first version was quick and dirty. (As a rule I also don't use commas in macro names, but that is just me.) Another way to skin the cat is to just pass the values back to KM separately with setvariable.

tell application "Keyboard Maestro Engine"
    setvariable "localMacroName" to theName ¬
        instance (system attribute "KMINSTANCE")
    setvariable "localNumberOfActions" to nActions ¬
         instance (system attribute "KMINSTANCE")
end tell

My real motivation was to shine a light on tools available in the shell. I often see things on the forum where a sizable collection of KM actions and/or code could be replaced on one line by piping together some sed, grep, awk and the like. The shell is also where you have tools that tell you anything you want to know about the state of the system. As an example I routinely use lsof to see whether a browser download is complete.

In this case there seemed to be enough of a difference in economy to make it worth mentioning.

Although I'll admit that part of the economy for me reflects the fact that command line stuff rolls off of my fingers pretty easily. JXA doesn't yet. We use the tools that we own! :wink:

Well, this one had me in stitches so I thought I'd share the story.

First, it has never occurred to me to find out how many actions were in a macro but I thought it would be a nasty little problem to solve with all that nesting so I read along hoping to glean some juicy AppleScript and JavaScript.

But the command line solution by @JeffHester just tickled my keyboard and I thought why not turn that into a general purpose counter (not just macro actions)?

Well, there's a good reason why not, it turns out. Which is that grep doesn't return the number of matches but the number of matching lines. Which is fine if you're looking for ActionUID which sits on a line by itself in a .kmmacros export.

But as a general purpose tool, you need something that can find more than one match a line and has more robust regexp support. Which naturally made me think of Perl. So I wrote up a macro using Perl and it was fast and accurate and worked on .kmmacros files and everything else.

Then I slapped my head because the Concordance module in Literary Toolbox does exactly the same thing (presuming to ignore case). It's just called Concordance instead of KM Action Counter.

Which just goes to show how undervalued an education in the liberal arts can be. If one only knew what all those English majors know...

3 Likes

@mrpasini Love it! But...

When using grep, \n isn't so much a new line character as it is a string delimiter. You can use sed (and sometimes tr) to put those delimiters anywhere you like, including in front of whatever item you are interested in isolating and counting.

Say, for example, you want to count the number of instances of the letter 'a' in the clipboard. The following does the trick.

pbpaste | sed "s/a/\n&/g" | grep "a" | wc -l

Of course 'a' could be whatever you want it to be, including the contents of a shell variable, which can be the value of a KM variable.

pbpaste | sed "s/$KMVAR_varName/\n&/g" | grep "$KMVAR_varName" | wc -l

(You do have to worry about special characters and the like, but that is always the case.)

A somewhat more interesting case. Suppose that you want to know not only how many actions there are in a macro, but wanted a complete list of XML key types present along with the number of each.

pbpaste | sed "s/<key>/\n&/g" | grep "<key>" | awk -F'<key>|</key>' '{print $2}' | sort | uniq -c

sed "s/<key>/\n&/g" puts every instance of "<key>" at the start of its own line.
grep "<key>" finds those lines.
awk uses regular expressions as field separators, so awk -F'<key>|</key>' '{print $2}' prints the text between <key> and </key>.

sort alphabetizes the result and passes it on to uniq -c, which prints out each unique word along with the number of times it was found. Among those results will be something like "95 ActionUID", which is the number of actions.

The suite of UNIX command line tools grew up kind of organically over the course of the last 50 years or so as people had to tackle exactly these sorts of problems, among others. A long time ago I stopped asking "can you do such and such?" because the answer almost always turned out to be, "yes."

A statement, I have been delighted to discover, that is also true of Keyboard Maestro.

4 Likes

Hey Folks,

pbpaste | sed 's/<key>/\n&/g' # | grep '<key>' | awk -F'<key>|</key>' '{print $2}' | sort | uniq -c

This script doesn't work as you'd expect on older versions of macOS (pre Big Sur?) where sed doesn't parse the newline character. (Apple only recently updated their command line tools to reasonably new versions.)

The script actually does accomplish its mission on Mojave, but unfortunately that's by accident. An "n" character is inserted in the output rather than a newline character, but this error is made moot since the XML keys are already on lines of their own.

To get the script working as expected you need to install gnu-sed [1] and substitute gsed for sed in the script.

OR

Wrap the old sed command in a C-String to enable the newline character:

pbpaste | sed $'s/<key>/\\\n&/g' | grep "<key>" | awk -F'<key>' '{print $2}' | sort | uniq -c

Then again – you can do without sed altogether and shorten up the script a bit:

pbpaste | awk -F'<key>|</key>' '/<key>/ { print $2 }' | sort | uniq -c

[1] Package Managers for macOS capable of installing gsed:

MacPorts
Homebrew


-Chris

3 Likes

Nitpick: grep as used returns matching lines -- it's wc -l that counts them.

Not as pointless a nitpick as it looks, because an even simpler solution suggests itself. Copy fish dog fish cat fish cow to the clipboard then run:

pbpaste | grep -o "fish" | wc -l

Bish, bash, bosh...

Totally agree with this, and I really should get better with awk, sed, etc.

2 Likes

I happen to have a fondness for awk, and I've used the shell with admiration since the early 80s
but more generally I find in practice that the shell costs much more wasted time experimenting to get things right.

Why ? Everything is a string. This is not a strength, and it costs, in practice, a lot of time :slight_smile:

I find now that I can get to a solidly working solution (which won't trip me up and distract me) very much faster in a more strongly typed context, for example a language in which Bool ≠ Int ≠ String ≠ Array ≠ Dict etc etc, and you get helpful messages when the type is wrong.

There are more helpfully typed versions of the shell environment, of course. See, for example:

Turtle.Tutorial

and

Shelly.hs/README.md at master · gregwebs/Shelly.hs

2 Likes

This is really what I was referring to when I said shell tools "grew up kind of organically." Sometimes that's a feature. Sometimes it's a bug.

Agreed. For example, awk is a really amazing tool that warrants the books that have been written about it. But it won't be my tool of choice if I am doing much more than parsing a string and generating some nicely formatted output. Most of my awk code fits on one line and includes a printf statement somewhere.

The thread started out with a well-defined question, "How do I get the count of all actions in a macro?" The problem is well constrained, you can make assumptions about what the XML generated by KM looks like, and so on. Given that context, a very short, reliable, and easily debugged bit of command line code "does the job."

But what does "does the job" mean, and what kind of tool does it demand? There are some fun and interesting philosophical questions buried in there. Are you trying to develop a general purpose tool for the world, or are you trying to get an answer to the question so that you can get on with life? A developer tends to think about the world from the first perspective.

No. Let me say that more carefully. A developer needs to think about the world from the first perspective.

On the other end of things, a scientist usually thinks about the world from second perspective. The time spent developing the bulletproof code is a distraction from the need for a tool that is useful here and now, gives an answer, and lets one get on with the task at hand. (While I have spent time in the first of those worlds, I live in the second.)

One of the things that I really love about Keyboard Maestro is that it plays beautifully across that spectrum.

4 Likes