如何在文件末尾添加换行符?


190

使用版本控制系统,当差异说出来时我感到很烦恼No newline at end of file

所以我想知道:如何在文件末尾添加换行符以消除这些消息?



1
下面的不错的解决方案可以递归地清理所有文件。@Patrick Oscity的回答
Qwerty


展望未来,文本编辑器通常会提供选项来确保您和您的协作者可以使用一条尾随的换行符来保持整洁。
尼克T

Answers:


44

为了递归清理项目,我使用以下单件纸:

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
Per Lundberg

@StéphaneChazelas好的建议,将尝试将其纳入我的答案。
Patrick Oscity

@PerLundberg,您还可以传递一个模式,git ls-files该模式仍将使您免于编辑版本控制中未跟踪的文件。
Patrick Oscity

@StéphaneChazelas添加IFS= 以取消设置分隔符可以很好地保留周围的空白。仅当您的文件或目录的名称中带有换行符时,以null结尾的条目才有意义,这似乎有些牵强,但是我认为这是处理一般情况的更正确方法。只是一个小警告:POSIX sh中不提供-dto选项read
Patrick Oscity

是的,因此是我的zsh / bash的。另请参阅我使用tail -n1 < "$f"来避免以-tail -n1 -- "$f"不适用于-)开头的文件名出现问题。您可能需要澄清答案,现在特定于zsh / bash。
斯特凡Chazelas

202

在这里,您可以

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:从man sed$ Match the last line.但也许它只是偶然的作品。您的解决方案也可以。
l0b0 2012年

1
您的解决方案也更加优雅,我已经测试并提交了它,但是它如何工作?如果$与最后一行匹配,为什么不将另一个换行符添加到已经包含换行符的字符串中呢?
l0b0 2012年

27
的含义有两种$。在正则表达式内,例如在形式中/<regex>/,它具有通常的“匹配行尾”的含义。否则,sed用作地址,它具有特殊的“文件的最后一行”含义。该代码之所以有效,是因为sed默认情况下会在输出中添加换行符(如果尚不存在的话)。代码“ $ a \”仅表示“匹配文件的最后一行,不添加任何内容。” 但隐含地,如果sed $不存在,则sed会将换行符添加到它处理的每一行(例如此行)。
jwd 2012年

1
关于联机帮助页:您所引用的引用位于“地址”部分下。将其放入/regex/其中具有不同的含义。我认为FreeBSD的联机帮助页内容更丰富:freebsd.org/cgi/man.cgi?
query=sed

2
如果文件已经以换行符结尾,则不会对其进行更改,但是会重写并更新其时间戳。这可能或可能不重要。
基思·汤普森

39

看一看:

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

所以echo "" >> noeol-file应该做到这一点。(或者您是要请求识别修复这些文件?)

编辑删除""echo "" >> foo(见@ yuyichao的评论) EDIT2增加""再次(看到@Keith汤普森的评论)


4
""不是必需的(至少对于bash而言),并且tail -1 | wc -l可以用来查找文件而无需在末尾添加新行
yuyichao 2012年

5
@yuyichao:""bash并不是必须的,但是我已经看到echo没有参数调用时实现不打印任何内容的实现(尽管我现在找不到这些实现)。 echo "" >> noeol-file可能会更健壮。 printf "\n" >> noeol-file更是如此。
基思·汤普森

2
@KeithThompson,cshecho是一个当没有通过任何参数已知输出什么。不过,如果我们要支持非类似Bourne外壳,我们应该让echo ''不是echo ""echo ""将输出中""<newline>rces为实例。
斯特凡Chazelas

1
@StéphaneChazelas:而且tcshcsh与之不同的是,不带任何参数调用时,它会打印换行符-与的设置无关$echo_style
基思·汤普森

16

使用的另一种解决方案ed。此解决方案仅在\n缺少最后一行时才起作用:

ed -s file <<< w

它实际上是通过脚本打开文件进行编辑的工作,该脚本是单个w命令,它将文件写回到磁盘。它基于ed(1)手册页中的以下句子:

局限性
       (...)

       如果文本(非二进制)文件未以换行符终止,
       然后ed在读/写时附加一个。在二进制情况下
       文件,ed不会在读/写时添加换行符。

1
这不会为我添加换行符。
Olhovsky

4
为我工作;它甚至会打印“附加了换行符”(在Arch Linux上为ed-1.10-1)。
Stefan Majewsky 2015年

12

一种简单的,可移植的,与POSIX兼容的方式,可以在文本中添加缺少的最终换行符,即文本文件:

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

这种方法不需要读取整个文件;它可以简单地寻求EOF并从那里开始工作。

这种方法也不需要在背后创建临时文件(例如sed -i),因此硬链接不会受到影响。

仅当命令替换的结果为非空字符串时,echo才会在文件中添加换行符。请注意,只有在文件不为空并且最后一个字节不是换行符时,才会发生这种情况。

如果文件的最后一个字节是换行符,tail将其返回,然后命令替换将其删除;结果是一个空字符串。-n测试失败,并且echo无法运行。

如果文件为空,则命令替换的结果也是空字符串,并且再次运行echo。这是理想的,因为空文件不是无效的文本文件,也不等同于带有空行的非空文本文件。


