文字檔案中的行分隔符
這可能是關於換行符最全面的一篇文章。即使現在不是,後面也會將新的內容補充進來,讓它成為最全面的一篇。
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 0aa.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 0aa..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 0aar.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 0aa..b..c.. $ $ sed -i '' $'s/\r//g' a.txt $ xxd -g1 a.txt 00000000: 61 0a 62 0a 63 0aa.b.c. $ $ sed -i '' "s/$/$(printf '\r')/" a.txt $ xxd -g1 a.txt 00000000: 61 0d 0a 62 0d 0a 63 0d 0aa..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中間和前面。
寫了整整兩個工作日的晚上,希望對自己和大家都有幫助。寫得比較辛苦,也希望轉載能註明出處連結,當然,也只是希望。
參考連結
- ofollow,noindex">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
- bash" target="_blank" rel="nofollow,noindex">How does the leading dollar sign affect single quotes in Bash?
macOS中\r
正確被解釋的方法 - Line Break Characters
正則表示式中換行和開頭結尾的相對位置。