理解“IFS=读取-r行”

理解“IFS=读取-r行”

我显然知道可以为内部字段分隔符变量添加值。例如:

$ IFS=blah
$ echo "$IFS"
blah
$ 

我还了解到这read -r line会将数据保存到stdin名为 的变量line

$ read -r line <<< blah
$ echo "$line"
blah
$ 

然而,命令如何给变量赋值呢?它是否首先存储stdinto 变量的数据line,然后给出lineto的值IFS

答案1

在 POSIX shell 中read,不带任何选项的 不会读取线,它写着来自(可能是反斜杠继续的)行,其中单词被$IFS分隔,反斜杠可用于转义分隔符(或连续行)。

通用语法是:

read word1 word2... remaining_words

read一次读取 stdin 一个字节,直到找到未转义的换行符(或输入结束),根据复杂的规则将其拆分,并将拆分结果存储到$word1, $word2...中$remaining_words

例如对于这样的输入:

  <tab> foo bar\ baz   bl\ah   blah\
whatever whatever

并使用默认值$IFS,read a b c将分配:

  • $afoo
  • $bbar baz
  • $cblah blahwhatever whatever

现在,如果仅传递一个参数,则不会变成read line.依然是read remaining_words。反斜杠处理仍然完成,IFS 空白字符²仍然从开头和结尾删除。

-r选项删除反斜杠处理。因此上面的相同命令-r将改为分配

  • $afoo
  • $bbar\
  • $cbaz bl\ah blah\

现在,对于分割部分,重要的是要认识到有两类字符$IFS:IFS 空白字符²(包括空格和制表符(以及换行符,尽管在这里除非使用 -d ,否则这并不重要),这也会发生为$IFS) 和其他的默认值。对这两类角色的处理是不同的。

