なぜテキストファイルは改行で終わるべきなのか?

coding
Author

Ryo Nakagami

Published

2025-07-30

Modified

2025-07-30

POSIX標準の考え方

Definition 1 3.195 Incomplete Line

  • A sequence of one or more non-<newline> characters at the end of the file.

Definition 2 3.206 Line

  • A sequence of zero or more non-<newline> characters plus a terminating <newline> character.
  • Lineとは,1つ以上の <newline> 文字以外の文字と,行末の <newline> 文字によって成り立つ

多くのUNIX系のツールは Definition 2 に基づいており,改行文字で終わらない「行」は Definition 1 のように「行」とはみなされません.

Editorと改行

エディタ 末尾の改行の挙動
VS Code 設定次第で自動追加 (files.insertFinalNewline)
Vim デフォルトで改行追加(:set nofixeol で抑制可)
Emacs デフォルトで改行追加(require-final-newline 変数)

catコマンドと結合

cat でファイルを結合する場合,改行で終わるファイル (a.txtc.txt) と改行で終わらないファイル (b.txt) では,結合時の挙動が異なります.

まず,改行ありと改行なしの.txtファイルを生成します

改行ありファイルの生成

#!/bin/bash

words=(foo bar baz)
files=(a b c)

for i in "${!words[@]}"; do
  echo "${words[i]}" > "${files[i]}.txt"
done
#!/bin/zsh

words=(foo bar baz)
files=(a b c)

