23

I want to search and replace each occurrence of a certain pattern with a decimal number which begins at 1 and increments by one for each match.

I can find similarly worded questions that turn out not to be about incrementing a counter but modifying each match by a fixed amount. Other similar questions are about inserting line numbers rather than an incrementing counter.

Example, before:

#1
#1.25
#1.5
#2

After:

#1
#2
#3
#4

My real data has a lot more text all around the stuff I want to re-number.

2
  • 2
    if you have perldo, you can use :%perldo s/#\K\d+(\.\d+)?/++$i/ge
    – Sundeep
    Commented Jul 10, 2017 at 7:01
  • @Sundeep: i should've mentioned I'm on vanilla Windows 10, sorry! Commented Jul 10, 2017 at 12:54

5 Answers 5

24
 :let @a=1 | %s/search/\='replace'.(@a+setreg('a',@a+1))/g

But beware, it will overwrite your register a. I think it is a little bit more straight forward than luc's answer, but maybe his is faster. If this solution is somehow worse than his, I would love to hear any feedback why his answer is better. Any feedback to improve the answer will be highly appreciated!

(It is also based on a SO answer of mine https://stackoverflow.com/questions/43539251/how-to-replace-finding-words-with-the-different-in-each-occurrence-in-vi-vim-edi/43539546#43539546)

8
  • 1
    I don't see how @a+setreg('a',@a+1) is shorter than len(add(t,1)). Otherwise, this is a nice other dirty trick :). I haven't though of this one. Regarding the use of a dictionary mutating function in the replacement text, of :s and substitute(), I have noted this is much faster that explicit loops -- hence the implementation of my list functions in lh-vim-lib. I guess your solution will be on par with mine, may be a little faster, I don't know. Commented Jul 10, 2017 at 11:02
  • 2
    Regarding preferences, I prefer my solution for a single reason: it leaves @a unmodified. In scripts, this is important IMO. While in interactive mode, as a end-user, I'll know which register I can use. Messing with a register is less important. In my solution, in interactive mode, a global variable is messed with; in a script it'd be a local variable. Commented Jul 10, 2017 at 11:05
  • @LucHermitte Sorry, my solution is indeed not shorter than yours, I should read it better before writing such a statement. I have removed the said statement from my answer and would like to apologize! Thank you for your interesting feedback, I appreciate it.
    – B.G.
    Commented Jul 10, 2017 at 11:07
  • Don't worry about it. Because of the regex, it's easy to think there is a lot to type. Also, I voluntarily admit my solution is convoluted. You're welcomed for the feedback. :) Commented Jul 10, 2017 at 11:09
  • 2
    Indeed you're rigth. Most of the time I extract another information that I store in the last position of the array, which is what (the last element) I insert in the end. For instance, for a +3, I could write something like \=add(thelist, 3 + get(thelist, -1, 0))[-1]. Commented Jul 10, 2017 at 11:16
18

You need substitution with a state. I remember having provided a (/several?) complete solution for this kind of problems on SO.

Here is another way to proceed (1). Now, I'll proceed into 2 steps:

  • a dummy list variable I need for the dirty and convoluted trick I'll employ
  • a substitution where I insert the len of this dummy array I'm filling on each matching occurrence.

Which gives:

:let t=[]
:%s/#\zs\d\+\(\.\d\+\)\=\ze/\=len(add(t,1))/g

If you're not used to vim regexes, I use :h /\zs and \ze to specify which sub-pattern I'm matching, then I match a series of digits possibly followed by a dot and other digits. This is not perfect for any floating point number, but this is enough here.

Note: You'll have to wrap it up in a couple function + command for a simple interface. Again, there are examples on SO/vim (here, here, here) Nowadays I know enough of vim to not care about wrapping this trick into a command. Indeed I'll be able to write this command on the first try, while I'll take several minutes to remember the name of the command.


(1) The objective is to be able to maintain a state in-between substitutions, and to replace the current occurrence with something that depends on the current state.

Thanks to :s\= we are able to insert something resulting from a computation.

Remains the problem of the state. Either we define a function that's managing an external state, or we update ourselves an external state. In C (and related languages), we could have used something like length++ or length+=1. Unfortunately, in vim scripts, += cannot be used out of the box. It needs to be used either with :set or with :let. This means that :let length+=1 increments a number, but doesn't return anything. We cannot write :s/pattern/\=(length+=1). We need something else.

We need mutating functions. i.e. functions that mutates their inputs. We have setreg(), map(), add() and probably more. Let's start with them.

  • setreg() mutates a register. Perfect. We can write setreg('a',@a+1) as in @Doktor OSwaldo's solution. And yet, this is not enough. setreg() is more of a procedure than a function (for those among us who know Pascal, Ada...). This means it doesn't return anything. Actually, it does return something. Nominal exit (i.e. non-exceptionnal exits) always return something. By default, when we forgot to return something, 0 is returned -- it also applies with built-in functions. That's why in his solution the replacement expression is actually \=@a+setreg(...). Tricky, isn't it?

  • map() could also be used. If we start from a list with a single 0 (:let single_length=[0]), we could increment it thanks to map(single_length, 'v:val + 1'). Then we need to return the new length. Unlike setreg(), map() returns its mutated input. That's perfect, the length is stored at the first (and unique, and thus last as well) position of the list. The replacement expression could be \=map(...)[0].

  • add() is the one I often use out of habit (I've just though about map() actually, and I haven't benched their respective performances yet). The idea with add() is to use a list as the current state, and to append something at the end before each substitution. I often store the new value at the end of the list, and use it for the replacement. As add() also returns its mutated input list, we can use: \=add(state, Func(state[-1], submatch(0)))[-1]. In OP's case, we only need to remember how many matches have been detected so far. Returning the length of this state list is enough. Hence my \=len(add(state, whatever)).

4
  • I think I understand this one, but why the trick with the array and it's length compared to adding one to a variable? Commented Jul 10, 2017 at 13:41
  • 1
    This is because \= expects an expression, and because unlike C, i+=1 isn't something that do increment and do return an expression. This means that behind \= I need something that can modify a counter and that returns an expression (equal to that counter). So far, the only things I've found are the list (and dictionary) manipulation functions. @Doktor OSwaldo has used another mutating function (setreg()). the difference is that setreg() never returns anything, which means it always returns the number 0. Commented Jul 10, 2017 at 15:09
  • Wow, interesting! Both your trick and his are so magical that I think your answers would benefit from explaining them in your answers. I would assume only the most fluent vimscripters would know such unintuitive idioms. Commented Jul 11, 2017 at 3:42
  • 2
    @hippietrail. Explanations added.Let me know if you need more specific precisions. Commented Jul 13, 2017 at 11:46
8

I found a similar but different question I asked a couple of years ago and managed to change one of its answers without fully understanding what I was doing and it's been working great:

:let i = 1 | g/#\d\+\(\.\d\+\)\=/s//\=printf("#%d", i)/ | let i = i+1

Specifically I don't understand why mine doesn't use % or why I just use a plain variable that the other answers avoid for some reason.

3
  • 1
    that is also a possiblity. I think the main disadvantage here is, that you use one substitute command per match. So it is probably slower. The reason why we don't use a plain variable, is, that it will not be updated in a normal s//g statement. Anyway it is an interesting solution. Maybe @LucHermitte can tell you more about pros and cons, since my knowledge about vimscript is quite limited compared with his.
    – B.G.
    Commented Jul 10, 2017 at 14:36
  • 1
    @DoktorOSwaldo. I guess this solution has been working for a longer time -- without printf() though -- as Lists were introduced in Vim 7. But I must admit I wouldn't have expected (/didn't remember?) the <bar> to belong to the scope of :global -- IOW, the scenario I'd have expected was to apply the :sub on the matching lines, then increment i once at the very end. I expect this solution to be slightly slower. But does it really matter? The important thing is how easily we're able to come over with a working solution from memory + trial & error. For instance, Vimgolfers prefer macros. Commented Jul 10, 2017 at 16:51
  • 1
    @LucHermitte yeah i ecpexted the same, and no the speed doesn't matter. I think it is a good answer, and I again learned something from it. Maybe the g/s// scope behaviour allowes for other dirty tricks. So thank you both for the interesting answers and discussion, I do not often learn that much from giving an answer =).
    – B.G.
    Commented Jul 11, 2017 at 6:03
