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

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

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

read "${read_flags[@]}" -srn 1 && key "$REPLY"

的值read_flags已设定像这样:

read_flags=(-t 0.05)

(因此,最终的read调用是read -t 0.05 -srn 1)。

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

read_flags="-t 0.05"
read "$read_flags" -srn 1 && key "$REPLY"

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

经过调查,我想出了一个测试脚本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[@]}"

使用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...)

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

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

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

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

echo 'Using string-based option...'
show string "$opt_string" x y z
date "$opt_string"
echo
echo 'Using array-based option...'
show array "${opt_array[@]}" x y z
date "${opt_array[@]}"

(唯一的区别是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

没有错误。

我一直在寻找这个问题的答案,但徒劳无功。另外,作者还写了这样一段话直接一口气用了数组,这让我想知道。

先感谢您。

9 月 3 日更新:这是我在博客文章中写下了迄今为止我从阅读中学到的东西fff,并且我也引用了这个问题以及其中的精彩答案:探索 fff 第 1 部分 - 主要

答案1

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

但是,首先要做的事情。在这两个示例中,您都按照建议在 shell 变量的取消引用周围放置了引号,无论是"${read_flags[@]}"在数组情况下还是"$read_flags"在标量情况下。建议始终引用 shell 变量的主要原因是为了防止不需要的分词。考虑以下

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

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

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

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


(1) 请注意,使用@(而不是*有很大的不同在这里,因为当引用数组引用时,所有数组元素将显示为好像它们被单独引用一样,因此可以包含空格本身而无需进一步拆分

(2) 原则上,还有第三种选择:存储-t 0.05在标量变量中$opt_string,但将其传递为$opt_string 没有引号。在这种情况下,我们会在空格处进行分词,然后再次items-t0.05,将分别传递给程序。但是,这不是推荐的方法,因为有时您的参数值将具有需要保留的显式空格。

答案2

read_flags="-t 0.05"
read "$read_flags" -srn 1

这里,"$read_flags"是用双引号引起来的,所以不是分词。正如你所看到的,结果与运行相同

read "-t 0.05" -srn 1

这意味着指定的超时确实有一个前导空格。现在,显然 Bash 解析数字时所做的一切都不喜欢这样。

额外空间的作用完全取决于程序。解析数字时,应该很容易忽略任何前导空格,标准strtod()函数就是这样做的。对于date -d,它必须解析更复杂的字符串,因此它对空格不严格也就不足为奇了。 (它可能是类似的东西12:00 Jun 4 2019 UTC + 5 days,而不仅仅是一个数字。)很难说为什么 Bash 在这里如此挑剔。

现在,如果您传递一个文件名,带有前导空格的字符串将是与没有前导空格的字符串不同的文件名,并且任何程序都很难知道忽略它。


使用如此简单的值(没有全局字符以及您想要分割的位置)每个运行空格,假设默认值IFS),您确实可以使用字符串而不是数组,您只需要不是引用它,以便将其分成两个不同的参数。所以,read $read_flags ...。或者只是设置timeoutflag=-t0.05然后read "$timeoutflag" ...。但请注意,这read "$timeoutflag"不是最佳选择,因为如果变量为空,它将作为不同的空参数传递,从而给出错误。

一般来说,数组是存储和使用任意参数列表的正确方法,不会出现任何问题。

有点相关:我们如何运行存储在变量中的命令?

相关内容