为什么你不能使用cat命令逐行读取一个文件,其中每一行都有分隔符?

23

我有一个文本文件,里面包含了类似这样的内容:

abc 123, comma
the quick brown fox
jumped over the lazy dog
comma, comma

我写了一个脚本

for i in `cat file`
do
   echo $i
done

由于某种原因,脚本的输出并不是逐行输出文件内容,而是在逗号和换行符处中断。为什么使用catfor blah in `cat xyz`会出现这种情况,我该如何避免这种情况?我知道可以使用一个方法。
while read line
do
   blah balh blah
done < file

但是我想知道为什么cat或者for var in会这样做,以进一步了解Unix命令。 cat的手册对我没有帮助,查看bash手册中的forlooping也没有得到答案(http://www.gnu.org/software/bash/manual/bashref.html)。 提前感谢您的帮助。
6个回答

26
问题不在于cat,也不在于for循环本身;而是在于使用反引号。当你写下以下任何一种形式时:
for i in `cat file`

或者(更好):
for i in $(cat file)

或者(在 ksh、zsh 或 bash 中):

for i in $(<file)

shell执行命令并将输出捕获为字符串,删除尾随的换行符(以及使用bash删除所有的NUL),在字符$IFS处分隔单词,并在结果单词上执行globbing,也称为文件名生成路径扩展。如果您想要将输入行传递给$i,您需要调整IFS或使用while循环。如果处理的文件可能很大,while循环更好;它不需要一次性将整个文件读入内存,也不执行文件名生成,并且不跳过空行,而不像使用$(...)的版本。

IFS='
'
set -o noglob # disable globbing
for i in $(<file)
do printf '%s\n' "$i"
done

在大多数情况下,使用引号将"$i"括起来是一个很好的做法。在这个上下文中,通过修改了$IFS并禁用了通配符展开,实际上并不是必要的,但养成良好的习惯总是好的。相比于echoprintf更好,因为echo对于包含-n-nene-eee或者根据echo的实现和/或环境处理反斜杠的输入行,可能输出空白行或空白内容。这在以下脚本中是重要的:

old="$IFS"
IFS='
'
set -o noglob
for i in $(<file)
do
   (
   IFS="$old"
   set +o noglob
   printf '%s\n' "$i"
   )
done

当数据文件包含制表符或多个空格(这两者都是默认值$IFS)或通配符或前导尾随空白时
$ cat file
abc                  123
  foo
-Enee
/e* /b*
$ 

输出:

$ sh bq.sh
abc                  123
  foo
-Enee
/e* /b*
$

使用echo而不带双引号:
$ cat bq.sh
old="$IFS"
IFS='
'
set -o noglob
for i in $(<file)
do
   (
   IFS="$old"
   set +o noglob
   echo $i
   )
done
$ sh bq.sh
abc 123
foo
/etc /bin /boot
$

对于 while read 循环,语法应该是:

while IFS= read -r line
do
   printf '%s\n' "$line"
done < file
  • 没有-rread会破坏反斜杠。
  • 没有IFS=read会删除前导和尾随的空格和制表符(假设默认值为$IFS)。
  • 应该使用printf而不是echo,并且对$line进行引用,原因同上。

虽然在bash中这种优化的效果要小得多,因为bash仍然会fork一个子进程来执行扩展。

谢谢您的帮助和回复。我在使用bash/*nix时有些困惑。我没有改变IFS,它默认设置为换行符。我用echo "IFS = $IFS word test"检查了一下,发现字符串"word test"被打印到了下一行,所以我们知道它默认是\n。无论如何,使用默认的IFS,它会在逗号处断开我的行,即使IFS=\n。当我按照您上面建议的那样,将IFS显式设置为\n时,它就可以打印整行而不会在逗号处断开。有什么想法吗?为什么显式设置为\n时可以工作,而默认情况下IFS已经是\n时不能工作?再次感谢。 - Classified
2
IFS 的默认值是(使用一段 bash 语言)$' \t\n';也就是说,它由空格、制表符和换行符组成。这可能会改变您的分析结果。当您说“在逗号处断开”时,我相信您指的是在逗号后面的空格处断开,这与 IFS 包含空格(以及制表符和换行符)是一致的。 - Jonathan Leffler

7
cat filename | while read i
do
    echo $i
done

最明智的想法! - jian
1
UUOC,read缺少IFS=-r$i周围缺少引号,使用printf而不是echo。这里集合了所有可能的糟糕实践。 - Stephane Chazelas

6
您可以使用 IFS 变量来指定换行符作为字段分隔符:
IFS=$'\n'
for i in `cat file`
do
   echo $i
done

2
不安全 - 您已经防止了字符串分割,但您没有防止通配符扩展。如果一行包含 *,则在 echo 过程中它将被扩展为当前目录中名称列表。 - Charles Duffy

3

使用for循环结合内部字段分隔符(IFS)的更改,可以按预期读取文件。

输入示例如下:

abc 123, comma
the quick brown fox
jumped over the lazy dog
comma, comma

与IFS变更配合使用的for循环

old_IFS=$IFS
IFS=$'\n'
for i in `cat file`
do
        echo $i
done
IFS=$old_IFS

导致
abc 123, comma
the quick brown fox
jumped over the lazy dog
comma, comma

2
只需使用 IFS= read -r line 来保留行中的所有空格。 - chepner
1
唯一导致 while 循环中间丢失空格的原因是您使用 echo $line 而不是 echo "$line"。如果空格很重要,请用双引号括起变量引用。 - Jonathan Leffler
1
正如chepner所说,应该使用read -r以避免意外的副作用(评估反斜杠转义序列)。 - Charles Duffy

2

IFS - 内部字段分隔符可以设置以获取您想要的内容。

要一次读取整行,请使用: IFS = ""


1
为了防止新的IFS设置改变您的shell,您不需要执行其他提到的操作...
通过将命令放在括号中使用子shell(我也喜欢使用转义序列来设置IFS;这样更容易阅读所做的内容):
(IFS=$'\n'; for i in $(cat file); do echo $i; done)

分号可以替代本应需要的换行符,括号可以打开子shell,$(...)语法将子shell调用替换为其输出到标准输出的内容,而在单引号前面的$会导致单引号内的转义序列解释(不适用于双引号,是bashism,即不适用于任何其他POSIX shell)。
或者,您也可以...
(IFS=$'\n'
for i in $(cat file); do
    echo $i
done)

甚至可以将do或括号放在它们自己的行上,如果这是您想要的。


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