1
请注意,yash如果文件中的最后一个字符是多字节字符(例如,在UTF-8语言环境中),或者语言环境为C并且文件中的最后一个字节设置了第8位,则该功能将不起作用。对于其他外壳程序(zsh除外),如果文件以NUL字节结尾,则不会添加换行符(但是再次声明,即使添加了换行符,输入也将是非文本的)。
斯特凡Chazelas

1
@StéphaneChazelas 已添加yash解决方案
sorontar

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中进行循环。 然后,您只需支付一次启动费用。对我来说,启动成本的一半是整个snipit时间的一半以上,我认为这是相当大的开销。(再次,如果只做少量文件则无关紧要)
Kevin Cox

2
echo ""似乎比echo -n '\n'。或者您可以使用printf '\n'
Keith Thompson

2
这对我来说很好
Daniel Gomez Rico

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. 适用于ash,bash,lksh,mksh,ksh93,attsh和zsh,但不适用于yash。

  3. 如果不需要添加换行符,则不更改文件时间戳。
    此处介绍的所有其他解决方案都会更改文件的时间戳。
  4. 以上所有解决方案都是有效的POSIX。

如果您需要一个可移植的解决方案(以及上面列出的所有其他外壳),它可能会变得更加复杂:

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

7

测试文件的最后一个字节是否为换行符的最快方法是仅读取该最后一个字节。那可以用完成tail -c1 file。但是,测试字节值是否为新行的简单方法取决于外壳程序,通常在命令扩展中删除尾随新行时(例如),如果文件中的最后一个字符为UTF-,则在yash中失败。 8值。

查找文件的最后字节是否为新行的,正确的,符合POSIX的,所有(合理的)shell方法是使用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我确实相信它会添加有用的信息。
sorontar

2
请注意,POSIX实用程序xxd也不hexdump是。在POSIX工具箱中,od -An -tx1需要获取一个字节的十六进制值。
斯特凡Chazelas

@StéphaneChazelas请张贴它作为答案;我来这里寻求此评论的次数太多了:)
凯尔文

@kelvin,我已经更新了我的答案
斯特凡Chazelas

请注意,POSIX不能保证LF的值为0x0a。仍然有POSIX系统不是(基于EBCDIC的),尽管这些天来极为罕见。
StéphaneChazelas

4

您最好对上次编辑该文件的用户的编辑器进行更正。如果您是最后一个编辑文件的人-您使用的是什么编辑器,我猜是textmate ..?


2
Vim是有关的编辑器。但总的来说,您是对的,我不仅应该修复
症状

6
对于vim,您必须竭尽所能,执行保存二进制文件的操作,以使vim 不在文件末尾添加新行-只是不要执行该操作。或者,只需更正现有文件,即可在vim中打开它们并保存文件,vim将为您“修复”缺少的换行符(可以轻松地为多个文件编写脚本)
AD7six 2012年

3
emacs不要在文件末尾添加换行符。
enzotib 2012年

2
感谢@ AD7six的评论,当我提交东西时,我不断从diff获得幻象报告,内容涉及原始文件末尾没有换行符。无论我如何用vim编辑文件,我都无法在其中不添加换行符。因此,这只是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

...只要总是在一个infile的末尾追加一个换行符就足够了,如果它还没有一个。它只需要读一次输入文件就可以正确处理它。


那样就行不通了,因为stdin和stdout共享相同的打开文件描述(因此光标位于文件中)。您需要paste infile 1<> infile替代。
斯特凡Chazelas

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:”(“行”),如果文件以换行符结尾,则为“ 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"

缺少最后一个换行符可能会导致脚本中的错误,因为在读取此类文件时,某些版本的Shell和其他实用程序将无法正确处理缺少的最后一个换行符。

以我的经验,缺少最后一个换行符是由使用各种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

要更正多个文件,请检查:对于许多文件,如何解决“文件末尾没有换行符”的问题?在SO

为什么这如此重要?为了使我们的文件与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 2015年

希望我在运行它之前已经读过此评论/想法。那好吧。
kstev

0

至少在GNU版本中,简单地grep ''awk 1规范化其输入,并添加最后的换行符(如果尚不存在的话)。他们确实会在此过程中复制文件,如果文件很大,则会花费一些时间(但是源代码应该不会太大而无法读取吗?)并更新modtime,除非您执行类似的操作

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

(尽管在您签入的文件上,因为修改了文件,这可能没问题),除非您更加小心,否则它将丢失硬链接,非默认权限和ACL等。


或者只是grep '' file 1<> file,尽管那样仍然可以完全读写文件。
斯特凡Chazelas

-1

这在AIX ksh中有效:

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

就我而言,如果文件缺少换行符,该wc命令将返回值,2然后我们编写一个换行符。


反馈将以增票或减票的形式出现,或者将在评论中要求您概述更多答案/问题,而无须在答案正文中提出。保持重点,欢迎加入stackexchange!
k0pernikus 2015年

-1

添加到Patrick Oscity的答案中,如果您只想将其应用于特定目录,则还可以使用:

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有一个有趣的格式,我为您修复了它:-)
peterh

-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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.