16

Is there a way to insert the value from some sort of counter variable in Vim :substitute command?

For instance, to convert this document:

<SomeElement Id="F" ... />
<SomeElement Id="F" ... />
<SomeElement Id="F" ... />

to this resulting document:

<SomeElement Id="1" ... />
<SomeElement Id="2" ... />
<SomeElement Id="3" ... />

I imagine, the command would look like so:

:%s/^\(\s*<SomeElement Id="\)F\(".*\)$/\1<insert-counter-here>\2/g

I am using a very recent Windows build, from their provided installer. I strongly prefer not to install any additional tools. Ideally, I'd like to also avoid having to install scripts to support this, but I'm willing to, if it is the only way to do it.

| improve this question | |
10

Vim wiki instructions seems to be the easiest solution (at least for me).

Example below replaces all occurences of PATTERN with REPLACE_[counter] (REPLACE_1, REPLACE_2 etc.):

:let i=1 | g/PATTERN/s//\='REPLACE_'.i/ | let i=i+1

To answer the question it might look like this:

:let i=1 | g/SomeElement Id="F"/s//\='SomeElement Id="'.i.'"'/ | let i=i+1

Alternative solution

If anyone is interested in a solution with %s syntax I would advise to look at the @ib. answer which is:

:let n=[0] | %s/Id="\zsF\ze"/\=map(n,'v:val+1')/g
| improve this answer | |
  • Better than the first answer because it doesn't mess with keybinds, and shows what's going on in a much simpler way. It is identical to the version submitted by anubina, but also links to semi-official documentation. – Merlyn Morgan-Graham Nov 19 '16 at 23:58
  • But I don't understand, how to use it with '<,'> aka current selection. The pattern for find-replace looks odd (e.g. g in the beginning, s is in the center, what's going on??), is there a way to make it work with the usual s/from/to command? – Hi-Angel Sep 18 '18 at 4:51
  • I just hate how the regex syntax here is different than the one used in %s vim substitutions – Gabriel Ziegler Apr 14 '19 at 15:10
  • @jmarceli Can you please the %s equivalent in your answer ? – SebMa Feb 7 at 18:09
19

It is possible to have a counter using the substitute-with-an-expression feature (see :help sub-replace-\=). Unfortunately, since the \= construct allows only expressions, the :let command cannot be used, and therefore, a variable cannot not be set the usual way.

However, there is a simple trick to change the value of a variable in expression if that variable is a list or a dictionary. In that case, its contents could be modified by the map() function.

In such a manner, substitution for the case described in the question would look as follows:

:let n=[0] | %s/Id="F"/\='Id="'.map(n,'v:val+1')[0].'"'/g

The tricky part here is in the substitute part of the replacement. Since it starts with \=, the rest of it is interpreted as an expression by Vim. Thus, 'Id="'.map(n, 'v:val+1').'"' is an ordinary expression. Here a string literal 'Id="' is concatenated (using the . operator) with return value of the function call map(n, 'v:val+1'), and with another string, '"'. The map function expects two arguments: a list (as in this case) or a dictionary, and a string containing expression that should be evaluated for each of the items in the given list or dictionary. Special variable v:val denotes an individual list item. So the 'v:val+1' string will be evaluated to a list item incremented by one.

In this case, we can even simplify the command further:

:let n=[0] | %s/Id="\zsF\ze"/\=map(n,'v:val+1')/g

The \zs and \ze pattern atoms are used to set the start and the end of the pattern to replace, respectively (see :help /\zs and :help /\ze). That way the whole search part of the substitute command is matched, but only the part between \zs and \ze is replaced. This avoids clumsy concatenations in the substitute expression.

Either of these two short one-liners completely solves the issue.

For frequent replacements, one can even define an auxiliary function

function! Inc(x)
    let a:x[0] += 1
    return a:x[0]
endfunction

and make substitution commands even shorter:

:let n=[0] | %s/Id="\zsF\ze"/\=Inc(n)/g
| improve this answer | |
  • +1. I am going to leave the first accepted answer, since it was first, and works (and might also be useful in different scenarios), but I will admit that this is slightly closer to what I had in mind. Kudos! – Merlyn Morgan-Graham May 10 '11 at 4:44
  • @Merlyn Thanks! For me, this is almost idiomatic way of substituting when a counter is necessary (adding line numbers in listing, replacing ids, etc). – ib. May 10 '11 at 6:40
  • What's with all the extra single quotes? I asked in the other answer, but still can't figure it out (by staring at it) :) – Merlyn Morgan-Graham May 10 '11 at 9:56
  • @Merlyn I've added more explanations to my answer. – ib. May 10 '11 at 11:58
  • 3
    @ib: Wow, this question has turned up a wealth of information. \zb and \ze will get rid of a ton of zero-width assertion overhead in a lot of my searches. I wish I could post-answer-bounty as a tip ;) – Merlyn Morgan-Graham May 10 '11 at 19:25
12

Hmm this it little tricky one. Here is what I got so far. Try these 2 map command in a vim session:

:nmap %% :let X=1<cr>1G!!
:nmap !! /^\s*<SomeElement Id="F"<cr>:s/F"/\=X.'"'/<cr>:let X=X+1<cr>!!

Once that is there press %% to start the fun part :)

It makes your given file as:

<SomeElement Id="1" ... />
<SomeElement Id="2" ... />
<SomeElement Id="3" ... />
<SomeElement Id="4" ... />

Explanation:

First nmap command is mapping following sequences to keystrokes %%:

  • initializing variable X to 1
  • moving to start of first file
  • calling another mapped keystroke !!

Second nmap command is mapping following sequences to keystrokes !!:

  • Search for next occurrence of pattern ^\s*<SomeElement Id="F"
  • If above pattern is found then search and replace F" by variable X and a quote "
  • increment the vim variable X by 1
  • Recursively call itself by making a call to !!
  • Single dot . is used for concatenation of strings in vim, very similar to php

This recursive calls stop when pattern ^\s*<SomeElement Id="F" is not found anymore in the file.

| improve this answer | |
  • +1. This is very helpful! My actual file has lines besides these, though. How do I advance to the correct line before executing s/F"/\=X.'"'/. Also, what does the .'"' part do? – Merlyn Morgan-Graham May 9 '11 at 23:37
  • Cool, it worked, once I figured out to (more or less) replace j with :/F"<cr> :) Can you add a blow-by-blow of your script? I understand it, but I think it would be a great thing to add to the answer. – Merlyn Morgan-Graham May 9 '11 at 23:39
  • Added explanation and did some modification to 2nd map command to make sure to replace F" by incrementing numbers only in the lines matched by pattern ^\s*<SomeElement Id="F" – anubhava May 10 '11 at 3:13
  • Sorry forgot to add that dot (.) is used for concatenation Of strings in vim, very similar to php. – anubhava May 10 '11 at 10:43
  • 1
    @anubhava I would map one of the function keys as they will not collide (:h map-which-keys). – Peter Rincker May 10 '11 at 13:24
7

Very simple solution. I've had to do this several times.

:let i=1 | g/^\(\s*<SomeElement Id="\)F\(".*\)$/s//\=submatch(1).i.submatch(2)/ | let i=i+1

Based off of the following tip. http://gdwarner.blogspot.com/2009/01/vim-search-and-replace-with-increment.html

| improve this answer | |
1

Put this in your vimrc or execute it in your current session:

function! Inc(x) 
  let a:x[0] += 1 
  return a:x[0] 
endfunction

function IncReplace(pos, behind, ahead, rep) 
  let poss=a:pos-1 
  let n=[poss] 
  execute '%s/' . a:behind . '\zs' . a:rep . '\ze' . a:ahead . '/\=Inc(n)/g' 
endfunction

Then execute :call IncReplace(1, 'Id="', '"', 'F')

The first argument is the number you want to start from, the second is what you want to match behind the number, the third is what you want to match ahead of the number and the fourth is what you actually want to replace.

| improve this answer | |
0

Maybe plugin increment.vim will help

| improve this answer | |

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.