【问题标题】:sed only replaces first leading white space match for only a particular file - dealing with CR-only line endingssed 仅替换特定文件的第一个前导空白匹配 - 处理仅 CR 行结尾
【发布时间】:2016-02-15 22:32:46
【问题描述】:

编者注:
后来在事后对标题进行了修改;有两个明显的问题:
(a) 原来输入文件有 \r-only (CR-only) 行尾(经典 Mac OS 风格)
(b) 尝试在sed 正则表达式中使用\t\r 失败,因为BSD Sed(在OSX 上使用)不支持这种转义。

我正在开发一个 Automator 程序,该程序使用 Python 来查找和替换文本文件中的某些单词。该程序使用字典,并且在某些情况下用作替换的值是''(意思是什么都没有)。我不认为该程序导致了这个问题,但我只是通过上下文提及这一点。 (我认为问题在于sed,所以我不愿意标记Python。)

文件中的某些行具有前导空格,这些空格是在文件开头的某些单词被空替换后无意中创建的。我想摆脱它们,我认为sed 是这种情况下工作的最佳工具。

假设这是文本文件的样子:

  Display
  Display
 BOX,

所以我正在使用 sed 运行编辑过的文件:

sed -e 's/^[ \t]*//g'

这是结果:

 Display
  Display
 BOX,

仅编辑第一个匹配项。为什么?

通过测试,我创建了一个全新的纯文本文件,如下所示:

 hello
 hello
 hello

然后我运行上面的命令。这实际上按预期工作。为什么?

是否有可能使用了由 Python 程序创建的其他形式的空间(不可打印的字符?)?但那为什么sed 至少会工作一次呢?

顺便说一句,我愿意接受另一种与 OS X 兼容的便携式解决方案或工具,用于修剪纯文本文件中每一行的前导空白。

编辑:这是文件的一些xxd 输出(用X 替换了大多数实际内容):

0000000: 2044 6973 706c 6179 2043 616c 6962 7261   X X
0000010: 7469 6f6e 2046 6978 7475 7265 2046 4952  X X X
0000020: 4d57 4152 4520 4b49 545e 4d20 4469 7370  X X^M X
0000030: 6c61 7920 4361 6c69 6272 6174 696f 6e20  X X 
0000040: 4669 7874 7572 6520 524d 6163 426f 6f6b  X X
0000050: 2041 6972 2028 3131 2d69 6e63 682c 204d   X X
0000060: 6964 2032 3031 3229 2050 4f52 5420 4b49  X X) X X
0000070: 545e 4d42 4f58 2c20 5245 434f 5645 5259  T^MBOX, X

【问题讨论】:

  • cat -v 为该输入文件显示什么?
  • cat -v 在带有空格的行和除第一行之外的所有其他行的开头显示^M 。每行都以^M 开头,因为我相信它是回车符。这些空格似乎只是一个常规空格。
  • 它们不是常规空间。如果您将该文件通过管道传送到xxd,您可以看到它们实际上是什么。
  • 您是否将cat -v 的输出通过管道传输到xxd?它显示文字^M。您必须通过管道传输未修改的文件以理解 xxd 输出。
  • sed 是一个面向行的流编辑器。整个文本被sed 视为单行。这意味着正则表达式^ 行首仅适用于第一个单词Display,因为文件的其余部分是延续。您可能希望将这些回车符转换为换行符。

标签: macos bash awk sed


【解决方案1】:

tl;dr

以下解决方案均未更新输入文件就地;可以使用 -i '' 调整独立的 sed 命令来做到这一点; awk 解决方案需要先保存到不同的文件。

  • OP 的输入似乎是带有经典 Mac OS \r-only 换行符的文件 谢谢,@alvits。 .
  • sed 总是整体读取这样的文件,这通常是不受欢迎的,并且会妨碍 OP 的行前空白修剪方法。
  • awk 因此是更好的选择,因为它允许指定什么构成换行符(通过所谓的输入记录分隔符):

更新:将原来的 awk 命令替换为更简单、更快的替代方法,改编自 peak's solution

awk -v RS='\r' '{ sub(/^[ \t]+/, ""); print }'

如果还可以从每一行修剪 尾随 空格(如果有)并将一行上的单词之间的空格规范化到每个空格,您可以简化到:

awk -v RS='\r' '{ $1=$1; print }'

请注意,输出行将以\n 分隔,这通常是需要的。 有关说明和背景信息,包括如何将 \r 保留为换行符,请继续阅读。


注意:答案的第一部分一般适用,但假设输入有\n-终止的行; OP 的特殊情况,其中行显然是\r-only-terminated,在第二部分处理。

在 OSX 上使用的 BSD Sed 仅支持 \n 作为控制字符转义序列;因此,\t 用于匹配制表符字符。不支持。

要仍然匹配制表符,您可以拼接一个ANSI C-quoted string 以产生一个实际的制表符字符。进入你的 Sed 脚本 ($'\t'):

sed 's/^[ '$'\t'']*//'

在这个简单的例子中,您可以为 整个 Sed 脚本 (sed -e $'s/^[ \t]*//') 使用 ANSI C 引用的字符串,但是对于更复杂的脚本,这可能会变得很棘手,因为这样的字符串有自己的转义规则。

  • 请注意,选项 g 已被删除,因为它毫无意义,因为正则表达式已锚定到输入的开头 (^)。
  • 有关 GNU 和 BSD Sed 之间差异的摘要,请参阅我的 this answer

正如@alvits 在评论中指出的那样,输入文件实际上可能有\r 实例而不是 Sed 需要分隔行的\n 实例

即,文件可能具有 Pre-OSX Mac OS 行终止符:\r by itself 终止行。

一种简单的验证方法是将输入文件传递给cat -et\r 实例可视为^M,而\n 实例可视为$(此外,\t 实例被可视化为^I)。

