末尾改行の不思議

2022/03/18更新

目次

概要

末尾改行とは以下のようにファイルの最後や文字列の最後についている改行文字のことである。

末尾改行あり

末尾改行無し

ファイルの場合

ここは1行目です。[改行]
ここは2行目です。[改行]
ここは3行目です。[改行]
ここは1行目です。[改行]
ここは2行目です。[改行]
ここは3行目です。

文字列の場合

これは文字列です。[改行]
これは文字列です。

末尾改行は状況によって扱われ方が様々であることが以前から気になっていた。いろいろな状況において、末尾改行がどのように扱われるかを調べたのでまとめておく。

以降のサンプルでは、上記の内容のファイルを扱った場合の例を示していく。

末尾改行の表示

各種エディタでの表示

ファイルの末尾改行のあり・無しは、エディタではどのように表示されるのか。Windowdsのメモ帳、Visual Studio Codeでは以下のように、末尾改行がある場合は最後の行の次に何もない行が表示されるため、容易に見分けがつく(左端の数字は行番号、|は最後のカーソル位置である)。これは、改行文字を「行の区切りを表すもの」として扱っているからであり、表示される行数は必ず「改行文字の数+1」行になる。

末尾改行あり

末尾改行無し

Windowsのメモ帳
Visual Studio Code

1 ここは1行目です。
2 ここは2行目です。
3 ここは3行目です。
4 |
1 ここは1行目です。
2 ここは2行目です。
3 ここは3行目です。|

一方、Vimでは以下のように末尾改行があっても4行目が表示されることはなく、どちらも同じ表示となって区別ができなかった。これは改行文字を「行の区切りを表すもの」ではなく「行の終端を表すもの」とするPOSIXの文化に基づくもので、「1行は必ず改行文字で終わることが前提」となっているためである。Vimで末尾改行無しのファイルを開いた状態で、何も編集せずに上書き保存すると、末尾改行が勝手に追加されてしまう。末尾改行の無いファイルを保存するには、別途設定が必要となる。

末尾改行あり

末尾改行無し

Vim

1 ここは1行目です。
2 ここは2行目です。
3 ここは3行目です。|
1 ここは1行目です。
2 ここは2行目です。
3 ここは3行目です。|

また、この仕様によるよくあるミスとして、末尾改行をつけるつもりで前述のWindowsのメモ帳やVisual Studio Codeのように最後に空行をつけて保存すると、Vimでは末尾に改行文字が2個ついてしまうことになる。

シェルプロンプトの表示

Bashなどのシェル上で改行の無いファイルを表示すると、次のプロンプトが行頭に来ないことがある。これも「テキストファイルは必ず改行で終わる」という前提で作られているためである。あらかじめプロンプトの先頭に改行を入れるよう設定しておけば回避できる。

末尾改行あり

末尾改行無し

シェルプロンプトの位置

$ cat newline.txt
ここは1行目です。
ここは2行目です。
ここは3行目です。
$
$ cat no-newline.txt
ここは1行目です。
ここは2行目です。
ここは3行目です。$

入力時の末尾改行

Perlの<>(行入力演算子)

Perlの<>(行入力演算子)は、特殊変数$/に設定された入力行セパレータ(デフォルトでは改行文字)とその次の文字の間で入力を分割する演算子である。split関数などのように「区切り文字で分割される」のではなく、「区切り文字とその次の文字の間で分割される」ので、区切り文字自身は各行の末尾に含まれることになる。ただし、最後の文字が区切り文字でない場合は、最後の行だけは末尾に区切り文字が含まれない。

open(my $fh, '<', 'file.txt');
while (my $line = <$fh>) {
    print "1行はここから【$line】ここまで。";
}
close($fh);

末尾改行あり

末尾改行無し

上記コードの出力

1行はここから【ここは1行目です。[改行]
】ここまで。1行はここから【ここは2行目です。[改行]
】ここまで。1行はここから【ここは3行目です。[改行]
】ここまで。
1行はここから【ここは1行目です。[改行]
】ここまで。1行はここから【ここは2行目です。[改行]
】ここまで。1行はここから【ここは3行目です。】ここまで。

Bashのwhile read

シェルスクリプトにおいてwhile readは標準入力から1行ずつ読み込みたいときの定型文であるが、このreadコマンドは末尾改行の無い行は読み込んでくれない。前述のPerlなどのプログラミング言語での行入力を経験した後だと、知らなければ必ずハマってしまう罠である。これも「1行は必ず改行文字で終わる」という思想に基づいているためである。また、変数に代入されると改行文字は取り除かれる。

while read line
do
  echo "1行はここから【$line】ここまで。"
done < file.txt

末尾改行あり

末尾改行無し

上記コードの出力

