Bash 在基于字符串的超时选项规范上读取内置错误,而不是基于数组的超时选项规范。为什么?

qma*_*cro 4 bash shell-script quoting shell-builtin bash-array

在阅读fff的源代码以了解有关 Bash 编程的更多信息时,我看到一个超时选项read作为数组传递给这里

read "${read_flags[@]}" -srn 1 && key "$REPLY"
Run Code Online (Sandbox Code Playgroud)

的值read_flags设置如下

read_flags=(-t 0.05)
Run Code Online (Sandbox Code Playgroud)

read因此,预期的调用是read -t 0.05 -srn 1)。

我不太明白为什么不能使用字符串,即:

read_flags="-t 0.05"
read "$read_flags" -srn 1 && key "$REPLY"
Run Code Online (Sandbox Code Playgroud)

这种基于字符串的方法会导致“无效的超时规范”。

经过调查,我想出了一个测试脚本parmtest

show() {
  for i in "$@"; do printf '[%s]' "$i"; done
  printf '\n'
}

opt_string="-t 1"
opt_array=(-t 1)

echo 'Using string-based option...'
show string "$opt_string" x y z
read "$opt_string"
echo
echo 'Using array-based option...'
show array "${opt_array[@]}" x y z
read "${opt_array[@]}"
Run Code Online (Sandbox Code Playgroud)

使用bash parmtest( $BASH_VERSIONis 5.1.4(1)-release) 运行它,给出:

Using string-based option...
[string][-t 1][x][y][z]
parmtest: line 11: read:  1: invalid timeout specification

Using array-based option...
[array][-t][1][x][y][z]
(1 second delay...)
Run Code Online (Sandbox Code Playgroud)

我可以从调试输出中看到,1基于数组的方法中的值是独立的并且没有空格。我还可以从错误消息中看到1:之前有一个额外的空间read: 1: invalid timeout specification。我的怀疑在那个领域。

奇怪的是,如果我将此方法与另一个命令一起使用,例如date,问题不存在:

Using string-based option...
[string][-t 1][x][y][z]
parmtest: line 11: read:  1: invalid timeout specification

Using array-based option...
[array][-t][1][x][y][z]
(1 second delay...)
Run Code Online (Sandbox Code Playgroud)

(唯一的区别是opt_stringopt_array现在指定-dnot-t并且我在每种情况下都调用datenot read)。

运行时bash parmtest会产生:

Using string-based option...
[string][-d 1][x][y][z]
Wed Sep  1 01:00:00 UTC 2021

Using array-based option...
[array][-d][1][x][y][z]
Wed Sep  1 01:00:00 UTC 2021
Run Code Online (Sandbox Code Playgroud)

没有错误。

我已经搜索过,但徒劳无功,找到了这个问题的答案。而且,作者直接一口气写了这个,马上就用了一个数组,这让我很纳闷。

先感谢您。

9 月 3 日更新:这是我写下到目前为止从通读中学到的东西的博客文章fff,我也参考了这个问题和其中的好答案:探索 fff 第 1 部分 - 主要

Adm*_*Bee 11

原因是read内置函数和date命令解释它们的命令行参数的方式不同。

建议始终引用您的 shell 变量的主要原因是为了防止不必要的分词。考虑以下

  • 您有一个My favorite songs.txt包含空格的文件,并希望将其移动到目录playlists/.
  • 如果将文件名存储在变量中$fname并调用
    mv $fname playlists/
    
    Run Code Online (Sandbox Code Playgroud)mv命令会看到4个参数:Myfavoritesongs.txtplaylists/和试运行三个不存在的文件Myfavoritesongs.txt到该目录playlists/。显然不是你想要的。
  • 相反,如果您将$fname引用放在双引号中,如
    mv "$fname" playlists/
    
    Run Code Online (Sandbox Code Playgroud) 它确保 shell 将包括空格在内的整个字符串作为一个词传递给mv,以便它识别出它只是一个需要移动的文件(尽管名称中有空格)。

现在,您希望将选项参数存储在 shell 变量中。这些很棘手,因为有时它们很长,有时很短,有时它们会取值。关于如何指定带参数的选项的方法有很多种,通常如何解析它们完全由程序员自行决定(请参阅此问答)进行讨论)。因此,Bash 的read内置date命令和命令反应不同的原因可能在于这两者如何解析其命令行参数的内部工作。不过,我们可以稍微推测一下。

  • 当存储-t 0.05在标量 shell 变量中并将其作为 传递时"$opt_string",接收者会将其视为一个包含空格的字符串(见上文)。
  • 当将-t和存储0.05在数组变量中并将其作为"${opt_array[@]}"接收者传递时,会将其视为两个单独的项目,即-t0.05(1)
  • 许多程序将使用getopt()GNU C 库中的函数来解析命令行参数,这是 POSIX 指南所推荐的。
  • getopt()区分“短”选项和“长”选项的格式,如date -udate --utc在的情况下date命令。选项(例如/ )的选项的解释方式通常是or短选项和/或长选项。-o--optiongetopt-ovalue-o value--option=value--option value
  • 当路过-t 0.052个字的工具使用getopt(),它会第一个字符后,-作为选项名称和下一个单词的选项值(语法)。因此,将作为选项名称和选项值。-o valuereadt0.05
  • -t 0.05作为一个词传递时,它将被解释为语法:将(再次)将 the 之后的第一个字符作为选项名称,并将字符串的其余部分作为选项值,因此该值将带有前导空格。-ovaluegetopt()- 0.05
  • read命令显然不接受带有前导空格的超时规范。事实上,如果你打电话
    read -t " 0.05" -srn 1
    
    Run Code Online (Sandbox Code Playgroud) 其中该值明确是一个带前导空格的字符串,read 也会抱怨这个。

作为结论date当涉及到选项值时,该命令显然以更宽松的方式编写,-d并且不关心值字符串是否以空格开头。这可能并不意外,因为日期规范可以采用的值非常多样化,这与超时规范(显然)需要是数字的情况相反。


(1) 请注意,使用@(相对于*)在这里有很大的不同,因为当引用数组引用时,所有数组元素将看起来好像它们被单独引用一样,因此可以包含空格而不会被进一步拆分