Replace Only the First Instance of a Substring

I need to search for a substring that occurs several times in a text, replacing only the first instance of it. In other words, I need to replace the first "[xyz]" in the following but not the second:

Lorem ipsum [xyz] dolor sit [xyz] amet.

As I understand it (correct me if I'm wrong), the KM Search and Replace facility will always replace every matching instance it is given.

Any ideas as to the simplest/most efficient solution? I suspect that the answer may be 'do it in JavaScript' but I am hoping that there might be a more straightforwardly KM way.

Thanks.

Hey Phillip,

This is not too difficult if you're only dealing with a single line of text, OR the first instance in every line of text.

I can whip up something more complicated if your requirement is different.

Note that I had to escape the brackets in the find pattern in sed, because those are reserved characters in grep.

-Chris


Replace Only the First Instance of a Pattern v1.00.kmmacros (5.7 KB)

You can do this with only native Keyboard Maestro actions using a little regex magic.

-Chris


Replace Only the First Instance of a Pattern (KM Native) v1.00.kmmacros (6.5 KB)

2 Likes

That is correct. And we have made an Enhancement Request to allow us to set the KM Replace Action to replace ONLY the first match:

Request: Add Switch to KM Search/Replace to Turn Global Matches On/Off

The simplest way is to use JavaScript for Automation (JXA) taking advantage of the native JavaScript RegEx function:

The actual JavaScript command is just one line:
sourceStr.replace(RegExp(reFind), replaceStr)

All of the work is getting the KM Variables into the script.
But this is a completely reusable script using Local Variables, so you can store it somewhere (like in your KMFAM library) and easily use whenever you need it.

Example Output

Below is just an example written in response to your request. You will need to use as an example and/or change to meet your workflow automation needs.

Please let us know if it meets your needs.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

MACRO:   Regex Replace ONLY First Match Using JXA [Example]

-~~~ VER: 1.0    2021-07-10 ~~~
Requires: KM 8.2.4+   macOS 10.11 (El Capitan)+
(Macro was written & tested using KM 9.0+ on macOS 10.14.5 (Mojave))

DOWNLOAD Macro File:

Regex Replace ONLY First Match Using JXA [Example].kmmacros
Note: This Macro was uploaded in a DISABLED state. You must enable before it can be triggered.


ReleaseNotes

Author.@JMichaelTX

PURPOSE:

  • Regex Replace ONLY First Match Using JXA [Example]
    • Since KM Does NOT Offer a Replace of only the First Match, we much use a script.

HOW TO USE

  1. First, make sure you have followed instructions in the Macro Setup below.
  2. See the below "How to Use" Comment Action
  3. This macro is just an example written in response to your request. You will need to use as an example and/or change to meet your workflow automation needs.

MACRO SETUP

  • Carefully review the Release Notes and the Macro Actions
    • Make sure you understand what the Macro will do.
    • You are responsible for running the Macro, not me. ??
      .
      Make These Changes to this Macro
  1. Assign a Trigger to this Macro .
  2. Move this macro to a Macro Group that is only Active when you need this Macro.
  3. ENABLE this Macro, and the Macro Group it is in.
    • For more info, see KM Wiki article on Macro Activation
      .
  • REVIEW/CHANGE THE FOLLOWING MACRO ACTIONS:
    (all shown in the magenta color)
    • SET Source String
    • Set RegEx Find Pattern
    • SET REPLACE String

REQUIRES:

  1. KM 9.0+ (may work in KM 8.2+ in some cases)
  2. macOS 10.12.6 (Sierra)+

TAGS: @Example @RegEx @Replace

==USE AT YOUR OWN RISK==

  • While I have given this a modest amount of testing, and to the best of my knowledge will do no harm, I cannot guarantee it.
  • If you have any doubts or questions:
    • Ask first
    • Turn on the KM Macro Debugger from the KM Status Menu, and step through the macro, making sure you understand what it is doing with each Action.

1 Like

Thanks both @ccstone and @JMichaelTX. I am in awe of the generosity with which you respond to these requests!

I will dig into the solutions suggested here and report back.

Okay, I've experimented and this is what I've found:

The JXA provided by @JMichaelTX works nicely; however, it is a little bit slow when the script is called many times within the same macro (as, in practice, my macro will need to do).

The native KM/regex solution provided by @ccstone is far snappier and therefore preferable in this respect. The only problem is that does not work across line breaks.

I should share my real use case at this point, rather than the simplified example I gave initially.

In short, my objective is to take markdown text that contains reference information delimited by backticks and put each backtick-delimited substring into a unique footnote. In other words, it is converting an in-line citation style into a footnote (or endnote) style, within markdown.

So, this is what I have:


Convert backtick-delimited citations to footnotes [TEST].kmmacros (8.1 KB)

The output of the above macro, at present, is this:

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua [^fn1]. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat [^fn2]. 

Ut enim ad minim veniam `[Jessop, 2011, #16381]`, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua `[Rogers, 2018, #4879; Ruiz, 1976, #29023]`. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.


[^fn1]: [Jessop, 2011, #16381]
[^fn2]: [Jessop, 2011, #16381]
[^fn3]: [Jessop, 2011, #16381]
[^fn4]: [Rogers, 2018, #4879; Ruiz, 1976, #29023]

So, as you can see, the macro replaces the first two citations correctly but stops at the first paragraph.

You can also now see why I need to search and replace only the first instance of each substring. If I used the standard Search and Replace, all three instances of "[Jessop, 2011, #16381]" would become "[^fn1]". (N.B. There are citation styles where this would be correct but these styles are not used within my field.)

In any case, I've been learning some regex recently but this goes beyond my limited understanding! Pointers and corrections all appreciated.

P.S. the other problem with my current macro is that it needs to place the in-line [^fn#] marker to the right of the . or , not to the left (it will become superscript when exported to rich text). However, that should be easy to fix once the main problem is resolved.

Well, you will find that JXA is very fast when called outside of KM, or called just once by KM.

So I'd suggest that you pass you entire source string to one JavaScript for Automation Action.

Of course, the simple JXA script I provided will need to be adjusted to work with the entire string.

If @peternlewis would add the requested option to the KM RegEx change Action, then this could all be easily and quickly done within KM without the need for a script.

Peter, I do hope you understand this important use case for KM users who don't know how to use scripts.

Hey Phillip,

This is why one needs to give the true scope of a problem when asking for help.

If you haven't read this it's worth a couple of minutes of your time.

Tip: How Do I Get The Best Answer in the Shortest Time?

Convert backtick-delimited citations to footnotes (Perl) v1.00.kmmacros (7.1 KB)

This is less than elegant, but it gets the job done.

I've thought of at least one way that will probably work in Keyboard Maestro and another method for Perl that's probably more efficient than what I've already done, but I've spent enough time on the problem already.

-Chris

One workaround is to temporarily convert your document into a single line by searching for \n and replacing each with a temporary marker, like @~return~@. Then use Chris' solution. Then replace all the instances of @~return~@ with \n.

1 Like