98

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

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

用于遍历文件中行的代码(存储DATAFILE

cat "$DATAFILE" | while read line 

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

我们更新了代码以使用以下代码来迭代行,问题就解决了:

for line in `cat "$DATAFILE"` 

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

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

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

  • 之前写入文件的进程依赖于进程结束来关闭文件描述符。
  • 问题脚本启动并提前打开文件的速度足够快,虽然前一个进程已经“结束”,但它还没有“关闭/清理”到足以让系统自动关闭文件描述符。

话虽如此,似乎如果您在 shell 脚本中有 2 个命令,那么在脚本运行第二个命令时,第一个命令应该完全关闭。

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

4

7 回答 7

126

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> 应充当转义字符。未转义的 <backslash> 应保留后面字符的文字值,<newline> 除外。如果 <newline> 跟在 <backslash> 之后,read 实用程序应将其解释为行继续。<反斜杠> 和<newline>应在将输入拆分为字段之前删除。在将输入拆分为字段后,应删除所有其他未转义的 <backslash> 字符。

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

应从输入中删除终止的 <newline> (如果有) ,并将结果拆分为参数扩展结果的 shell 中的字段(请参阅字段拆分);[...]

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

标准输入

标准输入应为文本文件。

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

但是,同一页面上的理由是:

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

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

文本文件的 POSIX 定义是:

3.395文本文件

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

这并没有直接规定“以 <newline> 结尾”,但确实遵循 C 标准,并且确实说“包含组织成零行或多的字符的文件”,当我们查看“行”的 POSIX 定义时“ 它说:

3.206线

零个或多个非 <newline> 字符加上终止 <newline> 字符的序列。

因此,根据 POSIX 定义,文件必须以终止换行符结尾,因为它由行组成,并且每行必须以终止换行符结尾。


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

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

$ 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 中可能有足够的理由这样做。显然,最好确保您的文本文件确实是以换行符结尾的文本文件。

于 2012-10-16T14:18:26.997 回答
92

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

while read line || [ -n "$line" ]; do
于 2012-10-16T16:57:30.973 回答
34

添加一些附加信息:

  1. 无需使用catwhile 循环。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)
于 2015-07-14T05:43:36.443 回答
2

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

echo -e "\n" >> $file_path

这将确保读取文件中先前的所有行。我们需要将 -e 参数传递给 echo 以启用转义序列的解释。 https://superuser.com/questions/313938/shell-script-echo-new-line-to-file

于 2013-02-04T16:40:07.837 回答
1

我在命令行中对此进行了测试

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

使用您的第一个表单进行测试(管道到 while-loop)

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 分隔成多个字段,所以你得到

line1 line2 line3 ... lineM 

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

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

于 2012-10-16T15:44:31.077 回答
1

使用 sed 匹配文件的最后一行,如果不存在,它将附加一个换行符,并让它对文件进行内联替换:

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

代码来自这个 stackexchange链接

注意:我添加了空单引号,-i ''因为至少在 OS X 中,-i它被-e用作备份文件的文件扩展名。我很乐意对原始帖子发表评论,但缺少 50 分。也许这会让我在这个线程中获得一些,谢谢。

于 2013-09-23T17:33:24.787 回答
0

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

于 2014-02-03T18:53:02.987 回答