Comment puis-je remplacer à partir d'une liste de chaînes dans VIM?
-
05-07-2019 - |
Question
Je suis un utilisateur de vim et je souhaite pouvoir effectuer une boucle sur une gamme de sous-chaînes lorsque je le remplace. Comment puis-je utiliser de la magie vim pour partir d'un ensemble de lignes comme ceci:
Afoo
Bfoo
Cfoo
Dfoo
à
Abar
Bbar
Cbaz
Dbaz
?
Je souhaite rechercher dans mon fichier dès le début la prochaine occurrence de foo
et remplacer les deux premières instances par bar
et les deux autres par baz
. L'utilisation d'une boucle for est-elle la meilleure option? Si c'est le cas, comment utiliser la variable de boucle dans la commande de substitution?
La solution
Je voudrais utiliser une fonction qui a un état et appeler cette fonction à partir de% s. Quelque chose comme:
" 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
Et utilisez-les avec:
:call InitRotateSubst()
:%s/foo/\=RotateSubst(['bar', 'bar', 'baz', 'baz'])/
L'appel aux deux commandes peut être encapsulé dans une seule commande si vous le souhaitez.
EDIT: Voici une version intégrée en tant que commande qui:
- accepte autant de remplacements que nous le souhaitons, tous les remplacements doivent être séparés par le caractère de séparation;
- prend en charge les références arrière;
- ne peut remplacer que les N premières occurrences, N == le nombre de remplacements spécifié si l'appel de commande est claqué (avec un!)
- ne supporte pas les drapeaux habituels comme g, i (
:h :s_flags
) - pour cela, il faudrait par exemple imposer l'appel de commande pour toujours se terminer par un / (ou le caractère de séparation), sinon le caractère le dernier texte est interprété comme un drapeau.
Voici la définition de la commande:
: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
qui pourrait être utilisé de cette façon:
:%RotateSubstitute#foo#bar#bar#baz#baz#
ou même, en considérant le texte initial:
AfooZ
BfooE
CfooR
DfooT
la commande
%RotateSubstitute/\(.\)foo\(.\)/\2bar\1/\1bar\2/
produirait:
ZbarA
BbarE
RbarC
DbarT
Autres conseils
Ceci n'est pas strictement ce que vous voulez mais peut être utile pour les cycles.
J'ai écrit un plugin swapit http: //www.vim. org / scripts / script.php? script_id = 2294 , ce qui peut notamment vous aider à parcourir les listes de chaînes. P. ex.
:Swaplist foobar foo bar baz
puis tapez
This line is a foo
créer une simple ligne yank / coller, aller au dernier mot et ctrl-un échange.
qqyyp$^A
puis exécutez le modèle d'échange
100@q
pour obtenir
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
...
Il pourrait probablement être appliqué à votre problème même si son {cword} sensible.
Pourquoi pas:
:%s/\(.\{-}\)foo\(\_.\{-}\)foo\(\_.\{-}\)foo\(\_.\{-}\)foo/\1bar\2bar\3baz\4baz/
Je ne suis pas sûr que cela couvre l’ampleur du problème, mais il a le mérite d’être un simple substitut. Une solution plus complexe peut couvrir la solution si celle-ci ne le fait pas.
Voici comment je tenterais cette 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.
Tous les conseils sur la façon de le faire mieux sont les bienvenus.
merci, Martin.
Il sera probablement beaucoup plus facile d’enregistrer une macro qui peut remplacer les deux premières, puis d’utiliser: s pour le reste.
La macro pourrait ressembler à /foo^Mcwbar^[
. Si vous n'êtes pas familier avec le mode macro, appuyez simplement sur q
, a
(le registre dans lequel vous souhaitez l'enregistrer), puis sur les touches /foo <Enter> cwbar <Escape>
.
Une fois que vous avez obtenu cette macro, faites 2@a
pour remplacer les deux premières occurrences dans le tampon actuel et utilisez :s
normalement pour remplacer le reste.