Is There a Cross Reference Tool for Documenting Macro Callers/Callees?

I've been using Keyboard Maestro many years, and find it increasingly difficult update and adapt sets of interdependent macros. It is extremely tedious and (for me) error-prone, using KM's built-in Search to determine who calls and who is called by a given macro.

It would help immensely if I had a tool to generate, at minimum, a macro call list—all of the macros called by a given macro. A full cross-reference would be great, but the initial call list is the hard part. I can generate the rest of the cross-reference myself.

I know that the information is available in the XML of KM's exported .kmmacros file format, but I really don't want to take the time to create the extraction tool myself if I can avoid it. All of the free XSLT/XPath tools I know of don't handle beyond version 1 of the two specs. I tried to create a similar cross-reference tool for a different app, and found version 1 of XSLT to be too primitive for my needs.

Does anyone know of such a tool? Or is this information already available in KM and I just haven't looked in the right place?

As of KM 8+, you can use AppleScript to loop through all of your macros, search each for "Execute Macro" Actions, and extract the target sub-macro.

See Scripting the Keyboard Maestro editor .

If I get some time over the next few days, I'll try to provide an example script.

Thanks for the tip. This should simplify at least part of my solution.

Using AppleScript appears doable, but easier said than done. Actions are nested inside other actions, so you must search inside some actions recursively. The nested action types don't appear to be directly recognizable in AppleScript. It looks like you either have to examine the action's XML, or use try/on error to distinguish and manage the different cases.

An added complication which may or may not be unique to me is that I cannot reliably look for actions whose names start with "Execute Macro", since I frequently Rename my actions for inline documentation purposes. I don't do it often for "Execute Macro", but I have done it. That still means examining the XML do detect macro calls, and then I have to identify the callee by its UID, not by name.

So, the approach I'm currently considering is extracting and storing all macro UIDs. Then saving each macro in its own .kmmacro file. Then searching all the .kmmacro files for ExecuteMacro and the corresponding UID.

If I've overlooked something in my first pass analysis of the Scripting capabilities of KM, I'd be happy to hear it.

Thanks,
Gerrie

Actually you don't. All you have to do is loop through all top-level Actions in a Macro.
The XML for each top-level Action contains XML for ALL of its contained Actions.

Also not an issue, since we can search for XML keys that are NOT changeable by the user. Here's a sample Action XML:

<dict>
  <key>ActionName</key>
  <string>Call Sub-Macro</string>
  <key>Asynchronously</key>
  <false/>
  <key>MacroActionType</key>
  <string>ExecuteMacro</string>
  <key>MacroUID</key>
  <string>02011118-ECD0-446E-9A9D-F1BBEC6D9902</string>
  <key>Parameter</key>
  <string>%Variable%SingleLine%</string>
  <key>TimeOutAbortsMacro</key>
  <true/>
  <key>UseParameter</key>
  <true/>
</dict>

Note that I have renamed this action.
All we have to search for is:

  <key>MacroActionType</key>
  <string>ExecuteMacro</string>
  <key>MacroUID</key>
  <string>02011118-ECD0-446E-9A9D-F1BBEC6D9902</string>

Here's the RegEx to do this search:
(?is)<string>ExecuteMacro<\/string>.+<key>MacroUID<\/key>.+<string>([\w-]+)<\/string>

This returns a Capture Group with the UUID of the Execute Macro Action.
We can easily get the Macro Name by:

set exMacroName to name of (get macro id macroUUID)

I've got a working AppleScript that does the extraction for on macro. I just need to put it in loop for all macros.

Unless I get distracted, I should have my completed script by tomorrow, if not sooner.

Merry Christmas to all! :christmas_tree:

I’m excited. This looks great. I’m sure I could get it working from what you’ve given here, but I’m also sure you can do it more quickly than I can.

I really appreciate all you’ve done so far. Please don’t take time away from family and holiday activities for my problem.

Merry Christmas to you, too.

Here is an early Beta version (no guarantees!) of my script to do what you ask, but I has only had very, very limited testing. If you like it perhaps you could do us the favor of providing more extensive testing and verification.

It is a bit slow -- takes ~20 sec to process 2,000 macros, or 0.01 sec/macro

