如何在VIM中从字符串列表中进行替换?

5

我是vim用户,当我进行替换时,希望能够循环遍历一系列子字符串。如何使用vim魔法从这样的一组行中进行操作:

Afoo
Bfoo
Cfoo
Dfoo

to

Abar
Bbar
Cbaz
Dbaz

我想从文件开头开始搜索下一个出现的foo,并将前两个实例替换为bar,第二个两个实例替换为baz。使用for循环是最好的选择吗?如果是,那么如何在替换命令中使用循环变量?

5个回答

11
我会使用一个具有状态的函数,并从%s调用这个函数。类似这样的东西:
" untested code
function! InitRotateSubst() abort
    let s:rs_idx = 0
endfunction

function! RotateSubst(list) abort
    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 abort
  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) . sep . 'g'
  " echom cmd
  " and run it
  exe cmd
endfunction

function! s:DoRotateSubst(do_loop, list, replaced, ...) abort
  " 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

这不是您严格要求的,但对于循环可能会有用。

我编写了一个名为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}敏感,但它很可能适用于您的问题。


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

我不确定它是否涵盖了问题的广度,但它具有简单替代品的优点。如果这个简单的替代方案不行,一个更复杂的可能会解决问题。


嗨 Derek, 只是想说我昨天在 Vimeo 上观看了你的一些 Vim 视频。我想说我很喜欢看它们,并学到了一些东西。如果我对正则表达式更熟练,你的解决方案就是我会选择的。感谢你提供这个简单的解决方案。 - Ksiresh

1

这是我尝试编写该宏的方式。

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.

欢迎提供任何改进建议。

谢谢, 马丁。


看起来在你最后一个 . 前面缺少了一个 n - Rich

0

记录一个可以替换前两个的宏可能会更容易,然后使用 :s 替换其余部分。

该宏可能看起来像 /foo^Mcwbar^[。如果您不熟悉宏模式,请按下 qa(要存储宏的寄存器),然后按下键盘上的 /foo <Enter> cwbar <Escape>

现在一旦您拥有了该宏,请执行 2@a 来替换当前缓冲区中的前两个出现次数,并正常使用 :s 替换其余部分。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接