我正在编写一个包含set -u
的bash脚本,我遇到了空数组扩展的问题:在扩展期间,bash似乎将空数组视为未设置的变量:
$ set -u
$ arr=()
$ echo "foo: '${arr[@]}'"
bash: arr[@]: unbound variable
(declare -a arr
也无济于事。)
对此的常见解决方案是使用${arr[@]-}
代替,从而替换空字符串而不是(“undefined”)空数组。然而,这不是一个好的解决方案,因为现在你无法辨别出一个带有一个空字符串的数组和一个空数组。 (@ -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
更新:由于ikegami的解释,删除了bugs
代码。
答案 0 :(得分:64)
根据文件,
如果为下标指定了值,则认为数组变量已设置。空字符串是有效值。
没有为下标分配值,因此未设置数组。
但是虽然文档表明此处存在错误,但情况不再是since 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进行测试。
答案 1 :(得分:26)
${arr[@]+"${arr[@]}"}
这已经是ikegami's answer中的建议,但是此线程中存在很多错误信息和猜测。其他模式,例如${arr[@]-}
或${arr[@]:0}
在Bash的所有主要版本中都不安全。
如下表所示,在所有现代Bash版本中可靠的唯一扩展是${arr[@]+"${arr[@]}"}
(列+"
)。值得注意的是,Bash 4.2中还有其他一些扩展失败,包括(不幸的是)较短的${arr[@]:0}
习惯用法,它不仅会产生错误的结果,而且实际上会失败。 如果您需要支持4.4之前的版本,特别是4.2,则这是唯一有效的习惯用法。
不幸的是,乍看之下看起来相同的其他+
扩展确实产生了不同的行为。 :+
扩展不是是安全的,因为:
-expansion将具有单个空元素(('')
)的数组视为“ null”,因此不会(一致)扩展到相同的结果。
用完全扩展而不是嵌套数组("${arr[@]+${arr[@]}}"
)来代替,在我看来,嵌套数组(norms = [np.linalg.norm(m, ord='fro') for m in mats]
)在4.2中同样不安全。
您可以在this gist中看到生成此数据的代码以及bash几个其他版本的结果。
答案 2 :(得分:22)
@ 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...
答案 3 :(得分: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
答案 4 :(得分:13)
在最近发布的(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
答案 5 :(得分:6)
@ 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)
您的里程可能会有所不同。
答案 6 :(得分: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我们可以说错误在定义(" 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[*]}
)扩展。
答案 7 :(得分:2)
以下是一些使用哨兵的方法 另一个使用条件追加:
#!/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[@]}"
答案 8 :(得分:2)
我正在补充@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是由
@
或*
下标的索引数组名称,则结果是以${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
,而不会导致“未绑定的变量错误”。
答案 9 :(得分:1)
有趣的不一致;这使您可以定义“未被视为已设置”但仍显示在declare -p
arr=()
set -o nounset
echo $arr[@]
=> -bash: arr[@]: unbound variable
declare -p arr
=> declare -a arr='()'
答案 10 :(得分:-2)
最简单和兼容的方式似乎是:
$ set -u
$ arr=()
$ echo "foo: '${arr[@]-}'"