Como substituo de uma lista de strings em Vim?
-
05-07-2019 - |
Pergunta
Eu sou um usuário do VIM e quero ser capaz de dar um loop sobre uma variedade de substringas quando estou substituindo. Como posso usar um pouco de magia do vim para ir de um conjunto de linhas como esta:
Afoo
Bfoo
Cfoo
Dfoo
para
Abar
Bbar
Cbaz
Dbaz
?
Eu quero pesquisar meu arquivo desde o início para a próxima ocorrência de foo
, e substitua as duas primeiras instâncias por bar
, e os dois segundos com baz
. O uso de um loop é a melhor opção? Se sim, como uso a variável de loop no comando de substituição?
Solução
Eu usaria uma função que possui um estado e chamaria essa função de %s. Algo como:
" 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
E use -os com:
:call InitRotateSubst()
:%s/foo/\=RotateSubst(['bar', 'bar', 'baz', 'baz'])/
A chamada para os dois comandos pode ser encapsulada em um único comando, se desejar.
EDIT: Aqui está uma versão integrada como um comando que:
- Aceita quantas substituições que desejamos, todas as substituições precisam ser separadas com o caractere separador;
- suporta referências de volta;
- pode substituir apenas as primeiras ocorrências n, n == o número de substituições especificadas se a chamada de comando for batida (com A!)
- não suporta bandeiras habituais como g, eu (
:h :s_flags
)-Para isso, teríamos, por exemplo, para impor a chamada de comando para sempre acaba com A / (ou qualquer qualquer outro personagem separador), se não o último texto, é interpretado como sinalizadores.
Aqui está a definição de comando:
: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
que poderia ser usado desta maneira:
:%RotateSubstitute#foo#bar#bar#baz#baz#
ou até, considerando o texto inicial:
AfooZ
BfooE
CfooR
DfooT
o comando
%RotateSubstitute/\(.\)foo\(.\)/\2bar\1/\1bar\2/
produziria:
ZbarA
BbarE
RbarC
DbarT
Outras dicas
Isso não é estritamente o que você deseja, mas pode ser útil para ciclos.
Eu escrevi uma troca de plug -in http://www.vim.org/scripts/script.php?script_id=2294 que, entre outras coisas, podem ajudar no ciclismo através de listas de cordas. Por exemplo.
:Swaplist foobar foo bar baz
então digite
This line is a foo
Crie uma linha simples de Yank/Colar, vá para a última palavra e swap Ctrl-A.
qqyyp$^A
Em seguida, execute o padrão de troca
100@q
para obter
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
...
Provavelmente poderia ser aplicado ao seu problema, embora seja {cword} sensível.
Por que não:
:%s/\(.\{-}\)foo\(\_.\{-}\)foo\(\_.\{-}\)foo\(\_.\{-}\)foo/\1bar\2bar\3baz\4baz/
Não tenho certeza se ele cobre a amplitude do problema, mas tem a virtude de ser um substituto simples. Um mais complexo pode cobrir a solução se esta não.
É assim que eu tentaria essa macro.
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.
Qualquer conselho sobre como fazer melhor é bem -vindo.
Obrigado, Martin.
Provavelmente será muito mais fácil gravar uma macro que pode substituir os dois primeiros e depois usar: S para o resto.
A macro pode parecer /foo^Mcwbar^[
. Se você não está familiarizado com o modo macro, basta acertar q
, a
(o registro para armazená -lo) e depois as teclas /foo <Enter> cwbar <Escape>
.
Agora, quando você tiver essa macro, faça 2@a
para substituir as duas primeiras ocorrências no buffer atual e usar :s
normalmente para substituir o resto.