5

我是 vim 用户,我希望在替换时能够循环一系列子字符串。我如何使用一些 vim 魔法从这样的一组行中走出来:

Afoo
Bfoo
Cfoo
Dfoo

Abar
Bbar
Cbaz
Dbaz

?

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

4

5 回答 5

11

我会使用一个有状态的函数,并从 %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 == 如果命令调用被 banged 指定的替换次数(带有!)
  • 不支持像 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
于 2009-11-27T17:01:53.837 回答
2

严格来说,这不是您想要的,但对循环很有用。

我写了一个插件 swapit http://www.vim.org/scripts/script.php?script_id=2294可以帮助循环遍历字符串列表。例如。

 :Swaplist foobar foo bar baz

然后输入

 This line is a foo

创建一个简单的 yank/paste 行,转到最后一个单词并 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} 敏感,但它可能适用于您的问题。

于 2009-11-28T00:26:51.333 回答
2

为什么不:

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

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

于 2009-12-12T19:20:25.873 回答
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.

欢迎任何关于如何做得更好的建议。

谢谢,马丁。

于 2009-11-27T16:49:49.630 回答
0

录制一个可以替换前两个的宏可能会容易得多,然后使用 :s 其余的。

宏可能看起来像/foo^Mcwbar^[. 如果您不熟悉宏模式,只需点击q, a(存储它的寄存器)然后击键/foo <Enter> cwbar <Escape>

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

于 2009-11-27T16:27:35.200 回答