Merry Christmas to all! :christmas_tree:


Example Results

Results are returned and put on clipboard.

List of Macros with Execute Macros 

[DanTest] Reveal Macro and Group From UUID v1.1a (Author.@DanThomas)
    • [DanTest] Filter Variable with Strip JXA Result Warnings (Author.@DanThomas)
[FT] @Dial iPhone with Selected Number @FaceTime
    • [WIN] Move & Set Window Size for MY Prefs
[KM] Execute (Trigger) Macro by Name (Spotlight) (Author.@DanThomas)
    • Spotlight Search Prompt (Author.@DanThomas)
[KM] Insert 'Execute a Macro' from List of ALL Macros (Spotlight) (Author.@DanThomas)
    • Spotlight Search Prompt (Author.@DanThomas)
[KM] Insert 'Execute a Macro' from List of PRE-SELECTED Macros and Spotlight) (Author.@DanThomas)
    • Spotlight Search Prompt (Author.@DanThomas)
[KM] Set Focus to @KM Group [Sub-Macro]
    • [KM] Set Focus to @KM Group [Sub-Macro]
[KM] Set Variable by Sub-Macro Passed as Parameter [Example]
    • [KM] Sub-Macro to Set Variable Passed as Parameter [Example]
Trigger Different Sub-Macro on Each Trigger
    • Sub-Macro 1
    • Sub-Macro 2
    • Sub-Macro 3
OL REPLY to SD6 Message
    • OL Find [SUB]
    • [KM] DELETE List of KM Variables [SUB-MACRO]
Go To Macro by Name (Spotlight) (Author.@DanThomas)
    • Spotlight Search Prompt (Author.@DanThomas)
    • Macro Scroll Position - Store for Current Macro (Sub-Macro)
    • Macro Scroll Position - Restore for Current Macro (Sub-Macro) (Author.@DanThomas)

. . .
and many more

Beta Script to Get List of Macros and Their Sub-Macros

("Sub-Macro" = "Execute Macro" Action)

property ptyScriptName : "Get List of All Sub-Macros in All KM Macros"
property ptyScriptVer : "BETA 0.2"
property ptyScriptDate : "2018-12-24"
property ptyScriptAuthor : "JMichaelTX"

use AppleScript version "2.5" -- Sierra 10.12+ or later
use framework "Foundation"
use scripting additions

property LF : linefeed

(*
  Action XML Containing Execute Macro
  
  <string>ExecuteMacro</string>
  <key>MacroUID</key>
  <string>02011118-ECD0-446E-9A9D-F1BBEC6D9902</string>

*)
--- RegEx to Extract Execute Macro UUID ---

## (?i)<string>ExecuteMacro<\/string>.*?\n.*?<key>MacroUID<\/key>.*?\n.*?<string>([\w-]+?)<\/string>
set reMacroUUID to "(?i)<string>ExecuteMacro<\\/string>.*?\\n.*?<key>MacroUID<\\/key>.*?\\n.*?<string>([\\w-]+?)<\\/string>"


