Как добавить новую строку в конец файла?


191

При использовании систем контроля версий меня раздражает шум, когда говорит diff No newline at end of file.

Поэтому мне было интересно: как добавить новую строку в конце файла, чтобы избавиться от этих сообщений?



1
Хорошее решение внизу, которое рекурсивно очищает все файлы. Ответ @Patrick Oscity
Qwerty


В дальнейшем у текстовых редакторов часто есть варианты, чтобы обеспечить наличие новой строки, которую вы и ваши сотрудники могли бы использовать для поддержания чистоты.
Ник Т

Ответы:


45

Для рекурсивной очистки проекта я использую этот oneliner:

git ls-files -z | while IFS= read -rd '' f; do tail -c1 < "$f" | read -r _ || echo >> "$f"; done

Объяснение:

  • git ls-files -zперечисляет файлы в хранилище. Он принимает необязательный шаблон в качестве дополнительного параметра, который может быть полезен в некоторых случаях, если вы хотите ограничить операцию определенными файлами / каталогами. В качестве альтернативы вы можете использовать find -print0 ...или аналогичные программы для NULвывода списка затронутых файлов - просто убедитесь, что он генерирует неограниченное количество записей.

  • while IFS= read -rd '' f; do ... done перебирает записи, безопасно обрабатывая имена файлов, которые включают пробелы и / или переводы строк.

  • tail -c1 < "$f" читает последний символ из файла.

  • read -r _ выходы с ненулевым статусом выхода, если завершающий символ новой строки отсутствует.

  • || echo >> "$f" добавляет новую строку в файл, если состояние выхода предыдущей команды было ненулевым.


Вы также можете сделать это следующим образом, если вы хотите просто санировать подмножество ваших файлов:find -name \*.java | while read f; do tail -n1 $f | read -r _ || echo >> $f; done
По Лундбергу

@ StéphaneChazelas хорошие предложения, постараюсь включить это в мой ответ.
Патрик Осцити

@PerLundberg, вы также можете передать шаблон, git ls-filesкоторый все равно спасет вас от редактирования файлов, которые не отслеживаются в системе контроля версий.
Патрик Осцити

@ StéphaneChazelas добавление IFS= разделителя для сброса полезно для сохранения окружающих пробелов. Записи с нулевым символом завершения актуальны только в том случае, если у вас есть файлы или каталоги с новой строкой в ​​названии, которая кажется несколько надуманной, но я согласен, что это более правильный способ обработки общего случая. Так же, как небольшое предупреждение: -dопция readне доступна в POSIX sh.
Патрик Осцити

Да, отсюда и мой zsh / bash . Смотрите также мое использование, tail -n1 < "$f"чтобы избежать проблем с именами файлов, которые начинаются с -( tail -n1 -- "$f"не работает для вызываемого файла -). Вы можете уточнить, что ответ теперь зависит от zsh / bash.
Стефан Шазелас

203

Вот, пожалуйста .

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

1
@jwd: From man sed: $ Match the last line.Но, возможно, это работает только случайно. Ваше решение также работает.
10

1
Ваше решение также более элегантно, и я проверил и подтвердил его, но как оно может работать? Если $соответствует последней строке, почему он не добавляет еще одну новую строку в строку, которая уже содержит новую строку ?
10

27
Есть два разных значения $. Внутри регулярного выражения, например, с формой /<regex>/, оно имеет обычное значение «совпадение конца строки». В противном случае, используемый в качестве адреса, sed дает ему специальное значение «последняя строка в файле». Код работает, потому что sed по умолчанию добавляет к выходу символ новой строки, если его там еще нет. Код «$ a \» просто говорит «соответствует последней строке файла и ничего не добавляет к нему». Но неявно sed добавляет новую строку к каждой строке, которую он обрабатывает (например, к этой $строке), если ее там еще нет.
JWD

