Programing

`set -u`로 빈 배열 확장을 배시

lottogame 2020. 9. 16. 08:19
반응형

`set -u`로 빈 배열 확장을 배시


나는 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의 설명으로 인해 태그가 제거 되었습니다.


문서에 따르면

첨자에 값이 할당 된 경우 배열 변수가 설정된 것으로 간주됩니다. 널 문자열은 유효한 값입니다.

값이 할당 된 첨자가 없으므로 배열이 설정되지 않습니다.

그러나 문서는 여기에 오류가 적절하다고 제안하지만 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로 테스트되었습니다.


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

이것은 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

최근 릴리스 (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

참으로 "흥미로운"불일치.

더욱이,

$ 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[@]}아무것도 확장되지 않습니다.

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 so unbound 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.


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

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.

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.

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


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

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

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.

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

  • ${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.

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

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 0 for empty arrays, regardless of set -u, without causing an 'unbound variable error'.


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

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

The most simple and compatible way seems to be:

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

참고URL : https://stackoverflow.com/questions/7577052/bash-empty-array-expansion-with-set-u

반응형