为什么我的工具输出会互相覆盖,我该如何修复?

22
这个问题的目的是成为一个涵盖所有类似问题的规范答案,这些问题的答案归结为“你正在将DOS行尾符输入到Unix工具中”。任何有相关问题的人都应该找到一个清晰的解释,为什么他们被引导到这里以及可以解决他们问题的工具,以及可能解决方案的优缺点和注意事项。关于这个主题的一些现有问题只接受了只说“运行这个工具”的答案,没有解释或者只是明显危险的答案,不应该使用。
现在来看一个会导致转介到这里的典型问题:
我有一个包含1行的文件。
what isgoingon

当我使用这个awk脚本打印它来颠倒字段的顺序时:
awk '{print $2, $1}' file

而不是看到我期望的输出:

isgoingon what

我得到了应该在行尾的字段出现在行首,覆盖了行首的一些文本。
 whatngon

或者我将输出分成两行:

isgoingon
 what

什么可能是问题所在,我该怎么解决?

2
感谢您创建这个问题。这是最有用的问题,因为它是最常见的错误!应该默认链接到所有awksed问题。 - kvantour
1
这在精神上与 https://dev59.com/iFkS5IYBdhLWcg3wu4rL 非常相似 - 我们需要多个规范吗? - tripleee
3个回答

28
问题在于您的输入文件使用了DOS的换行符CRLF而不是UNIX的换行符LF,而您正在运行一个UNIX工具对其进行操作,因此CR仍然是UNIX工具操作的一部分数据。CR通常用\r表示,在运行cat -vE命令时,可以看到它显示为控制字符^M,而LF则是\n,在cat -vE命令中显示为$
因此,您的输入文件实际上不只是:
what isgoingon

其实是这样的:

what isgoingon\r\n

如你所见,通过cat -vE命令:
$ cat -vE file
what isgoingon^M$

od -c

$ od -c file
0000000   w   h   a   t       i   s   g   o   i   n   g   o   n  \r  \n
0000020

所以当你在UNIX工具(比如awk)上运行一个文件时,它会将\n视为行结束符。读取行的过程中,\n会被消耗掉,但是这会导致2个字段的存在。
<what> <isgoingon\r>

注意第二个字段末尾的\r\r表示回车符,字面上是将光标返回到行的起始位置的指令。所以当你执行以下操作时:
print $2, $1

awk会将其打印到终端,终端会打印出isgoingon并将光标返回到行的起始位置,然后打印一个空格,接着打印what,这就是为什么what似乎覆盖了isgoingon的起始部分。
解决方法如下:
dos2unix file
sed 's/\r$//' file
awk '{sub(/\r$/,"")}1' file
perl -pe 's/\r$//' file

显然,在某些UNIX变体(例如Ubuntu),dos2unix也被称为fromdos
如果你决定使用tr -d '\r',要小心,因为这会删除文件中所有\r,而不仅仅是每行末尾的\r。(更多细节见下文。)
注释
使用awk处理DOS换行符
GNU awk可以通过适当设置RS来解析具有DOS换行符的文件:
gawk -v RS='\r\n' '...' file

但其他的awk不允许这样做,因为POSIX只要求awk支持单个字符的RS,而大多数其他的awk会将RS='\r\n'静默截断为RS='\r'。你可能需要添加-v BINMODE=3来让gawk能够看到\r,因为底层的C原语会在某些平台上去除它们,例如cygwin。

包含换行符的CSV数据

需要注意的一件事是,由Windows工具如Excel创建的CSV文件会使用CRLF作为行尾,但可以在CSV的特定字段中嵌入LF,例如:

"field1","field2.1
field2.2","field3"

真的很:
"field1","field2.1\nfield2.2","field3"\r\n

