使用vim正则表达式将多个连续空格替换为一个空格

75

我经常使用的文本文件中,单词分隔符的空格数量不定(像 Word 这样的文本处理器会这样做,为了在某些字体中由于字母大小的不同而公平分配空格数量,甚至在保存为纯文本时,它们也会放置这些令人讨厌的变量长度的空格)。

我想自动化替换这些具有可变长度的空格序列为单个空格。我猜测可以用正则表达式实现,但段落开头也有空格(通常是四个,但不总是),我希望只留下这些空格不变,所以基本上我的正则表达式也不应该触及这些前导空格,这增加了复杂性。

我正在使用 vim,如果可以的话,vim 正则表达式语言中的正则表达式对我非常有用。

目前我的进展如下:

:%s/ \+/ /g

但它不能正确工作。

我也考虑编写一个vim脚本,逐行解析文本行,逐个字符处理每一行并跳过第一个字符后的空格,但我觉得这可能有点过度。


适用于重新格式化垂直对齐的代码 :) - JackHasaKeyboard
7个回答

133
这将替换掉2个或更多的空格。
s/ \{2,}/ /g

或者你可以在你的版本中\+之前添加一个额外的空格。

s/  \+/ /g

10
我认为这可能是最好、最简单的答案。它还有一个附加的好处,可以在其他正则表达式方言中使用! - TrinitronX
1
这绝对是最好的和最简单的答案。 - RubyFanatic
同意 - 这是最好的答案。 - John
1
为什么我们需要在“{2,}”之前添加“\”? - BiBi
3
这个要求是“也不应该触及前导空格”,但这个答案似乎没有满足这个要求。 - paxdiablo
@BiBi 所以要将 { 视为特殊字符;否则它会尝试匹配 {。 - Hipponax43

63

这个方法可以解决问题:

%s![^ ]\zs  \+! !g

使用 \zs\ze 元序列,Vim 中可以比其他正则表达式方言更轻松地进行许多替换。它们的作用是从最终结果中排除匹配的一部分,要么是序列之前的部分 (\zs, “s” 代表 “从这里开始”),要么是序列之后的部分 (\ze, “e” 代表 “到这里结束”)。在这种情况下,模式必须首先匹配一个非空格字符 ([^ ]),但是接下来的 \zs 表示最终匹配结果(将被替换的内容)从那个字符之后开始。

由于没有办法在行前空格字符前放置非空格字符,因此它不会被该模式匹配,因此替换不会替换它。简单明了。


1
我想提出这个替代方案:%s!\S\@<= \+! !g\@<=是一只非常美丽的鸭子,我喜欢使用它。另请参阅:help /\@<= - Benoit
1
我只是更喜欢使用 zs 这种简化的手指技巧,而不是打 @<=... 就像我更喜欢 Vim 胜过 E(scape)M(eta)A(lt)C(ontrol)S(hift)一样(尽管程度较轻)。:) 另一方面,一个人的风格感总是值得一些牺牲的,所以请随意。 - Aristotle Pagaltzis
像个老板一样。谢谢。 - Nick Res

43

出于实用主义的考虑,我倾向于将其作为一个三阶段的过程来完成:

:g/^    /s//XYZZYPARA/g
:g/ \+/s// /g
:g/^XYZZYPARA/s//    /g

我并不怀疑可能会有更好的方法(例如使用宏或者纯正则表达式方式),但当我匆忙之时,我通常发现这种方法奏效。当然,如果你有以 XYZZYPARA 开头的行,你可能需要调整字符串 :-)。

对于转换来说,这已经足够好了:

    This is a new paragraph
spanning       two lines.
    And    so    is   this but on one line.

转化为:

    This is a new paragraph
spanning two lines. 
    And so is this but on one line.

顺便提一下:如果你想知道为什么我使用:g而不是:s,那主要是习惯问题。:g可以做:s能做的所有事情,而且更多。实际上,它是在所选行上执行一个任意的命令的方式。在这种情况下,要执行的命令恰好是s,所以实际上没有区别,但是,如果你想成为vi高级用户,应该在某个时候研究一下:g


