Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I make vim substitute repeat until there are no more matches?

I have a file with text that looks like

{\o H}{\o e}{\o l}{\o l}{\o o} {\o W}{\o o}{\o r}{\o l}{\o d}

I want to make the text look like this

{\o Hello} {\o World}

There are probably multiple tools I can use to solve this problem, but I am trying to use vim substitutions. So far, I have

:%s/{\\o \(.\{-}\)}{\\o \(.\{-}\)}/{\\o \1\2}/g

The patterns here are

.\{-} match some characters, non-greedy

{\\o .\{-}} match the regex string {\o .*} but being non-greedy with the .*

and \( ... \) creates the capture groups for backreferencing.

When I run this substitution once, I get

{\o He}{\o ll}{\o o} {\o Wo}{\o rl}{\o d}

I could run this command two more times to get

{\o Hell}{\o o} {\o Worl}{\o d}

and then

{\o Hello} {\o World}

but I would really love if there was a way to do this with one command, i.e., to tell vim substitution to keep passing over the file until there are no more matches. It seems to me that this ought to be possible.

Does anyone know what magic I need to add to achieve this? It doesn't look like there's a flag that achieves this. Maybe some trick I don't know with labels?

like image 655
xenakaaii Avatar asked Dec 24 '22 02:12

xenakaaii


2 Answers

Sub-replace Expression method

You can use a sub-replace-expression, \=, to execute a second substitution inside the replacement part of :s

:%s/\({\\o \S}\)\+/\='{\o '.substitute(submatch(0), '{\\o \(\S\)}', '\1', 'g').'}'/g

The basic idea is to capture a "word", e.g. {\o H}{\o e}{\o l}{\o l}{\o o}, and then do a second substitution on the captured "word", submatch(0). The second substitution will remove the starting {\o and trailing } via substitute(submatch(0), '{\\o \(\S\)}', '\1', 'g'). This leaves just the letters, Hello in this example. Once every letter is extracted from the word we add back in the starting {\o and trailing }.

For more help see:

:h :s
:h sub-replace-expresion
:h substitute(
:h submatch(
:h literal-string
:h /\S

Two substitutions

You can also use 2 separate substitutions. One to remove all the {\o and }. The other substitution will add them back in, but word-wise this time.

:%s/{\\o \(.\)}/\1/g
:%s/\S+/{\\o &}/g

This is arguably the an easier method assuming you do not have this text mixed in with other text that should not be in this format.

You can also do this in one command by using |:

:%s/{\\o \(.\)}/\1/g|%s/\S+/{\\o &}/g
like image 184
Peter Rincker Avatar answered May 12 '23 01:05

Peter Rincker


Here's a much simpler solution:

Recursive macros. The basic idea of a recursive macro, is a macro that calls itself until an error occurs. When you run a substitute command and there are no matches, this will throw an error. So here's the basic layout of what you need to do.

  1. Clear macro 'q'. This is essential so that it doesn't do anything weird on the first run through. You can do this with

    qqq
    

    or

    :let @q=""
    

    Either one works.

  2. Start recording. This is just:

    qq
    
  3. Run your substitute command, then call the macro. This would be

    :%s/{\\o \(.\{-}\)}{\\o \(.\{-}\)}/{\\o \1\2}/g<cr>@q
    

    Since @q is the command to run a macro. This won't do anything while you are recording, since you have already cleared out register 'q'. Lastly,

  4. Stop recording, and call the macro.

    q@q
    

    This will loop the set of keystrokes you just pressed over and over until there are no matches.

To put this all together:

qqqqq:%s/{\\o \(.\{-}\)}{\\o \(.\{-}\)}/{\\o \1\2}/g<cr>@qq@q

That's 5 'q's at the beginning. You could also assign register 'q' from the command line, with:

:let @q=':%s/{\\o \(.\{-}\)}{\\o \(.\{-}\)}/{\\o \1\2}/g<cr>@q'

Then hit

<cr>@q

As a side note, there is a dedicated vim-site that I highly recommend.

like image 29
DJMcMayhem Avatar answered May 12 '23 01:05

DJMcMayhem