tell application "Keyboard Maestro"
  
  set macroList to every macro
  --set macroList to selected macros
  
  set exMacrosInAllMacros to {}
  set numMacros to count of macroList
  log "Number of Macros to Process: " & numMacros
  
  set i to 0
  repeat with oMacro in macroList
    set i to i + 1
    set exMacrosInThisMacro to {}
    set currentMacroName to name of oMacro
    log "Now Processing Macro# " & i & ": " & currentMacroName
    
    set actionList to actions in oMacro
    
    repeat with oAction in actionList
      set actionXML to xml of oAction
      
      --- Find ALL Execute Macros in This Action & All Contained Actions ---
      --    Item 1: Full Match;   Item 2: Capture Group List
      set exMacroMatches to my regexFind(reMacroUUID, actionXML, true)
      
      if (exMacroMatches ≠ {}) then -- get Macro Name for each Execute Macro
        
        repeat with oMatch in exMacroMatches
          
          --- Extract UUID and Get Macro Name ---
          set macroUUID to contents of item 2 of oMatch
          
          
          set exMacroName to name of (get macro id macroUUID)
          
          if (exMacrosInThisMacro does not contain exMacroName) then
            --- ADD Found Execute Macro to List ---
            set end of exMacrosInThisMacro to exMacroName
          end if
          
        end repeat -- oMatch
        
        
      end if
      
    end repeat -- oAction
    
    if (exMacrosInThisMacro ≠ {}) then
      --- ADD NEW Item for Each Macro ---
      --   Sub-Item 1: Containing Macro Name   Sub-Item 2: List of Excute Macro Names
      set AppleScript's text item delimiters to (linefeed & tab & "• ")
      set macroTextList to tab & "• " & (exMacrosInThisMacro as text)
      --set exMacrosInAllMacros to {(name of oMacro), exMacrosInThisMacro}
      set end of exMacrosInAllMacros to {(name of oMacro), macroTextList}
    end if
    
  end repeat -- oMacro in macroList
  
  set AppleScript's text item delimiters to linefeed
  set msgStr to "List of Macros with Execute Macros " & LF & LF & (exMacrosInAllMacros as text)
  set titleStr to (name of me)
  
  display dialog msgStr ¬
    with title titleStr ¬
    with icon stop
  
  
end tell

set the clipboard to msgStr
return msgStr

