Как заменить строки из списка в VIM?
-
05-07-2019 - |
Вопрос
Я пользователь vim и хочу иметь возможность перебирать диапазон подстрок при замене.Как я могу использовать магию vim, чтобы перейти к такому набору строк:
Afoo
Bfoo
Cfoo
Dfoo
к
Abar
Bbar
Cbaz
Dbaz
?
Я хочу выполнить поиск в своем файле с самого начала на предмет следующего появления foo
, и замените первые два экземпляра на bar
, а вторые два с baz
.Является ли использование цикла for лучшим вариантом?Если да, то как мне использовать переменную цикла в команде подстановки?
Решение
Я бы использовал функцию, имеющую состояние, и вызывал бы эту функцию из %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
Другие советы
Это не совсем то, что вам нужно, но может быть полезно для циклов.
Я написал плагин swapit http://www.vim.org/scripts/script.php?script_id=2294 что, среди прочего, может помочь при циклическом переборе списков строк.Например.
:Swaplist foobar foo bar baz
затем введите
This line is a foo
создайте простую строку копирования/вставки, перейдите к последнему слову и нажмите Ctrl-A.
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}.
Почему нет:
:%s/\(.\{-}\)foo\(\_.\{-}\)foo\(\_.\{-}\)foo\(\_.\{-}\)foo/\1bar\2bar\3baz\4baz/
Я не уверен, что он охватывает всю глубину проблемы, но у него есть то преимущество, что он является простой заменой.Более сложное решение может охватывать решение, если это не так.
Вот как я бы попробовал этот макрос.
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.
Приветствуются любые советы о том, как это сделать лучше.
спасибо, Мартин.
Вероятно, будет намного проще записать макрос, который заменит первые два, а затем использовать :s для остальных.
Макрос может выглядеть так /foo^Mcwbar^[
.Если вы не знакомы с макрорежимом, просто нажмите q
, a
(регистр для его хранения), а затем нажатия клавиш /foo <Enter> cwbar <Escape>
.
Теперь, когда у вас есть этот макрос, сделайте 2@a
чтобы заменить первые два вхождения в текущем буфере и использовать :s
обычно для замены остальных.