1行はここから【ここは1行目です。】ここまで。[改行]
1行はここから【ここは2行目です。】ここまで。[改行]
1行はここから【ここは3行目です。】ここまで。[改行]
1行はここから【ここは1行目です。】ここまで。[改行]
1行はここから【ここは2行目です。】ここまで。[改行]
  • 各行の「○行目です。」の後の改行が無くなっているのが分かる。また、末尾改行無しの最終行は読み込まれていない。

  • 「ここまで。」の後ろの改行文字はechoコマンドが自動的に出力しているものである。

Node.jsのreadline

Node.jsで1行ずつ読み込む必要がある場合、readlineモジュールが使われる。末尾改行が無くても問題無く読み込まれるが、改行文字は取り除かれる。

const fs = require('fs');
const readline = require('readline');
const reader = readline.createInterface({
    input: fs.createReadStream('file.txt', {})
});
reader.on('line', (data) => {
    console.log(`1行はここから【${data}】ここまで。`);
});

末尾改行あり

末尾改行無し

上記コードの出力

1行はここから【ここは1行目です。】ここまで。[改行]
1行はここから【ここは2行目です。】ここまで。[改行]
1行はここから【ここは3行目です。】ここまで。[改行]
1行はここから【ここは1行目です。】ここまで。[改行]
1行はここから【ここは2行目です。】ここまで。[改行]
1行はここから【ここは3行目です。】ここまで。[改行]
  • 各行の「○行目です。」の後の改行が無くなっているのが分かる。

  • 「ここまで。」の後ろの改行文字はconsole.log()自動的に出力しているものである。

VBAのLine Input

VBAで1行ずつ読み込むにはLine Inputが使われるが、Node.jsと同様に、末尾改行が無くても問題無く読み込まれるものの、改行文字は取り除かれる。

Dim buf As String
Open ThisWorkbook.Path & "\file.txt" For Input As #1
Do Until EOF(1)
    Line Input #1, buf
    Debug.Print "1行はここから【" & buf & "】ここまで。"
Loop
Close #1

末尾改行あり

末尾改行無し

上記コードの出力

1行はここから【ここは1行目です。】ここまで。[改行]
1行はここから【ここは2行目です。】ここまで。[改行]
1行はここから【ここは3行目です。】ここまで。[改行]
1行はここから【ここは1行目です。】ここまで。[改行]
1行はここから【ここは2行目です。】ここまで。[改行]
1行はここから【ここは3行目です。】ここまで。[改行]
  • 各行の「○行目です。」の後の改行が無くなっているのが分かる。

  • 「ここまで。」の後ろの改行文字はDebug.Printによるものである。

出力時の末尾改行

Perlのprint

Perlのprint関数やprintf関数は、標準では自動で改行が付与されることはない。自分で明示的に"\n"を渡さない限り、改行は出力されない。ただし、特殊変数の$\で改行文字などを末尾に付加するかどうかは設定ができる。

open(my $fh1, '>', 'file1.txt');
print $fh1 "末尾改行のある文字列です。\n";
open(my $fh2, '>', 'file2.txt');
print $fh2 "末尾改行の無い文字列です。";

file1.txt

file2.txt

上記コードの出力

末尾改行のある文字列です。[改行]
末尾改行の無い文字列です。

Linuxのechoコマンド

Linuxのechoコマンドは、通常は自動的に改行が付加され-nオプションをつけると改行が付加されなくなる。

echo "末尾改行のある文字列です。" > file1.txt
echo -n "末尾改行の無い文字列です。" > file2.txt

file1.txt

file2.txt

上記コードの出力

末尾改行のある文字列です。[改行]
末尾改行の無い文字列です。

ただ、元から末尾改行を含む文字列変数をechoしようとしたときは注意が必要となる。Bashの文字列で特殊文字を使う方法も参照されたい。

コード

出力結果

理由

str=$'末尾改行を含む文字列\n'
echo $str
末尾改行を含む文字列[改行]
  • echoコマンドに裸で変数を渡すと、変数中の改行文字は引数の区切りとして消費されてしまう。

  • -nオプションで改行を出力しないようにすると、改行が無くなっていることが分かる。

str=$'末尾改行を含む文字列\n'
echo -n $str
末尾改行を含む文字列
str=$'末尾改行を含む文字列\n'
echo "$str"
末尾改行を含む文字列[改行]
[改行]
  • echoコマンドに渡す変数をクォートで囲むと、含まれる改行もそのまま出力される。

  • -nオプションで改行を出力しないようにしても、変数に含まれていた改行が残っているのが分かる。

str=$'末尾改行を含む文字列\n'
echo -n "$str"
末尾改行を含む文字列[改行]

Node.jsのconsole.log

