Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 604
Ksiresh Avatar asked Nov 27 '09 16:11

Ksiresh


2 Answers

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

" untested code
function! InitRotateSubst()
    let s:rs_idx = 0
endfunction

function! RotateSubst(list)
    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
  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)
  " echom cmd
  " and run it
  exe cmd
endfunction

function! s:DoRotateSubst(do_loop, list, replaced, ...)
  " 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
like image 200
Luc Hermitte Avatar answered Sep 23 '22 01:09

Luc Hermitte


This is Not strictly what you want but can be useful for cycles.

I've written a plugin swapit http://www.vim.org/scripts/script.php?script_id=2294 which among other things can help with cycling through lists of strings. Eg.

 :Swaplist foobar foo bar baz

then type

 This line is a foo

create a simple yank/paste line, go to last word and ctrl-a swap.

 qqyyp$^A

then execute the swap pattern

 100@q

to get

This line is foo
This line is bar
This line is baz
This line is foo
This line is bar
This line is baz
This line is foo
This line is bar
This line is baz
This line is foo
This line is bar
This line is baz
...

It could probably be applied to your problem although its {cword} sensitive.

like image 40
michael Avatar answered Sep 25 '22 01:09

michael