Вопрос или проблема
Используя системы управления версиями, меня раздражает сообщение, когда дифф выводит No newline at end of file
.
Мне интересно: как добавить новую строку в конце файла, чтобы избавиться от этих сообщений?
sed -i -e '$a\' file
А для OS X sed
:
sed -i '' -e '$a\' file
Это добавляет \n
в конец файла только, если уже нет новой строки. Так что, если выполнить это дважды, дополнительная новая строка не будет добавлена:
$ cd "$(mktemp -d)"
$ printf foo > test.txt
$ sed -e '$a\' test.txt > test-with-eol.txt
$ diff test*
1c1
< foo
\ No newline at end of file
---
> foo
$ echo $?
1
$ sed -e '$a\' test-with-eol.txt > test-still-with-one-eol.txt
$ diff test-with-eol.txt test-still-with-one-eol.txt
$ echo $?
0
Как это работает:
$
обозначает конец файлаa\
добавляет следующий текст (который отсутствует в данном случае) на новой строке
Другими словами, если в последней строке есть символ, не являющийся новой строкой, добавьте новую строку.
Чтобы рекурсивно обработать проект, я использую следующую команду:
git ls-files -z | while IFS= read -rd '' f; do if file --brief --mime-encoding "$f" | grep -qv binary; then tail -c1 < "$f" | read -r _ || echo >> "$f"; fi; done
Объяснение:
-
git ls-files -z
перечисляет файлы в репозитории. Дополнительно может принять шаблон, если вы хотите ограничить операцию определенными файлами/директориями. В качестве альтернативы можно использоватьfind -print0 ...
или аналогичные программы для перечисления затронутых файлов – главное, чтобы они выводилиNUL
-разделенные записи. -
while IFS= read -rd '' f; do ... done
итерируется по записям, безопасно обрабатывая имена файлов, которые включают пробелы и/или новые строки. -
if file --brief --mime-encoding "$f" | grep -qv binary
проверяет, находится ли файл в бинарном формате (например, изображения) и пропускает такие. -
tail -c1 < "$f"
читает последний символ из файла. -
read -r _
завершает с ненулевым статусом выхода, если отсутствует завершающая новая строка. -
|| echo >> "$f"
добавляет новую строку в файл, если статус выхода предыдущей команды был ненулевым.
Посмотрите:
$ echo -n foo > foo
$ cat foo
foo$
$ echo "" >> foo
$ cat foo
foo
так что echo "" >> noeol-file
должно сработать. (Или вы хотели спросить, как идентифицировать эти файлы и исправить их?)
редактировать убрал ""
из echo "" >> foo
(см. комментарий @yuyichao)
редактировать2 добавил ""
снова (но см. комментарий @Keith Thompson)
Простой, переносимый, POSIX-совместимый способ добавить отсутствующую финальную новую строку в файл:
[ -n "$(tail -c1 file)" ] && echo >> file
Этот подход не требует считывания всего файла; он просто перемещается к концу файла и работает оттуда.
Этот подход также не создает временные файлы “за вашей спиной” (например, sed -i), поэтому жесткие ссылки не затрагиваются.
echo добавляет новую строку в файл только тогда, когда результат подстановки команды является непустой строкой. Обратите внимание, что это может произойти только в том случае, если файл не пуст и последний байт не является новой строкой.
Если последний байт файла является новой строкой, tail возвращает его, затем подстановка команды удаляет его; результат – пустая строка. -n тест не проходит, и echo не запускается.
Если файл пуст, результат подстановки команды также является пустой строкой, и echo снова не запускается. Это желательно, так как пустой файл не является недопустимым текстовым файлом и не эквивалентен не пустому текстовому файлу с пустой строкой.
Еще одно решение с использованием ed
. Это решение затрагивает только последнюю строку и только если отсутствует \n
:
ed -s file <<< w
Это работает, открывая файл для редактирования через скрипт, который представляет собой единственную команду w
, записывающую файл обратно на диск. Это основано на этой фразе, найденной в man-странице ed(1)
:
LIMITATIONS (...) Если текстовый (не бинарный) файл не заканчивается символом новой строки, то ed добавляет его при чтении/записи. В случае с бинарный ed не добавляет новую строку при чтении/записи.
Добавить новую строку в любом случае:
echo >> filename
Вот способ проверить, есть ли новая строка в конце, перед тем как добавить ее, используя Python:
f=filename; python -c "import sys; sys.exit(open(\"$f\").read().endswith('\n'))" && echo >> $f
Самое быстрое решение:
[ -n "$(tail -c1 file)" ] && printf '\n' >>file
-
Очень быстро.
На файле среднего размераseq 99999999 >file
это занимает миллисекунды.
Другие решения занимают много времени:[ -n "$(tail -c1 file)" ] && printf '\n' >>file 0.013 sec vi -ecwq file 2.544 sec paste file 1<> file 31.943 sec ed -s file <<< w 1m 4.422 sec sed -i -e '$a\' file 3m 20.931 sec
-
Работает в ash, bash, lksh, mksh, ksh93, attsh и zsh, но не в yash.
- Не изменяет метку времени файла, если новая строка не добавляется.
Все другие решения, представленные здесь, изменяют метку времени файла. - Все вышеперечисленные решения валидны по POSIX.
Если вам нужна переносимая на yash (и все выше перечисленные оболочки) решение, это может быть чуть сложнее:
f=file
if [ "$(tail -c1 "$f"; echo x)" != "$(printf '\nx')" ]
then printf '\n' >>"$f"
fi
Самый быстрый способ проверить, является ли последний байт файла новой строкой, это прочитать только последний байт. Это можно сделать с помощью tail -c1 file
. Однако примитивный способ проверки, является ли значение байта новой строкой, в зависимости от обычного удаления новой строки внутри замены команды, не сработает (например) в yash, когда последний символ в файле является значением UTF-8.
Правильный, совместимый с POSIX, способ для всех (разумных) оболочек выяснения, является ли последний байт файла новой строкой, это использовать либо xxd, либо hexdump:
tail -c1 file | xxd -u -p
tail -c1 file | hexdump -v -e '/1 "%02X"'
Затем сравнение вывода выше с 0A
обеспечит надежный тест.
Это полезно, чтобы избежать добавления новой строки в иначе пустой файл.
Файл, который не предоставит последний символ 0A
, конечно:
f=file
a=$(tail -c1 "$f" | hexdump -v -e '/1 "%02X"')
[ -s "$f" -a "$a" != "0A" ] && echo >> "$f"
Коротко и сладко. Это занимает очень мало времени, так как только читает последний байт (перемещается к концу файла). Не имеет значения, если файл большой. Затем добавляет только один байт, если это нужно.
Временные файлы не нужны и не используются. Жесткие ссылки не затрагиваются.
Если тест выполняется дважды, это не добавит еще одну новую строку.
По крайней мере в GNU-версиях, простой grep ''
или awk 1
канонизирует его ввод, добавляя финальную новую строку, если ее изначально не было. Они копируют файл в процессе, что занимает время, если файл большой (но источник не должен быть слишком большим для чтения, разве что?) и обновляют модификацию времени, если вы не сделаете что-то типа
mv file old; grep '' <old >file; touch -r old file
(хотя это может быть нормально для файла, который вы отмечаете измененным из-за его модификации)
и это теряет жесткие ссылки, нестандартные разрешения и ACL и т. д., если вы не будете еще более осторожными.
Если вы просто хотите быстро добавить новую строку при обработке некоторого канала, используйте это:
outputting_program | { cat ; echo ; }
Это также совместимо с POSIX.
Затем, конечно, вы можете перенаправить это в файл.
При условии, что в вводе нет нулей:
paste - <>infile >&0
…достаточно, чтобы всегда только добавлять новую строку в хвостовую часть infile, если ее до этого не было. И ему нужно прочитать входной файл только один раз, чтобы сделать это правильно.
Редакторы vi
/vim
/ex
автоматически добавляют <EOL>
в конец файла, если его изначально не было.
Так что попробуйте одно из этого:
vi -ecwq foo.txt
что эквивалентно:
ex -cwq foo.txt
Тестирование:
$ printf foo > foo.txt && wc foo.txt
0 1 3 foo.txt
$ ex -scwq foo.txt && wc foo.txt
1 1 4 foo.txt
Чтобы исправить несколько файлов, ознакомьтесь с: Как исправить ‘No newline at end of file’ для множества файлов? на SO
Почему это так важно? Чтобы наши файлы были совместимы с POSIX.
Хотя это не дает прямого ответа на вопрос, вот связанный скрипт, который я написал, чтобы обнаруживать файлы, которые не заканчиваются новой строкой. Он очень быстрый.
find . -type f | # sort | # сортировка имен файлов, если хотите
/usr/bin/perl -lne '
open FH, "<", $_ или { print " error: $_"; next };
$pos = sysseek FH, 0, 2; # переход к EOF
если (!defined $pos) { печатать " error: $_"; следующая }
если ($pos == 0) { печатать " empty: $_"; следующая }
$pos = sysseek FH, -1, 1; # переход к последнему символу
если (!defined $pos) { печатать " error: $_"; следующая }
$cnt = sysread FH, $c, 1;
если (!$cnt) { печатать " error: $_"; следующая }
если ($c eq "\n") { печатать " EOL: $_"; следующая }
еще { печатать "no EOL: $_"; следующая }
'
Этот perl скрипт читает список имен файлов из stdin и для каждого файла читает последний байт, чтобы определить, заканчивается ли файл новой строкой или нет. Это очень быстро, так как избегает считывания всего содержимого каждого файла. Он выводит одну строку для каждого считываемого файла, префиксируя их “error:”, если происходит какая-то ошибка, “empty:”, если файл пуст (не заканчивается новой строкой!), “EOL:” (“конец строки”), если файл заканчивается новой строкой, и “no EOL:”, если файл не заканчивается новой строкой.
Примечание: скрипт не обрабатывает имена файлов, содержащие новые строки. Если вы находитесь в системе GNU или BSD, вы можете обрабатывать все возможные имена файлов, добавив -print0 в find, -z в sort и -0 в perl, как это:
find . -type f -print0 | sort -z |
/usr/bin/perl -ln0e '
open FH, "<", $_ или { print " error: $_"; next };
$pos = sysseek FH, 0, 2; # переход к EOF
если (!defined $pos) { печатать " error: $_"; следующая }
если ($pos == 0) { печатать " empty: $_"; следующая }
$pos = sysseek FH, -1, 1; # переход к последнему символу
если (!defined $pos) { печатать " error: $_"; следующая }
$cnt = sysread FH, $c, 1;
если (!$cnt) { печатать " error: $_"; следующая }
если ($c eq "\n") { печатать " EOL: $_"; следующая }
еще { печатать "no EOL: $_"; следующая }
'
Конечно, вам все еще нужно придумать способ кодирования имен файлов с новыми строками на выходе (оставляется как упражнение для читателя).
Вывод может быть отфильтрован, если это нужно, чтобы добавить новую строку в те файлы, в которых ее нет, наиболее просто с помощью
echo >> "$filename"
Отсутствие новой строки может вызвать ошибки в скриптах, так как некоторые версии оболочек и другие утилиты не будут правильно работать, если отсутствует финальная новая строка, при чтении такого файла.
По моему опыту, отсутствие финальной новой строки вызывается использованием различных утилит Windows для редактирования файлов. Я никогда не видел, чтобы vim вызывал отсутствие финальной новой строки при редактировании файла, хотя он сообщает об таких файлах.
Наконец, существуют гораздо более короткие (но медленные) скрипты, которые могут обходить свои именованные входные файлы, чтобы напечатать те, которые не заканчиваются новой строкой, такие как:
/usr/bin/perl -ne 'print "$ARGV\n" if /.\z/' -- FILE1 FILE2 ...
Для применения принятого ответа ко всем файлам в текущей директории (включая поддиректории):
$ find . -type f -exec sed -i -e '$a\' {} \;
Это работает на Linux (Ubuntu). На OS X вам, вероятно, придется использовать -i ''
(не проверено).
Вы можете написать скрипт fix-non-delimited-line
таким образом:
#! /bin/zsh -
zmodload zsh/system || exit
ret=0
for file do
if sysopen -rwu0 -- "$file"; then
if sysseek -w end -1; then
read -r x || print -u0
else
syserror -p "Can't seek in $file before the last byte: "
ret=1
fi
else
ret=1
fi
done
exit $ret
В отличие от некоторых из предложенных здесь решений, он
- должен быть эффективным, так как он не порождает ни одного процесса, читает только один байт для каждого файла и не переписывает файл (просто добавляет новую строку)
- не нарушает симлинки/жесткие ссылки и не влияет на метаданные (также ctime/mtime обновляются только, если новая строка добавляется)
- должен работать нормально, даже если последний байт является NUL или частью многобайтового символа.
- должен работать нормально, независимо от того, какие символы или не символы файлы могут содержать
- должен правильно обрабатывать не читаемые, не записываемые или не доступные для поиска файлы (и сообщать об ошибках соответствующим образом)
- не должен добавлять новую строку в пустые файлы (но сообщает об ошибке об недопустимом поиске в этом случае)
Вы можете использовать это, например, как:
that-script *.txt
или:
git ls-files -z | xargs -0 that-script
POSIX-совместимым способом, вы могли бы сделать что-то функционально *равное с помощью
export LC_ALL=C
рет=0
для файла выполнить
[ -s "$file" ] || продолжаю
{
c=$(tail -c 1 | od -An -vtc)
случае $c в
(*'\n'*) ;;
(*[![:space:]]*) printf '\n' >&0 || ret=$?;;
(*) ret=1;; # tail вероятно, не успешен
esac
} 0<> "$file" || ret=$? # записать неудачу при открытии
выполнено
Чтобы исправить все файлы в git-репозитории, выполните
git ls-files --eol |\
grep -e 'i/lf' |\
grep -v 'attr/-text' |\
sed 's/.*\t//' |\
xargs -d '\n' sed -b -i -e '$a\'
git ls-files --eol
перечисляет все файлы, отслеживаемые git, с их атрибутомeol
grep -e 'i/lf'
фильтрует файлы, добавленные в индекс сLF
grep -v 'attr/-text'
пропускает файлы, которые помечены какbinary
или-text
в.gitattributes
sed 's/.*\t//'
исключает все, кроме путейxargs -d '\n' sed -b -i -e '$a\'
добавляет новую строку в конец файла-b
считать файл бинарным (не трогать окончания строк)-i
редактирует файл на месте-e '$a\'
добавляет новую строку в конец файла, но только если нет новой строки в конце файла и файл не пуст.
perl -0777pe 's/\R?$/\n/' file
-0
без аргументов эквивалентно no record separator
(рассматривает весь файл как одну строку), так что $
равно EOF
, а не EOL
.
\R
эквивалентно CRLF (Windows) или LF (Linux) или CR (MAC).
Другой вариант – использовать dos2unix
или unix2dos
, которые имеют опцию командной строки для этого (в дополнение к нормализации окончаний строк):
-e, --add-eol добавьте перевод строки в конец, если его нет
Добавляя к ответу Патрика Оскити, если вы просто хотите применить его к определенной директории, вы также можете использовать:
find -type f | while read f; do tail -n1 $f | read -r _ || echo >> $f; done
Запустите это внутри директории, в которой вы хотите добавить новые строки.
echo $'' >> <FILE_NAME>
добавит пустую строку в конец файла.
echo $'\n\n' >> <FILE_NAME>
добавит 3 пустые строки в конец файла.
.
Ответ или решение
Добавление новой строки в конец файла часто необходимо для соблюдения стандартов форматирования и проверок контроля версий. Это актуально для программистов, которые регулярно сталкиваются с тревожными сообщениями типа «No newline at end of file» при выполнении diff
или во время коммитов в системах контроля версий, таких как Git.
Теория: Почему это важно
Отсутствие новой строки в конце файла может не только вызывать упомянутые сообщения об ошибках, но и приводить к различным проблемам при автоматизированной обработке файлов. Некоторые утилиты и языки программирования могут неправильно обрабатывать файл, если он не завершен новой строкой. Согласно POSIX, текстовые файлы должны оканчиваться новой строкой, чтобы предотвратить непредвиденные ошибки. Это правило помогает поддерживать консистентность в разных окружениях и инструментах.
Пример: Решения с использованием командной строки
-
Использование
sed
:
Для пользователей Linux, добавление новой строки можно выполнить с помощью командыsed
:sed -i -e '$a\' file
Для MacOS команда слегка изменится:
sed -i '' -e '$a\' file
Здесь
$
обозначает конец файла, аa\
добавляет строку (в данном случае пустую) после последней строки. -
Командлет на Python:
Вы можете использовать Python для проверки наличия новой строки и ее добавления:f=filename; python -c "import sys; sys.exit(open(\"$f\").read().endswith('\n'))" && echo >> $f
-
Простой подход с
echo
:
Если вы хотите добавлять новую строку независимо от ее текущего наличия:echo >> filename
-
Использование
ed
:
Командаed
автоматически добавит новую строку, если ее нет:ed -s file <<< w
Применение: Как автоматически применять к множеству файлов
Если необходимо обработать множество файлов в проекте, например, все текстовые файлы в репозитории Git, можно использовать более сложные скрипты. Один из методов включает использование git
и sed
для рекурсивного добавления новой строки к каждому файлу:
git ls-files -z | while IFS= read -rd '' f; do
if file --brief --mime-encoding "$f" | grep -qv binary; then
tail -c1 < "$f" | read -r _ || echo >> "$f";
fi;
done
Этот скрипт проверяет каждый файл в репозитории Git. Он исключает бинарные файлы и добавляет новую строку, если последнего символа нет.
Заключение
Поддержание корректного окончания файла с новой строкой — это важная часть управления кодовой базой и данных, особенно в большой команде разработчиков или в сложных проектах. Это не только удовлетворяет требованиям POSIX, но и улучшает совместимость между платформами и инструментами. Использование приведенных выше методов поможет автоматически и эффективно решать эту задачу, избегая проблем с контрольными системами и обеспечивая надлежащую интероперабельность.