read -N and IFS

5
根据手册页中的“read -N”说明:

-N nchars 仅在读取完确切的NCHARS个字符后返回,除非遇到EOF或超时,忽略任何分隔符

然而,在回应以下命令时:
$ echo 'a b' | while read -N1 c; do echo ">>>$c<<<"; done
>>>a<<<
>>><<<
>>>b<<<
>>><<<

在这个命令中,空格和换行符都被翻译成了空字符串。

$ echo 'a b' | while IFS= read -N1 c; do echo ">>>$c<<<"; done
>>>a<<<
>>> <<<
>>>b<<<
>>>
<<<

空格和换行符已经被正确地存储在变量中。

因此,似乎分隔符在“read”或“while”命令中仍然有一些处理,我不理解。

我们可以将这些结果与使用“read -n”得到的结果进行比较,该手册描述为:

-n nchars 读取NCHARS个字符后返回,而不是等待换行符,但如果在分隔符之前读取的字符少于NCHARS,则会遵守分隔符

$ echo 'a b' | while read -n1 c; do echo ">>>$c<<<"; done
>>>a<<<
>>><<<
>>>b<<<
>>><<<

$ echo 'a b' | while IFS= read -n1 c; do echo ">>>$c<<<"; done
>>>a<<<
>>> <<<
>>>b<<<
>>><<<
4个回答

4

这是POSIX行为。当赋值给变量时,IFS字符应该被剥离:结果应该像参数展开的结果一样在shell中被分成字段(当然,-n和-N不是POSIX)。

这通过read源代码注释得以证明:

/* This code implements the Posix.2 spec for splitting the words
     read and assigning them to variables. */
  orig_input_string = input_string;

  /* Remove IFS white space at the beginning of the input string.  If
     $IFS is null, no field splitting is performed. */

非常有趣。只有在“while IFS= read -n1 c”中将换行符转换为空字符串似乎很难与这些描述相匹配。我期望的是“循环结束”并且没有输出或者换行符赋值。实际上,这种情况是-n1和-N1测试之间唯一的区别。 - pasaba por aqui
换行符是默认 IFS 设置的一部分,换句话说它是一个分隔符。我没有看到任何不一致之处。 - cdarke
是的,但默认IFS(未设置IFS)与空IFS不同,这是此集合中使用的那个。此外,空格也在默认分隔符集中,在此测试中以不同的方式处理。 - pasaba por aqui
2
默认IFS不是未设置的IFS。默认设置为' \r\n' - cdarke

3
在我看来,使用选项-N时,当以下情况发生时,read的行为会有所不同:
  • 读取作为输入的分隔符
  • 该分隔符分配给变量
当它正在读取字符时,分隔符被视为与非分隔符相同,read会计算它们。但是,当read正在分配分隔符时,它会考虑读取的输入是否为分隔符,如果是分隔符,则将空值分配给相应的变量。
因此,IFS=将改变将空格分配给变量的行为,并导致将空格分配给c而不是空值。

2

使用 hexdump 可以让我们看到构成输出的确切字符,因此略微更改查询可能会有所帮助:

(1)使用普通IFS并使用 -N 选项

$ (echo 'a b' | while read -N1 c; do c="$c<"; echo -n "$c"; done | hexdump -C)
00000000  61 3c 3c 62 3c 3c                                 |a<<b<<|
00000006 

在第一种情况下,对于0x0a和空格字符的read内置返回空字符串,因为这些字符在默认IFS中,而IFS中的字符在输出中被忽略,这是由cdarke所解释的原因。
(2) 当IFS为空且使用-N选项时。
$ (IFS=""; echo 'a b' | while read -N1 c; do c="$c<"; echo -n "$c"; done | hexdump -C)
00000000  61 3c 20 3c 62 3c 0a 3c                              |a< <b<.<|
00000008

在这种情况下,read内置命令将匹配echo命令输出的四个字符,因为空IFS允许字符被分配给本地变量c,所以输出中会看到0x0a和一个空格。
(3)使用正常IFS和-n选项。
$ (echo 'a b' | while read -n1 c; do c="$c<"; echo -n "$c"; done | hexdump -C)
00000000  61 3c 3c 62 3c 3c                                 |a<<b<<|
00000006 

这将产生与情况(1)完全相同的输出,尽管语义上有点不同:对于0x0a和空格字符的读取内置函数均返回空字符串,因为(i)这两个字符都在默认IFS中,且(ii)无论如何,read内置函数的-n选项也不会传递结尾的0x0a字符。

(4)使用空IFS和-n选项

$ (IFS=""; echo 'a b' | while read -n1 c; do c="$c<"; echo -n "$c"; done | hexdump -C)
00000000  61 3c 20 3c 62 3c 3c                              |a< <b<<|
00000007

在这里,我们观察到了使用 read 命令的 -n 和 -N 选项之间的区别:使用 -n 选项时,read 内置命令会将换行符特殊处理并且删除,因此排除 IFS 中的 0x0a 并没有机会将其传递给本地变量 c


非常好的解释,但仍有一个未解决的问题,在第四种情况下的新行符,它以一种不同于空格的方式处理,即使没有属于 IFS 的字符。也就是说,在第三种情况下,我们不能说“0x0a 和空格字符返回空字符串,因为这两个字符都在默认 IFS 中”。 - pasaba por aqui
@pasabaporaqui - 你说得很对:-n开关的语义意味着IFS中的0x0a是多余的。我已经修改了讨论,以明确这种冗余。 - Charles Stewart
完美,只是编辑意见(2): "IFS unset" 应该是 "IFS empty"。 - pasaba por aqui

1

read 无法在读取字符之前确定其是否为定界符(以便忽略它),并且 read 必须为 c 分配 某些 值,即使该值为空字符串。当读取并随后丢弃分隔符时,必须将 c 的值设置为 某些 值,因此将其分配为空字符串。

这与使用没有 -n/-N 选项的 read 一致;只有在读取并且不需要设置所提供参数的值时,才会丢弃定界符 。最简单的情况是当您不向 read 提供任何参数时:

$ read <<< " a b c "
$ echo ">>>$REPLY<<<"
>>> a b c <<<

当只有一个显式参数时,前导和尾随的分隔符将被删除:

$ read line <<< " a b c "
$ echo ">>>$line<<<"
>>>a b c<<<

带有两个参数时,一旦读取第一个分隔符,它就会被忽略。第二个保留,因为该字符串只需要分成两个单词来填充提供的参数。

$ read field1 field2 <<< " a b c """
$ echo ">>>$field1<<<"
>>>a<<<
$ echo ">>>$field2<<<"
>>>b c<<<

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