Подтвердить что ты не робот

Bash пустое расширение массива с `set -u`

Я пишу bash script, у которого есть set -u, и у меня есть проблема с пустым расширением массива: 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

Обновление: Удалено тег bugs из-за объяснения ikegami.

4b9b3361

Ответ 1

Согласно документации,

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

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

Но хотя документация предполагает, что здесь уместна ошибка, это не так, начиная с 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 [email protected]; say "$_: $ARGV[$_]" for 0..$#ARGV' -- "[email protected]" ; }

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

Ответ 2

@ikegami принят ответ тонко неправильно! Правильное заклинание ${arr[@]+"${arr[@]}"}:

$ countArgs () { echo "$#"; }
$ arr=('')
$ countArgs "${arr[@]:+${arr[@]}}"
0   # WRONG
$ countArgs ${arr[@]+"${arr[@]}"}
1   # RIGHT
$ arr=()
$ countArgs ${arr[@]+"${arr[@]}"}
0   # Let make sure it still works for the other case...

Ответ 3

это может быть другим вариантом для тех, кто предпочитает не дублировать 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

Оказывается, обработка массива была изменена в недавно выпущенном (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

@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

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

Кроме того,

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

Хотя я согласен с тем, что текущее поведение не может быть ошибкой в ​​том смысле, что @ikegami объясняет, IMO мы могли бы сказать, что ошибка указана в определении (самого "набора" ) и/или на том, что она была непоследовательно применена, В предыдущем абзаце на странице man говорится:

... ${name[@]} расширяет каждый элемент имени до отдельного слова. Когда нет элементов массива, ${name[@]} расширяется до нуля.

что полностью согласуется с тем, что он говорит о расширении позиционных параметров в "[email protected]". Не то, чтобы не было других несоответствий в поведении массивов и позиционных параметров... но для меня нет намека на то, что эта деталь должна быть несовместимой между ними.

Продолжение,

$ 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

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

#!/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

Я дополняющий на @Икегах (принято) и @kevinarpe в (также хороших) ответы.

Вы можете сделать "${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. Это также не ' не требует так много повторений. Это расширение работает независимо от set -u, поэтому вы можете использовать его всегда. Страница man говорит (под расширением параметров):

  • ${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, не вызывая "ошибку несвязанной переменной".

Ответ 9

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

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

Ответ 10

Самый простой и совместимый способ:

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