2
是的,我内心中那个纯粹主义者/理想主义者早就开始退居二线了。现在我只想完成工作,特别是如果另一种选择是一个600个字符的正则表达式,带有回溯和前瞻,三个月后当我回来调试它时,我将无法理解 :-) - paxdiablo
我在上面的变体中使用了以下命令::g/ \+/s// /g我知道空格和+匹配一个或多个字符,但不知道/s/是什么意思,有人知道吗? - anteatersa
1
@anteatersa,s是替换命令本身。如果您阅读我的答案的最后一部分,它解释了g只是选择行,然后在每个行上执行任意命令,其中s是其中之一。例如,:g/^$/d将在所有空行上运行d命令(删除行)。您可以尝试各种有趣的操作,例如使用:g/^/m0 :-) - paxdiablo
太棒了!我在想是否可以将它转换为 Vim 中的函数,而不是一次复制粘贴三个命令? - Rushi Agrawal

7

这里有很多好的答案(尤其是Aristotle的:\zs\ze值得学习)。为了完整起见,您还可以使用负回顾断言来实现:

:%s/\(^ *\)\@<! \{2,}/ /g

这段话意思是“查找两个或更多空格(' \{2,}'),它们不是由‘行开头后跟零个或多个空格’所引导的”。如果您希望减少反斜杠的数量,您也可以这样做:
:%s/\v(^ *)@<! {2,}/ /g

但这只能为您节省两个字符!如果您不介意进行大量冗余更改(即将单个空格更改为单个空格),您也可以使用'+'而不是' {2,}'
您还可以使用负回顾来仅检查单个非空格字符:
:%s/\S\@<!\s\+/ /g

这与亚里士多德的方法略有不同,它将空格和制表符视为相同,以节省一些打字时间:

:%s/\S\zs \+/ /g

请参见:

:help \zs
:help \ze
:help \@<!
:help zero-width
:help \v

并且(请全部阅读!):
:help pattern.txt

3
回答了,但我仍然想分享一下我的工作流程。
%s/  / /g
@:@:@:@:@:@:@:@:@:@:@:@:(repeat till clean)

快速简单易记。虽然还有更加优雅的解决方案,但这是我的个人看法。

2
这不是一个好的解决方案:首先它会删除前导空格,而问题的作者希望避免这种情况。其次,您可以执行100@:以运行寄存器中内容的100次:(即最后一个ex命令)。 - Benoit
1
因此我在回复中说这不是最佳答案 :) - wom
2
尽管它没有很好地回答OP的问题,但我仍然认为这个答案很有用。 - Vladislavs Dovgalecs
谢谢。您的解决方案易于记忆。Vim维基页面上关于删除不需要的空格的说明介绍了如何结合\s\+来查找多个空格和制表符字符。例如,使用以下命令在选择中替换所有空格为一个空格::'<,'>s/\s\+/ /g。现在与亚里士多德的保留行开头缩进的方法相结合::'<,'>s/[^\s]\zs\s\+/ /g - Paul Rougieux

2

这个可以工作吗?

%s/\([^ ]\)  */\1 /g

在这种情况下最好使用 %s/[^ ]\zs \+/ /g:help /\zs)。 - Benoit
啊!太好了。我非常同意。谢谢你。 - frogstarr78

2

我喜欢这个版本 - 它类似于Aristotle Pagaltzis的预测版本,但我发现它更容易理解。(可能只是因为我对\zs不熟悉)

s/\([^ ]\) \+/\1 /g

或者对于所有空格
s/\(\S\)\s\+/\1 /g

我理解为“将除空格外的任何东西和多个空格替换为该东西和一个空格”。

当然,这个版本在输入和即时制定方面更加挑剔——而且这几乎是最琐碎的模式之一。熟悉\zs\ze会对更复杂的模式(特别是同时使用两者时)的可写性和可读性产生奇妙的影响,你会受益匪浅。 - Aristotle Pagaltzis
我一定会看\zs\ze,但我也经常在Python和sed中使用我的正则表达式。因此,拥有一个可以跨多个应用程序工作的解决方案会很不错。 - Michael Anderson

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