Как мне заменить из списка строк в VIM? - PullRequest
5 голосов
/ 27 ноября 2009

Я - пользователь vim, и я хочу иметь возможность циклически перемещаться по ряду подстрок при замене. Как я могу использовать магию vim для перехода из набора строк, подобных этому:

Afoo
Bfoo
Cfoo
Dfoo

до

Abar
Bbar
Cbaz
Dbaz

Я хочу найти в моем файле с самого начала следующее вхождение foo и заменить первые два экземпляра на bar, а вторые два на baz. Является ли использование для цикла лучшим вариантом? Если так, то как мне использовать переменную цикла в команде замещения?

Ответы [ 5 ]

9 голосов
/ 27 ноября 2009

Я бы использовал функцию с состоянием и вызвал бы эту функцию из% s. Что-то вроде:

" 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

И используйте их с:

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

При желании вызов двух команд может быть заключен в одну команду.


РЕДАКТИРОВАТЬ: Вот версия, интегрированная в команду, которая:

  • принимает столько замен, сколько мы хотим, все замены должны быть разделены символом-разделителем;
  • поддерживает обратные ссылки;
  • может заменить только N первых вхождений, N == количество замен, указанных, если вызов команды ударил (с!)
  • не поддерживает обычные флаги, такие как g, i (:h :s_flags) - для этого нам нужно, например, навязать вызов команды, всегда заканчивающийся символом / (или любым другим символом-разделителем), если не последний текст интерпретируется как флаги.

Вот определение команды:

: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

, который можно использовать таким образом:

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

или даже, учитывая исходный текст:

AfooZ
BfooE
CfooR
DfooT

команда

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

даст:

ZbarA
BbarE
RbarC
DbarT
2 голосов
/ 12 декабря 2009

Почему бы и нет:

:%s/\(.\{-}\)foo\(\_.\{-}\)foo\(\_.\{-}\)foo\(\_.\{-}\)foo/\1bar\2bar\3baz\4baz/

Я не уверен, что это охватывает широту проблемы, но обладает преимуществом простой замены. Более сложный может охватывать решение, если этого не происходит.

2 голосов
/ 28 ноября 2009

Это не совсем то, что вы хотите, но может быть полезно для циклов.

Я написал плагин swapit http://www.vim.org/scripts/script.php?script_id=2294, который, помимо прочего, может помочь в циклическом просмотре списков строк. Например.

 :Swaplist foobar foo bar baz

затем введите

 This line is a foo

создать простую строку yank / paste, перейти к последнему слову и ctrl-a swap.

 qqyyp$^A

затем выполните шаблон обмена

 100@q

чтобы получить

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
...

Возможно, это применимо к вашей проблеме, хотя она чувствительна к {cword}.

1 голос
/ 27 ноября 2009

Так я бы попробовал этот макрос.

qa          Records macro in buffer a
/foo<CR>    Search for the next instance of 'foo'
3s          Change the next three characters
bar         To the word bar
<Esc>       Back to command mode.
n           Get the next instance of foo
.           Repeat last command
n           Get the next instance of foo
3s          Change next three letters
baz         To the word bar
<Esc>       Back to command mode.
.           Repeat last command
q           Stop recording.
1000@a      Do a many times.

Любой совет, как это сделать лучше, приветствуется.

спасибо, Martin.

0 голосов
/ 27 ноября 2009

Вероятно, будет гораздо проще записать макрос, который может заменить первые два, а затем использовать: s для остальных.

Макрос может выглядеть как /foo^Mcwbar^[. Если вы не знакомы с режимом макросов, просто нажмите q, a (регистр, в котором он будет храниться), а затем нажмите /foo <Enter> cwbar <Escape>.

Теперь, когда вы получили этот макрос, выполните 2@a, чтобы заменить первые два вхождения в текущем буфере, и обычно используйте :s, чтобы заменить остальные.

...