为什么经常使用`while IFS= read`,而不是`IFS=; 阅读时..`?

Pet*_*r.O 94 shell text-processing environment-variables

似乎通常的做法会将 IFS 的设置放在 while 循环之外,以免在每次迭代时重复设置......这只是一种习惯性的“猴子看,猴子做”的风格,就像这只猴子直到我读过man read,还是我在这里错过了一些微妙(或明显明显)的陷阱?

roz*_*acz 92

陷阱在于

IFS=; while read..
Run Code Online (Sandbox Code Playgroud)

设置IFS为外循环整个shell环境,而

while IFS= read
Run Code Online (Sandbox Code Playgroud)

仅为read调用重新定义它(在 Bourne shell 中除外)。你可以检查做一个循环

while IFS= read xxx; ... done
Run Code Online (Sandbox Code Playgroud)

然后在这样的循环之后,echo "blabalbla $IFS ooooooo"打印

blabalbla
 ooooooo
Run Code Online (Sandbox Code Playgroud)

而之后

IFS=; read xxx; ... done
Run Code Online (Sandbox Code Playgroud)

重新定义的IFS 保持:现在echo "blabalbla $IFS ooooooo"打印

blabalbla  ooooooo
Run Code Online (Sandbox Code Playgroud)

所以如果你使用第二种形式,你必须记住重置 : IFS=$' \t\n'


这个问题的第二部分已经合并到这里,所以我从这里删除了相关的答案。


Gil*_*il' 51

让我们看一个例子,其中包含一些精心制作的输入文本:

text=' hello  world\
foo\bar'
Run Code Online (Sandbox Code Playgroud)

这是两行,第一行以空格开头,以反斜杠结尾。首先,让我们看看在没有任何预防措施的情况下会发生什么read(但使用printf '%s\n' "$text"仔细打印$text没有任何扩展风险)。(下面$ ?是 shell 提示。)

$ printf '%s\n' "$text" |
  while read line; do printf '%s\n' "[$line]"; done
[hello worldfoobar]
Run Code Online (Sandbox Code Playgroud)

read吃掉反斜杠:backslash-newline 会导致换行被忽略,而 backslash-anything 会忽略第一个反斜杠。为避免对反斜杠进行特殊处理,我们使用read -r.

$ printf '%s\n' "$text" |
  while read -r line; do printf '%s\n' "[$line]"; done
[hello  world\]
[foo\bar]
Run Code Online (Sandbox Code Playgroud)

更好,我们有预期的两条线。这两行几乎包含了所需的内容:hello和之间的双倍空格world已被保留,因为它在line变量内。另一方面,最初的空间被吃光了。这是因为read读取的单词与您传递给它的变量一样多,除了最后一个变量包含该行的其余部分——但它仍然从第一个单词开始,即初始空格被丢弃。

因此,为了逐字阅读每一行,我们需要确保没有进行分。我们通过将IFS变量设置为空值来做到这一点。

$ printf '%s\n' "$text" |
  while IFS= read -r line; do printf '%s\n' "[$line]"; done
[ hello  world\]
[foo\bar]
Run Code Online (Sandbox Code Playgroud)

请注意我们如何IFS 专门为read内置. 的IFS= read -r line设置环境变量IFS专门为执行(为空值)read。这是一般简单命令语法的一个实例:一个(可能是空的)变量赋值序列,后跟一个命令名称及其参数(此外,您可以在任何时候抛出重定向)。由于read是内置变量,因此变量实际上永远不会出现在外部进程的环境中;尽管如此$IFS,只要read正在执行¹, 的值就是我们在那里分配的值。请注意,这read不是一个特殊的内置,因此分配仅在其持续时间内持续。

因此,我们注意不要更改IFS可能依赖它的其他指令的值。无论周围代码IFS最初设置为什么,这段代码都可以工作,如果循环内部的代码依赖于IFS.

与此代码片段相反,它在冒号分隔的路径中查找文件。从文件中读取文件名列表,每行一个文件名。

IFS=":"; set -f
while IFS= read -r name; do
  for dir in $PATH; do
    ## At this point, "$IFS" is still ":"
    if [ -e "$dir/$name" ]; then echo "$dir/$name"; fi
  done
done <filenames.txt
Run Code Online (Sandbox Code Playgroud)

如果循环是while IFS=; read -r name; do …,则for dir in $PATH不会拆分$PATH为以冒号分隔的组件。如果代码是IFS=; while read …,则在循环体中IFS未设置为更明显:

当然,IFS执行后也可以恢复 的值read。但这需要知道先前的值,这是额外的努力。IFS= read是最简单的方法(而且,方便,也是最短的方法)。

¹并且,如果read被捕获信号中断,可能是在捕获执行时——这不是由 POSIX 指定的,并且在实践中取决于 shell。

  • 谢谢 Gilles.. 一个非常好的导游..(你的意思是 'set -f' 吗?).. 现在,对于读者来说,重申已经说过的话,我想强调这个问题我看错了。首先也是最重要的事实是,结构`while IFS= read`(`=` 后没有分号)**不是**`while` 或`IFS` 或`read` 的特殊形式。 . 构造是通用的:即。`anyvar=anyvalue anycommand`。设置 `anyvar` 后缺少 `;` 使得 `anyvar` 的范围 *local* 为 `anycommand`.. while--do/done 循环与 `any_var` 的本地范围 100% *无关*。 (5认同)

小智 5

除了,和惯用语IFS之间的(已经澄清的)范围差异(每个命令与脚本/shell 范围的变量范围),带回家的教训是,如果 IFS 变量,您将丢失输入行的前导尾随空格设置为(包含一个)空格。while IFS='' readIFS=''; while readwhile IFS=''; readIFS

如果正在处理文件路径,这可能会产生非常严重的后果。

因此,将 IFS 变量设置为空字符串绝不是一个坏主意,因为它确保行的前导和尾随空格不会被剥离。

另请参阅:Bash,使用 IFS 从文件中逐行读取

(
shopt -s nullglob
touch '  file with spaces   '
IFS=$' \t\n' read -r file <<<"$(printf '%s' *file*with*spaces*)"
ls -l "$file"
IFS='' read -r file <<<"$(printf '%s' *file*with*spaces*)"
ls -l "$file"
)
Run Code Online (Sandbox Code Playgroud)