【问题标题】:Bash empty array expansion with `set -u`使用 `set -u` 进行 Bash 空数组扩展
【发布时间】:2011-09-28 00:22:35
【问题描述】:

我正在编写一个包含 set -u 的 bash 脚本,但我遇到了空数组扩展的问题: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

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

【问题讨论】:

    标签: bash


    【解决方案1】:

    根据文档,

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

    没有下标被赋值,所以数组没有被设置。

    但是,虽然文档表明此处存在错误是合适的,但 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 测试。

    【讨论】:

    • 谁能解释它是如何以及为什么起作用的?我对 [@]+ 的实际作用以及为什么第二个 ${arr[@]} 不会导致未绑定错误感到困惑。
    • ${parameter+word} 仅在 parameter 未设置时扩展 word
    • ${arr+"${arr[@]}"} 更短,似乎也能正常工作。
    • @Per Cerderberg,不起作用。 unset arrarr[1]=aargs ${arr+"${arr[@]}"}args ${arr[@]+"${arr[@]}"}
    • 准确地说,在+ 扩展不发生(即空数组)的情况下,扩展被替换为nothing,这正是空数组扩展为。 :+ 是不安全的,因为它还将单个元素 ('') 数组视为未设置,并且类似地扩展为空,丢失值。
    【解决方案2】:

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

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

    这已经是ikegami's answer 中的建议,但是这个帖子中有很多错误信息和猜测。其他模式,例如 ${arr[@]-}${arr[@]:0},在所有主要版本的 Bash 中并不安全。

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

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

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

    您可以在this gist 中查看生成此数据的代码以及其他几个 bash 版本的结果。

    【讨论】:

    • 我没有看到你在测试"${arr[@]}"。我错过了什么吗?据我所知,它至少在5.x 中有效。
    • @x-yuri 是的,Bash 4.4 修复了这种情况;如果您知道您的脚本只能在 4.4+ 上运行,则无需使用此模式,但许多系统仍使用早期版本。
    • 当然。尽管看起来不错(例如格式化),但多余的空格是 bash 的一大弊端,会造成很多麻烦
    • 在 bash 4.4.20(1) 上,这不能按预期工作。此答案中的变量扩展引用不计算数组中的项目数。更糟糕的是,它会使变量不被引用。
    • @inetknght 你能分享一个MCVE 你正在观察什么吗?尽管语法很奇怪,但这个 被正确引用了。当数组非空时,外部(未引用)扩展扩展为内部(引用)扩展。
    【解决方案3】:

    @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[@]}"} 表单允许通过添加/不添加冒号来区分空值/非空值。
    • arr=('') && countArgs ${arr[@]:+"${arr[@]}"} -> 0, arr=('') && countArgs ${arr[@]+"${arr[@]}"} -> 1.
    • 很久以前我的回答中已经解决了这个问题。 (事实上​​,我确定我之前已经对此答案发表了评论?!)
    【解决方案4】:

    事实证明,最近发布的 (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[@]}" 就足够了。
    【解决方案5】:

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

    【讨论】:

    • 如果您只是对变量进行插值,这将起作用,但如果您想在 for 中使用数组,则当数组未定义/定义为时,这将得到一个空字符串-empty,如果未定义数组,您可能希望循环体不运行。
    • 感谢@AshBerlin,我在答案中添加了一个 for 循环,以便读者了解
    • -1 对于这种方法,这是不正确的。这用一个空字符串替换了一个空数组,这是不一样的。接受的答案${arr[@]+"${arr[@]}"} 中建议的模式正确地保留了空数组状态。
    • 另见 my answer 显示此扩展失败的情况。
    • 这不是错误的。它明确表示它将给出一个空字符串,甚至还有两个示例可以看到空字符串。
    【解决方案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) 您的里程可能会有所不同。

    【讨论】:

    • ikegami 最初有这个,但因为它在理论上(没有理由应该起作用)和实践中(OP 的 bash 版本不接受它)都不可靠而将其删除。
    • @hvd:感谢您的更新。读者:如果您发现上述代码不起作用的 bash 版本,请添加评论。
    • hvp 已经做到了,我也会告诉你:"${arr[@]:0}" 给了-bash: arr[@]: unbound variable
    • 应该跨版本工作的一件事是将默认数组值设置为arr=("_dummy_"),并在任何地方使用扩展${arr[@]:1}。这在其他答案中提到,指的是哨兵值。
    • @init_js:很遗憾,您的编辑被拒绝了。我建议您添加为单独的答案。 (参考:stackoverflow.com/review/suggested-edits/19027379
    【解决方案7】:

    确实“有趣”的不一致。

    此外,

    $ 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 的每个元素扩展为一个单独的单词。当没有数组成员时,${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[*]})扩展。

    【讨论】:

      【解决方案8】:

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

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

      语法晦涩难懂。请注意,表达式的右侧会进行参数扩展,因此应特别注意引号的一致性。

      : 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 开始。它也不需要 as很多重复。无论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,不会导致“未绑定变量错误”。

      【讨论】:

      • 不幸的是,:0 在 Bash 4.2 中失败了,所以这不是一个安全的方法。见my answer
      【解决方案9】:

      这里有几种方法可以做这样的事情,一种使用哨兵 另一个使用条件附加:

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

      【讨论】:

        【解决方案10】:

        现在,从技术上讲,"${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
        

        切勿出于任何原因使用这种数组扩展。

        【讨论】:

          【解决方案11】:

          有趣的不一致;这使您可以定义“未设置”但显示在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,下次建议编辑而不是投票。顺便说一句,如果您自己尝试过echo $arr[@],您会发现错误消息有所不同。
          【解决方案12】:

          最简单兼容的方式似乎是:

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

          【讨论】:

          • OP 本身表明这不起作用。它扩展为一个空字符串,而不是什么都没有。
          • 对,所以字符串插值可以,但不能循环。
          猜你喜欢
          • 2012-10-10
          • 2015-02-25
          • 1970-01-01
          • 1970-01-01
          • 2012-03-09
          • 2013-05-05
          • 1970-01-01
          相关资源
          最近更新 更多