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...
- Type in the pattern
- Type or paste this:
/\=substitute(submatch(0), '', '', 'g')/
- 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)/
:s
works on line ranges so the minimal unit for substitute would be a single line not a part of it.