使用‘ set-u’对空数组进行 Bash 扩展

我正在编写一个 bash 脚本,其中包含 set -u,并且我遇到了一个空数组展开的问题: bash 在展开过程中似乎将一个空数组作为一个未设置的变量:

$ set -u
$ arr=()
$ echo "foo: '${arr[@]}'"
bash: arr[@]: unbound variable

(declare -a arr也没有帮助。)

一个常见的解决方案是使用 ${arr[@]-}代替,因此用空字符串代替(“未定义”)空数组。然而,这不是一个好的解决方案,因为现在您无法区分一个包含单个空字符串的数组和一个空数组。(在 bash 中,@扩展是特殊的,它将 "${arr[@]}"扩展为 "${arr[0]}" "${arr[1]}" …,这使它成为构建命令行的完美工具。)

$ countArgs() { echo $#; }
$ countArgs a b c
3
$ countArgs
0
$ countArgs ""
1
$ brr=("")
$ countArgs "${brr[@]}"
1
$ countArgs "${arr[@]-}"
1
$ countArgs "${arr[@]}"
bash: arr[@]: unbound variable
$ set +u
$ countArgs "${arr[@]}"
0

那么,除了检查 if中数组的长度(参见下面的代码示例) ,或者关闭该短片的 -u设置之外,还有其他解决这个问题的方法吗?

if [ "${#arr[@]}" = 0 ]; then
veryLongCommandLine
else
veryLongCommandLine "${arr[@]}"
fi

更新: 由于 ikegami 的解释删除了 bugs标签。

25690 次浏览

根据文件记录,

如果下标已分配值,则认为已设置数组变量。空字符串是有效值。

没有为下标分配值,因此没有设置数组。

但是,虽然文档表明这里有一个错误是合适的,但是 从4.4开始不再是这种情况。

$ bash --version | head -n 1
GNU bash, version 4.4.19(1)-release (x86_64-pc-linux-gnu)


$ set -u


$ arr=()


$ echo "foo: '${arr[@]}'"
foo: ''

有一个条件,您可以使用内联来实现您想要的旧版本: 使用 ${arr[@]+"${arr[@]}"}而不是 "${arr[@]}"

$ function args { perl -E'say 0+@ARGV; say "$_: $ARGV[$_]" for 0..$#ARGV' -- "$@" ; }


$ set -u


$ arr=()


$ args "${arr[@]}"
-bash: arr[@]: unbound variable


$ args ${arr[@]+"${arr[@]}"}
0


$ arr=("")


$ args ${arr[@]+"${arr[@]}"}
1
0:


$ arr=(a b c)


$ args ${arr[@]+"${arr[@]}"}
3
0: a
1: b
2: c

使用 bash 4.2.25和4.3.11进行测试。

这里有几种方法可以做到这一点,其中一种是使用哨兵 另一个使用条件附加:

#!/bin/bash
set -o nounset -o errexit -o pipefail
countArgs () { echo "$#"; }


arrA=( sentinel )
arrB=( sentinel "{1..5}" "./*" "with spaces" )
arrC=( sentinel '$PWD' )
cmnd=( countArgs "${arrA[@]:1}" "${arrB[@]:1}" "${arrC[@]:1}" )
echo "${cmnd[@]}"
"${cmnd[@]}"


arrA=( )
arrB=( "{1..5}" "./*"  "with spaces" )
arrC=( '$PWD' )
cmnd=( countArgs )
# Checks expansion of indices.
[[ ! ${!arrA[@]} ]] || cmnd+=( "${arrA[@]}" )
[[ ! ${!arrB[@]} ]] || cmnd+=( "${arrB[@]}" )
[[ ! ${!arrC[@]} ]] || cmnd+=( "${arrC[@]}" )
echo "${cmnd[@]}"
"${cmnd[@]}"

对于那些不愿意复制 arr [@]并且可以使用 空字符串的人来说,这可能是另一种选择

echo "foo: '${arr[@]:-}'"

测试:

set -u
arr=()
echo a "${arr[@]:-}" b # note two spaces between a and b
for f in a "${arr[@]:-}" b; do echo $f; done # note blank line between a and b
arr=(1 2)
echo a "${arr[@]:-}" b
for f in a "${arr[@]:-}" b; do echo $f; done

@ ikegami 的回答是正确的,但是我认为 ${arr[@]+"${arr[@]}"}的语法很糟糕。如果使用长数组变量名,那么它看起来比平时更像意大利面条。

试试这个:

$ set -u


$ count() { echo $# ; } ; count x y z
3


$ count() { echo $# ; } ; arr=() ; count "${arr[@]}"
-bash: abc[@]: unbound variable


$ count() { echo $# ; } ; arr=() ; count "${arr[@]:0}"
0


$ count() { echo $# ; } ; arr=(x y z) ; count "${arr[@]:0}"
3

看起来 Bash 数组切片操作符非常宽容。

那么为什么 Bash 使处理数组的边缘情况变得如此困难呢?我不能保证你的版本会允许这种滥用的数组切片运算符,但它工程花哨为我。

注意: 我正在使用 GNU bash, version 3.2.25(1)-release (x86_64-redhat-linux-gnu) 你的情况可能会有所不同。

有趣的不一致性; 这使您可以定义一些“尚未考虑设置”但在 declare -p的输出中显示的内容

arr=()
set -o nounset
echo ${arr[@]}
=>  -bash: arr[@]: unbound variable
declare -p arr
=>  declare -a arr='()'

更新: 正如其他人提到的,固定在4.4发布后,这个答案被张贴。

@ ikegami 公认的答案是微妙的错误! 正确的咒语是 ${arr[@]+"${arr[@]}"}:

$ countArgs () { echo "$#"; }
$ arr=('')
$ countArgs "${arr[@]:+${arr[@]}}"
0   # WRONG
$ countArgs ${arr[@]+"${arr[@]}"}
1   # RIGHT
$ arr=()
$ countArgs ${arr[@]+"${arr[@]}"}
0   # Let's make sure it still works for the other case...

真是“有趣”的矛盾。

而且,

$ set -u
$ echo $#
0
$ echo "$1"
bash: $1: unbound variable   # makes sense (I didn't set any)
$ echo "$@" | cat -e
$                            # blank line, no error

虽然我同意当前的行为可能不是像@ikegami 解释的那样是一个 bug,但是我们可以说 bug 在于(“ set”的)定义本身,以及/或者它的应用不一致的事实。手册页中的前一段说明

... ${name[@]}将每个名称元素扩展为一个单独的单词。当没有数组成员时,${name[@]}扩展为零。

这与 "$@"中位置参数的展开完全一致。并不是说在数组和位置参数的行为中没有其他不一致的地方... ... 但是对我来说,没有任何迹象表明这两者之间的细节应该是不一致的。

继续,

$ arr=()
$ echo "${arr[@]}"
bash: arr[@]: unbound variable   # as we've observed.  BUT...
$ echo "${#arr[@]}"
0                                # no error
$ echo "${!arr[@]}" | cat -e
$                                # no error

所以 arr[]不是 所以未绑定的,所以我们不能得到它的元素(0)的计数,或者它的键的(空)列表?对我来说,这些都是合理的,有用的——唯一的例外似乎是 ${arr[@]}(和 ${arr[*]})扩展。

原来数组处理在最近发布的 bash 4.4(2016/09/16)中已经发生了变化(例如,可以在 Debian 区域中使用)。

$ bash --version | head -n1
bash --version | head -n1
GNU bash, version 4.4.0(1)-release (x86_64-pc-linux-gnu)

现在空数组展开不会发出警告

$ set -u
$ arr=()
$ echo "${arr[@]}"


$ # everything is fine

最简单和兼容的方法似乎是:

$ set -u
$ arr=()
$ echo "foo: '${arr[@]-}'"

我在补充 @ ikegami’s(接受)和 @ kevinarpe’s(也很好)的答案。

你可以用 "${arr[@]:+${arr[@]}}"来解决这个问题。右边(例如,在 :+之后)提供了一个表达式,在左边未定义/null 的情况下将使用该表达式。

语法很复杂。注意,表达式的右边将进行参数展开,因此应该特别注意保持引号的一致性。

: example copy arr into arr_copy
arr=( "1 2" "3" )
arr_copy=( "${arr[@]:+${arr[@]}}" ) # good. same quoting.
# preserves spaces


arr_copy=( ${arr[@]:+"${arr[@]}"} ) # bad. quoting only on RHS.
# copy will have ["1","2","3"],
# instead of ["1 2", "3"]

就像@kevinarpe 提到的,一个不那么神秘的语法是使用数组片符号 ${arr[@]:0}(在 Bash 版本 >= 4.4上) ,它从索引0开始扩展到所有参数。也不需要太多的重复。无论 set -u如何,这个扩展都可以工作,因此您可以在任何时候使用它。手册页(在 参数展开下)显示:

  • ${parameter:offset}

  • ${parameter:offset:length}

    ... 如果参数是由 @*订阅的索引数组名称,则结果为以 ${parameter[offset]}开头的数组的长度成员。一个负偏移量相对于一个负偏移量 大于指定数组的最大索引。它是 如果长度计算为小于零的数字,则为展开错误。

这是@kevinarpe 提供的示例,使用替代格式将输出置于证据中:

set -u
function count() { echo $# ; };
(
count x y z
)
: prints "3"


(
arr=()
count "${arr[@]}"
)
: prints "-bash: arr[@]: unbound variable"


(
arr=()
count "${arr[@]:0}"
)
: prints "0"


(
arr=(x y z)
count "${arr[@]:0}"
)
: prints "3"

这种行为因 Bash 的不同版本而异。您可能还注意到,对于空数组,长度运算符 ${#arr[@]}总是求值为 0,而不管 set -u是什么,而不会导致“未绑定变量错误”。

只有安全成语是 ${arr[@]+"${arr[@]}"}

除非您只关心 Bash 4.4 + ,但是如果是这样的话,您就不会看到这个问题:)

这已经是 池上的回答中的建议,但是在这个线程中有很多错误信息和猜测。其他模式,如 ${arr[@]-}${arr[@]:0},在所有主要版本的 Bash 中都是 没有安全的。

如下表所示,在所有现代版的 Bash 版本中唯一可靠的扩展是 ${arr[@]+"${arr[@]}"}(列 +")。值得注意的是,Bash 4.2中的其他几个扩展失败了,包括(不幸的是)较短的 ${arr[@]:0}习惯用法,它不仅产生了不正确的结果,而且实际上失败了。如果您需要支持4.4之前的版本,特别是4.2之前的版本,那么这是唯一可用的习惯用法。

Screenshot of different idioms across versions

不幸的是,其他看起来相同的 +扩展确实会产生不同的行为。例如,使用 :+而不是 +(表中的 :+")不起作用,因为 :-扩展将带有单个空元素((''))的数组视为“ null”,因此不会(始终如一地)扩展到相同的结果。

引用完整的扩展而不是嵌套数组(表中的 "${arr[@]+${arr[@]}}""+)(我原本以为它们大致相等)在4.2中同样是不安全的。

您可以在 这个要点中看到生成此数据的代码以及其他几个 bash 版本的结果。

现在,正如技术上正确的“ ${ arr [@] + “ ${ arr [@]}”版本一样,您永远不会想要使用这种语法来附加到数组中,永远不会!

这是因为这个语法实际上扩展了数组,然后附加。 这意味着在计算和内存方面有很多事情要做!

为了证明这一点,我做了一个简单的比较:

# cat array_perftest_expansion.sh
#! /usr/bin/bash


set -e
set -u


loops=$1


arr=()
i=0


while [ $i -lt $loops ] ; do
arr=( ${arr[@]+"${arr[@]}"} "${i}" )
#arr=arr[${#arr[@]}]="${i}"


i=$(( i + 1 ))
done


exit 0

然后:

# timex ./array_perftest_expansion.sh 1000


real           1.86
user           1.84
sys            0.01

但如果启用了第二行,只需直接设置最后一个条目:

arr=arr[${#arr[@]}]="${i}"






# timex ./array_perftest_last.sh 1000


real           0.03
user           0.02
sys            0.00

如果这还不够,当您试图添加更多条目时,情况会变得更糟!

当使用4000而不是1000个循环时:

# timex ./array_perftest_expansion.sh 4000


real          33.13
user          32.90
sys            0.22

正在设置最后一项:

# timex ./array_perftest_last.sh 4000


real           0.10
user           0.09
sys            0.00

而且这变得越来越糟... 我不能等待扩展版本完成一个10000的循环!

用最后一个元素代替:

# timex ./array_perftest_last.sh 10000


real           0.26
user           0.25
sys            0.01

不要以任何理由使用这样的数组扩展。