使用“while read…”,echo 和 printf 得到不同的结果

使用“while read…”,echo 和 printf 得到不同的结果

根据这个问题“在 Linux 脚本中使用“while read...”

echo '1 2 3 4 5 6' | while read a b c;do echo "$a, $b, $c"; done

结果:

1, 2, 3 4 5 6

但是当我echo替换printf

echo '1 2 3 4 5 6' | while read a b c ;do printf "%d, %d, %d \n" $a $b $c; done

结果

1, 2, 3
4, 5, 6

有人能告诉我这两个命令有什么不同吗?谢谢~

答案1

这不仅仅是 echo 与 printf

首先,让我们了解一下read a b cpart 发生了什么。read将根据IFS变量的默认值(即 space-tab-newline)执行分词,并根据该值调整所有内容。如果输入多于可容纳它的变量,它将把分割的部分放入第一个变量中,而无法容纳的部分将放入最后一个变量中。我的意思如下:

bash-4.3$ read a b c <<< "one two three four"
bash-4.3$ echo $a
one
bash-4.3$ echo $b
two
bash-4.3$ echo $c
three four

这正是bash手册中描述的(参见答案末尾的引文)。

在您的情况下,会发生的情况是,1 和 2 适合 a 和 b 变量,而 c 则接受其他所有变量,即3 4 5 6

您还会经常看到人们while IFS= read -r line; do ... ; done < input.txt逐行读取文本文件。再次强调,IFS=这里是为了控制分词,或者更具体地说 - 禁用它,并将一行文本读入变量。如果没有它,read就会尝试将每个单词放入line变量中。但那是另一个故事,我鼓励您稍后研究,因为这while IFS= read -r variable是一种非常常用的结构。

echo 与 printf 行为

echo此处执行的操作与您所期望的完全一致。它完全按照您安排的方式显示变量read。这已在之前的讨论中得到证实。

printf非常特殊,因为它会继续将变量放入格式字符串中,直到所有变量都用尽为止。因此,当您执行printf "%d, %d, %d \n" $a $b $cprintf 时,会看到带有 3 个小数的格式字符串,但参数多于 3 个(因为您的变量实际上扩展为单独的 1、2、3、4、5、6)。这听起来可能令人困惑,但存在的原因是为了改进真实的 printf()函数在C语言中的作用。

您在此处所做的操作也会影响输出,即变量未加引号,这允许 shell ( not printf) 将变量分解为 6 ​​个单独的项。将其与加引号的情况进行比较:

bash-4.3$ read a b c <<< "1 2 3 4"
bash-4.3$ printf "%d %d %d\n" "$a" "$b" "$c"
bash: printf: 3 4: invalid number
1 2 3

正是因为$c变量被引用,它现在被识别为一个完整的字符串,3 4并且它不符合%d格式,即只是一个整数

现在做同样的事情但不引用:

bash-4.3$ printf "%d %d %d\n" $a $b $c
1 2 3
4 0 0

printf再次说:“好的,你有 6 个项目,但是格式只显示 3 个,所以我将继续拟合内容,并将无法与用户实际输入匹配的内容留空”。

在所有这些情况下,你不必相信我的话。只需运行strace -e trace=execve并亲自查看命令实际上“看到”了什么:

bash-4.3$ strace -e trace=execve printf "%d %d %d\n" $a $b $c
execve("/usr/bin/printf", ["printf", "%d %d %d\\n", "1", "2", "3", "4"], [/* 80 vars */]) = 0
1 2 3
4 0 0
+++ exited with 0 +++

bash-4.3$ strace -e trace=execve printf "%d %d %d\n" "$a" "$b" "$c"
execve("/usr/bin/printf", ["printf", "%d %d %d\\n", "1", "2", "3 4"], [/* 80 vars */]) = 0
1 2 printf: ‘3 4’: value not completely converted
3
+++ exited with 1 +++

补充笔记

正如 Charles Duffy 在评论中正确指出的那样,它bash有自己的内置版本printf,也就是您在命令中使用的版本,strace实际上将调用/usr/bin/printf版本,而不是 shell 的版本。除了细微的差别外,对于我们对这个特定问题的兴趣,标准格式说明符是相同的,行为也是相同的。

还应该记住的是,printf语法比 更易于移植(因此更受欢迎)echo,更不用说语法对 C 或任何具有printf()函数的类 C 语言来说都更为熟悉。请参阅此terdon 的出色回答关于printfvs echo。虽然您可以根据特定版本的 Ubuntu 上的特定 shell 定制输出,但如果要跨不同系统移植脚本,您可能应该选择printf而不是 echo。也许您是使用 Ubuntu 和 CentOS 机器的初级系统管理员,或者甚至是 FreeBSD - 谁知道呢 - 所以在这种情况下您必须做出选择。

引用自 bash 手册,SHELL BUILTIN COMMANDS 部分

读取 [-ers] [-a aname] [-d delim] [-i text] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [name ...]

从标准输入或作为 -u 选项的参数提供的文件描述符 fd 中读取一行,并将第一个单词分配给第一个名称,将第二个单词分配给第二个名称,依此类推,将剩余的单词及其中间分隔符分配给最后一个名称。如果从输入流读取的单词少于名称,则将剩余的名称分配为空值。IFS 中的字符用于将行拆分为单词,使用与 shell 用于扩展的相同规则(上文单词拆分中所述)。

答案2

这只是一个建议,并不是要取代 Sergiy 的答案。我认为 Sergiy 写了一个很好的答案,解释了为什么它们在打印上有所不同。读取的变量如何与剩余的变量一起分配,$c并分配3 4 5 6给和。不会 为您拆分变量,其中将使用s。12abechoprintf%d

但是,您可以通过操纵命令开头的数字的回显,让它们基本上给出相同的答案:

/bin/bash可以使用:

echo -e "1 2 3 \n4 5 6"

/bin/sh可以使用:

echo "1 2 3 \n4 5 6"

Bash 使用 -e 来启用 \ 转义字符,其中 sh 不需要它,因为它已经启用。 \n导致它创建一个新行,所以现在 echo 行被分成两个单独的行,现在可以在 echo 循环语句中使用两次:

:~$ echo -e "1 2 3 \n4 5 6" | while read a b c; do echo "$a, $b, $c"; done
1, 2, 3
4, 5, 6

使用以下printf命令会产生相同的输出:

:~$ echo -e "1 2 3 \n4 5 6" | while read a b c ;do printf "%d, %d, %d \n" $a $b $c; done
1, 2, 3 
4, 5, 6 

sh

$ echo "1 2 3 \n4 5 6" | while read a b c; do echo "$a, $b, $c"; done
1, 2, 3
4, 5, 6
$ echo "1 2 3 \n4 5 6" | while read a b c ;do printf "%d, %d, %d \n" $a $b $c; done
1, 2, 3 
4, 5, 6 

希望这可以帮助!

相关内容