在阅读源代码时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_VERSION
is 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_string
和opt_array
现在指定-d
not-t
并且我在每种情况下都调用date
not 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
命令将看到四参数:My
、favorite
、songs.txt
和playlists/
并尝试将三个不存在的文件My
、favorite
和移动songs.txt
到目录playlists/
。显然不是你想要的。 - 相反,如果将
$fname
引用放在双引号中,如
它确保 shell 传递整个字符串,包括空格一word tomv "$fname" playlists/
mv
,以便它识别出这只是一个需要移动的文件(尽管其名称中带有空格)。
现在你想要存储的情况选项shell 变量中的参数。这些很棘手,因为有时它们很长,有时很短,有时它们需要一个值。有多种方法可以指定带参数的选项,通常如何解析它们完全由程序员决定(看本次问答)进行讨论)。因此,Bash 的read
内置函数和命令的反应不同的原因date
可能在于这两者如何解析其命令行参数的内部工作方式。不过,我们可以稍微推测一下。
- 当存储
-t 0.05
在标量 shell 变量中并将其作为 传递时"$opt_string"
,接收者会将其视为包含空格的字符串(见上文)。 - 当将
-t
和存储0.05
在数组变量中并将其作为"${opt_array[@]}"
接收者传递时,会将其视为两个单独的项目,即-t
和0.05
。(1) (2) - 许多程序将使用
getopt()
GNU C 库中的函数来解析命令行参数,正如 POSIX 指南所建议的那样。 - 例如,在命令的情况下,区分
getopt()
“短”选项和“长”选项格式。方式选项date -u
date --utc
date
价值观选项(例如-o
/ )通常--option
由 is 解释,对于短选项和或对于长选项。getopt
-ovalue
-o value
--option=value
--option value
- 当传递
-t 0.05
为二对于使用 的工具getopt()
,它将采用 后的第一个字符-
作为选项名称,下一个单词作为选项值(语法)。因此,将作为选项名称和选项值。-o value
read
t
0.05
- 当传递
-t 0.05
为一单词,它将被解释为语法:将(再次)将 the 之后的第一个字符作为选项名称,将字符串的其余部分作为选项值,因此该值将是-ovalue
getopt()
-
0.05
有前导空格。 - 该
read
命令显然不接受带有前导空格的超时规范。事实上,如果你打电话
其中该值明确是一个带有前导空格的字符串,read -t " 0.05" -srn 1
read
还对此有所抱怨。
作为结论date
,当涉及到选项值时,该命令显然以更宽松的方式编写-d
,并且不关心值字符串是否以空格开头。这也许并不意外,因为日期规范可以采用的值非常多样化,这与(显然)需要是数字的超时规范的情况相反。
(1) 请注意,使用@
(而不是*
)有很大的不同在这里,因为当引用数组引用时,所有数组元素将显示为好像它们被单独引用一样,因此可以包含空格本身而无需进一步拆分。
(2) 原则上,还有第三种选择:存储-t 0.05
在标量变量中$opt_string
,但将其传递为$opt_string
没有引号。在这种情况下,我们会在空格处进行分词,然后再次二items-t
和0.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"
不是最佳选择,因为如果变量为空,它将作为不同的空参数传递,从而给出错误。
一般来说,数组是存储和使用任意参数列表的正确方法,不会出现任何问题。
有点相关:我们如何运行存储在变量中的命令?