17

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), vim substitutes all underscores in the line.

How do I replace the underscores just within the quotes?

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

5 Answers 5

20

Yes, this is a long answer. That's because I try to cover the sometimes intimidating sub-replace expression as clearly as I can. Don't be frightened...please dive in! :)

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 (i.e. quoted text in this case). Then you need to match part of that string (i.e. underscores) and do the replacement on that.

Basic regexes weren't really meant to handle nested searches (depending on the regex variant a complex solution may be possible). Fortunately, Vim has something called the sub-replace expression and it is commonly used to solve these problems. Here's how we'd apply that to question's example:

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

In the pattern component we match quoted strings then segregate the text within from the quotes with the \zs...\ze pair. So the pattern will match just the string inside the double quotes. Breaking it down...

  • " : match an opening double-quote
  • \zs : exclude from the final match (to which substitution is applied) all preceding text
  • [^"]\+ : match one or more characters excluding double quotes
  • \ze : exclude from the final match all following text
  • " : match a (closing) double-quote

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

In the expression we invoke substitute(). This function's parameters are:

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

Parameters 2-4 mirror the components of a substitution command, i.e. :s/pattern/replacement/flags. The first parameter must resolve to the string to which the other parameters apply. We are using submatch({matchnum}) which, with parameter value 0, returns the complete matched string from the pattern described previously. (Values other than 0 will return, naturally, pattern sub-matches which are denoted by surrounding \( and \). We don't have any sub-matches here but below is an example that uses them.)

In this case the return value and thus the replacement text is just what we're looking for: the string between the quotes expect with spaces instead of underscores.


Bringing a rocket launcher to a knife fight?

A few people have opined that this approach 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 reasonable familiarity with 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

Since there seems to a bit of interest around this topic I figured I'd add a couple variations of the solution above in the hopes of shining additional light on things. We all like lots of examples, right?

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 the 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 our solution is...

s/\v(['"]_)([^"]+)(_['"])/\=submatch(1) . substitute(submatch(2), '_', ' ', 'g') . submatch(3)/
7
  • 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
    Commented Jul 23, 2019 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
    Commented Jul 23, 2019 at 16:17
  • 1
    So good, thank you so much.
    – Milimetric
    Commented Mar 1, 2021 at 21:44
  • What if I want to exclude a part of the string? Say I want to replace everywhere except within double quotes ?
    – kargirwar
    Commented Jan 26, 2023 at 5:07
  • @kargirwar Well, the closest analogue to what I cover in my answer would probably be something like s/\v(.*)("[^"]+")(.*)/\=substitute(submatch(1), ...) . submatch(2) . substitute(submatch(3), ...)/ where the unspecified args (...) of both substitute() calls would be the same, i.e. whatever you want the replacement to be for text outside the quotes. Of course, whether this is a good approach or not depends on the specifics of your use case (e.g. this command only works for lines containing a single quoted string).
    – B Layer
    Commented Jan 29, 2023 at 0:48
13

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.

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

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.

6
  • 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
    Commented Jul 23, 2019 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
    Commented Jul 23, 2019 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
    Commented Jul 23, 2019 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
    Commented Jul 23, 2019 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
    Commented Jul 24, 2019 at 3:09
3

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

2

Here's another method that no-one else has mentioned yet. First, note that, in your example, "within the quotes" is equivalent to "after the first quote".

Therefore, you can run the following substitution:

:'<,'>s/\v(".*)@<=_/ /g
 '<,'>                   # In the visual selection
      s/                 # substitute
        \v               # using "very magic" (this just means we don't need
                         # as many backslash escapes)
                  _      # match any underscore
               @<=       # that follows
          (   )          # a group containing
           "             # a quote
            .*           # followed by anything
                   / /   # replacing it with a space.
                      g  # globally within each line

An alternative version that uses a slightly simpler regular expression is:

:'<,'>s/".*\zs_/ /
 '<,'>              # In the visual selection
      s/            # substitute
        "           # a quote
         .*         # followed by anything
           \zs      # start the match (this means that anything before this
                    # won't be included in the replacement)
              _     # followed by an underscore
               / /  # with a space.

Note that this will only replace the last underscore on the line, even if you use the g flag (because the expression can only match the quote once). Therefore to replace the remaining underscores in the visual area, you can repeat the substitution many times by typing:

9@:

This will re-run the previous command-line command 9 times. You can increase the count if necessary.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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