Расширение пустого массива Bash с помощью ' set-u`


Я пишу сценарий bash, который имеет set -u, и у меня есть проблема с расширением пустого массива: bash, похоже, обрабатывает пустой массив как переменную unset во время расширения:

$ 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 тег из-за объяснения Икегами.

10 69

10 ответов:

во-первых, это не ошибка.

переменная массива считается установленной, если индексу присвоено значение. Пустая строка является допустимым значением.

ни одному индексу не было присвоено значение, поэтому массив не установлен.


есть условное вы можете использовать встроенный для достижения того, что вы хотите: Use ${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[@]+"${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

в то время как предлагаемое решение (действительно здорово, спасибо за это) терпит неудачу с bash-4.4:

$ set -u
$ arr2=()
$ arr2=( ${arr2[@] + "${arr2[@]}"} 'foo' )
bash: ${arr2[@] + "$arr2[@]"}: bad substitution

есть ли у кого-нибудь предложения по (более или менее) независимому от версии решению без дополнительных проверок длины массива или версии bash ? Я все еще на связи расследуя последние изменения Баша сам.

Edit

неправильный синтаксис является источником проблемы, которые я упомянул. Следует убедиться, что следующая синтаксическая конструкция не имеет никаких дополнительных пространств внутри (особенно вокруг"+"), несмотря на то, что она имеет тенденцию добавлять его для удобства чтения. Этот синтаксис (дополнительные пробелы) работал для bash 4.3, но не с bash 4.4

arr=( ${arr[@]+"${arr[@]}"} 'foo' )

@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) Ваш пробег может отличаться.

" интересная " непоследовательность действительно.

кроме того,

$ set -u
$ echo $#
0
$ echo ""
bash: : 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[] не так unbound, что мы не можем получить количество его элементов (0), или (пустой) список его ключей? Для меня они разумны и полезны-единственным выбросом, по-видимому, является ${arr[@]}${arr[*]}) расширение.

вот несколько способов сделать что-то подобное, один с помощью sentinels а другой с помощью условных добавлений:

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

я дополняю на @ikegami (принято) и @kevinarpe это (тоже хорошо) ответы.

можно сделать "${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[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, не вызывая "несвязанной переменной ошибки".

интересная несогласованность; это позволяет определить то, что" не считается установленным", но отображается в выводе declare -p

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

самый простой и совместимый способ кажется:

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