对于IFS=::不是 IFS 空白字符),像这样的输入:foo::bar::将被分成"""foo"""和(以及一些实现的额外内容bar,尽管除了 之外这并不重要)。而如果我们将其替换为空格,则会将其拆分为仅和。也就是说,前导和尾随的序列将被忽略,并且它们的序列将被视为一个序列。当空格和非空格字符在 中组合时,还有其他规则。某些实现可以通过加倍 IFS 中的字符(或)来添加/删除特殊处理。""""read -a:foobar$IFSIFS=::IFS=' '

所以在这里,如果我们不想删除前导和尾随的未转义空白字符,我们需要从 IFS 中删除那些 IFS 空白字符。

即使使用 IFS 非空白字符,如果输入行包含一个(且仅一个)这些字符,并且它是POSIX shell(不是也不是某些版本)的行中的最后一个字符(就像IFS=: read -r word在类似 的输入上) ,则该输入被视为一个单词,因为在这些 shell 中,字符被视为foo:zshpdkshfoo$IFS终结者,所以word将包含foo,不包含foo:

因此,使用内置函数读取一行输入的规范方法read是:

IFS= read -r line

(请注意,对于大多数read实现,这只适用于文本行,因为除 之外不支持 NUL 字符zsh)。

使用var=value cmd语法确保IFS仅在该命令的持续时间内进行不同的设置cmd

历史笔记

read内置函数是由 Bourne shell 引入的,并且已经可以阅读,不是线。与现代 POSIX shell 有一些重要的区别。

Bourne shellread不支持-r选项(由 Korn shell 引入),因此除了用类似的方法预处理输入之外,没有办法禁用反斜杠处理sed 's/\\/&&/g'

Bourne shell 没有两类字符的概念(这也是由 ksh 引入的)。在 Bourne shell 中,所有字符都经过与 ksh 中的 IFS 空白字符相同的处理,即IFS=: read a b c在输入上(如foo::bar分配给bar$b,而不是空字符串。

在 Bourne shell 中,使用:

var=value cmd

如果cmd是内置的(就像read是),则在完成后var保持设置。这一点尤其重要,因为在 Bourne shell 中,它用于分割所有内容,而不仅仅是扩展。另外,如果您从Bourne shell 中删除空格字符,则不再起作用。valuecmd$IFS$IFS$IFS"$@"

在 Bourne shell 中,重定向复合命令会导致它在子 shell 中运行(在最早的版本中,甚至类似read var < fileexec 3< file; read var <&3不起作用),因此在 Bourne shell 中很少用于read除终端上的用户输入之外的任何内容(行延续处理有意义的地方)

一些 Unices(例如 HP/UX,在 中也有一个util-linux)仍然有一个line命令来读取一行输入(这曾经是一个标准的 UNIX 命令,直到单一 UNIX 规范版本 2)。

这基本上是相同的,head -n 1只是它一次读取一个字节以确保它不会读取超过一行。在这些系统上,您可以执行以下操作:

line=`line`

当然,这意味着生成一个新进程,执行命令并通过管道读取其输出,因此效率比 ksh 低很多IFS= read -r line,但仍然直观得多。


1 尽管在可查找输入上,某些实现可以恢复为按块读取,然后作为优化进行回溯。 ksh93 更进一步,它会记住所读取的内容并将其用于下一次read调用目前已损坏

²IFS 空白字符,每个 POSIX 是在语言环境中分类的字符[:space:],并且恰好$IFS在 ksh88(POSIX 规范所基于的)和大多数 shell 中,这仍然限于 SPC、TAB 和 NL。我发现在这方面唯一符合 POSIX 标准的 shell 是yash.ksh93并且bash(自 5.0 起)还包括其他空格(例如 CR、FF、VT...),但仅限于单字节空格(请注意某些系统(如 Solaris),其中包括单字节的不间断空格在某些地区)

答案2

理论

这里有两个概念在起作用:

  • IFS是输入字段分隔符,这意味着读取的字符串将根据 中的字符进行分割IFS。在命令行上,IFS通常是任何空白字符,这就是命令行在空格处分割的原因。
  • 做类似的事情VAR=value command意味着“修改命令环境,使其VAR具有价值value”。基本上,该命令command将被视为VAR具有值value,但此后执行的任何命令仍将被视为VAR具有其先前的值。换句话说,该变量将仅针对该语句进行修改。

在这种情况下

因此,在执行 时IFS= read -r line,您所做的就是设置IFS一个空字符串(不会使用任何字符来拆分,因此不会发生拆分),以便read读取整行并将其视为将分配给变量的一个单词line。更改IFS仅影响该语句,因此任何后续命令都不会受到更改的影响。

作为旁注

虽然命令是正确的并且将按预期工作,但IFS在这种情况下的设置并不正确 可能1不是必要的。正如内置部分bash的手册页中所写read

从标准输入 [...] 读取一行,第一个单词分配给第一个名称,第二个单词分配给第二个名称,依此类推,将剩余单词及其中间分隔符分配给姓氏。如果从输入流中读取的单词少于名称,则其余名称将分配为空值。中的字符IFS用于将行分割成单词。 [...]

由于您只有line变量,因此每个单词都会被分配给它,所以如果您不需要任何前面和后面的空白字符1你可以只写read -r line并完成它。

unset[1] 作为默认值或默认$IFS值如何导致read考虑前导/尾随的示例IFS 空白,你可以尝试:

echo ' where are my spaces? ' | { 
    unset IFS
    read -r line
    printf %s\\n "$line"
} | sed -n l

IFS运行它,你会发现如果不取消设置,前面和后面的字符将不会保留。此外,如果$IFS在脚本的较早位置进行修改,可能会发生一些奇怪的事情。

答案3

您应该分两部分阅读该语句,第一部分清除 IFS 变量的值,即相当于更具可读性IFS="",第二部分是line从 stdin 读取变量read -r line

该语法的具体之处在于 IFS 影响是暂时的并且仅对命令有效read

除非我遗漏了一些东西,否则在这种特殊情况下,清除IFS没有任何效果,尽管无论IFS设置什么,整行都将在line变量中读取。仅当多个变量作为参数传递给指令时,行为才会发生变化read

编辑:

允许-r以 结尾的输入\不进行特殊处理,即反斜杠包含在line变量中而不是作为连续字符以允许多行输入。

$ read line; echo "[$line]"   
abc\
> def
[abcdef]
$ read -r line; echo "[$line]"  
abc\
[abc\]

清除 IFS 具有阻止读取以修剪潜在的前导和尾随空格或制表符的副作用,例如:

$ echo "   a b c   " | { IFS= read -r line; echo "[$line]" ; }   
[   a b c   ]
$ echo "   a b c   " | { read -r line; echo "[$line]" ; }     
[a b c]

感谢 rici 指出了这一差异。

答案4

这是一个好答案从shell语法角度来看:

一个简单的命令是可选变量赋值的序列后跟空格分隔单词和重定向,并由控制操作符终止。第一个字指定要执行的命令,并作为参数零传递。其余单词作为参数传递给调用的命令。

bash参考3.7.4:

任何简单命令或功能的环境都可以通过以下方式暂时增强:在其前面添加参数分配,如 Shell 参数中所述。这些赋值语句仅影响该命令所看到的环境

IFS=""仅可由此处的命令看到read。换句话说,除了这一行之外,IFS不会更改 的值。IFS

相关内容