7

I have the following line in my code:

    INCORRECT_EMAIL_MOBILE_COMBINATION("incorrect_email_mobile_combination");

When I try to substitute the underscores, with spaces, in visual mode (using :'<,'>s/_/ /g). It substitutes all underscores in the line. How do I replace the underscores just within the quotes?

  • :s works on line ranges so the minimal unit for substitute would be a single line not a part of it. – Maxim Kim Jul 23 at 6:45
  • 2
    I've git some excellent answers on this thread so far! Thanks a lot everyone :) – TheChetan Jul 24 at 4:45
8

This can be looked at as a search-replace where the search is multi-level or nested. That is, first you need to match part of the line (the quoted string in this case). Then you need to match part of that string and do the replacement on that (underscores for us).

Basic regexes weren't really meant to handle nested searches (there are methods, often complex, depending on the regex variant). Fortunately, Vim has something called the sub-replace expression and it is commonly used to solve these problems:

:s/"\zs[^"]\+\ze"/\=substitute(submatch(0), '_', ' ', 'g')/

In the search part we identify quoted strings then segregate the text from the quotes with the \zs...\ze pair. IOW, the matched text is the string inside the double quotes.

  • " : match an opening dub-quote
  • \zs : anything before this is not included in substitution
  • [^"]\+ : match one or more characters not including double quote
  • \ze : anything after this is not included in substitution
  • " : match a (closing) dub-quote

In the replacement part we use the special operator \= which tells Vim to treat everything that follows as an expression, evaluate it, and use the result as the replacement of the matched text.

Specifically we do a substitute() on text returned by submatch(0) which is the full matched string. The other params should be pretty self-explanatory but here is the function spec:

substitute({expression}, {pattern}, {substitution}, {flags})

The return value and replacement text is what we're looking for: the string between the quotes but with spaces in place of underscores.


Bringing a rocket launcher to a knife fight?

A few people have opined that this is too complicated. While I wouldn't use it for one-off editing of a line or two, respectfully, non-Euclidean geometry is complicated. For this all you need is enough of a grasp of regexes and \zs..\ze to match the part(s) of the line to which you want to apply substitution. Then...

  1. Type in the pattern
  2. Type or paste this: /\=substitute(submatch(0), '', '', 'g')/
  3. Fill in params 2 and 3 with replacee and replacer text

Bonus Material

At the risk of contradicting the last section...

Since there seems to a bit of interest around this topic I figured I'd add a couple variations of the above. We all like examples.

Solving without \zs or \ze

In the original solution I use the Vim specific \zs..\ze token pair because knowing how to use those gives a lot of flexibility (with respect to sub-replace expressions and generally speaking when using regex in Vim). In reality though OPs specific case doesn't need them...

 s/"[^"]\+"/\=substitute(submatch(0), '_', ' ', 'g')/

We just include the quotes in the matched string and it's fine because the substitution only affects underscores.

A slightly harder problem

Our input string is

INCORRECT_EMAIL_MOBILE_COMBINATION("_incorrect_email_mobile_combination_");

...and we want to replace the underscores inside the double-quotes but only those that have word characters on both sides. So we want to end up with...

INCORRECT_EMAIL_MOBILE_COMBINATION("_incorrect email mobile combination_");

Yes, \zs..\ze would come in handy again here but let's pretend we need a portable regex...

Solution:

s/"_\([^"]\+\)_"/\='"_' . substitute(submatch(1), '_', ' ', 'g') . '_"'/

First we wrap the part of the string to be modified in a capture group \(..\). This allows us to isolate the string from the quotes and outer underscores when we do the substitution by using submatch(1) (returns string contained in first capture group) instead of submatch(0) (returns entire matched string).

Of course we will lose the quote-underscore pairs if we stop at the substitute() call. But we can just insert them into the sub-replace expression literally by (single) quoting them and using the concatenation operator (.)...problem solved. Okay but what if the surrounding characters were not fixed? Let's say the quotes could be either single or double, ['"]_.._['"]

Then we'll have to use more capture groups and change the literal string to submatch() calls with appropriate indices.