所以,如果你只是将\r\n转换为\n,那么你就无法再区分字段内的换行符和行尾的换行符了。所以,如果你想要做到这一点,我建议先将所有字段内的换行符转换为其他字符,例如,将所有字段内的LF转换为制表符,并将所有行尾的CRLF转换为LF:
gawk -v RS='\r\n' '{gsub(/\n/,"\t")}1' file

不使用GNU awk进行类似操作留作练习,但使用其他awk时,需要在读取时将不以CR结尾的行合并。

Awk的默认FS

还要注意的是,尽管CR是[[:space:]] POSIX字符类的一部分,但它不是在使用默认FS " "时作为分隔字段的空白字符之一,其空白字符只有制表符、空格和换行符。如果输入中可以在CRLF之前有空格,这可能会导致混淆的结果:

$ printf 'x y \n'
x y
$ printf 'x y \n' | awk '{print $NF}'
y
$
$ printf 'x y \r\n'
x y
$ printf 'x y \r\n' | awk '{print $NF}'

$

这是因为在以LF换行符结尾的行的开头/结尾处忽略了尾随字段分隔符的空白,但如果它之前的字符是空白,则\r是以CRLF换行符结尾的行上的最后一个字段。
$ printf 'x y \r\n' | awk '{print $NF}' | cat -Ev
^M$

2
我理解你对于 tr -d '\r' 谨慎的评论,但出于专业好奇,你是否曾经遇到过一个 Windows CSV 文件,在某个地方有一个预期的 '\r' 负载? - Arminius
我编写了File::Edit::Portable来使跨平台的文件读写变得无缝。 - stevieb
@Arminius,我昨天刚弄了。那个csv文件当然是有问题的,但它包含了firstname\rlastnamefirst\nlast - James Brown
2
@JamesBrown 这就是我向 @EdMorton 提出问题的原因。我必须处理大量的输入数据,而在数据中找到一个孤立的 \r 会使我的验证程序发出“哔哔”声。我曾经遇到过这样的情况(不骗你!):几年前有人将 \r 用作列分隔符,将 \n 用作行分隔符。 :-) - Arminius

4
您可以在未知换行符的文件中使用\R 速记字符类PCRE中。甚至还有更多的换行符需要考虑,例如Unicode或其他平台。 \R形式是来自Unicode联盟的建议字符类,用于表示所有通用换行符的形式。
因此,如果您有“额外”的内容,可以使用正则表达式s/\R$/\n/查找并删除它,将任何换行符的组合规范化为\n。或者,您可以使用s/\R/\n/g捕获任何“行结束”概念,并将其标准化为一个\n字符。
给定:
$ printf "what\risgoingon\r\n" > file
$ od -c file
0000000    w   h   a   t  \r   i   s   g   o   i   n   g   o   n  \r  \n
0000020

Perl、Ruby和大多数PCRE的变种都实现了与字符串结尾断言$(在多行模式下为行尾)组合使用的\R
$ perl -pe 's/\R$/\n/' file | od -c
0000000    w   h   a   t  \r   i   s   g   o   i   n   g   o   n  \n    
0000017
$ ruby -pe '$_.sub!(/\R$/,"\n")' file | od -c
0000000    w   h   a   t  \r   i   s   g   o   i   n   g   o   n  \n    
0000017

请注意两个单词之间的 \r 会被正确保留。

如果您没有 \R,您可以在 PCRE 中使用等效的 (?>\r\n|\v)

使用纯 POSIX 工具,您最好的选择可能是像这样使用 awk

$ awk '{sub(/\r$/,"")} 1' file | od -c
0000000    w   h   a   t  \r   i   s   g   o   i   n   g   o   n  \n    
0000017

以下是一些可以使用但需要注意局限性的方法:

tr 命令可以删除所有的 \r,即使在其他上下文中使用(尽管使用 \r 的情况很少,而且 XML 处理要求删除 \r,因此 tr 是一个很好的解决方案):

$ tr -d "\r" < file | od -c
0000000    w   h   a   t   i   s   g   o   i   n   g   o   n  \n        
0000016

