How Can I Get List of All KM Variables in a Given Macro?

@peternlewis, I need your help please when you have a few moments.

I'm writing an AppleScript to get all KM Variables for a selected macro.
AFAIK, the only way to do this is by searching the Macro XML. If you know otherwise, please advise.

Please confirm/correct that all KM Variables, in all KM Actions, can be identified by this bit of XML from the Macro XML:

            <key>Variable</key>
            <string>SomeKMVariableNameHere</string>

This appears to work even in IF/THEN Actions and in Conditions.
Do you know of any Action where this would not work?

If this is correct, then we can extract the KM Variable Name with this simple RegEx:
(?i)<key>Variable<\/key>[^<]+<string>([^<]+)<\/string>

If anyone has any ideas, I'm all open to suggestions.

Thanks.

Hey @JMichaelTX,

Thanks for tackling this task. I've been wanting to for some time but haven't had the oomph.

@peternlewisplease, please expose the XML of the selected macro(s) to AppleScript. So many things you don't want to fool with will become possible for users to build themselves if you do.

I realize this will be a relatively small set of users, but the tools we build will trickle down to others.

By we I mean: @ccstone, @ComplexPoint, @DanThomas, @JMichaelTX, @Tom, and others who escape me at the moment.

-Chris

2 Likes

There are, of course, routes through the clipboard etc.

In a JS idiom, assuming we have copied a macro, we can read the parts of an array of objects with something like:

(() => {
    "use strict";

    ObjC.import("AppKit");

    // main :: IO ()
    const main = () => {
        const
            uti = "com.stairways.keyboardmaestro.macrosarray",
            maybePlistArray = ObjC.deepUnwrap(
                $.NSPasteboard.generalPasteboard
                .propertyListForType(uti)
            );

        return Array.isArray(maybePlistArray) ? (
            maybePlistArray
        ) : alert(
            "Copy KM macro as JSON"
        )(`No clipboard content found for type:\n\t${uti}`);
    };

    // ------------------- GENERIC -------------------

    // 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
            );
        };

    // typesInClipboard :: () -> IO [UTI]
    const typesInClipboard = () =>
        ObjC.deepUnwrap(
            $.NSPasteboard.generalPasteboard
            .pasteboardItems.js[0].types
        );

    return main();
})();

e.g. to just list the variable names of a copied macro:

(() => {
    "use strict";

    ObjC.import("AppKit");

    // main :: IO ()
    const main = () => {
        const
            uti = "com.stairways.keyboardmaestro.macrosarray",
            maybePlistArray = ObjC.deepUnwrap(
                $.NSPasteboard.generalPasteboard
                .propertyListForType(uti)
            );

        return Array.isArray(maybePlistArray) ? (
            maybePlistArray[0].Actions.flatMap(
                dict => {
                    const variableName = dict.Variable;

                    return Boolean(variableName) ? (
                        [variableName]
                    ) : [];
                }
            )
        ) : alert(
            "Copy KM macro as JSON"
        )(`No clipboard content found for type:\n\t${uti}`);
    };

    // --------------------- GENERIC ---------------------

    // 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
            );
        };

    // typesInClipboard :: () -> IO [UTI]
    const typesInClipboard = () =>
        ObjC.deepUnwrap(
            $.NSPasteboard.generalPasteboard
            .pasteboardItems.js[0].types
        );

    // sj :: a -> String
    const sj = (...args) =>
        // Abbreviation of showJSON for quick testing.
        // Default indent size is two, which can be
        // overriden by any integer supplied as the
        // first argument of more than one.
        JSON.stringify.apply(
            null,
            1 < args.length && !isNaN(args[0]) ? [
                args[1], null, args[0]
            ] : [args[0], null, 2]
        );

    return sj(main());
})();

Jim - Here's some JXA I wrote many years ago that does what you're talking about. I think the regex is similar, but if it's different, for all I know yours is better - I didn't compare them too closely. You have to pass the XML to it, because at the time I was new to JXA and I didn't know how to get the XML using JXA:

function run() {
'use strict';

var _kme = Application("Keyboard Maestro Engine");
var _string = _kme.getvariable("blvdMacroSource");

var _re = new RegExp("(\\<key>Variable\\</key>\\s*\\<string>)([^\\<]*)", "g");

var _matches = getMatches(_string, _re, 2).filter(onlyUnique).sort().join("\n");

return _matches;

function getMatches(string, regex, groupIndex) {
  var _matches = [];
  var _match;
  while (_match = regex.exec(string)) {
  	var _varName = _match[groupIndex].trim();
	if (!_varName.startsWith("%"))
	    _matches.push(_varName);
  }
  return _matches;
}

function onlyUnique(value, index, self) { 
    return self.indexOf(value) === index;
}
}

