KM -- Execute Last Run Macro Whose Name Ends with "TM" Macro (v9.2)

KM -- Execute Last Run Macro Whose Name Ends with "TM" Macro (v9.2)

Update (2021/09/20 19:16):
After more learning about sed. I have further simplified the script.

DOWNLOAD Macro File:

KM -- Execute Last Run Macro Whose Name Ends with "TM".kmmacros
Note: This Macro was uploaded in a DISABLED state. You must enable before it can be triggered.

Hey Martin,

What are you trying to do with $d in:

| sed -En '


To be honest, I don't know. It was from your original macro. I did not understand this. But since I got the desired result. I assumed it was necessary to keep it. :joy:
Can you explain why you put it there? :joy::joy:


Ah, so it is.

Hmm... Grey cells switching ON...

$d deletes the last line of the text first thing.

$ ⇢ Represents the last line of the file/string.
d ⇢ Is the command for delete.

I can't see that $d has any usefulness in this context, and I don't recall what I was thinking when I wrote the script.

I might have been trying to delete the training linefeed Unix files normally have, but I’m pretty sure that wouldn't matter at all in this context.


1 Like

Ah. I see. That was because you wanted to exclude the current macro. I was a blind follower, haha. It was mainly because I did not comprehend the script (I still don't understand the sed command there).

After many tries and fails, I managed to accomplish with tail, grep, awk, grep, tail. (Since I only want to find the target macro, I don't have to restrict myself to the last 10 macros. I just use the last, say, 40 lines, of the engine log.

I like this version more and have replaced the one in the OP with this one.

Ah, so. You're exactly right.

| sed -En '
   /Execute macro/{
      s!^([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}) Execute macro “(.+)”.+!\1  ⇢  \2!
' \
-E ⇢ Extended Regular Expressions.
-n ⇢ Suppresses the printing of every line parsed.

/Execute macro/ ⇢ Find the given pattern.

{ … } ⇢ Perform the contained command(s) on each line found by the find command.


s!^([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}) Execute macro “(.+)”.+!\1  ⇢  \2!

p ⇢ Print the line.

Search finds lines of this format:

2021-09-20 17:41:15 Execute macro “Activate Last Application v1.00” from trigger The Hot Key ⌘Escape is pressed

Search-and-Replace transforms those lines to this format:

2021-09-20 17:41:15 ⇢ Activate Last Application v1.00


1 Like

Thank you very much, Chris. Your explanation is extremely helpful! Now I can fully understand.

After further learning, I have been able to further simplify my macro. Now, I need only tail, sed, tail (see my updated post above):

kmEngineLog=~/'Library/Logs/Keyboard Maestro/Engine.log'

tail -n $tailNum "$kmEngineLog" \
| sed -En "s/.* Execute macro “(.+${grepString})”.*/\1/p" \
| tail -1
1 Like
if [[ $tailNum =~ ^[0-9]+$ && $tailNum -gt 0 ]]; then
    kmEngineLog=~/'Library/Logs/Keyboard Maestro/Engine.log'

    tail -n $tailNum "$kmEngineLog" \
    | awk -v g="$grepString" '/Execute macro/ && $0 ~ g {gsub(/.*“|”.*/, "", $0); print $0}' \
    | tail -1
    echo "Invalid value for \$tailNum: $tailNum"

This modification changes a few things.

  1. Changes from sed to awk for text manipulation because sed has issues processing the smart quotes that are used in the lines of the KM log that I was working with. Specifically:
    • Execute macro “Google sheets - Insert rows below” from trigger Name
    • (" vs )
  2. RegEx looks for line endings matching grepString2 (KMVariable) and returns what is inside the smart quotes.
  3. Uses $KMVAR_grepString instead of $KMVAR_local__grepString and other non-localized variables so that I can pass values to this macro as a subroutine.
  4. Adds minimal validation and logging.


I'm pretty new to this stuff and stumbled through it with a lot of back and forth with ChatGPT. In case others in a similar situation come here I'll post what I understand (mostly from ChatGPT) below. Corrections and clarifications are welcome.

  1. The value of $KMVAR_last_engine_log_lines is stored in a variable called $tailNum.
  2. The if statement checks if $tailNum is a positive integer. If it is, the script continues with the rest of the code. If it's not, an error message is displayed indicating that $tailNum is invalid.
  3. If $tailNum is valid, the value of $KMVAR_grepString is stored in a variable called $grepString.
  4. The path to the Keyboard Maestro engine log is stored in a variable called kmEngineLog.
  5. The tail command is used to extract the last $tailNum lines from the kmEngineLog file.
  6. The output of the tail command is piped to the awk command, which filters only the lines that contain the string "Execute macro" and match the search pattern stored in $grepString.
  7. The gsub function is used to remove everything before the first occurrence of and everything after the last occurrence of . The resulting string contains only the macro name.
  8. The print function is used to print the macro name to the output.
  9. The output of the awk command is piped to the second tail command, which returns only the last occurrence of the extracted macro name.

So the updated script essentially extracts the last $tailNum lines from the Keyboard Maestro engine log, filters only the lines that contain the string "Execute macro" and match the search pattern stored in $KMVAR_grepString2, and extracts the macro name from the filtered lines.

sed and awk are pretty cool.

Here is the explanation for the awk line.

awk -v g="$grepString" '/Execute macro/ && $0 ~ g {gsub(/.*“|”.*/, "", $0); print $0}'
  1. The awk command is prefixed by a | pipe which connects the output of one command to the input of another command. In this case, it's going from tail to awk and back to tail
  2. This awk command has two parts separated by &&:
    • /Execute macro/: This is a pattern that matches lines that contain the string "Execute macro".
    • $0 ~ g: This is a pattern that matches lines that contain the search pattern stored in the $grepString variable. The -v option is used to pass the value of $grepString to the awk command.
    • By using && to combine these two patterns, the awk command filters only the lines that contain the string "Execute macro" and that match the search pattern stored in $grepString.
  3. The filtered lines are then processed by the action in braces {}:
    • gsub(/.*“|”.*/, "", $0): This function removes everything before the first occurrence of and everything after the last occurrence of in the line.
    • print $0: This function prints the resulting line to the output.
  4. So the awk command essentially extracts the macro name from the lines that match the pattern /Execute macro/ && $0 ~ g, where g is the search pattern stored in $grepString. The resulting macro name is then printed to the output.

sed and smart quotes

The issue with using "smart quotes" or "curly quotes" in the sed command is that the character used to surround the string in the regular expression is not recognized by sed as a valid delimiter. This can cause sed to interpret the regular expression incorrectly or not at all, resulting in unexpected behavior or errors.

For example, when using smart quotes in the sed command, the regular expression may not match any lines in the input, even if there are lines that should match. This is because sed is treating the smart quotes as literal characters in the regular expression, rather than as delimiters.

In general, it's best to use standard ASCII characters for delimiters in sed commands to avoid any potential issues with character encoding or interpretation.

Hey Joshua,

@martin is not using smart quotes as delimiters in the sed code – they are part of the regular expression.

He is using regular double-quotes (instead of the usual singles) so that his variable gets properly expanded.

The biggest potential problem here is that older versions of sed are completely case-sensitive, and that can ruin your day if you mistype a letter (or don't know the correct case).

The fix is to use a later version of sed that respects the I switch:

grepString='trigger macro by name.*'
kmEngineLog=~/'Library/Logs/Keyboard Maestro/Engine.log'
tail -n $tailNum "$kmEngineLog" \
| gsed -En "/.* Execute macro “.*${grepString}”.+/Ip"

(I think GNU sed is included with macOS past Catalina, but I'm still using Mojave and am not certain. I've installed my copy with MacPorts.)

Later versions of awk have the IGNORECASE flag.


Ah, yes. Thank you for explaining, although it took me a minute. It's not a problem that there are smart quotes in the regular expression. They just can't be used as delimiters. Got it.

I am but a simple, small-town, scripter and all this seding and awking is rather confusing...

How about:

tail - $tailNum "$kmEngineLog" | grep -Eo "Execute macro.*${grepString}”" | grep -Eo "“.*”" | tail -1 | tr -d "“”"

Main problem would be the removal of any smart quotes from the macro's name in that last step, but I think that similar applies in the awk version because of the greedy outside-in matches.

But I do wonder if we're being a bit too clever. tailing the log file by line count seems to be there solely to limit the amount of text to process (compared to, say, something that limited the search to "macros run in the last 24 hours") and we're then jumping through hoops in the shell script to get a bunch of matches from which we then discard all but one.

All we need to do is grab the log, start at the end, and search backwards for the first match. KM can't do that in one action -- but "Filter" can reverse lines in a variable, "Search Using Regular Expression" returns the first match, and KM is pretty fast at file reads and text operations. So with a bit of error handling included:

Last Run Macro with Certain Suffix.kmmacros (4.6 KB)


I don't know what a typical Engine.log weighs in at, but mine has 26k lines and is processed in 1/10th of a second. Slower than line-limiting shell script versions, but not too shabby.

1 Like