GNU sed能够正常工作,但是POSIX sed由于不支持\r\x0D而不能正常工作。

仅适用于GNU sed:

$ sed 's/\x0D//' file | od -c   # also sed 's/\r//'
0000000    w   h   a   t  \r   i   s   g   o   i   n   g   o   n  \n    
0000017

Unicode 正则表达式指南 可能是最权威的“换行符”定义的解释。


在我看来,只有在你必须操作不知道行尾字符串是什么但可以保证其他可能的行尾字符不会出现在输入中时,使用\R才有用。我的意思是,如果我有使用\r\n行尾并且可以包含字段内的\v\n的输入文件(我希望我可以用Excel生成),那么我可能会有一个1个字段记录,即"foo\v\nbar"\r\n,那么我如何使用\R来识别行?我可以将行标识为由\r\n分隔的字符串,但不能通过\R\n来标识行,因为后者会包括记录中间的\v\n - Ed Morton
抱歉留下了多个评论,我只是想不明白为什么要使用\R,而且我绝对不理解这里发生了什么:1) od -c < file 输出 " f o o \v \n b a r " \r \n 2) perl -pe 's/\r$/\n/' file | od -c 输出 " f o o \v \n b a r " \n \n 3) perl -pe 's/\R$/\n/' file | od -c1 输出 " f o o \n \n b a r " \n。正如我预期的那样,使用\R会破坏记录中的\v\n,但为什么在正则表达式中使用\r$\r\n变成了\n\n,而使用\R$时只有一个\n?第二个\n去哪了? - Ed Morton
@EdMorton:2- 单独的\n即使在引用时也会被 Perl 视为换行符/记录分隔符。在正则表达式s/\R$/\n/中,\v被视为额外的换行符,因此用于替换序列\v\n的结果是\n\n。序列\r\n中的\n再次被视为换行符。s/\R$/\n/\r\n视为单个换行符,因此得到单个\n。如果要将"foo\v\nbar"\r\n视为单个记录,则需要使用 CSV 分析器或更完整的描述正则表达式。 - dawg
@EdMorton:3 - \R 的尝试是成为一个“通用换行符”,适用于 UTF-X、XML 或未知行结尾的通用文本。您可以使用动词来控制包含什么。假设您已经设置好了工具以正确读取行,正则表达式\R$将删除任何未包含在工具的行处理中的\R中的字符。请注意,PCRE的\v字符类与ANSI C的\v字符定义不同。字符类\v等效于/[\n\cK\f\r\x85\x{2028}\x{2029}]/ - dawg
这与我的口味有些不同于BRE和ERE,我觉得猜测可能不正确的行尾出现在输入的其他位置是个坏主意,但我想在某些情况下它必须是有用的,否则“他们”就不会提出来了。感谢您的解释。 - Ed Morton
\R不是一个简写字符类,而是原子组内不同换行序列的替代。这就是为什么你不能写像[\R]这样的东西。 - Casimir et Hippolyte

2
运行dos2unix。虽然您可以使用自己编写的代码来操作行结束符,但在Linux / Unix世界中已经存在一些实用程序可以为您完成此操作。
如果在Fedora系统上,dnf install dos2unix将安装dos2unix工具(如果尚未安装)。
对于基于Debian的系统,也有类似的dos2unix deb软件包可用。
从编程角度来看,转换很简单。搜索文件中的所有字符以查找序列\r\n,并将其替换为\n
这意味着几乎可以使用任何工具从DOS转换为Unix,有数十种方法可供选择。其中一种简单的方法是使用命令tr,只需将\r替换为无。
tr -d '\r' < infile > outfile

2
表格tr -d '\r' < infile > outfile会破坏文件中所有\r,而不是Windows行结尾的一部分。最好使用sed 's/\r$//',因为它只替换行结尾。 - dawg
2
@dawg 说得好。因此使用 dos2unix 更安全。 - Edwin Buck

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