程序员Shell脚本小工具我爱编程

文本文件中的行分隔符

2018-11-30  本文已影响4人  SpaceCat

这可能是关于换行符最全面的一篇文章。即使现在不是,后面也会将新的内容补充进来,让它成为最全面的一篇。

1、介绍

当我们用一个编辑器打开一个文本文件,在其中输入一个字符'a',这时候,就会有一个对应的字符'a'的编码(如果编码格式是ACII码,那么这里记入的编码就是“97”,写成16进制就是“0x61”)记入到该文件中。类似的输入一个'b',文件中便会记入一个对应的字符'b'的编码。然而,如果我们按下键盘上的‘Enter’键,现象上看,文本内容发生了换行。但是,这时候,对应的文件中究竟记入了什么内容,来标记文件发生了换行呢?
实际上,对于这个问题,不同的操作系统,沿用了不同的操作传统。如下:

操作系统 ASCII码 转义字符 名称 英文名称 历史描述
Unix 0A \n 换行 LineFeed 使光标下移一格
Mac 0D \r 回车 CarriageReturn 使光标到行首
Dos和Windows 0A0D \r\n 回车换行 CRLF

注:
Mac OS 9 以及之前的系统的换行符是CR,从Mac OS X (后来改名为“OS X”)开始的换行符是LF即‘\n',和Unix统一了。

2、可能会引发的问题和解决

不同平台的换行符不同,会导致的各种异响不到的问题。比如:Unix/Mac系统下的文件在Windows里打开的话,所有文字会变成一行;而Windows里的文件在Unix/Mac下打开的话,在每行的结尾可能会多出一个^M符号。
如果只是将文件在编辑器中打开,供人肉眼阅读,这个问题还是挺好处理的。换一个更加智能的编辑器就好了。有的编辑器能够自动识别行分隔符,有的甚至允许用户自己指定行分隔符。这里面我遇到的对这个问题处理最好的编辑器,是JetBrains公司出的Java集成开发环境IntelliJ IDEA。


IDEA中行分隔符识别和切换

在打开文本文件的左下方,标签标识当前文件的行分隔符,鼠标点击,会弹出一个上拉列表,允许用户修改不同的行分隔符,非常方便。(类似地,文件编码的修改也在这个位置,不能更好用了。)
比人肉眼阅读麻烦的是,写程序处理文本文件的时候。一个按行处理文本文件的程序可能能够正确处理Windows上生成的文本文件,但是换成一个平台上产生的文件,可能就无法正确运行。这时候,可能就需要先识别是不是文件的分隔符导致的问题,然后,决定是不是要做必要的转换。

3、行分隔符的命令行识别

上面已经提到过了,更加智能的编辑器肯定是能够识别行分隔符的。但是,很多时候,我们有的只是一个终端、命令行。所以,这部分主要介绍如何通过命令来识别行分隔符。

3.1 xxd命令

如果能看到文件存储的二进制字节,自然可以知道文件的行分隔符是什么,图形化的智能编辑器大部分都自带这个功能。命令行下也有好多工具可以查看文本文件的16进制输出,这里以xxd命令为例介绍(如下测试,连同本文的其他测试都是在macOS Mojave 版本号10.14.1环境下执行的)。

$ cat a.txt 
a
b
c
$ xxd -g1 a.txt 
00000000: 61 0a 62 0a 63 0a                                a.b.c.
$ 

上面的命令中-g1的参数是指一个字节为一组查看16进制编码。从命令的结果可以看出,该文件的行分隔符是0a,也就是\n。xxd命令输出的右边a.b.c.,是带表文件文本内容,其中的点就是带表不可打印字符\n。而在下面的执行结果中,不难看出文件b.txt的行分隔符是\r\n

$ cat b.txt 
a
b
c
$ xxd -g1 b.txt 
00000000: 61 0d 0a 62 0d 0a 63 0d 0a                       a..b..c..
$ 

3.2 cat命令

有的操作系统发行版中,自带的命令行中没有上面的xxd工具,通过cat命令其实也可以查看文本文件的行分隔符。如下是cat命令各个选项的解释:

-A, --show-all           等价于 -vET
-b, --number-nonblank    对非空输出行编号
-e                       等价于 -vE
-E, --show-ends          在每行结束处显示 $
-n, --number     对输出的所有行编号,由1开始对所有输出的行数编号
-s, --squeeze-blank  有连续两行以上的空白行,就代换为一行的空白行 
-t                       与 -vT 等价
-T, --show-tabs          将跳格字符显示为 ^I
-u                       (被忽略)
-v, --show-nonprinting   使用 ^ 和 M- 引用,除了 LF 和 TAB 之外

可以看出-A选项的作用就是在文件每行结尾显示$,同时显示除了LF(\n换行符)和TAB之外的所有不可打印字符。如下是从维基百科扒下来的不可打印字符列表:

$ cat -A a.txt | head -1
cat: illegal option -- A
usage: cat [-benstuv] [file ...]
$ 

