Почему важно всегда ставить символ переноса строки в конце текстовых файлов?

Опубликовано: 31 мая 2020, Вс, автор: Андрей Семакин Обновлено: 31 мая 2020, Вс 5 минут

Иногда при просмотре диффов коммитов через git log или git diff можно заметить следующий вывод:

\ No newline at end of file

Или на GitHub в интерфейсе для просмотра диффов:

GitHub "now newline at end of file" warning

Почему это так важно, что Git и GitHub предупреждают нас об этом? Давайте разберемся.

Что такое символ переноса строки?

Что может быть проще, чем текстовый файл? Просто текстовые данные — как хранятся на диске, так и отображаются. На самом деле правительство нам врёт всё немного сложнее.

Оффтопик про управляющие символы ASCII

Не все символы, которые содержатся в текстовых файлах, имеют визуальное представление. Такие символы ещё называют "управляющими", и к ним относятся, например:

  • нулевой символ (x00, \0) — часто используется для кодирования конца строки в памяти; т.е. программа считывает символы из памяти по одному до тех пор, пока не встретит нулевой символ, и тогда строка считается завершённой;
  • табуляция (\x09, \t) — используется для выравнивания данных по границе столбца, так что это выглядит как таблица;
  • перевод строки (\x0a, \n) — используется для разделения текстовых данных на отдельные строки;
  • возврат каретки (\x0d, \r) — переместить курсор в начало строки;
  • возврат на один символ (\x08, \b) — переместить курсор на один символ назад;
  • звонок (\x07, \a) — если набрать этот символ в терминале, то будет бибикающий символ; именно так консольные программы, типа vim, бибикают на пользователей;
  • и другие.

Многие эти символы пришли к нам из эпохи печатных машинок, поэтому у них такие странные названия. И действительно, в контексте печатной машинки или принтера такие операции, как перевод строки (сместить лист бумаги вверх так, чтобы печатающая головка попала на следующую строку), возврат каретки (переместить печатающую головку в крайнее левое положение) и возврат на один символ назад, обретают смысл. При помощи возврата на один символ назад создавались жирные символы (печатаешь символ, возвращаешься назад и печатаешь его ещё раз) и буквы с диакритическими знаками, такие как à или ã (печатаешь символ, возвращаешься назад и печатаешь апостроф или тильду). Но зачем печатной машинке бибикалка?

Сегодня многие из этих символов потеряли смысл, но некоторые до сих пор выполняют функцию, схожую с исходной.


Текстовые редакторы отображают текстовые файлы в некоем адаптированном виде, преобразуя непечатаемые символы, например, переносы строк и табуляции преобразуются в настоящие отдельные строки или выравнивающие отступы.

Для набора символа переноса строки достаточно нажать клавишу "Enter", но на разных платформах этот символ закодируется по-разному:

  • в Unix-совместимых системах (включая современные версии macOS) используется один символ перевода строки (LF);
  • в Windows используется сразу два символа — возврат каретки (CR) и перевод строки (LF);
  • в очень старых версиях Mac OS (до 2001 года) использовался один символ CR.

Как видите, Windows точнее всего эмулирует поведение печатной машинки.

В языках программирования символ новой строки часто кодируют при помощи бэкслэш-последовательностей, таких как \n или \r\n. Нужно понимать разницу между такой последовательностью и настоящим символом переноса строки. Если в редакторе в файле *.txt просто набрать \n и сохранить, то вы получите ровно то, что написали. Символом переноса строки оно не станет. Нужно что-то, что заменит эти бэкслэш-последовательности на настоящие символы переноса строки (например, компилятор или интерпретатор языка программирования).

Почему перенос строки в конце файла важен?

Согласно определению из стандарта POSIX, который тоже пришёл к нам из эпохи печатных машинок:

Строка — это последовательность из нуля или более символов, не являющихся символом новой строки, и терминирующего символа новой строки.

Почему важен этот стандарт? Возможен миллиард способов реализовать одно и то же, и только благодаря стандартам, таким как POSIX, мы имеем сейчас огромное количество качественного ПО, которое не конфликтует друг с другом.

Т.е. если вы не ставите символ переноса строки в конце строки, то формально по стандарту такая строка не является валидной. Множество утилит из Unix, которыми я пользуюсь каждый день, написано в согласии с этим стандартом, и они просто не могут правильно обрабатывать такие "сломанные" строки.

Давайте, например, через Python создадим такой файл со сломанными строками:

with open("broken.txt", "w") as f:
    f.write("qwe\n")
    f.write("asd\n")
    f.write("zxc")

Сколько по-вашему в этом файле строк? Три? Давайте посмотрим, что об этом файле думает утилита wc, которая с флагом -l умеет считать количество строк в файле:

$ wc -l broken.txt 
2 broken.txt

Упс! wc нашла только 2 строки!

Давайте создадим еще один файл:

with open("broken2.txt", "w") as f:
    f.write("rty\n")
    f.write("fgh\n")
    f.write("vbn")

И попробуем теперь склеить два созданных файла при помощи утилиты cat:

$ cat broken.txt broken2.txt 
qwe
asd
zxcrty
fgh
vbn

Название cat — это сокращение от "конкатенация", и никак не связано с котиками. А жаль.

И опять какой-то странный результат! В большинстве случаев это не то, чего вы бы ожидали, но вполне возможны ситуации, когда вам нужен именно такой результат. Именно поэтому утилита cat не может самостоятельно вставлять отсутствующие символы переноса строки, иначе это сделало бы её поведение неконсистентным.

Это только пара примеров, но многие другие утилиты, которые работают с текстом (например, diff, grep, sed), имеют такие же проблемы. Собственно говоря, это даже не проблемы, а их задокументированное поведение.

Ещё доводы:

Настраиваем редактор

Самый простой способ перестать думать о пустых строках и начать жить — это настроить свой текстовый редактор или IDE на автоматическое добавление символа переноса строки в конец файлов:

  • PyCharm и другие IDE JetBrains: Settings > Editor > General > Ensure an empty line at the end of a file on Save;
  • VS Code: "files.insertFinalNewline": true.

Для других редакторов смотрите настройку здесь.

Кстати, если вы пользуетесь форматтером black, то у меня хорошие новости — он всегда добавляет перенос строки в конец всех файлов *.py.

Заключение

Возможно, такая маленькая деталь, как перенос строки в конце файла и не кажется очень важной, а тема вообще кажется спорной, но боюсь, что у нас нет другого выбора, кроме как принять это правило за данность и просто выработать привычку (или настроить инструментарий) всегда ставить символ новой строки в любых текстовых файлах, даже если этого не требуется явно. Это считается распространённой хорошей практикой, и как минимум убережёт вас и ваших коллег от всяких неожиданных эффектов при работе с утилитами Unix.

В текстовом редакторе это выглядит как лишняя пустая строка в конце файла:

Пример того, как это должно выглядеть в текстовом редакторе

Дополнительное чтение

тэги: good practice