--~~~~~~~~~~~ END OF MAIN SCRIPT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
on regexFind(pFindRegEx, pSourceString, pGlobalBool) -- @RegEx @Find @Search @Strings @ASObjC @Shane
  --–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
  (*  VER: 1.1    2017-11-22
    PURPOSE:  Find Match(s) & Return Match with All Capture Groups as List of Lists
    METHOD:    Uses ASObjC RegEx (which is based on ICU Regex)
    PARAMETERS:
      • pFindRegEx    | text |  RegEx pattern to search for
      • pSourceString | text |  Source String to be searched
      • pGlobalBool   | bool |  Set true for Global search (all matches)

    RETURNS: IF pGlobalBool:  List of Lists, one list per match
                ELSE:  Single List of first match
                Each Match List is a List of Full Match + One Item per Capture Group
                  {<Full Match>, <CG1>, <CG2>, <CG3>, ...}
                  IF CG not found, Item is returned as empty string
                If NO matches, return empty list {}

    AUTHOR:  JMichaelTX
    ## REQUIRES:  use framework "Foundation"
    REF:  
      1. 2017-11-22, ShaneStanley, Does SD6 Find RegEx Support Case Change?
           • Late Night Software Ltd., 
          • http://forum.latenightsw.com//t/does-sd6-find-regex-support-case-change/816/8
      2.  ICU RegEx Users Guide
          http://userguide.icu-project.org/strings/regexp
    --–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
  *)
  
  local theFinds, theResult, subResult, groupCount
  set LF to linefeed
  
  try
    
    set pSourceString to current application's NSString's stringWithString:pSourceString
    set {theRegEx, theError} to current application's NSRegularExpression's regularExpressionWithPattern:pFindRegEx options:0 |error|:(reference)
    if theRegEx is missing value then error ("Invalid RegEx Pattern." & LF & theError's localizedDescription() as text)
    
    if (pGlobalBool) then ### FIND ALL MATCHES ###
      set theFinds to theRegEx's matchesInString:pSourceString options:0 range:{0, pSourceString's |length|()}
      
    else ### FIND FRIST MATCH ###
      set theFind to theRegEx's firstMatchInString:pSourceString options:0 range:{0, pSourceString's |length|()}
      set theFinds to current application's NSMutableArray's array()
      (theFinds's addObject:theFind)
    end if
    
    set theResult to current application's NSMutableArray's array()
    
    repeat with aFind in theFinds
      set subResult to current application's NSMutableArray's array()
      set groupCount to aFind's numberOfRanges()
      
      repeat with i from 0 to (groupCount - 1)
        
        set theRange to (aFind's rangeAtIndex:i)
        if |length| of theRange = 0 then
          --- Optional Capture Group was NOT Matched ---
          (subResult's addObject:"")
        else
          --- Capture Group was Matched ---
          (subResult's addObject:(pSourceString's substringWithRange:theRange))
        end if
      end repeat
      
      (theResult's addObject:subResult)
      
    end repeat -- theFinds
    
  on error errMsg number errNum
    set errMsg to "ASObjC RegEx ERROR #" & errNum & LF & errMsg
    set the clipboard to errMsg
    display dialog errMsg & LF & ¬
      "(error msg is on Clipboard)" with title (name of me) with icon stop
    error errMsg
    
  end try
  
  return theResult as list
end regexFind
--~~~~~~~~~~~~~~~~~~~~ END of Handler ~~~~~~~~~~~~~~~~~~~~~~~~~~~~



2018/12/27 02:18 CST -- Version 1.0(b0002) has been posted below.


Hey @gerrie,

Like JM's macro this one is a very early beta and is only lightly tested.

It runs in about 2 seconds on my elderly system with about 1600 macros in Keyboard Maestro.

REQUIRES: BBEdit

(The BBEdit demo will revert to BBEdit-Lite after 30 days, but it is still far more powerful than most text editors available for the Mac and remains fully AppleScriptable.)

Produces a hierarchical list of macros/sub-macros.

-Chris


List Keyboard Maestro Macros with Sub-Macros v1.0(b0001).kmmacros (14 KB)

1 Like

Chris, fascinating script -- well done!
Clearly much faster than my script by ~10X.

But I must say it is a very complex, yet innovative script -- you were clearly thinking out of the box. Maybe if you have the time, you could explain the method you used so the rest of us could learn from it.

I have been quite impressed in the past by the work that you, JM and other regular contributors post to this forum, but never in my wildest dreams would have expected this much in answer to my original query.

Thank you JM, for showing me that AppleScript was available and the things that were possible with it. And, Chris, thanks for the quick response with an amazingly complete solution.

Must not be too elderly, mine took ~3 seconds for 150 macros on my late 2013 i7 MacBook Pro. But still, more than adequately zippy!

I'll second that.

Thanks again.

-Gerrie

Hey Guys,

Here's the first revision v1.0(b0002) of the macro/script.

  • I removed some extraneous code.
  • Made the script more efficient.
  • Managed to retain an alpha-sort of the macros
  • Changed methods such that you can step through the script with Script Debugger now.
  • Still requires BBEdit.

Try it out and let me know how it works and then I'll consider making a video explaining its inner mysteries.

Output looks like this:

[KM] Execute Macro by Name (Spotlight) ⇢ Macro Group ⇢ KM

   ❖ Spotlight Search Prompt

[KMFAM] Add Action(s) ⇢ Macro Group ⇢ KMFAM Favorite Actions and Macros

   ❖ [kmfam]~Add (Sub-Macro)

[KMFAM] Add Macro ⇢ Macro Group ⇢ KMFAM Favorite Actions and Macros

   ❖ [kmfam]~Add (Sub-Macro)

[KMFAM] Edit ⇢ Macro Group ⇢ KMFAM Favorite Actions and Macros

   ❖ [kmfam]~Verify Paths (Sub-Macro)
   ❖ [kmfam]~Core (Sub-Macro)
   ❖ [kmfam]~Core (Sub-Macro)

-Chris


List Keyboard Maestro Macros with Sub-Macros v1.0(b0002).kmmacros (12 KB)

3 Likes

Chris, thanks for the update/changes. I like them.

One thing I wanted, but didn't find, was the ability to remove duplicate sub-macros for a given parent macro.

So, I made a few minor mods to add that feature, and make some formatting changes to my preferences. Hopefully I didn't break anything. All looks good with my limited testing, but please let me know if see any issues.

One thing I'd like to so, but did not try here, is to sort by Macro Group, Parent Macro, Sub-Macro, and show that as a hierarchy.

Example Output

[SE] NEW Script Document from Template  ❯ MG: Script Editor ❮
  • [SE] Set Script Editor Doc Properties

[SE] Script Editor Launch Setup  ❯ MG: [GLOBAL] ❮
  • [SE] Set Script Editor Doc Properties
  • [SE] NEW Script Document from Template

[SE] Set Script Editor Doc Properties  ❯ MG: Script Editor ❮
  • [WIN] Move & Set Window Size for MY Prefs
  • [SE] Show Log and Messages

[TEMPLT] Std Macro Template BACKUP  ❯ MG: Templates ❮
  • [KM] DELETE List of KM Variables [SUB-MACRO]

Key Changes


--- All changes are marked with "## JMTX" ---

--- NEAR THE TOP ---
property removeDupMacros : true ## JMTX Add

--- DOWN BELOW ---

        tell theMacro
          set end of tempList to its name & "  ❯ MG: " & its macro group's name & " ❮" ## JMTX Mod
          ##        set end of tempList to ""  ## JMTX Mod
          set actionXML to (xml of its actions whose xml contains "ExecuteMacro")
          set actionXML to (my joinList:actionXML usingStr:linefeed)
          set uuidList to (my regexFindWithCapture:"<string>ExecuteMacro</string>\\n\\h*<key>MacroUID</key>\\n\\h*<string>([0-9A-Z-]+)</string>" fromString:actionXML resultTemplate:"$1")
          
          
          set macroNameList to {} ## JMTX Add
          repeat with i in uuidList
            set macroName to getMacroNameFromUUID(i) of me
            
            ## JMTX: Revise to Allow Removal of Dup Sub-Macros for a Given Macro ##
            
            if (removeDupMacros and (macroNameList does not contain macroName)) then
              set end of tempList to tab & "• " & macroName ## JMTX Mod
              set end of macroNameList to macroName
            else if not removeDupMacros then
              set end of tempList to tab & "• " & macroName ## JMTX Mod
            end if
            ## JMTX:  END of Block Changes for Dup Sub-Macros
            
          end repeat
          
        end tell
  

Full Script with Changes

----------------------------------------------------------------
# Auth: Christopher Stone  (minor mods by @JMichaelTX)
# dCre: 2018/12/25 15:03
# dMod: 2018/12/27 17:35
# Appl: Keyboard Maestro
# Task: Find macros that use sub-macros and list by name.
# Libs: None
# Osax: None
# Tags: @Applescript, @Script, @Keyboard_Maestro, @Find, @Macros, @Sub-Macros, @SubMacros, @List, @Name
# Vers: 1.0(b0003)  by @JMichaelTX
----------------------------------------------------------------
use AppleScript version "2.4"
use framework "Foundation"
use scripting additions
----------------------------------------------------------------
(*
  ~~~~~~~~~~~ MODS BY @JMichaelTX ~~~~~~~~~~~~~~~~~~
  Ver b0003   Date:  2018-12-27  17:37 GMT-6
  All changes are marked with "## JMTX"
  
  1. Add feature to remove dup Sub-Macros for a Given Parent Macro
      • Set property removeDupMacros
  2. Revised BBEdit formatting to my preference of Macro Group, bullets and spacing
      • Changed code around line numbers 63-80
*)

property removeDupMacros : true ## JMTX Add

try
  
  tell application "Keyboard Maestro"
    
    set actionList to actions of macros where its xml contains "ExecuteMacro"
    
    try
      
      actionList / 0
      
    on error errorStr
      
      set macroList to errorStr
      set macroList to (my regexFindWithCapture:"«class MKma» id \"[0-9A-Z-]+\"" fromString:macroList resultTemplate:("$0" & " of application \"Keyboard Maestro\""))
      
      set tempList to {}
      
      repeat with i in macroList
        if contents of i is not in tempList then
          set end of tempList to contents of i
        end if
      end repeat
      
      copy tempList to macroList
      
      set macroList to my joinList:macroList usingStr:", "
      set macroList to "{" & macroList & "}"
      set macroList to run script macroList
      
      set tempList to {}
      
      repeat with theMacro in macroList
        
        tell theMacro
          set end of tempList to its name & "  ❯ MG: " & its macro group's name & " ❮" ## JMTX Mod
          ##        set end of tempList to ""  ## JMTX Mod
          set actionXML to (xml of its actions whose xml contains "ExecuteMacro")
          set actionXML to (my joinList:actionXML usingStr:linefeed)
          set uuidList to (my regexFindWithCapture:"<string>ExecuteMacro</string>\\n\\h*<key>MacroUID</key>\\n\\h*<string>([0-9A-Z-]+)</string>" fromString:actionXML resultTemplate:"$1")
          
          
          set macroNameList to {} ## JMTX Add
          repeat with i in uuidList
            set macroName to getMacroNameFromUUID(i) of me
            
            ## JMTX: Revise to Allow Removal of Dup Sub-Macros for a Given Macro ##
            
            if (removeDupMacros and (macroNameList does not contain macroName)) then
              set end of tempList to tab & "• " & macroName ## JMTX Mod
              set end of macroNameList to macroName
            else if not removeDupMacros then
              set end of tempList to tab & "• " & macroName ## JMTX Mod
            end if
            ## JMTX:  END of Block Changes for Dup Sub-Macros
            
          end repeat
          
        end tell
        
        set end of tempList to ""
        
      end repeat
      
      set reportStr to my joinList:tempList usingStr:linefeed
      
      bbeditNewDoc(reportStr, "activate") of me
      
    end try
    
  end tell
  
on error e number n
  set e to e & return & return & "Num: " & n
  if n ≠ -128 then
    try
      tell application (path to frontmost application as text) to set ddButton to button returned of ¬
        (display dialog e with title "ERROR!" buttons {"Copy Error Message", "Cancel", "OK"} ¬
          default button "OK" giving up after 30)
      if ddButton = "Copy Error Message" then set the clipboard to e
    end try
  end if
end try

----------------------------------------------------------------
--» HANDLERS
----------------------------------------------------------------
on bbeditNewDoc(_text, _activate)
  tell application "BBEdit"
    set newDoc to make new document with properties {text:_text, bounds:{0, 44, 1920, 1200}}
    tell newDoc
      select insertion point before its text
    end tell
    if _activate = true or _activate = 1 or _activate = "activate" then activate
  end tell
end bbeditNewDoc
----------------------------------------------------------------
on cngStr:findString intoString:replaceString inString:dataString
  set anNSString to current application's NSString's stringWithString:dataString
  set dataString to (anNSString's stringByReplacingOccurrencesOfString:findString withString:replaceString ¬
    options:(current application's NSRegularExpressionSearch) range:{0, length of dataString}) as text
end cngStr:intoString:inString:
----------------------------------------------------------------
on getMacroNameFromUUID(macroUUID)
  tell application "Keyboard Maestro Engine"
    return process tokens "%MacroNameForUUID%" & macroUUID & "%"
  end tell
end getMacroNameFromUUID
----------------------------------------------------------------
on joinList:theList usingStr:theStr
  set anNSArray to current application's NSArray's arrayWithArray:theList
  set theString to anNSArray's componentsJoinedByString:theStr
  return theString as text
end joinList:usingStr:
----------------------------------------------------------------
on regexFindWithCapture:thePattern fromString:theString resultTemplate:templateStr
  set theString to current application's NSString's stringWithString:theString
  set theRegEx to current application's NSRegularExpression's regularExpressionWithPattern:thePattern options:0 |error|:(missing value)
  set theFinds to theRegEx's matchesInString:theString options:0 range:{0, theString's |length|()}
  set theResult to current application's NSMutableArray's array()
  
  repeat with aFind in theFinds
    set foundString to (theRegEx's replacementStringForResult:aFind inString:theString |offset|:0 template:templateStr)
    (theResult's addObject:foundString)
  end repeat
  
  return theResult as list
  
end regexFindWithCapture:fromString:resultTemplate:
----------------------------------------------------------------
on splitString:someText withString:mySeparator usingRegex:regexFlag ignoringCase:caseFlag ignoringDiacrits:diacritsFlag
  -- build options
  set theOptions to 0
  if caseFlag then set theOptions to (current application's NSCaseInsensitiveSearch)
  if regexFlag then
    set theOptions to theOptions + (current application's NSRegularExpressionSearch as integer)
  else -- ignoring diacriticals only applies if not doing regex
    if diacritsFlag then set theOptions to theOptions + (current application's NSDiacriticInsensitiveSearch as integer)
  end if
  set theString to current application's NSString's stringWithString:someText
  if theOptions = 0 then -- simplest case: literal, considering case and ignoring diacrits
    set theList to theString's componentsSeparatedByString:mySeparator
  else
    set theList to {} -- to hold strings
    -- get length and set some intital variables
    set theFullLength to theString's |length|()
    set theLength to theFullLength
    set theStart to 0
    repeat
      -- look for range of next match
      set theRange to theString's rangeOfString:mySeparator options:theOptions range:{location:theStart, |length|:theLength}
      if theRange's |length|() = 0 then -- no match found, so grab the whole string then exit repeat
        set end of theList to (theString's substringWithRange:{location:theStart, |length|:theLength}) as text
        exit repeat
      end if
      -- grab the text before the match
      set end of theList to (theString's substringWithRange:{location:theStart, |length|:(location of theRange) - theStart}) as text
      -- reset values for next search
      set theStart to current application's NSMaxRange(theRange) -- straight after the match
      set theLength to theFullLength - theStart
    end repeat
  end if
  return theList as list
end splitString:withString:usingRegex:ignoringCase:ignoringDiacrits:
----------------------------------------------------------------

Thanks again for sharing your solution.

1 Like

Hey JM,

I think I prefer leaving the duplicates, so I can tell at a glance how many times the sub-macro is used in a given macro.

-Chris

I actually see use for both approaches—JM's for brevity, Chris's for completeness. One thing I've done in past similar situations is list each one once, but append the actual occurrence count in some way. I do prefer JM's elimination of the blank line between the macro and its sub-macro list.

I still have post-processing to do, because, as I included in my original post, I also need the reverse information, for each sub-macro who are all its calling macros. When I want to modify/simplify/replace a macro, I need to know which calling macros I have to preserve support for, and which ones might also benefit from possible changes.

I will probably do the rest of that work in Mathematica. Your use of the Foundation framework in AppleScript (or any other language) is beyond my skill set at present. Mathematica will give me a more general, flexible and customizable solution than I could ever produce with AppleScript and plain text files. There I can generate the complete call graph, and easily move up and down the call "tree" (quoted because it isn't always a tree). A plain text file is not the place for me to manage all that. My ultimate objective is to create this same cross-reference information for FileMaker Custom Functions. My desire for the KM cross-reference came from the complexities of my KM macro set for extracting the necessary data from FileMaker—it all has to be done through UI simulation since FileMaker has zero scripting support for its Custom Functions.

I very much appreciate all the work both of you have done. It's way more than I expected to get. It's also nice to know that you both seem interested in using the result for your own purposes—I didn't just pose a problem whose solution only I was interested in.

-Gerrie

1 Like

Yes, that would be an interesting report. I'd love to see your Mathematica solution.

I think that report is also very doable in AppleScript -- I'd bet Chris @ccstone might take a hack at that. :wink:

You selected my post as your solution, but Chris really deserves all the credit.

I'm unfamiliar with how to tag things in the forum. I actually selected both as solutions, because of the completeness and brevity issues. However, I see now that I can only mark one solution, yours stuck because I marked it second. I've corrected that now.

It will likely be slow in coming. I'm still a neophyte with Mathematica interactive stuff.

-Gerrie

1 Like

JM, I apologize for not updating this earlier, but I gave up on a Mathematica solution—it would've taken more time than I was willing to invest.

What I did do was rework Chris's AppleScript and wrapped a KM macro around it to give me output that I can directly import into a database. I didn't post it, because I didn't think anyone else would be interested (maybe a bad assumption on my part?). It uses macro and group UUIDs and relies on the database to put all the pieces together in a human readable form. It also corrects one deficiency in Chris's solution. Callee identifiers must also include their group. I sometimes use the same name for two or more macros in different groups because they perform identical operations from the user perspective, but their underlying implementation is actually-context dependent, hence the different groups.

My macro creates three separate tab-separated-value-list files—one for Groups, one for Macros and one for Macro calls. These files are structured as follows:

Group file:

<group-UUID> <group-name>

Macro file:

<group-UUID> <macro-UUID> <macro-name>

Cross-reference file:

<caller-UUID> <callee-UUID> <callee-uses-parameter?> <parameter-value>

If someone is actually interested, I could cleanup my implementation and post it.

-Gerrie