Search code examples
vimsubstitution

How do I substitute from a list of strings in VIM?


I am a vim user, and I want to be able to loop over a range of substrings when I am substituting. How can I use some vim magic to go from a set of lines like this:

Afoo
Bfoo
Cfoo
Dfoo

to

Abar
Bbar
Cbaz
Dbaz

?

I want to search my file from the start for the next occurance of foo, and replace the first two instances with bar, and the second two with baz. Is using a for loop the best option? If so, then how do I use the loop variable in the substitution command?


Solution

  • I would use a function that has a state, and call this function from %s. Something like:

    " untested code
    function! InitRotateSubst() abort
        let s:rs_idx = 0
    endfunction
    
    function! RotateSubst(list) abort
        let res = a:list[s:rs_idx]
        let s:rs_idx += 1
        if s:rs_idx == len(a:list)
            let s:rs_idx = 0
        endif
        return res
    endfunction
    

    And use them with:

    :call InitRotateSubst()
    :%s/foo/\=RotateSubst(['bar', 'bar', 'baz', 'baz'])/
    

    The call to the two commands could be encapsulated into a single command if you wish.


    EDIT: Here is a version integrated as a command that:

    • accepts as many replacements as we wish, all the replacements needs to be separated with the separator-character ;
    • supports back-references ;
    • can replace only the N first occurrences, N == the number of replacements specified if the command call is banged (with a !)
    • does not support usual flags like g, i (:h :s_flags) -- for that, we would have for instance to impose the command call to always ends up with a / (or whatever separator-character), if not the last text is interpreted as flags.

    Here is the command definition:

    :command! -bang -nargs=1 -range RotateSubstitute <line1>,<line2>call s:RotateSubstitute("<bang>", <f-args>)
    
    function! s:RotateSubstitute(bang, repl_arg) range abort
      let do_loop = a:bang != "!"
      " echom "do_loop=".do_loop." -> ".a:bang
      " reset internal state
      let s:rs_idx = 0
      " obtain the separator character
      let sep = a:repl_arg[0]
      " obtain all fields in the initial command
      let fields = split(a:repl_arg, sep)
    
      " prepare all the backreferences
      let replacements = fields[1:]
      let max_back_ref = 0
      for r in replacements
        let s = substitute(r, '.\{-}\(\\\d\+\)', '\1', 'g')
        " echo "s->".s
        let ls = split(s, '\\')
        for d in ls
          let br = matchstr(d, '\d\+')
          " echo '##'.(br+0).'##'.type(0) ." ~~ " . type(br+0)
          if !empty(br) && (0+br) > max_back_ref
        let max_back_ref = br
          endif
        endfor
      endfor
      " echo "max back-ref=".max_back_ref
      let sm = ''
      for i in range(0, max_back_ref)
        let sm .= ','. 'submatch('.i.')' 
        " call add(sm,)
      endfor
    
      " build the action to execute
      let action = '\=s:DoRotateSubst('.do_loop.',' . string(replacements) . sm .')'
      " prepare the :substitute command
      let args = [fields[0], action ]
      let cmd = a:firstline . ',' . a:lastline . 's' . sep . join(args, sep) . sep . 'g'
      " echom cmd
      " and run it
      exe cmd
    endfunction
    
    function! s:DoRotateSubst(do_loop, list, replaced, ...) abort
      " echom string(a:000)
      if ! a:do_loop && s:rs_idx == len(a:list)
        return a:replaced
      else
        let res0 = a:list[s:rs_idx]
        let s:rs_idx += 1
        if a:do_loop && s:rs_idx == len(a:list)
            let s:rs_idx = 0
        endif
    
        let res = ''
        while strlen(res0)
          let ml = matchlist(res0, '\(.\{-}\)\(\\\d\+\)\(.*\)')
          let res .= ml[1]
          let ref = eval(substitute(ml[2], '\\\(\d\+\)', 'a:\1', ''))
          let res .= ref
          let res0 = ml[3]
        endwhile
    
        return res
      endif
    endfunction
    

    which could be used this way:

    :%RotateSubstitute#foo#bar#bar#baz#baz#
    

    or even, considering the initial text:

    AfooZ
    BfooE
    CfooR
    DfooT
    

    the command

    %RotateSubstitute/\(.\)foo\(.\)/\2bar\1/\1bar\2/
    

    would produce:

    ZbarA
    BbarE
    RbarC
    DbarT