Node.jsのconsole.log()は手軽に出力ができる関数として多用されるが、常に自動的に改行が付加される。逆に改行せずに出力することはできない。console.log()は内部で標準出力へ書き込むためのprocess.stdout.write()を使用している。これを直接使用すれば改行は付加されない。

console.log('末尾改行のある文字列です。');
process.stdout.write('末尾改行の無い文字列です。');

console.log()

process.stdout.write()

上記コードの出力

末尾改行のある文字列です。[改行]
末尾改行の無い文字列です。

ただし、当然これらの関数は意味や目的が異なるため、単に改行の有無だけで使い分けるのは望ましくない。例えば1行をfor文などで複数に分けて出力したい場合などは、文字列を連結してから行単位で出力すればよいだけのことである。

// 文字列を連結してから行単位で出力
let str = '';
for (let i = 0; 10 > i; i++) {
    str = `${str}${i} `;
}
console.log(str); // 0 1 2 3 4 5 6 7 8 9 [改行]

VBAのPrint

VBAのPrint文は、通常は自動的に改行が付加されるが、文末に;を付けると改行無しとなる。

Open ThisWorkbook.Path & "\file1.txt" For Output As #1
Print #1, "末尾改行のある文字列です。"
Close #1
Open ThisWorkbook.Path & "\file2.txt" For Output As #2
Print #2, "末尾改行の無い文字列です。";
Close #2

file1.txt

file2.txt

上記コードの出力

末尾改行のある文字列です。[改行]
末尾改行の無い文字列です。

末尾改行無しファイルへの追記

Perlの>>

Perlのopen関数の追記モード>>で、末尾改行の無いファイルに追記しても、単純に改行の無い状態に続いての追記となる。新たな行から書き込まれるわけではないため、想定外の結果を招く可能性があり、注意が必要と言える。

open(my $fh, '>>', 'file.txt');
print "ここは追記された行です。\n";
close($fh);
ここは1行目です。[改行]
ここは2行目です。[改行]
ここは3行目です。ここは追記された行です。[改行]

Bashの>>

Bashのリダイレクト>>を使って追記した場合もPerlと同じ結果となる。新たな行から書き込まれるわけではない。

echo "ここは追記された行です。" >> file.txt
ここは1行目です。[改行]
ここは2行目です。[改行]
ここは3行目です。ここは追記された行です。[改行]

末尾改行の除去

Perlのchomp

Perlのchomp関数は、$/で指定される入力行セパレータ文字を末尾から除去する。$/は通常、改行文字\nがセットされているので、<>による入力時に各行末に付いている(かどうか分からない)改行文字を除去するのに使われる。

open(my $fh, '<', 'file.txt');
while (my $line = <$fh>) {
    chomp($line);
    ...
}
close($fh);

なお、似た名前の関数にchopがあるが、こちらは改行や入力行セパレーターであるかどうかにかかわらず、単に最後の1文字を削除する関数である。

Bashのコマンド置換

$(~)`~`などのコマンド置換は、末尾の改行を自動的に削除する。このため、通常は最後に改行が出力されるコマンドでも、気にせずにそのまま変数に代入したり文字列に埋め込んだりできる。

echo -n $(echo "改行のある出力です。") > file.txt
改行のある出力です。
  • 内側のechoが改行を出力しているはずだが、コマンド置換で削除されている。

sedによる改行除去

sedコマンドは通常、行単位で置換操作が行なわれるため、行末の改行文字を直接操作することはできないが、-zオプションを使うと行分割の単位がnull文字になるため、改行文字を置換対象にすることが可能となる。

sed -z 's/\n//g' <<EOD
ここは1行目です。
ここは2行目です。
ここは3行目です。
EOD
ここは1行目です。ここは2行目です。ここは3行目です。

ヒアドキュメントの末尾改行

Perlのヒアドキュメント

Perlの<<でヒアドキュメントを記述した場合、最後に必ず改行文字が入る。ヒアドキュメントの終端文字列(下記の場合EOD)は必ず行頭に来るため、その直前には改行文字があると考えれば自然な結果である。

my $str = <<EOD;
ヒアドキュメントの1行目です。
ヒアドキュメントの2行目です。
ヒアドキュメントの3行目です。
EOD
print "ヒアドキュメントはここから【$str】ここまで。";
ヒアドキュメントはここから【ヒアドキュメントの1行目です。[改行]
ヒアドキュメントの2行目です。[改行]
ヒアドキュメントの3行目です。[改行]
】ここまで。

Bashのヒアドキュメント

BashのヒアドキュメントもPerlと同様に、最後には改行文字が入る。

perl -e 'undef $/; $str = <STDIN>; print "ヒアドキュメントはこここから【$str】ここまで。";' <<EOD
ヒアドキュメントの1行目です。
ヒアドキュメントの2行目です。
ヒアドキュメントの3行目です。
EOD
ヒアドキュメントはここから【ヒアドキュメントの1行目です。[改行]
ヒアドキュメントの2行目です。[改行]
ヒアドキュメントの3行目です。[改行]
】ここまで。

