如果文件结尾没有换行符,如何使用“while read”(Bash)读取文件的最后一行?

60

假设我有以下Bash脚本:

while read SCRIPT_SOURCE_LINE; do
  echo "$SCRIPT_SOURCE_LINE"
done

我注意到对于没有换行符的文件,这会有效地跳过最后一行。

我在搜索解决方案时找到了这个

当读到文件末尾而不是行末时,它确实会读取数据并将其分配给变量,但它会以非零状态退出。如果您的循环构造为“while read; do stuff; done”,则会出现这种情况。

因此,不要直接测试读取退出状态,而是测试一个标志,并让read命令从循环体内设置该标志。无论reads退出状态如何,整个循环体都会运行,因为read只是循环中的命令列表之一,而不是决定循环是否完全运行的决定因素。

DONE=false
until $DONE ;do
read || DONE=true
# process $REPLY here
done < /path/to/file.in
我该如何重写这个解决方案,使其与我之前使用的 while 循环完全相同,即不需要硬编码输入文件的位置?
8个回答

46

我使用以下结构:

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

它几乎适用于任何输入,除了空字符:

  • 以空行开头或结尾的文件
  • 以空格开头或结尾的行
  • 没有终止换行符的文件

7
在IFS中不需要包含换行符,你可以将其简单地设置为空字符串。此外,为了使其符合POSIX标准(当前仅在bash中有效,而不适用于/bin/sh),请执行IFS='' read -r LINE || [ -n "$LINE" ] - Richard Hansen
4
请注意,如果文件末尾没有换行符,这将在文件末尾添加一个额外的换行符。 - l0b0

19

在你的第一个例子中,我假设你正在从 stdin 读取。要使用第二个代码块执行相同操作,你只需删除重定向并输出 $REPLY:

DONE=false
until $DONE ;do
read || DONE=true
echo $REPLY
done

啊,重定向让我感到困惑。谢谢你的答案,它非常有效! - Mathias Bynens
3
若想避免在最后一行有换行符时仍处理额外(空)行,请在包含“read”的那一行之后添加以下内容:[[ ! $REPLY ]] && continue - Dennis Williamson
@Dennis:这似乎跳过了所有的空行,这正是我在这种情况下想要的。谢谢! - Mathias Bynens

6

使用 while 循环和 grep 命令:

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

使用grep .代替grep "",可以跳过空行。

注意:

  1. 使用IFS=可以保持任何行的缩进不变。

  2. 几乎总是应该使用-r选项来读取。

  3. 没有换行符结束的文件不是标准的Unix文本文件。


1
第三点补充:这是 POSIX 标准如何定义一行的方式(第 3206 行):https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_206 - thanos.a

5

不要使用read,而是尝试使用GNU Coreutils,比如teecat等。

从标准输入(stdin)中读取

readvalue=$(tee)
echo $readvalue

从文件中读取

readvalue=$(cat filename)
echo $readvalue

2

这是我一直在使用的模式:

while read -r; do
  echo "${REPLY}"
done
[[ ${REPLY} ]] && echo "${REPLY}"

这个方法的原理是,即使while循环因为从read中退出时返回了非零代码而结束,read仍会填充内置变量$REPLY(或者您选择用read分配的任何变量)的值。

1
基本问题在于,即使变量仍然被正确填充,read在遇到EOF时也会返回错误级别1。因此,在循环中可以立即使用read的错误级别,否则最后的数据将无法解析。但你可以这样做:
eof=
while [ -z "$eof" ]; do
    read SCRIPT_SOURCE_LINE || eof=true   ## detect eof, but have a last round
    echo "$SCRIPT_SOURCE_LINE"
done

如果你想要一种非常稳定的方式来解析你的行,那么你应该使用:
IFS='' read -r LINE

请记住:

  • NUL字符将被忽略
  • 如果您坚持使用echo来模拟cat的行为,您需要在检测到EOF时强制执行echo -n(您可以使用条件[ "$eof" == true ]

1

0

@Netcoder的回答很好,这种优化可以消除不必要的空行,同时也允许最后一行没有换行符,如果原始文本就是这样的话。

DONE=false
NL=
until $DONE ;do
if ! read ; then DONE=true ; NL='-n ';fi
echo $NL$REPLY
done

我使用了这个变体来创建两个函数,以允许包含'['的文本进行管道传输,从而使grep工具正常工作。(您可以添加其他翻译)
function grepfix(){
    local x="$@";
    if [[ "$x" == '-' ]]; then
      local DONE=false
      local xx=
      until $DONE ;do
         if ! IFS= read ; then DONE=true ; xx="-n "; fi
         echo ${xx}${REPLY//\[/\\\[}
      done
    else
      echo "${x//\[/\\\[}"
    fi
 }


 function grepunfix(){
    local x="$@";
    if [[ "$x" == '-' ]]; then
      local DONE=false
      local xx=
      until $DONE ;do
         if ! IFS= read ; then DONE=true ; xx="-n "; fi
         echo ${xx}${REPLY//\\\[/\[}
      done
    else
      echo "${x//\\\[/\[}"
    fi
 }

(将 - 作为 $1 传递,启用管道,否则只翻译参数)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接