使用`set -u`猛击空数组扩展

时间:2011-09-28 00:22:36

标签: bash

我正在编写一个包含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代码。

11 个答案:

答案 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,则这是唯一有效的习惯用法。

Screenshot of different idioms across versions

不幸的是,乍看之下看起来相同的其他+扩展确实产生了不同的行为。 :+扩展不是是安全的,因为:-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[@]-}'"