¿Cómo puedo sustituir de una lista de cadenas en VIM?
-
05-07-2019 - |
Pregunta
Soy un usuario de vim, y quiero poder recorrer un rango de subcadenas cuando estoy sustituyendo. ¿Cómo puedo usar un poco de magia vim para ir de un conjunto de líneas como esta:
Afoo
Bfoo
Cfoo
Dfoo
a
Abar
Bbar
Cbaz
Dbaz
?
Quiero buscar mi archivo desde el principio para la próxima aparición de foo
, y reemplazar las dos primeras instancias con bar
, y las dos últimas con baz
. ¿Usar un bucle for es la mejor opción? Si es así, ¿cómo uso la variable de bucle en el comando de sustitución?
Solución
Usaría una función que tiene un estado y llamaría a esta función desde% s. Algo así 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
Y úselos con:
:call InitRotateSubst()
:%s/foo/\=RotateSubst(['bar', 'bar', 'baz', 'baz'])/
La llamada a los dos comandos podría encapsularse en un solo comando si lo desea.
EDITAR: Aquí hay una versión integrada como un comando que:
- acepta tantos reemplazos como queramos, todos los reemplazos deben separarse con el carácter separador;
- admite referencias anteriores;
- puede reemplazar solo las N primeras apariciones, N == la cantidad de reemplazos especificados si la llamada de comando es golpeada (con un!)
- no admite marcas habituales como g, i (
:h :s_flags
); para eso, por ejemplo, tendríamos que imponer la llamada de comando para que siempre termine con un / (o cualquier carácter separador), si no el el último texto se interpreta como banderas.
Aquí está la definición del 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 podría usarse de esta manera:
:%RotateSubstitute#foo#bar#bar#baz#baz#
o incluso, considerando el texto inicial:
AfooZ
BfooE
CfooR
DfooT
el comando
%RotateSubstitute/\(.\)foo\(.\)/\2bar\1/\1bar\2/
produciría:
ZbarA
BbarE
RbarC
DbarT
Otros consejos
Esto no es estrictamente lo que desea, pero puede ser útil para los ciclos.
He escrito un intercambio de complementos http: //www.vim. org / scripts / script.php? script_id = 2294 que, entre otras cosas, puede ayudar a recorrer las listas de cadenas. Ej.
:Swaplist foobar foo bar baz
luego escriba
This line is a foo
crea una línea simple de tirar / pegar, ve a la última palabra y ctrl-a swap.
qqyyp$^A
luego ejecute el patrón de intercambio
100@q
para obtener
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
...
Probablemente podría aplicarse a su problema, aunque es {cword} sensible.
¿Por qué no?
:%s/\(.\{-}\)foo\(\_.\{-}\)foo\(\_.\{-}\)foo\(\_.\{-}\)foo/\1bar\2bar\3baz\4baz/
No estoy seguro de que cubra la amplitud del problema, pero tiene la virtud de ser un simple sustituto. Una más compleja puede cubrir la solución si esta no lo hace.
Así es como intentaría esa 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.
Cualquier consejo sobre cómo hacerlo mejor es bienvenido.
gracias, Martin.
Probablemente será mucho más fácil grabar una macro que pueda reemplazar a las dos primeras y luego usar: s para el resto.
La macro podría verse como /foo^Mcwbar^[
. Si no está familiarizado con el modo macro, simplemente presione q
, a
(el registro para almacenarlo) y luego presione las teclas /foo <Enter> cwbar <Escape>
.
Ahora, una vez que tenga esa macro, haga 2@a
para reemplazar las dos primeras ocurrencias en el búfer actual y use :s
normalmente para reemplazar el resto.