我有一个...我希望获得一些见解的 bash shell 脚本的奇怪问题。
我的团队正在开发一个脚本,该脚本循环访问文件中的行并检查每个行中的内容。我们有一个错误,当通过将不同脚本排序在一起的自动化过程运行时,看不到最后一行。
用于循环访问文件中的行的代码(存储在DATAFILE
中的名称是
cat "$DATAFILE" | while read line
我们可以从命令行运行脚本,它会看到文件中的每一行,包括最后一行,就好了。但是,当由自动化进程(运行在相关脚本之前生成 DATAFILE 的脚本)运行时,永远不会看到最后一行。
我们更新了代码以使用以下代码来迭代这些行,问题已解决:
for line in `cat "$DATAFILE"`
注意:DATAFILE 在文件末尾没有写换行符。
我的问题是两部分...为什么原始代码看不到最后一行,为什么这会有所作为?
我只觉得我能想出为什么看不到最后一行是:
- 写入文件的上一个进程依赖于进程结束以关闭文件描述符。
- 问题脚本之前启动和打开文件的速度足够快,以至于虽然前一个进程已经"结束",但它的"关闭/清理"不足以让系统自动关闭文件描述符。
话虽如此,似乎,如果您在 shell 脚本中有 2 个命令,则在脚本运行第二个命令时,第一个命令应该完全关闭。
对这些问题的任何见解,尤其是第一个问题,将不胜感激。
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 上测试。
波西斯怎么说?
POSIXread
命令规范说:
实用程序应从标准输入中读取一行。
默认情况下,除非指定
-r
选项,否则<反斜杠>应充当转义字符。未转义的<反斜杠>应保留以下字符的文字值,但<换行符>除外。如果<换行符>跟在<反斜杠>后面,则读取实用程序应将其解释为行延续。在将输入拆分为字段之前,应删除<反斜杠>和<newline>
。将输入拆分为字段后,应删除所有其他未转义的<反斜杠>字符。反斜杠>反斜杠>反斜杠>换行符>换行符>反斜杠>反斜杠>如果标准输入是终端设备,并且调用 shell 是交互式的,则 read 在读取以<反斜杠><换行符>结尾的输入行时,应提示输入延续行,除非指定了
-r
选项。换行符>反斜杠>终止<换行符>(如果有)应从输入中删除,并将结果拆分为参数扩展结果的 shell 中的字段(请参阅字段拆分);[...]换行符>
请注意,"(如果有的话)"(引号中添加了强调)! 在我看来,如果没有换行符,它仍然应该读取结果。 另一方面,它还说:
斯特丁
标准输入应为文本文件。
然后你回到关于不以换行符结尾的文件是否是文本文件的争论。
但是,同一页面上的基本原理文档:
尽管标准输入必须是文本文件,因此将始终以<换行符>结尾(除非它是空文件),但在不使用
-r
选项时处理继续行可能会导致输入不以<换行符>结尾。如果输入文件的最后一行以<反斜杠><换行符>结尾,则会发生这种情况。正是出于这个原因,在描述中的"终止<换行符>(如果有)应从输入中删除"中使用了"如果有"。它不是放宽对标准输入为文本文件的要求。换行符>换行符>反斜杠>换行符>换行符>
该理由必须意味着文本文件应以换行符结尾。
文本文件的 POSIX 定义是:
3.395 文本文件
包含组织成零行或多行的字符的文件。这些行不包含 NUL 字符,并且长度不能超过 {LINE_MAX} 字节,包括<换行符>字符。尽管 POSIX.1-2008 不区分文本文件和二进制文件(请参阅 ISO C 标准),但许多实用程序仅在对文本文件进行操作时生成可预测或有意义的输出。具有此类限制的标准实用程序始终在其 STDIN 或输入文件部分中指定"文本文件"。换行符>
这并没有直接规定"以<换行符>结尾,但确实遵循 C 标准,它确实说"包含组织成零行或多行的字符的文件",当我们查看">行"的 POSIX 定义时,它说:换行符>
3.206 线
由零个或多个非<换行符>字符加上 终止<换行符>字符。换行符>换行符>
因此,根据 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 中可能有足够的理由这样做。 显然,最好确保您的文本文件确实是以换行符结尾的文本文件。
根据读取命令的 POSIX 规范,如果"检测到文件结束或发生错误",它应返回非零状态。由于EOF在读取最后一行时被检测到,因此它会设置$line
然后返回错误状态,并且错误状态会阻止循环在最后一行"上执行。 解决方案很简单:如果 read 命令成功或将任何内容读入$line
,则使循环执行。
while read line || [ -n "$line" ]; do
添加一些附加信息:
- 无需将
cat
与 while 循环一起使用。while ...;do something;done<file
就够了。 - 不要阅读带有
for
的行。
使用 while 循环读取行时:
- 正确设置
IFS
(否则可能会丢失缩进)。 - 您几乎总是应该将 -r 选项与读取一起使用。
满足上述要求后,适当的 while 循环将如下所示:
while IFS= read -r line; do
...
done <file
并使其适用于末尾没有换行符的文件(从这里重新发布我的解决方案):
while IFS= read -r line || [ -n "$line" ]; do
echo "$line"
done <file
或者将grep
与 while 循环一起使用:
while IFS= read -r line; do
echo "$line"
done < <(grep "" file)
作为一种解决方法,在读取文本文件之前,可以在文件中附加换行符。
echo -e "n" >> $file_path
这将确保读取以前在文件中的所有行。我们需要将 -e 参数传递给回显,以便能够解释转义序列。 https://superuser.com/questions/313938/shell-script-echo-new-line-to-file
我在命令行中对此进行了测试
# create dummy file. last line doesn't end with newline
printf "%in%inNo-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`
扩展为
line1nline2n...lineM
使用 IFS 将其由外壳分隔为多个字段,因此您可以获得
line1 line2 line3 ... lineM
这就是为什么你仍然得到最后一行。
p/s:我不明白的是你是如何让第一个表单工作的......
使用 sed 匹配文件的最后一行,如果不存在换行符,它将附加换行符,并让它对文件进行内联替换:
sed -i '' -e '$a' file
代码来自此堆栈交换链接
注意:我在-i ''
中添加了空的单引号,因为至少在OS X中,-i
使用-e
作为备份文件的文件扩展名。我很乐意对原始帖子发表评论,但缺少 50 分。也许这会在这个线程中获得一些,谢谢。
我也有类似的问题。 我正在做一个文件的猫,将其管道化为排序,然后将结果管化为"读取 var1 var2 var3 时"。 结婚cat $FILE|排序 -k3|读取计数 IP 名称时do "do"下的工作是一个 if 语句,用于识别$Name字段中不断变化的数据,并根据更改或无更改执行$Count总和或将汇总行打印到报告中。 我还遇到了无法将最后一行打印到报告中的问题。 我采用了简单的权宜之计,将 cat/sort 重定向到一个新文件,将换行符回显到该新文件,然后在新文件上运行我的"同时读取计数 IP 名称"并取得了成功的结果。 结婚猫 $FILE|排序 -K3> 新文件 回显 "">> 新文件 猫新文件 |读取计数 IP 名称 做有时候简单、不优雅是最好的办法。