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

Вопрос или проблема

Используя системы управления версиями, меня раздражает сообщение, когда дифф выводит 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 

  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. Работает в ash, bash, lksh, mksh, ksh93, attsh и zsh, но не в yash.

  3. Не изменяет метку времени файла, если новая строка не добавляется.
    Все другие решения, представленные здесь, изменяют метку времени файла.
  4. Все вышеперечисленные решения валидны по 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, но и улучшает совместимость между платформами и инструментами. Использование приведенных выше методов поможет автоматически и эффективно решать эту задачу, избегая проблем с контрольными системами и обеспечивая надлежащую интероперабельность.

Оцените материал
Добавить комментарий

Капча загружается...