如果输出中只有^M 实例,但没有$ 实例,则意味着行不以\n 结尾(也),并且整个输入文件被视为单个字符串,这解释了为什么只处理第一个输入“行”:^ 只匹配整个字符串的开头。

由于 Sed 解决方案(没有预处理)导致整个文件被作为一个整体读取,awk 是更好的选择

创建\n-separated 输出,这是在类 Unix 平台上的惯例:

awk -v RS='\r' '{ sub(/^[ \t]+/, ""); print }'
  • -v RS='\r' 告诉 Awk 通过\r 实例将输入拆分为记录(特殊变量RS 包含输入记录分隔符)。

  • sub(/^[ \t]+/, "") 在输入行中搜索第一次出现的正则表达式 ^[ \t]+ 并将其替换为 "",即它有效地修剪每个输入行中的前导空格和制表符。请注意,没有显式第三个参数的 sub() 隐式地对整个输入行 $0 进行操作。

  • print 然后打印可能修改的修改输入行。

  • 由于\n 是Awk 的默认输出 记录分隔符(OFS),输出记录将是\n-终止的。

如果你真的想保留\r 作为行分隔符:

awk 'BEGIN { RS=ORS="\r" } { sub(/^[ \t]+/, ""); print }'
  • RS=ORS="\r" 将输入和输出记录分隔符设置为 \r

如果还可以从每一行修剪 尾随 空格(如果有)并将一行上的单词之间的空格规范化到每个空格,您可以简化\n-terminated 变体:

awk -v RS='\r' '{ $1=$1; print }'
  • 不使用-F(也没有在脚本中设置FS,输入字段分隔符)意味着Awk 通过运行空格(空格、制表符、换行符)将输入记录拆分为字段。

  • $1=$1 是虚拟赋值,其目的是触发输入行的重建,每当分配字段变量时都会发生这种情况。
    通过使用输出字段分隔符 OFS 连接字段来重建该行,默认为单个空格。
    实际上,前导和尾随空白因此被修剪,并且每行内部空白都被规范化为单个空格。


如果您确实想坚持使用sed1 - 即使这意味着一次读取整个文件:

sed $'s/^[ \t]*//; s/\r[ \t]*/\\\n/g' # note the $'...' to make \t, \r, \n work

这将输出以\n 结尾的行,这在 Unix 上很常见。

相比之下,如果您想保留\r 作为行分隔符,请使用以下命令 - 但请注意,BSD Sed 总是会在最后添加 \n

 sed $'s/^[ \t]*//; s/\r[ \t]*/\r/g'  

[1] peak's answer 最初更清楚地展示了一个实用的 multi-utility 替代方案:使用 tr 将所有 \r 实例替换为 \n 实例,并将结果通过管道传输到原始 sed 命令的 BSD-Sed 友好版本:
tr '\r' '\n' file | sed $'s/^[ \t]*//'

【讨论】:

  • 回车不被视为换行符。第一个单词之后的任何空格或制表符都不在行首,因此它们在锚点 ^ 处失败。
  • @alvits:好点子,谢谢 - 我专注于尝试使用转义序列 \t,这在 BSD Sed 中不起作用(\r 也不起作用)。我已根据您的指示更新了我的答案。
  • 非常感谢您的全面回答。您的第一个 awk 解决方案很好地解决了我的问题。
【解决方案2】:

如果(似乎是这种情况)输入文件使用 \r 作为“行尾”字符,那么无论做什么,将 '\r' 转换为 ' 可能是有意义的\n' 或 CRLF,取决于平台。假设 '\n' 是可以接受的,并且如果保存原始文件并将 CR 替换为 LF 有任何意义,您可以使用tr

tr '\r' '\n' < INFILE > OUTFILE

使用类似 bash 的 shell,您可以像这样调用 sed

sed -e $'s/^[ \t]*//' OUTFILE

trsed 命令当然可以串在一起 (tr ... | sed ...) 但这会产生管道的开销。

如果您对保存原始文件并将 CR 替换为 LF 不感兴趣,那么您不妨考虑以下一站式 awk 变体:

awk -v RS='[\r]' '{s=$0; sub(/^[ \t]*/,"",s); print s}'

这种变体既快速又安全,因为不涉及对字段的解析。

(正如在别处指出的那样,使用 awk 的一个优点是如果默认设置不令人满意,可以使用 ORS 设置输出记录分隔符。)

【讨论】:

  • ++;虽然不如单一实用程序awk 解决方案高效,但如果您想坚持使用sed,这肯定是最简单的。与 OP 自己的方法一样,g 毫无意义,因为正则表达式锚定在行首。
  • @mklement0 - g 只是 OP 的 sed 的遗物。走了。至于“效率”,它可能取决于标准和其他因素。你量过吗?
  • 好点 - 在现实世界中差异可能并不重要,尤其是在 速度 方面;在 resources 方面,假设所涉及的实用程序使用相当数量的内存:1 个进程比一个好,不使用管道 (FIFO) 比 1 个好。同样,在实践中可能无关紧要,但我现在很好奇:一定要告诉我你发现了什么。
  • 虽然我选择 @mklement0 的答案是因为该解决方案非常符合我的目的,但这也是一个很好且简单的解决方案。顺便说一句,快速测试表明这两个选项都一样快(@mklement0 的解决方案比它的价值快了 0.0001 秒)。
  • 感谢您的周到反馈,@celestialroad - 特别是相对性能比较;我们可以得出结论,这两种解决方案在时间上表现相同。
猜你喜欢
  • 2016-01-28
  • 2010-09-13
  • 2011-03-20
  • 1970-01-01
  • 2011-06-17
  • 2017-12-19
  • 2022-12-02
相关资源
最近更新 更多