Shell脚本读取缺less最后一行

我有一个奇怪的问题与一个bash shell脚本,我希望得到一些见解。

我的团队正在研究一个脚本,它遍历文件中的行并检查每个行中的内容。 我们有一个错误,当通过将不同脚本排列在一起的自动化过程运行时,最后一行没有被看到。

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

 cat "$DATAFILE" | while read line 

我们可以从命令行运行脚本,它会看到文件中的每一行,包括最后一行,都很好。 但是,当由自动化进程运行(运行脚本以在脚本之前生成DATAFILE)时,最后一行是不可见的。

我们更新了代码,使用以下代码遍历行,并清除了问题:

 for line in `cat "$DATAFILE"` 

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

我的问题是两部分…为什么最后一行不能被原始代码看到,为什么这会改变有所作为?

我只想到我可以想出为什么最后一行不会被看到是:

  • 上一个写入文件的进程依赖于结束closures文件描述符的过程。
  • 问题脚本启动并打开文件的速度足够快,以至于在前一个进程已经“结束”的时候,它没有“closures/清理”足够的系统来自动closures文件描述符。

话虽如此,如果你在一个shell脚本中有两条命令,第一条命令在脚本运行第二条命令时应该完全closures。

对于这些问题的深入了解,特别是第一个问题,我们将非常感激。

C标准说文本文件必须以换行符结束,否则最后换行符后的数据可能无法正确读取。

ISO / IEC 9899:2011§7.21.2stream

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

我不会意外地在文件末尾丢失一个换行符,导致在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`for line in `cat $DATAFILE`收集所有输出,并用一个空白replace空白的任意序列(我断定文件中的每一行都不包含空格)。

在Mac OS X 10.7.5上testing


POSIX说什么?

POSIX read命令规范说:

读取实用程序应从标准input中读取一行。

默认情况下,除非指定了-r选项,否则<backslash>应作为转义字符。 一个未转义的<backslash>应该保留以下字符的字面值,除了<newline>之外。 如果<换行符>跟在<反斜杠>之后,则读取实用程序应将其解释为行延续。 在将input拆分为字段之前,应将<backslash>和<newline>删除。 所有其他未转义的<backslash>字符在将input拆分为字段之后将被删除。

如果标准input是一个terminal设备,并且调用shell是交互式的,那么当读取一个以<backslash> <newline>结尾的input行时,read将提示input一个延续行,除非指定了-r选项。

终止的<newline> (如果有的话)将从input中删除,并且结果将被拆分成与参数展开结果的shell中相同的字段(参见Field Splitting)。 […]

注意'(如果有的话)'(强调加在报价单上)! 在我看来,如果没有换行符,它仍然应该读取结果。 另一方面,它也说:

STDIN

标准input应该是一个文本文件。

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

但是,在同一页面文件的理由:

虽然标准input必须是一个文本文件,因此总是以<换行符>结尾(除非它是一个空文件),当不使用-r选项时继续行的处理可能导致input不是以<换行符>结尾。 如果input文件的最后一行以<backslash> <newline>结尾,则会发生这种情况。 因为这个原因,在描述中的“终止<新行>(如果有的话)将被从input中删除”中使用“如果有的话”。 这不是标准input作为文本文件的要求的放松。

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

一个文本文件的POSIX定义是:

3.395文本文件

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

这并没有直接规定“以<newline>结束”,而是遵循C标准。


解决“无terminal换行”问题

注意Gordon Davisson的答案 。 一个简单的testing表明他的观察是准确的:

 $ 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 

将在文件末尾(至less在我的机器上)没有换行符的情况下工作。


我仍然惊讶地发现,这些shell会丢弃最后一个段(它不能被称为一行,因为它没有以换行符结尾),但是在POSIX中可能有足够的理由去做。 显然,最好确保你的文本文件真的是以换行符结尾的文本文件。

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

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

添加一些额外的信息:

  1. 没有必要在while循环中使用catwhile ...;do something;done<file就足够了。
  2. 不要用for读行。

使用while循环读取行时:

  1. 正确设置IFS (否则可能会丢失缩进)。
  2. 你应该几乎总是使用阅读-r选项。

满足上面的要求一个适当的while循环将看起来像这样:

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

并在最后使用不带换行符的文件(从这里重新发布我的解决scheme):

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

或者用while循环使用grep

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

使用sed来匹配文件的最后一行,如果不存在的话,它会附加一个换行符,然后让它执行文件的内联replace:

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

代码来自这个stackexchange 链接

注意:我已经将空单引号添加到-i ''因为至less在OS X中, -i是使用-e作为备份文件的文件扩展名。 我本来很乐意评论原文,但缺less50分。 也许这会在这个线程中获得一些,谢谢。

我怀疑在你的文件的最后一行没有换行符可能会导致这个问题。 对于testing,你可以稍微修改你的脚本,并像这样读取DATAFILE:

 while read line do echo $line # do processing here done < "$DATAFILE" 

看看这是否有所作为。

我在命令行中testing了这个

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

testing你的第一种forms(pipe道到while循环)

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

这错过了最后一行,这是有道理的,因为read得到以换行符结尾的input。


testing你的第二种forms(命令replace)

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

这也得到最后一行


如果只有换行符终止, readinput,这就是为什么你错过了最后一行。

另一方面,在第二种forms

 `cat testing` 

扩展到的forms

 line1\nline2\n...lineM 

这是由shell分隔成多个字段使用IFS,所以你得到

 line1 line2 line3 ... lineM 

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

p / s:我不明白的是你如何得到第一个表格的工作…

作为一种解决方法,在从文本文件读取之前,可以将新行附加到文件。

 echo "\n" >> $file_path 

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

我有一个类似的问题。 我正在做一个文件的猫,pipe道到一个sorting,然后pipe道结果'一边读var1 var2 var3'。 即: cat $ FILE | sort -k3 | while read读取IP名称do “do”下的工作是一个if语句,用于标识$ Name字段中的数据更改,并根据更改或无变化执行$ Count的总和或打印该报告的总结线。 我也遇到了无法将最后一行打印到报告的问题。 我用简单的方法将cat / sortredirect到一个新文件,并在新文件中回显一个换行符,然后在新文件上运行我的“计数IP名称”,结果成功。 即: cat $ FILE | sort -k3> NEWFILE echo“\ n”>> NEWFILE cat NEWFILE | while读取计数IP名称有时候简单,不雅是最好的方法。