If you care, here's the complete macro:
[KM] Get Variables Used in this Macro.kmmacros (71.9 KB)

Warning:

But be aware that this concept only works for variables that are specifically set in the macro. Because there's lots of ways for a variable to be referenced in a macro that don't fit that pattern. Actions that have Text fields, for instance.

If you're just talking about "defining" the variable (i.e. setting its value for the first time), there is at least one thing that wouldn't cover: If you call a sub-macro and pass the name of the result variable, and the sub-macro sets the variable's value. So in this instance it's entirely possible to have a variable set to a value, and then do something with the variable, without ever finding it using the regex.

I hope this makes sense.

Chris - Perhaps you could clarify what you mean by "expose the XML"? I mean, we know how to get the XML in a variety of ways, right? So what specifically do you mean by "expose the XML"?

I was wondering, is it possible to store individual macros in the exported macro forms, stored in subfolders with the macro group as it's folder name?

E.g.,
"KM Macros" (main folder) → "Global Macro Group" (subfolder) → all macros in the "Global Macro Group" in KM, the file name is the macro name user defined in KM editor.

Advantage:

  1. When a user edit a macro, it only edit the individual macro file. So when two editor windows are open, they will only be operating on two individual macro files. When users move a macro to another macro group, that macro is moved to another folder.
  2. For the question asked in the OP, we only need to search in that macro file.
  3. Rather than "Export" and then upload to forum, and then delete the file, we can add another item in the right click macro contextual menu: "Copy Macro (file)". This will copy that individual macro file to the clipboard, so that we can "Paste" it to the forum post right away.
  4. We can add another item to the contextual menu: "Locate macro in Finder". If copy and paste macro in point 3 does not work well, we can easily locate the macro file and drag or copy from Finder.

Thanks guys. I have a similar AppleScript that uses the KM Editor UI to copy the macro as XML.
It works fairly well, but as you know, any script that relies on an app's UI is somewhat fragile.

What Chris is asking for is much, much better. More robust, and much faster.
And it would be much simpler.

The requested change would provide XML as a properly of the Macro object, just like is provided for the Action object.

  tell application "Keyboard Maestro"
    set {oMacro} to (get selected macros)  -- gets first macro
    set macroXML to xml of oMacro   --- NEW property that is being requested
    set macroName to name of oMacro
  end tell

Dan, thanks for sharing your script. Your RegEx is essentially the same as mine.
The problem is that, as you say, that only gets KM Variables that are set using the KM Action Set Variable to.... Turns out that there are many other ways to set/create KM Variables.
Here are just two as an example:

image

image

Of course, a KM Macro can reference (use) Global Variables that are set in another Macro.
So, if we want a list of all Variables that are USED in the Macro, we need to include these as well.

Check out my Macro Repository Suite. It may do some of what you're hoping for.

2 Likes

Hey Dan,

I mean provide the XML of macros as a property.

tell application "Keyboard Maestro"
   set macroXML to XML of item 1 of (get selected macros)
end tell

Copying the XML of a macro to the clipboard is not convenient at all for scripted operations – especially for batch operations.

Actions have an XML property:

tell application "Keyboard Maestro"
   set myMacro to item 1 of (get selected macros)
   set actionXML to xml of action 1 of myMacro
end tell

Why not macros?

-Chris

1 Like

Well, yes and no. It depends on how robust you need it to be. I mean, I use my macro regularly, I'm just aware of its limitations. Most of the time it does enough of what I need.

The reason I say that is that I started writing something to handle all possible conditions a while back, and I eventually gave up, because there were just too many possibilities.

But I'll be rooting for you if you want to give it a try! :smile:

Wow~ this is another treasure of macros!
Thanks!
I wish this was done natively at the first place.

My wish still has a point though. Editing an individual macro file instead of editing the file that has all macros will make the KM editor much faster.

I doubt there is any way to do this.

Almost certainly not. Most variables, but there are likely some actions that don't. Plus there are lots of other things that refer to variables (eg the output of a script) which might have variable names stored in different ways. And that is ignoring any references to tokens.

The XML is not documented at this level, and even if it were is subject to change that would violate this.

That's too bad. Do you have a way within KM to get the list of all variables used in a Macro?

No. Keyboard Maestro internally keeps track of variables that are directly referenced by any macro, with reference counting, in order to provide the variables popup menus and such, but there is no connection to the containing macro, nor any sublists based on macros.