Replacing The Last Specific Character in a String

Ok maybe this is very simple, if I wanted to replace the last instance of a colon with a period how would I do that from a text string in KM?

00:09:46:49 --> 00:09:46.49

I often use to test my regex. Might help you develop a regex to use the regular expression action.

There will be a more elegant solution, but this will do it.

Using the Search and Replace action, search the text string for Regular Expression:


and replace with:



"\d" is any digit. "{2}" means twice. (So "\d\d" could have been used instead of "\d{2}").

"(...)" is a capture group. The first group matches 2 digits, a colon, 2 digits, a colon and 2 digits again. Then there is the final colon, which we don't want, and then another capture group for the final 2 digits.

When the string is found, it is replaced by $1 (representing the first capture group), a dot and $2 (second capture group).

Yes, it's a great resource.



Since we are (as far as I know) confident that the supplied string will always be a valid one of that known format, we could just search for:

Find 8 characters, then a colon and 2 more characters.


Yes, those are both ways that will work given that the input string won't change. You can use the negative lookahead feature of regex to look for the last semicolon in the string and just replace that.


This also works if the string input changes or has other text before the time code text string:


Awesome, I'm learning a ton!! All of these are great for my use case, which is very specific and unlikely to change. Appreciate the help!!

1 Like

Hey @hayleyh,

The simplest and most canonical way to do this with regex is:





This pattern performs a greedy search up to and including the last available colon character and then does the appropriate replace.

Search and Replace Text - 01.kmmacros (6.3 KB)
Keyboard Maestro Export

@MikeTheClicker's negative lookahead is perfectly viable as well – it's just a significantly more advanced regex method.



Ah great - yes, I had to dive into negative matches just last night as research for a new macro.

That certainly works (tested in KM), but I don't quite understand why. :thinking: Please let me know of any term for this method so I can read up on it!


If you put the regex in a tool like (there are others as well) it will attempt to explain what each part is doing.

This way works as well.


Yes, I did mention in my first reply to you that I know the site, but in this instance, its explanation is not of help (and indeed it doesn't even show the full match, no doubt for good reasons that are beyond my current level knowledge).

Since @ccstone's method is canonical, I was asking for how it's referred to so I can investigate elsewhere.

One day I am going to need all these! I do save them, I haven't needed a regex in a couple of years but one I do, I will be ready!
Stay well by the way.


ha! I didn't even know that was an option! I love it! So easy. Thanks for sharing that. This is why I jump in on these discussions. I always learn every time.




the first capture group is greedy so it will continue matching as much as it can until it hits the last matching capture group match which results in a capture of


with the replacement being


The second capture group is not used so essentially thrown away giving you the last colon replaced with a period. The remainder of the string is left alone so you end up with your target string of

1 Like

Right, that's what I didn't realise, and also explains the situation with Regex101. I'm used to having to match the entire string, but I now know that within this action, that's not necessary. Thanks!

1 Like

It isn't necessary as a rule. The trick is to identify the minimal pattern that you want to affect, not to devise a coded representation of the entire string. That single misconception no doubt makes it a lot harder for beginners to get comfortable with regular expressions.

And just for fun, did you try (.+): for a pattern? If you aren't going to use the second group ((:)) there's no reason to capture it, not that it does any harm.


While I love the regexing above -- I'm far too lazy to work it out and would let KM do the job for me:

INB4 "@kcwhat's already posted that" -- look closely...

1 Like

Or, split on ":" and then rejoin.

Either with a Keyboard Maestro variable array (with ":" as the custom delimiter)

or using a split function and a join function in a scripting language:

Split and rejoin.kmmacros (3.5 KB)

Expand disclosure triangle to view JS source
(() => {
    "use strict";

        xs = Application("Keyboard Maestro Engine")

    return `${xs.slice(0, 3).join(":")}.${xs[3]}`;

Yes, and in most contexts you have to account for elements that you don't want to affect, as cheaply as possible. The KM action seems able to reduce the cost further, silently. :slight_smile:

I think you'll agree that matching a string is not the same as devising a coded representation of it.

Whups. Quite.

Thanks for pointing that out.

No general term describes the pattern, but...


(  ==  start capture
.  ==  any character
+  ==  1 or more (greedy – e.g. matches as far as possible.)
)  ==  end capture
:  ==  literal colon character (capture removed as it wasn't needed - thanks Mike!)

This is very basic pattern matching in regex.

@kevinb – be sure to chime in if you don't understand something.

Note – I always build regular expressions in BBEdit which has excellent support for them – and the PCRE standard for regular expressions is relatively close to the ICU standard that macOS and Keyboard Maestro use.

The commercial version of BBEdit has a visual regex constructor called "Pattern Playground", and I use it from time to time. The free version has full support for regex search/replace but does not include the Pattern Playground.

On occasion I will resort to a couple of other visual constructors I have if the pattern I'm building is complex enough, and I've been known to use now and then – because it's probably the single best online regex tool and is particularly helpful when working with others.