Switching to very magic mode (\v) so the pattern is a little easier to read...

s/\v(['"]_)([^"]+)(_['"])/\=submatch(1) . substitute(submatch(2), '_', ' ', 'g') . submatch(3)/
  • You do have a point about learning regexps well and this solves the problem comprehensively, but I tend to find this approach way too complicated for use while editing. While really suitable for a plug-in or mapping, I wouldn't really recommend this kind of approach for something as simple as this (or 99% of what you encounter in practice while editing a file.) For this particular case, I think the \%V match is the easiest. – filbranden Jul 23 at 11:57
  • If it's a oneoff edit it's not recommended. If you're new to or allergic to regexes you may want to stay away. Other than that I really don't get this notion that it's super complicated and to be avoided. Use a regex to isolate the target text. After that the replacement section is almost entirely fixed, unchanging text. Worst case, if you haven't quite wrapped your head around it, you can pick a place from which you can copy/paste: /\=substitute(submatch(0), '', '', 'g')/. All that's left is to fill in params 2 and 3 which you already know since it's the problem you want to solve. – B Layer Jul 23 at 16:17
5

Since you mentioned visual mode... You can also use the \%V match to only match inside the current visual selection (or last one, if not in visual mode.)

Using \%V would allow you to use the same command, with minimal modification:

:'<,'>s/\%V_/ /g

That's assuming you have a visual selection around the quoted string, where you want to replace underscores.

Technically, the above will also match an underscore at the end of the selection, because \%V is a zero-width match and while itself will only match within the selection, the underscore right after it might be outside. For cases where this matters, you can add another \%V at the end of the pattern, to ensure the whole pattern is inside the selection.

:'<,'>s/\%V_\%V/ /g

I tend to find the \%V match quite useful, since it makes it very easy to quickly modify a :s command to only work inside a selection, without having to change approach to use a normal mode command or to use an expression with functions as the replacement.

  • 1
    Bonus points for a search pattern which looks like an ascii emoji??? 😂 or, better, \%V_\%V/ – filbranden Jul 23 at 11:48
  • 1
    Nice! Quite handy. Not as complicated as the accepted answer! Thank you and take my upvote! – Doktor OSwaldo Jul 24 at 13:06
2

How do I replace the underscores just within the quotes?

I assume that it's okay to replace all underscores just after the first quote.

Then you can do:

:'<,'>normal f"f_r ,.,.,.,.,.,.,.

That is, go find the quote, then go find the underscore, then replace it with space, then repeat find/replace until it's enough (or even more than enough).

Note that '< and '> are added automatically (because of the Visual mode), so you don't have to type them yourself.

Of course, you can modify it to make use of a register if you want.

Probably, this solution is a bit of a hack, but I think it's easier than studying and debugging regexps for such routine tasks.

  • 2
    But if you "study and debug regexps" then after a while they're easy and you no longer need to study and debug them. Or you can mostly avoid them and be studying and debugging them for the rest of your life. ;) – B Layer Jul 23 at 7:11
  • @BLayer Let's be honest, in this particular case using simply (c)onfirmation flag along with :s command was the fastest way to do it. And regexp looks like a cannon aiming at a sparrow. – Matt Jul 23 at 7:21
  • If they went to the trouble to ask the question here do you really think they only need to do it one time? A cannon? Maybe the little toy kind that fire those paper strips from a cap gun (for me and everyone else who gets comfortable with regexes). – B Layer Jul 23 at 7:29
  • My last comment isn't supposed to sound acerbic but I realize after the fact that it's open for interpretation...which is annoying me. FYI...I'm chill. ;) – B Layer Jul 23 at 20:39
  • @BLayer Me too ;) That's just a problem with Vim: it's more sophisticated than a microscope, but we ordinarily use it to hammer nails (to edit plain text files). So every time one has to be unsure whether to grip it as a microscope, or as a hammer. However, the hammers on average are more practical ;) – Matt Jul 24 at 3:09
2

You could also just match on uppercase characters:

:s/\L\zs_//g

or match the column numbers:

/\%>3\%<8

only matches between column 3 and 8

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.