各種Linuxコマンドでの扱い

wcコマンドによる行数

wc -lはファイルの「行数」を数えるコマンドとされるが、readと同様に末尾改行の無い行はカウントされず、実際よりも1行少ない数字が出てしまう。このため、末尾改行の無いファイルを扱う際は注意が必要となる。

「行数」を数えているというより、「改行文字の個数」を数えていると考える方が正しいと言える。

echo -n $'ここは1行目です。\nここは2行目です。\nここは3行目です。' | wc -l
# 2
echo -n $'ここは1行目です。\nここは2行目です。\nここは3行目です。\n' | wc -l
# 3

diffコマンドによる差分比較

diffコマンドで差分比較を実行したときに、末尾改行の無い行に差分があると警告が表示される。末尾改行の無い行自体が無視されることはない。

diff <(echo -n $'ここは1行目です。\n') <(echo -n $'ここは1行目です。')
1c1
< ここは1行目です。
---
> ここは1行目です。
\ ファイル末尾に改行がありません

バイナリファイルへの末尾改行追加の影響

ここまで見てきた通り、環境や出力の方法によっては、末尾改行が予期せず付加されてしまうことがある。私は昔、Perlで作ったCGIから任意のファイルをクライアントに返そうとして、以下のように書いてしまったことがある。

# $dataの後ろに改行文字が付加されてしまう。
print <<EOD;
Content-Type: $mime_type
Content-Disposition: attachment; filename="$file_name"

$data
EOD

$dataが任意のファイルのバイト列が入っている変数である。単純なHTMLを返すCGIであれば、このようにHTTPヘッダを含めてヒアドキュメントで出力することは特に大きな問題とはならないが、バイナリデータのバイト列を上記のようにして出力すると、$dataの後ろに改行文字が付加されてしまうことが問題となってくる。このCGIからダウンロードした一部のファイルが正しく開けなくなる事態が発生し、このミスに気付いた。

そこで、いくつかのファイル形式で予期しない末尾改行が付加されてしまった場合にどうなるかを調べた。結果、多くのファイルフォーマットでは、ファイル終端を表す構造が存在するため、末尾に何かが追加されても無視される仕様となっているものが多かった。ただ、問題無くファイルを開くことができたとしても、当然ファイルサイズやチェックサムは変わってしまう。また、問題無くファイルが開けてしまうことで、末尾改行が付加されたことに気づくのが遅れてしまうとも考えられる。

JPEG画像

JPEG画像は、最後に必ずEOI(End Of Image)マーカーというファイルの終わりを示すフラグがあり、その値は0xFF0xD9である。このEOIマーカー以降のバイト列は無視されることになっているため、末尾改行が付加されたとしても、たいていのソフトウェアでは正常に画像が表示される。

PNG画像

PNG画像は、複数のチャンクから成り立っており、最後は必ず「IENDチャンク」が来ることになっている。IENDチャンクは12バイト(チャンクの長さ(0バイト)を表す「0x000x000x000x00」の4バイト、チャンク名「IEND」の4バイト、そのCRC「0xAE0x420x600x82」の4バイト)であり、どんな画像でも同じである。これ以降のバイト列は無視されることになっているため、末尾改行が付加されたとしても、たいていのソフトウェアでは正常に画像が表示される。

GIF画像

GIF画像は、最後が必ずTrailerという1バイトのブロックであり、その値は0x3Bである。Trailerブロック以降を無視するソフトウェアでは正常に画像が表示される。

ZIPファイル

ZIPファイルの末尾には「セントラルディレクトリの終端レコード(EOCD: End Of Central Directory record)」というものがあり、その最後は「コメントのサイズ」と「コメント」になっている。つまり、末尾にはコメントとして一定のサイズのバイト列を入れられる仕様になっているが、そのためには直前の「コメントのサイズ」と実際の「コメント」の長さが一致していなければならない。単に末尾改行が付加されただけだと、「コメントのサイズ」との整合性が無くなるため、ソフトウェアによっては正しく解凍できなくなる可能性がある。「コメントのサイズ」以降を無視するようなソフトウェアの場合は、正常に解凍できることもある。

xlsxファイル

現在のExcelの主要なファイル形式であるxlsxファイルは、実体としてはZIPファイルであるが、Excelの場合、末尾改行が付加されたxlsxファイルを開こうとすると「問題が見つかりました」などと表示されて修復が試みられる。

PDFファイル

PDFファイルでは、%%EOFと書かれた行がファイルの終端を表す。これ以降のバイト列は無視されることになっているため、末尾改行が付加されたとしても、たいていの場合は正常にファイルを開くことができる。