Shell脚本读取缺少的最后一行


74

我对bash shell脚本有一个奇怪的问题,我希望对此有所了解。

我的团队正在研究一种脚本,该脚本在文件中的各行之间进行迭代,并检查每个文件中的内容。我们有一个错误,即通过自动流程将不同的脚本排序在一起时,没有看到最后一行。

名称存储在文件中使用遍历行(代码DATAFILE

cat "$DATAFILE" | while read line 

我们可以从命令行运行脚本,它将看到文件中的每一行,包括最后一行,都很好。但是,当由自动化过程运行时(该过程运行的脚本恰好在所涉及的脚本之前运行了生成DATAFILE的脚本),则永远不会看到最后一行。

我们更新了代码,以使用以下代码遍历各行,并解决了问题:

for line in `cat "$DATAFILE"` 

注意:DATAFILE在文件末尾没有换行符。

我的问题分为两部分:为什么原始代码看不到最后一行,为什么会有所不同?

我只以为我可以提出为什么看不到最后一行的原因是:

  • 上一个写入文件的过程依靠该过程结束以关闭文件描述符。
  • 问题脚本启动和打开文件的速度足够快,尽管先前的进程已“结束”,但它没有“关闭/清理”足够多的系统来自动关闭文件描述符。

话虽这么说,如果您在Shell脚本中有2个命令,那么在脚本运行第二个命令时,第一个命令应完全关闭。

任何对问题的见解,特别是第一个问题,将不胜感激。


顺便说一句,请注意,退出循环时,会破坏循环中cat somefile | while read设置的所有变量while。您可能想要while read ...; done <somefile代替;参见BashFAQ#24
查尔斯·达菲,

Answers:


99

C标准表示文本文件必须以换行符结尾,否则最后一个换行符之后的数据可能无法正确读取。

ISO / IEC 9899:2011§7.21.2流

文本流是由行组成的有序字符序列,每行由零个或多个字符加上一个终止换行符组成。最后一行是否需要终止换行符是实现定义的。可能必须在输入和输出上添加,更改或删除字符,以符合在主机环境中表示文本的不同约定。因此,流中的字符与外部表示中的字符之间不需要一一对应。仅在以下情况下,从文本流中读取的数据必须与早先写入该流中的数据进行比较:不能在换行符之前紧跟空格字符;最后一个字符是换行符。在实现中定义了在读入时是否出现在换行符之前立即写出的空格字符。

我不会在文件末尾出现意外的换行符而引起麻烦bash(或任何Unix Shell),但这似乎确实是问题所在($ 此输出中的提示):

$ echo xxx\\c
xxx$ { echo abc; echo def; echo ghi; echo xxx\\c; } > y
$ cat y
abc
def
ghi
xxx$
$ while read line; do echo $line; done < y
abc
def
ghi
$ bash -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ ksh -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ zsh -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ for line in $(<y); do echo $line; done      # Preferred notation in bash
abc
def
ghi
xxx
$ for line in $(cat y); do echo $line; done   # UUOC Award pending
abc
def
ghi
xxx
$

它也不仅限于bash-Korn shell(ksh),其zsh行为也是如此。我生活,我学习;感谢您提出这个问题。

如上面的代码所示,该cat命令读取整个文件。该for line in `cat $DATAFILE` 技术收集所有输出,并用单个空格替换空白序列(我得出结论,文件中的每一行均不包含空格)。

在Mac OS X 10.7.5上测试。


POSIX说什么?

POSIXread命令规范说:

读取实用程序应从标准输入读取一行。

默认情况下,除非-r指定了该选项,否则<backslash>将充当转义字符。未经转义的<反斜杠>应保留以下字符的字面值,但<换行符>除外。如果<newline>在<backslash>之后,则read实用程序应将此解释为行继续。<backslash>和<newline>必须在将输入拆分为字段之前删除。将输入拆分为多个字段后,应删除所有其他未转义的<反斜杠>字符。

如果标准输入是终端设备,并且调用外壳是交互式的,则除非-r指定了该选项,否则read在读取以<backslash> <newline>结尾的输入行时将提示输入连续行。

终止的<newline> (如果有的话)应从输入中删除,结果应像在shell中一样拆分为字段,以扩展参数(请参见字段拆分);[...]

请注意,“(如果有)”(加引号)!在我看来,如果没有换行符,它仍应读取结果。另一方面,它也说:

标准输入

标准输入应为文本文件。

然后回到关于不以换行符结尾的文件是否为文本文件的争论。

但是,同一页面上的基本原理记录了:

尽管标准输入必须是文本文件,因此将始终以<newline>结尾(除非它是空文件),但是当-r不使用该选项时继续行的处理可能导致输入不以结尾<newline>。如果输入文件的最后一行以<backslash> <newline>结尾,则会发生这种情况。出于这个原因,在描述中的“终止<newline>(如果有)应从输入中删除”中使用“如果有”。对于标准输入为文本文件的要求并不是放松。

该理由必须意味着该文本文件应以换行符结尾。

文本文件的POSIX定义为:

3.395文本文件

包含以零行或更多行组织的字符的文件。这些行不包含NUL字符,并且长度不能超过{LINE_MAX}个字节,包括<newline>字符。尽管POSIX.1-2008不能区分文本文件和二进制文件(请参阅ISO C标准),但是许多实用程序在对文本文件进行操作时只能产生可预测或有意义的输出。具有此类限制的标准实用程序始终在其STDIN或INPUT FILES部分中指定“文本文件”。

这不是直接规定“以<newline>结尾”,而是遵循C标准。


“无终端换行”问题的解决方案

注意戈登·戴维森答案。一个简单的测试表明他的观察是准确的:

$ while read line; do echo $line; done < y; echo $line
abc
def
ghi
xxx
$

因此,他的技术:

while read line || [ -n "$line" ]; do echo $line; done < y

要么:

cat y | while read line || [ -n "$line" ]; do echo $line; done

将适用于末尾没有换行符的文件(至少在我的机器上)。


我仍然惊讶地发现shell删除了输入的最后一段(它不能称为行,因为它没有以换行符结尾),但是POSIX中可能有足够的理由这样做。显然,最好确保您的文本文件确实是以换行符结尾的文本文件。


感谢您的广泛撰写。我认为两个命令的行为之间的区别描述得很好。我仍然对为什么第一个命令在作为生成文件的管道的一部分运行时失败而不是在独立运行时为何失败感到困惑。另外值得注意的是,它的行为似乎与您的read的非换行行为的体验相冲突。我可能需要返回到脚本,并确保我没有误解它的结果。
RHSeeger 2012年

@adrelanos:我使用read它是因为它在30年前效果很好,现在仍然对我有用。之所以要使用现代风格,是read -r因为它read受POSIX化过程的影响。您的电话-如果您使用read -r,那么我不会感到冒犯,只要您可以说明与使用相比它可以保护您免受什么损害read,并且您可以说明您为什么关心这种保护。
乔纳森·莱夫勒

这解决了我的问题。并且此答案应标记为“已接受”。谢谢。
K.Sopheak

解决此限制的一种方法是printf '\n' | cat myfile.txt - | while IFS= read -r VAR; do echo "$VAR"; done
Xlsx

73

根据read命令POSIX规范,如果“检测到文件结束或发生错误”,则它应返回非零状态。由于在读取最后一个“行”时检测到EOF,因此它将设置$line并返回错误状态,并且该错误状态阻止了循环在最后一个“行”上执行。解决方案很简单:如果read命令成功或者如果将任何内容读入,则使循环执行$line

while read line || [ -n "$line" ]; do

1
+1:有趣的观察,戈登。使用示例文件y,我运行了:while read line; do echo $line; done < y; echo $line确实得到了四个不同的值。我不确定这是特别有用还是直观的行为,但是...
Jonathan Leffler 2012年

这解决了我从文本文件读取单词而在文本文件末尾没有换行符的问题。
tauseef_CuriousGuy

29

添加一些其他信息:

  1. 无需cat与while循环一起使用。while ...;do something;done<file足够的。
  2. 不要阅读带有的行for

使用while循环读取行时:

  1. IFS正确设置(否则可能会缩进)。
  2. 您几乎应该始终将-r选项与read结合使用。

满足以上要求,适当的while循环将如下所示:

while IFS= read -r line; do
  ...
done <file

并使其与文件末尾没有换行符一起使用(从此处重新发布我的解决方案):

while IFS= read -r line || [ -n "$line" ]; do
  echo "$line"
done <file

或使用grepwhile循环:

while IFS= read -r line; do
  echo "$line"
done < <(grep "" file)

谢谢。我遇到了同样的问题,这对我
有用

1

解决方法是,在从文本文件读取之前,可以将换行符附加到文件中。

echo "\n" >> $file_path

这将确保读取文件中以前的所有行。


1

使用sed来匹配文件的最后一行,如果不存在该行,它将在行的末尾添加换行符,并对其进行行内替换:

sed -i '' -e '$a\' file

代码来自此stackexchange链接

注意:我添加了空单引号,-i ''因为至少在OS X中,它-i-e用作备份文件的文件扩展名。我很乐意评论原始帖子,但缺少50分。谢谢,也许这会使我对此有所了解。


0

我在命令行中测试了

# create dummy file. last line doesn't end with newline
printf "%i\n%i\nNo-newline-here" >testing

用您的第一种形式进行测试(管道到while循环)

cat testing | while read line; do echo $line; done

这错过了最后一行,这很有意义,因为read仅获取以换行符结尾的输入。


使用第二种形式进行测试(命令替换)

for line in `cat testbed1` ; do echo $line; done

这也得到了最后一行


read 仅当输入以换行符终止时才获取输入,这就是为什么错过最后一行的原因。

另一方面,第二种形式

`cat testing` 

扩展为

line1\nline2\n...lineM 

使用IFS,shell将其分为多个字段,因此您得到

line1 line2 line3 ... lineM 

这就是为什么您仍然得到最后一行。

p / s:我不了解的是您如何开始使用第一份表格...


我将回到脚本,并确保我不会误解某些内容。这些都是我正在帮助的工作的一部分,很可能我们误读了一些匆忙的内容以使其正常工作。
RHSeeger

0

我有一个类似的问题。我正在做一个文件的猫,将其输送到一个排序,然后将结果输送到“同时读取var1 var2 var3”。例如: cat $ FILE | sort -k3 |,同时读取Count IP Name do do “ do”下的工作是一个if语句,用于标识$ Name字段中的更改数据,并基于更改或不更改来进行$ Count或打印总和报告的汇总行。我还遇到了无法将最后一行打印到报告的问题。我采用了简单的权宜之计,将cat / sort重定向到一个新文件,在该新文件中添加换行符,然后在新文件上运行我的“ while read Count IP Name”,并获得了成功的结果。即: cat $ FILE | sort -k3> NEWFILE echo“ \ n” >> NEWFILE cat NEWFILE |同时读取计数IP名称 有时候简单,优雅是最好的选择。

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.