文本文件中的行分隔符
这可能是关于换行符最全面的一篇文章。即使现在不是,后面也会将新的内容补充进来,让它成为最全面的一篇。
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之外的所有不可打印字符。如下是从维基百科扒下来的不可打印字符列表:
- 0 (null, NUL, \0, ^@), originally intended to be an ignored character, but now used by many programming languages including C to mark the end of a string.
- 7 (bell, BEL, \a, ^G), which may cause the device to emit a warning such as a bell or beep sound or the screen flashing.
- 8 (backspace, BS, \b, ^H), may overprint the previous character.
- 9 (horizontal tab, HT, \t, ^I), moves the printing position right to the next tab stop.
- 10 (line feed, LF, \n, ^J), moves the print head down one line, or to the left edge and down. Used as the end of line marker in most UNIX systems and variants.
- 11 (vertical tab, VT, \v, ^K), vertical tabulation.
- 12 (form feed, FF, \f, ^L), to cause a printer to eject paper to the top of the next page, or a video terminal to clear the screen.
- 13 (carriage return, CR, \r, ^M), moves the printing position to the start of the line, allowing overprinting. Used as the end of line marker in Classic Mac OS, OS-9, FLEX (and variants). A CR+LF pair is used by CP/M-80 and its derivatives including DOS and Windows, and by Application Layer protocols such as FTP, SMTP, and HTTP.
- 26 (Control-Z, SUB, EOF, ^Z). Acts as an end-of-file for the Windows text-mode file I/o.
- 27 (escape, ESC, \e (GCC only), ^[). Introduces an escape sequence.
下面是cat命令查看行分隔符的一个例子:
$ 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中间和前面。
写了整整两个工作日的晚上,希望对自己和大家都有帮助。写得比较辛苦,也希望转载能注明出处链接,当然,也只是希望。
参考链接
-
Line termination: operating systems use different conventions
讲了不同文件分隔符现状的历史成因。 -
每天一个linux命令(10):cat 命令
cat命令各个选项的解释 -
Sed: replacing newlines with “-z”?
讲了sed命令的-z
选项 -
How can I replace a newline (\n) using sed?
sed写循环读入所有的文件内容 -
Removing Carriage return on Mac OS X using sed
macOS中sed无法识别\r
-
How does the leading dollar sign affect single quotes in Bash?
macOS中\r
正确被解释的方法 -
Line Break Characters
正则表达式中换行和开头结尾的相对位置。