1
Относительно man-страницы: цитата, на которую вы ссылаетесь, находится в разделе «Адреса». Помещение внутрь /regex/дает другое значение. Я думаю, что страницы
jwd

2
Если файл уже заканчивается новой строкой, это не меняет его, но переписывает и обновляет его метку времени. Это может иметь или не иметь значения.
Кит Томпсон,

39

Посмотри:

$ echo -n foo > foo 
$ cat foo
foo$
$ echo "" >> foo
$ cat foo
foo

так echo "" >> noeol-fileнадо делать свое дело. (Или вы хотели попросить идентифицировать эти файлы и исправить их?)

edit удалил ""из echo "" >> foo(см. комментарий @yuyichao ) edit2"" снова добавил ( но см. комментарий @Keith Thompson)


4
""не нужно (по крайней мере , для Баш) и tail -1 | wc -lможет быть использован , чтобы выяснить файл без новой строки в конце
yuyichao

5
@yuyichao: ""для bash это не обязательно, но я видел echoреализации, которые ничего не печатают при вызове без аргументов (хотя ни одна из тех, что я могу найти сейчас, не делает этого). echo "" >> noeol-fileвероятно, немного более устойчивый. printf "\n" >> noeol-fileтем более.
Кит Томпсон

2
@KeithThompson, csh«s echoэто один известный выводить ничего , когда не передается никаких аргументов. Но тогда, если мы собираемся поддерживать не-подобные Борну оболочки, мы должны сделать это echo ''вместо того, echo ""как echo ""было бы ""<newline>с rcили esнапример.
Стефан Шазелас

1
@ StéphaneChazelas: И tcsh, в отличие от этого csh, печатает новую строку, когда вызывается без аргументов - независимо от настройки $echo_style.
Кит Томпсон,

16

Другое решение с использованием ed. Это решение влияет только на последнюю строку и только если \nотсутствует:

ed -s file <<< w

По сути, это работает, открывая файл для редактирования через скрипт, скрипт - это единственная wкоманда, которая записывает файл обратно на диск. Это основано на этом предложении, найденном на ed(1)странице руководства:

ОГРАНИЧЕНИЯ
       (...)

       Если текстовый (недвоичный) файл не заканчивается символом новой строки,
       затем Эд добавляет один на чтение / запись. В случае двоичного
       файл, ed не добавляет новую строку при чтении / записи.

1
Это не добавляет новую строку для меня.
Ольховский

4
Работает для меня; он даже печатает «Новая строка добавлена» (ed-1.10-1 в Arch Linux).
Стефан Маевский

12

Простой, переносимый, POSIX-совместимый способ добавить отсутствующий, последний символ новой строки в текстовый файл:

[ -n "$(tail -c1 file)" ] && echo >> file

Этот подход не должен читать весь файл; он может просто стремиться к EOF и работать оттуда.

Этот подход также не требует создания временных файлов за вашей спиной (например, sed -i), поэтому жесткие ссылки не затрагиваются.

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

Если последний байт файла является новой строкой, tail возвращает его, а подстановка команд удаляет его; Результатом является пустая строка. Тест -n не проходит и эхо не запускается.

Если файл пуст, результатом подстановки команды также является пустая строка, и опять эхо не запускается. Это желательно, поскольку пустой файл не является недопустимым текстовым файлом и не является эквивалентом непустого текстового файла с пустой строкой.


1
Обратите внимание, что он не работает, yashесли последний символ в файле является многобайтовым символом (например, в языковых стандартах UTF-8) или если языковым стандартом является C и для последнего байта в файле установлен 8-й бит. С другими оболочками (кроме zsh) он не добавил бы новую строку, если файл заканчивался байтом NUL (но опять же, это означало бы, что ввод будет нетекстовым даже после добавления новой строки).
Стефан Шазелас


1
Можно ли запустить это для каждого файла в папке и подпапках?
Qwerty

12

Добавить новую строку независимо от:

echo >> filename

Вот способ проверить, существует ли новая строка в конце перед добавлением, используя Python:

f=filename; python -c "import sys; sys.exit(open(\"$f\").read().endswith('\n'))" && echo >> $f

1
Я бы не использовал версию Python в любом цикле из-за медленного времени запуска Python. Конечно, вы можете сделать цикл в Python, если хотите.
Кевин Кокс

2
Время запуска Python здесь составляет 0,03 секунды. Вы действительно считаете это проблематичным?
Александр

3
Время запуска имеет значение, если вы вызываете python в цикле, поэтому я сказал рассмотреть возможность выполнения цикла в python. Тогда вы только понесете стоимость запуска один раз. Для меня половина стоимости стартапа составляет более половины времени всего сниппита, я бы посчитал это существенными накладными расходами. (Опять же, не имеет значения, если делать только небольшое количество файлов)
Кевин Кокс

2
echo ""кажется более надежным, чем echo -n '\n'. Или вы могли бы использоватьprintf '\n'
Кит Томпсон

2
Это работало хорошо для меня
Даниэль Гомес Рико

8

Самое быстрое решение:

[ -n "$(tail -c1 file)" ] && printf '\n' >>file 

  1. Это действительно быстро.
    Для файла среднего размера 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
  2. Работает в золах, баш, лкш, мкш, кш93, атш и зш, но не в яше.

  3. Не изменяет временную метку файла, если нет необходимости добавлять новую строку.
    Все остальные решения, представленные здесь, изменяют временную метку файла.
  4. Все приведенные выше решения являются действительными POSIX.

Если вам нужно решение, переносимое на yash (и все другие оболочки, перечисленные выше), оно может стать немного сложнее:

f=file
if       [ "$(tail -c1 "$f"; echo x)" != "$(printf '\nx')" ]
then     printf '\n' >>"$f"
fi

7

Самый быстрый способ проверить, является ли последний байт файла новой строкой, - прочитать только этот последний байт. Это может быть сделано с 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"

Коротко и сладко. Это занимает очень мало времени, так как он просто читает последний байт (ищите EOF). Неважно, если файл большой. Затем добавьте только один байт, если необходимо.

Временные файлы не нужны и не используются. Жесткие ссылки не затрагиваются.

Если этот тест будет выполнен дважды, он не добавит еще один символ новой строки.


1
@crw Я верю, что это добавляет полезную информацию.
соронтарь

2
Обратите внимание, что ни POSIX xxdни hexdumpутилиты. В инструментарии POSIX есть od -An -tx1шестнадцатеричное значение байта.
Стефан Шазелас

@ StéphaneChazelas Пожалуйста, опубликуйте это как ответ; Я пришел сюда в поисках этого комментария слишком много раз :)
Кельвин

@kelvin, я обновил свой ответ
Стефан Шазелас

Обратите внимание, что POSIX не гарантирует, что значение LF будет 0x0a. Есть еще системы POSIX, где это не так (основанные на EBCDIC), хотя в наши дни они чрезвычайно редки.
Стефан Шазелас

4

Вам лучше исправить редактор пользователя, который последний раз редактировал файл. Если вы последний, кто редактировал файл - какой редактор вы используете, я полагаю, textmate ..?


2
Vim - рассматриваемый редактор. Но в целом вы правы, я должен не только исправить симптомы;)
k0pernikus

6
для vim вы должны выйти из своего пути и выполнить танец с бинарным файлом при сохранении, чтобы vim не добавлял новую строку в конце файла - просто не выполняйте этот танец. ИЛИ, чтобы просто исправить существующие файлы, откройте их в vim и сохраните файл, и vim 'исправит' недостающий символ новой строки для вас (может быть легко написан для нескольких файлов)
AD7six

3
Мой emacsне добавить новую строку в конце файла.
энзотиб

2
Спасибо за комментарий @ AD7six, я продолжаю получать фантомные отчеты из diffs, когда я фиксирую что-то о том, что в исходном файле нет новой строки в конце. Независимо от того, как я редактирую файл с помощью vim, я не могу заставить его не помещать туда новую строку. Так что это просто Вим делает это.
Стивен Лу

