用`set -u`重击空数组扩展


103

我正在编写一个具有的bash脚本set -u,并且我有一个空数组扩展问题:bash似乎在扩展过程中将空数组视为未设置的变量:

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

declare -a arr也无济于事。)

对此的一种常见解决方案是使用${arr[@]-},从而用一个空字符串代替(“未定义”)空数组。但是,这不是一个好的解决方案,因为现在您无法区分其中有一个空字符串的数组和一个空数组。(@ -expansion在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

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

Answers:


17

唯一的安全习惯用法${arr[@]+"${arr[@]}"}

这已经是ikegami答案中的建议,但是此线程中存在很多错误信息和猜测。其他模式(例如${arr[@]-}或)在Bash的所有主要版本${arr[@]:0}都不安全。

如下表所示,在所有现代Bash版本中可靠的唯一扩展是${arr[@]+"${arr[@]}"}(column +")。值得注意的是,Bash 4.2中还有其他一些扩展失败,包括(不幸的是)较短的${arr[@]:0}惯用语,它不仅会产生错误的结果,而且实际上会失败。如果您需要支持4.4之前的版本,尤其是4.2之前的版本,则这是唯一有效的习惯用法。

跨版本的不同用法的屏幕截图

不幸的+是,乍看之下,其他扩展确实确实发出了不同的行为。:+扩展并不安全,因为:-expansion将具有单个空元素((''))的数组视为“ null”,因此不会(一致)扩展为相同的结果。

"${arr[@]+${arr[@]}}"类似地,在4.2 中用全扩展而不是嵌套数组()是不安全的。

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


1
我看不到你在测试"${arr[@]}"。我想念什么吗?据我所知,它至少在中有效5.x
x-yuri

1
@ x-yuri是的,Bash 4.4解决了这种情况;如果您知道脚本只能在4.4+上运行,则不需要使用此模式,但是许多系统仍在较早版本上。
dimo414

绝对。尽管看起来不错(例如,格式化),但多余的空间却是bash的巨大
弊端

81

根据文档,

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

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

但是,尽管文档显示这里有一个错误是适当的,但自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测试。


4
谁能解释这是如何以及为什么起作用的?我对[@]+实际上是什么以及为什么第二个${arr[@]}不会引起未绑定的错误感到困惑。
Martin von Wittich '16

2
${parameter+word}wordparameter未设置时扩展。
ikegami

2
${arr+"${arr[@]}"}较短,似乎效果也一样。
Per Cederberg

3
@Per Cerderberg,不起作用。unset arrarr[1]=aargs ${arr+"${arr[@]}"}VSargs ${arr[@]+"${arr[@]}"}
池上

1
确切地说,在+不发生扩展的情况下(即,空数组),该扩展将替换为none,这正是空数组扩展为的内容。:+这是不安全的,因为它还将单个元素('')数组视为未设置,并且类似地扩展为空,从而丢失了值。
dimo414

23

@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...

不再有所作为。bash-4.4.23arr=('') && countArgs "${arr[@]:+${arr[@]}}"产生1。但是${arr[@]+"${arr[@]}"}形式允许通过添加/不添加冒号来区分空值/非空值。
x-yuri

arr=('') && countArgs ${arr[@]:+"${arr[@]}"}-> 0arr=('') && countArgs ${arr[@]+"${arr[@]}"}-> 1
x-yuri

1
很久以前我的答案中已解决此问题。(实际上,我敢肯定我之前已经对此效果发表了评论?!)
ikegami

16

事实证明,数组处理已在最近发布的(2016/09/16)bash 4.4中进行了更改(例如,在Debian Stretch中可用)。

$ 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

我可以确认,bash-4.4.12 "${arr[@]}"就足够了。
x-yuri

14

对于那些不希望重复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

10
如果您只是内插变量,这将起作用,但是如果您想在数组中使用数组,for那么当数组未定义/定义为空时,它将以一个空字符串结尾,而您可能希望使用循环体如果未定义数组,则不运行。
Ash Berlin-Taylor

谢谢@AshBerlin,我在答案中添加了一个for循环,以便使读者知道
Jayen

-1表示这种方法,这是完全错误的。这用一个空字符串替换一个空数组,这是不一样的。接受的答案中建议的模式${arr[@]+"${arr[@]}"}正确保留了空数组状态。
dimo414

另请参阅我的答案,以显示此扩展失败的情况。
dimo414

这是不正确的。它明确表示将提供一个空字符串,甚至还有两个示例可以在其中看到空字符串。
Jayen

7

@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) 您的里程可能会有所不同。


9
ikegami最初具有此功能,但由于从理论上(没有理由不起作用)和实践中(OP的bash版本不接受)的可靠性,将其删除。

@hvd:感谢您的更新。读者:如果您发现上面的代码不起作用的bash版本,请添加注释。
kevinarpe 2015年

hvp已经做到了,我也会告诉您:"${arr[@]:0}"gives -bash: arr[@]: unbound variable
ikegami 2015年

跨版本应该工作的一件事是将默认数组值设置为arr=("_dummy_"),并在${arr[@]:1}所有地方使用扩展。在其他答案中提到了哨兵值。
init_js

1
@init_js:您的编辑被遗憾地拒绝了。我建议您添加为单独的答案。(参考:stackoverflow.com/review/suggested-edits/19027379
kevinarpe

6

确实是“有趣的”矛盾。

此外,

$ 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解释的意义上当前的行为可能不是错误,但IMO我们可以说该错误是在(“集合”的)定义本身中,和/或不一致地应用的事实。手册页中的上一段说

... ${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[*]})扩展。


2

我补充上@池上的(接受)和@ kevinarpe的(也不错)的答案。

您可以"${arr[@]:+${arr[@]}}"解决该问题。右侧(即after :+)提供了一个表达式,该表达式将在左侧未定义/为空的情况下使用。

语法是奥术。请注意,表达式的右侧将进行参数扩展,因此应特别注意保持一致的引用。

: 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 Expansion下):

  • ${parameter:offset}

  • ${parameter:offset:length}

    ... 如果parameter是带下标@或的索引数组名称*,则结果是以开头的数组的长度成员${parameter[offset]}。相对于指定数组的最大索引的一个负偏移量。如果length的值小于零,则是扩展错误。

这是@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,而不会引起“未绑定变量错误”。


不幸的是,这个:0习惯用法在Bash 4.2中失败了,所以这不是一个安全的方法。看我的回答
dimo414

1

以下是执行类似操作的两种方法,一种使用前哨,另一种使用条件追加:

#!/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[@]}"

0

有趣的不一致;这使您可以定义一些“不认为已设置”但仍显示在declare -p

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

更新:正如其他人所提到的,此答案发布后已在4.4中修复。


那只是不正确的数组语法。您需要echo ${arr[@]}(但是在Bash 4.4之前,您仍然会看到错误)。
dimo414

感谢@ dimo414,下次建议您进行修改,而不要投票。顺便说一句,如果您尝试过,echo $arr[@]您将看到错误消息是不同的。
MarcH

-2

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

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

1
OP本身表明这是行不通的。它扩展为空字符串,而不是空字符串。
ikegami

是的,因此可以插值但不循环。
克雷格·林格
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.