Vim正则表达式可拆分字符串,但保留分隔符

时间:2020-01-16 02:06:11

标签: regex vim

据我目前的理解,下面的模式应该可以工作(预期['bar', 'FOO', 'bar']),但是只能找到第一种(在FOO之后但不是之前的零宽度匹配)。

echo split('barFOObar', '\v(FOO\zs|\zeFOO)')  " --> ['barFOO', 'bar']

Netiher我可以先行解决/后行解决。

echo split('barFOObar', '\v((FOO)\@<=|(FOO)\@=)')  " --> ['bar', 'bar']

将此与例如Python:

echo py3eval("re.split('(?=FOO)|(?<=FOO)', 'barFOObar')") " --> ['bar', 'FOO', 'bar']

(注意:在Python中,用括号括起来的'(FOO)'也可以做到这一点。)

为什么Vim的正则表达式中的上述示例无法按我认为的方式工作? (而且,那么在纯Vimscript中是否有或多或少的直接方法可以做到这一点?)

2 个答案:

答案 0 :(得分:2)

似乎没有办法使用单个split()来完成直接结果。实际上,split()的文档通过以下方式提到了保存分隔符的特殊情况:

如果要保留分隔符,也可以在模式末尾使用\zs

:echo split('abc:def:ghi', ':\zs')
['abc:', 'def:', 'ghi']

话虽如此,同时使用先行和后行确实有效。在您的示例中,您有语法错误。由于您使用的是非常魔术的模式,因此您不应逃避@,因为它已经很特殊了。 (感谢@user938271指出这一点!)

这有效:

:echo split('barFOObar', '\v((FOO)@<=|(FOO)@=)')
" --> ['bar', 'FOO', 'bar']

关于为\zs\ze使用标记:

:echo split('barFOObar', '\v(FOO\zs|\zeFOO)')
" --> ['barFOO', 'bar']

因此,这里遇到的第一个麻烦是|两侧的两个表达式都匹配相同的文本“ FOO”,因此,由于它们相同,因此第一个获胜,并且您将其放在左边一侧。

更改订单,您会在右侧看到它:

:echo split('barFOObar', '\v(\zeFOO|FOO\zs)')
" --> ['bar', 'FOObar']

现在的问题是,为什么第二个令牌“ FOObar”由于再次匹配而没有被拆分(后面的情况将拆分这个令牌,对吧?)

好吧,答案是实际上它又被拆分了,但是它在\zeFOO的第一种情况下又匹配了一次,并产生了一个包含空字符串的拆分。您可以通过传递keepempty参数来看到这一点:

:echo split('barFOObar', '\v(\zeFOO|FOO\zs)', 1)
" --> ['bar', '', 'FOObar']

这里仍未解决的一个问题是,为什么先行/后行起作用,而\zs\ze不起作用。我想我在this answer中以某种方式解决了语法组中的正则表达式用法。

这是行不通的,因为Vim不会两次尝试匹配相同的正则表达式来扫描相同的文本。

即使\zs使得结果匹配仅包含bar,Vim也需要消耗FOO才能匹配该正则表达式,如果已经匹配则不会这样做以及图案的另一半。

后面带有\@<=的后视图是不同的。它起作用的原因是Vim将首先搜索bar(或它正在考虑的任何文本),然后向后看FOO是否也匹配。因此,模式固定在bar而不是FOO上,并且不会出现尝试在已经与另一个表达式匹配的区域上开始匹配的问题。

通过使用Vim进行搜索,您可以轻松地看到差异。试试这个:

/\v(\zeFOO|FOO\zs)

并将其与此内容进行比较:

/\v((FOO)@<=|(FOO)@=)

您会注意到,后者在FOO之前和之前都匹配,而前者则不会。


将此与例如Python [re.split] ... 在Python中,用括号括起来的'(FOO)'也可以做到这一点。

Vim和Python的regex引擎是不同的野兽...

Vim引擎的许多局限性都源于vi的祖先。捕获组是其中一个特别的限制,您只能将捕获组限制为9个,

鉴于此限制,您会发现捕获组的使用频率通常比Python中的使用频率低(使用时捕获的功能也较弱)。

要考虑的一个选项是在Vim中使用Python而不是Vimscript。尽管通常会影响可移植性,但是就我个人而言,我不会单独切换此功能。


那么,在纯Vimscript中是否有或多或少的直接方法?

一个选择是使用split()重新实现保留定界符的matchstrpos()版本。例如:

function! SplitDelim(expr, pat)
    let result = []
    let expr = a:expr
    while 1
        let [w, s, e] = matchstrpos(expr, a:pat)
        if s == -1
            break
        endif
        call add(result, s ? expr[:s-1] : '')
        call add(result, w)
        let expr = expr[e:]
    endwhile
    call add(result, expr)
    return result
endfunction

答案 1 :(得分:1)

您可以先将FOO替换为-FOO-,然后分割字符串。例如:

:echo split(substitute('barFOObarFOObaz', 'FOO','-&-','g'),'-')
['bar', 'FOO', 'bar', 'FOO', 'baz']