for i in {1..${#words[@]}}; do
  echo "${words[i]}" > "${files[i]}.txt"
done

改行なしファイルの生成

#!/bin/bash

words=(foo bar baz)
files=(a b c)

for i in "${!words[@]}"; do
  printf "%s" "${words[i]}" > "${files[i]}_without_newline.txt"
done
#!/bin/zsh

words=(foo bar baz)
files=(a b c)

for i in {1..${#words[@]}}; do
  printf "%s" "${words[i]}" > "${files[i]}_without_newline.txt"
done

つぎに catコマンドで結合をしてみます.

cat コマンドで結合

% cat {a,b,c}.txt
foo
bar
baz
% cat {a,b,c}_without_newline.txt      
foobarbaz% 

wc commandと改行

Definition 3 wc コマンドマニュアル

  • A line is defined as a string of characters delimited by a <newline> character.

wcコマンドは <newline> の数で行数を数えています.実際に

## 改行なし
$ echo -n "Line not ending in a new line" | wc -l
0

## 改行あり
$ echo "Line ending with a new line" | wc -l
1

Example 1 結合ファイルとwcコマンド

% cat {a,b,c}.txt | wc -l
3
% cat {a,b,c}_without_newline.txt | wc -l
0

git trackingテキストファイルを対象に改行有無判定スクリプト

スクリプト全体

以下のシェルスクリプトは,バイナリファイルを除外した上で,ファイル末尾の改行の有無をチェックするスクリプトです

git ls-files -z | while IFS= read -r -d '' file; do
  file --mime "${file}" | grep -q -e "charset=binary" -e "image/svg+xml" ||
  tail -c1 "${file}" | read -r _ ||
  echo "Missing newline: ${file}"
done

アルゴリズム

\begin{algorithm}
\caption{Checking Files for Missing Trailing Newlines}
\begin{algorithmic}
\State file\_list \(\leftarrow\) Get all Git-tracked files with NUL delimiter
\ForAll{file in file\_list}
    \State mime\_info \(\leftarrow\) \texttt{file --mime file}
    \If{mime\_info contains "charset=binary" OR "image/svg+xml"}
        \State \textbf{continue (skip binary/SVG files)}
    \Else
        \State last\_byte \(\leftarrow\) Get final byte of file
        \If{last\_byte is not newline character}
            \State Output "Missing newline: file"
        \EndIf
    \EndIf
\EndFor
\end{algorithmic}
\end{algorithm}

各コマンド

コマンド 説明
git ls-files -z Gitで管理されているファイルを一覧表示
-zオプションで区切り文字としてNUL文字を使用(ファイル名に特殊文字が含まれる場合の安全な処理のため)
while IFS= read -r -d '' file NUL文字を区切りとしてファイルを順次処理
IFS=で空白文字の保持
-rでバックスラッシュの解釈を防止
-d '':read でヌル文字区切りに対応
file --mime "${file}" | grep -q -e "charset=binary" -e "image/svg+xml" || バイナリファイルとSVGファイルをスキップ
テキストファイルのみを処理
||は「このコマンドが失敗した場合に次のコマンドを実行」を意味
tail -c1 "${file}" | read -r _ || ファイルの最後の1バイトをチェック
readは改行があれば0,なければ1を返す
echo "Missing newline: ${file}" 改行のないファイルを報告

Example 2 カレントディレクトリ以下のファイルに対しての改行判定スクリプト

#!/bin/bash
find . -maxdepth 1 -type f -print0 | while IFS= read -r -d '' file; do 
  file --mime "${file}" | grep -q -e "charset=binary" -e "image/svg+xml" ||
  tail -c1 "${file}" | read -r _ ||
  echo "Missing newline: ${file}"
done
  • -print0: 各ファイル名の末尾に NUL文字 (\0) を付けて出力

改行なしと判定されたファイルに対して<newline>を付与する

シェルスクリプト
find . -maxdepth 1 -type f -print0 | while IFS= read -r -d '' file; do
  # バイナリファイルやSVGはスキップ
  file --mime "$file" | grep -q -e "charset=binary" -e "image/svg+xml" && continue

  # 最後の1バイトを確認(改行がないなら)
  if ! tail -c1 "$file" | read -r _; then
      echo >> "$file"; echo "✓ Newline added to: $file"
  fi
done
  • echo >> "$file"<newline>を行末に追加

改行忘れの対策

.editorconfigの用いた改行設定

EditorConfigとは?

  • コードスタイル(インデントや改行コードなど)を統一するための設定ファイルの仕組み
  • 異なるエディタ・OS間でのコードスタイルを統一させたいときに便利なツール

EditorConfigによる改行設定例

プロパティ名 内容
indent_style インデントの種類(space または tab indent_style = space
indent_size インデントの幅 indent_size = 4
end_of_line 改行コード(lf/crlf/cr end_of_line = lf
charset 文字コード charset = utf-8
trim_trailing_whitespace 行末の空白を削除するか trim_trailing_whitespace = true
insert_final_newline 最終行の末尾に改行を追加するか insert_final_newline = true
# .editorconfig

# このファイルが設定のルートであることを示す
root = true

# すべてのファイルに適用
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

VSCodeでの改行設定

settings.jsonで以下の項目を追加します

{
  // Editor Settings
  "files.insertFinalNewline": true,
  "notebook.insertFinalNewline": true
}
設定キー 意味
"files.insertFinalNewline": true ファイル保存時に、末尾に改行を自動で追加します(通常の .txt, .js, .py などすべて対象)
"notebook.insertFinalNewline": true Jupyter Notebook(.ipynb)などのノートブック形式のファイルにおいて、セル内のソースに末尾改行を追加する設定(拡張機能依存)

Appendix: 改行コードの種類

改行コード 記号 名称 主なOS バイナリ表現 説明
LF \n Line Feed(行送り) Linux / macOS(Unix系) 0x0A 行末で「次の行の先頭」に移動
CRLF \r\n Carriage Return + Line Feed Windows 0x0D 0x0A 行頭に戻りつつ次の行へ(古いタイプライタ由来)
CR \r Carriage Return(復帰) 古いMac OS(〜OS9) 0x0D 行頭に戻るだけ(現在はほぼ使われない)

歴史的背景

タイプライターの動きと対応させると改行には3つの考え方がありました

  • LF は紙を上に移動させ (水平方向の位置は変わらない)
  • CR は「キャリッジ」を戻して,次の入力文字が紙の左端の同じ行に表示
  • CR+LF は両方を実行し,新しい行の入力準備

References