可以看出mac系统自带的命令行cat工具不支持-A选项。不过,在支持的系统上,配合head命令,可以看出如果文件的换行符是\n输出行的末尾只会有一个$,如果换行符是\r\n,输出行的末尾就会是^M$。从上面cat命令的解释也不难看出这一点。

4、行分隔符的装换

如果确定了是行分隔符的导致的问题,有时候,就需要进行行分隔符的转换。最简单的方式,可能是上面提到的像IDEA那样的更加智能的图形化文本编辑器,在界面上点点点操作几下就完成了。然而,这不见得是最方便的,比如在命令行的环境中,除了命令一无所有。因此,这里着重介绍命令行下的解决方案。

4.1 sed命令

提到命令行下的文件编辑sed命令肯定是绕不过去的。如果要将行分隔符从\n换成\r\n最直觉的写法可能是(-i选项的意思是直接在原文件上进行编辑):

sed -i 's/\n/\r\n/g' a.txt

然而这个方法,却屡试屡败。原因就在于sed命令是按照行来读文件的,逐行处理,默认地sed认为行分隔符是\n,所以,不会出现在sed处理的文本行内容中,导致这个方案失败。所以,可能的解决办法就是将所有文件内容读进来处理,而不是逐行处理。解决的办法大概有如下几个:

4.1.1 绕过换行符进行替换

既然sed处理的文本行中不包含换行符,我们可以用$来辅助实现替换:

sed -i 's/$/\r/g' a.txt -- \n转\r\n
sed -i 's/\r//g' a.txt -- \r\n转\n

但是,在我的系统上,这样写的效果却是:

$ sed -i '' 's/$/\r/g' a.txt 
$ xxd -g1 a.txt
00000000: 61 72 0a 62 72 0a 63 72 0a                       ar.br.cr.
$ 

这里之所以-i选项后面加''是因为这个系统上sed要求-i时,必须指定扩展。然而,仍然运行失败的原因在于macos没法像Linux那样将\r识别为特殊字符。为了给sed传入\r需要写成:

$ sed -i '' $'s/$/\r/g' a.txt 
$ xxd -g1 a.txt
00000000: 61 0d 0a 62 0d 0a 63 0d 0a                       a..b..c..
$ 
$ sed -i '' $'s/\r//g' a.txt 
$ xxd -g1 a.txt
00000000: 61 0a 62 0a 63 0a                                a.b.c.
$ 
$ sed -i '' "s/$/$(printf '\r')/" a.txt
$ xxd -g1 a.txt
00000000: 61 0d 0a 62 0d 0a 63 0d 0a                       a..b..c..
$

这里$''的作用就是让其中的转义字符正确被翻译。同样的,用$()也可以达到这个效果,不过外面的单引号要换成双引号。

4.1.2 sed的z选项

对于GNU版本的sed,可以使用-z选项。

-z
--null-data
--zero-terminated

Treat the input as a set of lines, each terminated by a zero byte (the ASCII ‘NUL’ character) instead of a newline. This option can be used with commands like ‘sort -z’ and ‘find -print0’ to process arbitrary file names.

下面是一个例子:

sed -i -z 's/\n/\r\n/g' a.txt

4.1.3循环读入所有文件内容

对于GNU版本的sed,也可以写一个循环,将文件全部读入之后,再交给sed处理:

sed ':a;N;$!ba;s/\n/\r\n/g' a.txt
This will read the whole file in a loop, then replaces the newline(s) with a space.

Explanation:

Create a label via :a.
Append the current and next line to the pattern space via N.
If we are before the last line, branch to the created label $!ba ($! means not to do it on the last line as there should be one final newline).
Finally the substitution replaces every newline with a space on the pattern space (which is the whole file).

5、正则表达式中换行符和文件开头结尾标识的先后顺序

到这里,换行符的识别、转换等都介绍完了。这里讲最后一个之前令我困扰的问题,^$\r\n这几个符号在正则匹配中的先后顺序是什么。这里,直接贴下正则表达式网站上的介绍:

For anchors there's an additional consideration when CR and LF occur as a pair and the regex flavor treats both these characters as line breaks. Delphi, Java, and the JGsoft flavor treat CRLF as an indivisible pair. ^ matches after CRLF and $ matches before CRLF, but neither match in the middle of a CRLF pair. JavaScript and XPath treat CRLF pairs as two line breaks. ^ matches in the middle of and after CRLF, while $ matches before and in the middle of CRLF.

也就是说,Delphi、Java和JGsoft风格的正则将CRLF看成一个整体,^匹配CRLF后面,$匹配CRLF前面,两者都不匹配CRLF中间。而JavaScript和XPath认为CRLF是两个换行符,^匹配CRLF中间和后面,$匹配CRLF中间和前面。


写了整整两个工作日的晚上,希望对自己和大家都有帮助。写得比较辛苦,也希望转载能注明出处链接,当然,也只是希望。

参考链接

上一篇下一篇

猜你喜欢

热点阅读