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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/7577052/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-18 00:53:01  来源:igfitidea点击:

Bash empty array expansion with `set -u`

bash

提问by Ivan Tarasov

I'm writing a bash script which has set -u, and I have a problem with empty array expansion: bash appears to treat an empty array as an unset variable during expansion:

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

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

(declare -a arrdoesn't help either.)

declare -a arr也没有帮助。)

A common solution to this is to use ${arr[@]-}instead, thus substituting an empty string instead of the ("undefined") empty array. However this is not a good solution, since now you can't discern between an array with a single empty string in it and an empty array. (@-expansion is special in bash, it expands "${arr[@]}"into "${arr[0]}" "${arr[1]}" …, which makes it a perfect tool for building command lines.)

对此的常见解决方案是使用${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

So is there a way around that problem, other than checking the length of an array in an if(see code sample below), or turning off -usetting for that short piece?

那么有没有办法解决这个问题,除了检查一个数组的长度if(见下面的代码示例),或者关闭-u那个短片的设置?

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

Update:Removed bugstag due to explanation by ikegami.

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

采纳答案by dimo414

The onlysafe idiom is ${arr[@]+"${arr[@]}"}

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

This is already the recommendation in the accepted answer, but there's a lot of misinformation and guesswork in this thread. Other patterns, such as ${arr[@]-}or ${arr[@]:0}, are notsafe across all major versions of Bash.

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

As the table below shows, the only expansion that is reliable across all modern-ish Bash versions is ${arr[@]+"${arr[@]}"}(column +"). Of note, several other expansions fail in Bash 4.2, including (unfortunately) the shorter ${arr[@]:0}idiom, which doesn't just produce an incorrect result but actually fails. If you need to support versions prior to 4.4, and in particular 4.2, this is the only working idiom.

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

Screenshot of different idioms across versions

跨版本不同习语的屏幕截图

Unfortunately other +expansions that, at a glance, look the same do indeed emit different behavior. :+expansion is notsafe, because :-expansiontreats an array with a single empty element (('')) as "null" and thus doesn't (consistently) expand to the same result.

不幸的+是,乍一看看起来相同的其他扩展确实会发出不同的行为。:+扩张是安全的,因为:-expansion对待的阵列与单个空元素((''))为“空”,并因此不(一致地)扩展到相同的结果。

Quoting the full expansion instead of the nested array ("${arr[@]+${arr[@]}}"), which I would have expected to be roughly equivalent, is similarly unsafe in 4.2.

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

You can see the code that generated this data along with results for several additional version of bash in this gist.

您可以在此要点中查看生成此数据的代码以及几个附加版本的 bash 的结果。

回答by ikegami

According to the documentation,

根据文档,

An array variable is considered set if a subscript has been assigned a value. The null string is a valid value.

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

No subscript has been assigned a value, so the array isn't set.

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

But while the documentation suggests an error is appropriate here, this is no longer the case since 4.4.

但是,虽然文档表明这里有一个错误是合适的,但自 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: ''


There is a conditional you can use inline to achieve what you want in older versions: Use ${arr[@]+"${arr[@]}"}instead of "${arr[@]}".

您可以使用内联条件在旧版本中实现您想要的功能:使用${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

Tested with bash 4.2.25 and 4.3.11.

使用 bash 4.2.25 和 4.3.11 进行测试。

回答by ijs

@ikegami's accepted answer is subtly wrong! The correct incantation is ${arr[@]+"${arr[@]}"}:

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

回答by agg3l

Turns out array handling has been changed in recently released (2016/09/16) bash 4.4 (available in Debian stretch, for example).

事实证明,最近发布的(2016 年 9 月 16 日)bash 4.4 中的数组处理已更改(例如,在 Debian 中可用)。

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

Now empty arrays expansion does not emits warning

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

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

$ # everything is fine

回答by Jayen

this may be another option for those who prefer not to duplicate arr[@] and are okay to have an empty string

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

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

to test:

去测试:

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

回答by kevinarpe

@ikegami's answer is correct, but I consider the syntax ${arr[@]+"${arr[@]}"}dreadful. If you use long array variable names, it starts to looks spaghetti-ish quicker than usual.

@ikegami 的回答是正确的,但我认为语法很${arr[@]+"${arr[@]}"}糟糕。如果你使用长数组变量名,它开始看起来比平常更快。

Try this instead:

试试这个:

$ 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

It looks like the Bash array slice operator is very forgiving.

看起来 Bash 数组切片运算符非常宽容。

So why did Bash make handling the edge case of arrays so difficult? Sigh.I cannot guarantee you version will allow such abuse of the array slice operator, but it works dandy for me.

那么为什么 Bash 使处理数组的边缘情况如此困难? 叹。我不能保证你的版本会允许这样滥用数组切片运算符,但它对我来说很管用。

Caveat: I am using GNU bash, version 3.2.25(1)-release (x86_64-redhat-linux-gnu)Your mileage may vary.

警告:我正在使用GNU bash, version 3.2.25(1)-release (x86_64-redhat-linux-gnu)您的里程可能会有所不同。

回答by don311

"Interesting" inconsistency indeed.

确实“有趣”的不一致。

Furthermore,

此外,

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

While I agree that the current behavior may not be a bug in the sense that @ikegami explains, IMO we could say the bug is in the definition (of "set") itself, and/or the fact that it's inconsistently applied. The preceding paragraph in the man page says

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

... ${name[@]}expands each element of name to a separate word. When there are no array members, ${name[@]}expands to nothing.

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

which is entirely consistent with what it says about the expansion of positional parameters in "$@". Not that there aren't other inconsistencies in the behaviors of arrays and positional parameters... but to me there's no hint that this detail should be inconsistent between the two.

这与它所说的关于 中位置参数的扩展完全一致"$@"。并不是说数组和位置参数的行为没有其他不一致之处……但对我来说,没有迹象表明这两者之间的细节应该不一致。

Continuing,

继续,

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

So arr[]isn't sounbound that we can't get a count of its elements (0), or a (empty) list of its keys? To me these are sensible, and useful -- the only outlier seems to be the ${arr[@]}(and ${arr[*]}) expansion.

所以,arr[]是不是这样结合的,我们不能得到它的元素(0),或它的键的(空)列表的数量?对我来说,这些是明智的、有用的——唯一的异常值似乎是${arr[@]}(和${arr[*]})扩展。

回答by init_js

I am complementing on @ikegami's(accepted) and @kevinarpe's(also good) answers.

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

You can do "${arr[@]:+${arr[@]}}"to workaround the problem. The right-hand-side (i.e., after :+) provides an expression that will be used in case the left-hand-side is not defined/null.

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

The syntax is arcane. Note that the right hand side of the expression will undergo parameter expansion, so extra attention should be paid to having consistent quoting.

语法很神秘。请注意,表达式的右侧将进行参数扩展,因此应特别注意引用的一致性。

: 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"]

Like @kevinarpe mentions, a less arcane syntax is to use the array slice notation ${arr[@]:0}(on Bash versions >= 4.4), which expands to all the parameters, starting from index 0. It also doesn't require as much repetition. This expansion works regardless of set -u, so you can use this at all times. The man page says (under Parameter Expansion):

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

  • ${parameter:offset}

  • ${parameter:offset:length}

    ...If parameter is an indexed array name subscripted by @or *, the result is the length members of the array beginning with ${parameter[offset]}. A negative offset is taken relative to one greater than the maximum index of the specified array. It is an expansion error if length evaluates to a number less than zero.

  • ${parameter:offset}

  • ${parameter:offset:length}

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

This is the example provided by @kevinarpe, with alternate formatting to place the output in evidence:

这是@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"

This behaviour varies with versions of Bash. You may also have noticed that the length operator ${#arr[@]}will always evaluate to 0for empty arrays, regardless of set -u, without causing an 'unbound variable error'.

此行为因 Bash 版本而异。您可能还注意到,对于空数组,长度运算符${#arr[@]}将始终计算0为 ,而不管set -u,而不会导致“未绑定变量错误”。

回答by solidsnack

Here are a couple of ways to do something like this, one using sentinels and another using conditional appends:

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

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

回答by MarcH

Interesting inconsistency; this lets you define something which is "not considered set" yet shows up in the output of declare -p

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

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

UPDATE: as others mentioned, fixed in 4.4 released after this answer was posted.

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