7

There are already three great answers on this page, but, as Luc Hermitte suggested in a comment, if you're doing this off-the-cuff, the important thing is how quickly and easily you can alight upon a working solution.

As such, this is a problem that I wouldn't actually use :substitute for at all: it's one that can easily be solved using regular normal mode commands and a recursive macro:

  1. (If necessary) First, turn off 'wrapscan'. The regular expression we're going to use will match the desired result text as well as the initial text, so with 'wrapscan' on, the macro would otherwise continue playing back forever. (Or until you realise what's happening and press <C-C>.):

    :set nowrapscan
    
  2. Set up your search term (using the same base regular expression already mentioned in the existing answers):

    /#\d\+\(\.\d\+\)\?<CR>
    
  3. (If necessary) Jump back to the first match by pressing N as many times as required,

  4. (If necessary) Change the first match into the desired text:

    cE#1<Esc> 
    
  5. Clear out the "q register and start recording a macro:

    qqqqq
    
  6. Yank the current counter:

    yiW
    
  7. Jump to the next match:

    n
    
  8. Replace the current counter with the one we just yanked:

    vEp
    
  9. Increment the counter:

    <C-A>
    
  10. Play macro q. Register "q is still empty because we cleared it in step 5, so nothing happens at this point:

    @q
    
  11. Stop recording the macro:

    q
    
  12. Play the new macro, and watch!

    @q
    

As with all macros, this looks like a lot of steps when explained as I have done above, but note that actually typing these in is very quick for me: apart from the recursive-macro-recording-boilerplate they're all just the regular editing commands I'm performing all the time during editing. The only step where I had to do anything even approaching thinking was step 2, where I wrote the regular expression to perform the search.

Formatted as two command-line mode commands and a series of keystrokes, the speed of this type of solution becomes more clear: I can conjure up the following pretty much as fast as I can type it1:

:set nowrapscan
/#\d\+\(\.\d\+\)\?
cE#1<Esc>qqqqqyiWnvEp<C-A>@qq@q

I probably could have come up with the other solutions on this page with a bit of thought and some referencing of the documentation2, but, once you understand how macros work, they really are easy to churn out at whatever speed you usually edit.

1: There are situations where macros require more thought, but I find they don't come up much in practice. And generally the situations where they occur are ones where a macro is the only practical solution.

2: Not to imply that the other answerers couldn't have come up with their solutions similarly easily: they just require skills/knowledge that I personally don't have so easily at my fingertips. But all Vim users know how to use regular editing commands!

4

Just adding '#' at the end plus a counter 'c'

:let c=0 | g/^#\d\+.*/ let c+=1 | s//\='#' . c

The global part g/^#\d\+.*/ let c+=1 allow us to increment the counter only in the lines that have a pattern

2
  • Isnt that covered in hippietrails answer?
    – D. Ben Knoble
    Commented Apr 16, 2020 at 13:01
  • In this case, I think I have figured out a shorter way of doing something similar, maybe a comment would be better. Commented Apr 16, 2020 at 13:07

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.