1
@enzotib: у меня есть (setq require-final-newline 'ask)в моем.emacs
Keith Thompson

3

Если вы просто хотите быстро добавить новую строку при обработке какого-либо конвейера, используйте это:

outputting_program | { cat ; echo ; }

это также POSIX-совместимый.

Тогда, конечно, вы можете перенаправить его в файл.


2
Тот факт, что я могу использовать это в конвейере, полезен. Это позволяет мне подсчитать количество строк в файле CSV, исключая заголовок. И это помогает получить точное количество строк в файлах Windows, которые не заканчиваются символом новой строки или возврата каретки. cat file.csv | tr "\r" "\n" | { cat; echo; } | sed "/^[[:space:]]*$/d" | tail -n +2 | wc -l
Кайл Толле

3

При условии, что на входе нет нулей:

paste - <>infile >&0

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


Это не сработает, так как stdin и stdout имеют одинаковое описание открытого файла (поэтому курсор находится внутри файла). Тебе нужно paste infile 1<> infileвместо этого.
Стефан

2

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

find . -type f | # sort |        # sort file names if you like
/usr/bin/perl -lne '
   open FH, "<", $_ or do { print " error: $_"; next };
   $pos = sysseek FH, 0, 2;                     # seek to EOF
   if (!defined $pos)     { print " error: $_"; next }
   if ($pos == 0)         { print " empty: $_"; next }
   $pos = sysseek FH, -1, 1;                    # seek to last char
   if (!defined $pos)     { print " error: $_"; next }
   $cnt = sysread FH, $c, 1;
   if (!$cnt)             { print " error: $_"; next }
   if ($c eq "\n")        { print "   EOL: $_"; next }
   else                   { print "no EOL: $_"; next }
'

Сценарий perl читает список (необязательно отсортированных) имен файлов из stdin и для каждого файла читает последний байт, чтобы определить, заканчивается ли файл новой строкой или нет. Это очень быстро, потому что он избегает чтения всего содержимого каждого файла. Он выводит одну строку для каждого файла, который читает, с префиксом «error:», если возникает какая-либо ошибка, «empty:», если файл пустой (не заканчивается символом новой строки!), «EOL:» («конец line "), если файл заканчивается символом новой строки и" no EOL: ", если файл не заканчивается символом новой строки.

Примечание: скрипт не обрабатывает имена файлов, которые содержат переводы строк. Если вы работаете в системе GNU или BSD, вы можете обработать все возможные имена файлов, добавив -print0 для поиска, -z для сортировки и -0 для perl, например так:

find . -type f -print0 | sort -z |
/usr/bin/perl -ln0e '
   open FH, "<", $_ or do { print " error: $_"; next };
   $pos = sysseek FH, 0, 2;                     # seek to EOF
   if (!defined $pos)     { print " error: $_"; next }
   if ($pos == 0)         { print " empty: $_"; next }
   $pos = sysseek FH, -1, 1;                    # seek to last char
   if (!defined $pos)     { print " error: $_"; next }
   $cnt = sysread FH, $c, 1;
   if (!$cnt)             { print " error: $_"; next }
   if ($c eq "\n")        { print "   EOL: $_"; next }
   else                   { print "no EOL: $_"; next }
'

Конечно, вам все равно придется придумать способ кодирования имен файлов с помощью новых строк в выходных данных (оставленных в качестве упражнения для читателя).

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

 echo >> "$filename"

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

По моему опыту, отсутствие последней новой строки вызвано использованием различных утилит Windows для редактирования файлов. Я никогда не видел, чтобы vim вызывал пропущенную последнюю строку при редактировании файла, хотя он будет сообщать о таких файлах.

Наконец, есть намного более короткие (но более медленные) сценарии, которые могут циклически проходить через входные имена своих файлов для печати тех файлов, которые не заканчиваются символом новой строки, например:

/usr/bin/perl -ne 'print "$ARGV\n" if /.\z/' -- FILE1 FILE2 ...

1

В vi/ vim/ exредакторы автоматически добавлять <EOL>в EOF , если файл уже не имеет его.

Так что попробуйте либо:

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

Чтобы исправить несколько файлов, проверьте: Как исправить «Нет новой строки в конце файла» для большого количества файлов? в СО

Почему это так важно? Чтобы наши файлы были совместимы с POSIX .


0

Чтобы применить принятый ответ ко всем файлам в текущем каталоге (плюс подкаталоги):

$ find . -type f -exec sed -i -e '$a\' {} \;

Это работает в Linux (Ubuntu). На OS X вы, вероятно, должны использовать -i ''(не проверено).


4
Обратите внимание, что find .перечислены все файлы, включая файлы в .git. Исключить:find . -type f -not -path './.git/*' -exec sed -i -e '$a\' {} \;
Friederbluemle

Жаль, что я не прочитал бы этот комментарий / думал об этом прежде, чем я управлял этим. Ну что ж.
Кстев

0

По крайней мере, в версиях GNU просто grep ''илиawk 1 канонизирует свой ввод, добавляя заключительный символ новой строки, если его еще нет. Они копируют файл в процессе, который занимает много времени, если он большой (но источник не должен быть слишком большим для чтения в любом случае?), И обновляет время мод, если вы не сделаете что-то вроде

 mv file old; grep '' <old >file; touch -r old file

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


Или просто grep '' file 1<> file, хотя это все равно будет читать и писать файл полностью.
Стефан

-1

Это работает в AIX ksh:

lastchar=`tail -c 1 *filename*`
if [ `echo "$lastchar" | wc -c` -gt "1" ]
then
    echo "/n" >> *filename*
fi

В моем случае, если в файле отсутствует символ новой строки, wcкоманда возвращает значение, 2и мы записываем символ новой строки.


Отзывы будут приходить в форме положительных или отрицательных голосов, или вас попросят в комментариях более подробно изложить ваши ответы / вопросы, нет смысла спрашивать их в теле ответа. Держите это в точку, добро пожаловать в stackexchange!
k0pernikus

-1

В дополнение к ответу Патрика Осцити , если вы просто хотите применить его к определенному каталогу, вы также можете использовать:

find -type f | while read f; do tail -n1 $f | read -r _ || echo >> $f; done

Запустите это внутри каталога, в который вы хотите добавить новые строки.


-1

echo $'' >> <FILE_NAME> добавит пустую строку в конец файла.

echo $'\n\n' >> <FILE_NAME> добавит 3 пустых строки в конец файла.


У StackExchange есть забавное форматирование, я исправил это для вас :-)
user259412

-1

Если ваш файл оканчивается на конец строки Windows,\r\n и вы находитесь в Linux, вы можете использовать эту sedкоманду. Это только добавляет \r\nк последней строке, если это еще не там:

sed -i -e '$s/\([^\r]\)$/\1\r\n/'

Объяснение:

-i    replace in place
-e    script to run
$     matches last line of a file
s     substitute
\([^\r]\)$    search the last character in the line which is not a \r
\1\r\n    replace it with itself and add \r\n

Если последняя строка уже содержит a, \r\nто регулярное выражение поиска не будет совпадать, поэтому ничего не произойдет.


-1

Вы могли бы написать 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

POSIXly, вы можете сделать что-то функционально эквивалентное с

export LC_ALL=C
ret=0
for file do
  [ -s "$file" ] || continue
  {
    c=$(tail -c 1 | od -An -vtc)
    case $c in
      (*'\n'*) ;;
      (*[![:space:]]*) printf '\n' >&0 || ret=$?;;
      (*) ret=1;; # tail likely failed
    esac
  } 0<> "$file" || ret=$? # record failure to open
done
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.