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


104

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

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

( declare -a arrтоже не помогает.)

Обычным решением этой проблемы является использование ${arr[@]-}вместо нее пустой строки вместо ("неопределенного") пустого массива. Однако это не очень хорошее решение, так как теперь вы не можете отличить массив с одной пустой строкой от пустого массива. (@ -разложение особенное в Баше, он расширяется "${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.

Ответы:


18

Только безопасная идиома${arr[@]+"${arr[@]}"}

Это уже рекомендация в ответе ikegami , но в этой теме много дезинформации и догадок. Другие шаблоны, такие как ${arr[@]-}или ${arr[@]:0}, не безопасными во всех основных версиях Bash.

Как показано в таблице ниже, единственное расширение, которое является надежным для всех современных версий Bash, - это ${arr[@]+"${arr[@]}"}(столбец +"). Следует отметить, что несколько других расширений терпят неудачу в Bash 4.2, включая (к сожалению) более короткую ${arr[@]:0}идиому, которая не просто дает неверный результат, но фактически дает сбой. Если вам нужно поддерживать версии до 4.4 и, в частности, 4.2, это единственная рабочая идиома.

Скриншот различных идиом в разных версиях

К сожалению, другие +расширения, которые на первый взгляд выглядят одинаково, действительно проявляют другое поведение. :+расширение не безопасно, потому что :-разложение лечит массив с одного пустым элементом (('') ) как «нуль» и , таким образом , не (последовательно) расширить к тому же результату.

Цитирование полного раскрытия вместо вложенного array ( "${arr[@]+${arr[@]}}"), которое, как я ожидал, будет примерно эквивалентным, также небезопасно в 4.2.

Вы можете увидеть код, который сгенерировал эти данные, а также результаты для нескольких дополнительных версий bash в этой сущности .


1
Я не вижу, чтобы вы тестировали "${arr[@]}". Я что-то упускаю? Насколько я могу судить, он работает по крайней мере в 5.x.
x-yuri

1
@ x-yuri да, Bash 4.4 исправил ситуацию; вам не нужно использовать этот шаблон, если вы знаете, что ваш сценарий будет работать только на 4.4+, но многие системы все еще работают на более ранних версиях.
dimo414

Абсолютно. Несмотря на красивый
внешний

82

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

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

Нижнему индексу не присвоено значение, поэтому массив не задан.

Но хотя документация предполагает, что ошибка здесь уместна, с 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: ''

Существует условное выражение, которое вы можете использовать inline для достижения желаемого в более старых версиях: Используйте ${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.


4
Кто-нибудь может объяснить, как и почему это работает? Я не понимаю, что на [@]+самом деле делает и почему второй ${arr[@]}не вызовет несвязанную ошибку.
Мартин фон Виттих

3
${parameter+word}только расширяется, wordесли parameterне сброшено.
ikegami

2
${arr+"${arr[@]}"}короче и, кажется, работает так же хорошо.
Пер Седерберг,

3
@ Per Cerderberg, не работает. unset arr, arr[1]=a, args ${arr+"${arr[@]}"}Противargs ${arr[@]+"${arr[@]}"}
Ikegami

1
Если быть точным, в тех случаях, когда +раскрытие не происходит (а именно, пустой массив), расширение заменяется ничем , что является именно тем, до чего расширяется пустой массив. :+небезопасен, поскольку он также обрабатывает одноэлементный ('')массив как незаданный и аналогично расширяется до нуля, теряя значение.
dimo414

23

Принятый ответ @ 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.23: arr=('') && countArgs "${arr[@]:+${arr[@]}}"производит 1. Но ${arr[@]+"${arr[@]}"}форма позволяет различать пустое / непустое значение, добавляя / не добавляя двоеточие.
x-

arr=('') && countArgs ${arr[@]:+"${arr[@]}"}-> 0, arr=('') && countArgs ${arr[@]+"${arr[@]}"}-> 1.
x-

1
Это уже давно исправлено в моем ответе. (На самом деле, я уверен , что я ранее оставил комментарий на этот ответ на этот счет ?!)
Ikegami

16

Оказывается, обработка массивов была изменена в недавно выпущенном (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[@]}"хватит.
x-yuri

14

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

10
Это будет работать, если вы просто интерполируете переменную, но если вы хотите использовать массив в a, forэто приведет к единственной пустой строке, когда массив undefined / defined-as-empty, где, как вам может понадобиться, тело цикла не запускать, если массив не определен.
Эш Берлин-Тейлор

спасибо @AshBerlin, я добавил цикл for к своему ответу, чтобы читатели знали
Джейен

-1 к такому подходу, это просто неверно. Это заменяет пустой массив одной пустой строкой, что не то же самое. Шаблон, предложенный в принятом ответе ${arr[@]+"${arr[@]}"}, правильно сохраняет состояние пустого массива.
dimo414

См. Также мой ответ, показывающий ситуации, когда это расширение не работает.
dimo414

это не неправильно. он явно говорит, что выдаст пустую строку, и есть даже два примера, где вы можете увидеть пустую строку.
Джейен

7

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


9
Первоначально ikegami имел это, но удалил его, потому что он ненадежен, как в теории (нет причин, почему это должно работать), так и на практике (версия bash для OP не принимала это).

@hvd: Спасибо за обновление. Читатели: добавьте комментарий, если вы найдете версии bash, в которых приведенный выше код не работает.
kevinarpe

hvp уже делал, и я вам тоже скажу: "${arr[@]:0}"дает -bash: arr[@]: unbound variable.
ikegami

Одна вещь, которая должна работать в разных версиях, - это установить значение массива по умолчанию arr=("_dummy_")и ${arr[@]:1}везде использовать расширение . Это упоминается в других ответах, касающихся дозорных значений.
init_js 07

1
@init_js: К сожалению, ваше изменение было отклонено. Предлагаю добавить как отдельный ответ. (Ссылка: stackoverflow.com/review/suggested-edits/19027379 )
kevinarpe

6

Действительно "интересная" нестыковка.

Более того,

$ 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, ИМО, мы могли бы сказать, что ошибка заключается в самом определении («набора») и / или в том факте, что оно применяется непоследовательно. В предыдущем абзаце на странице руководства говорится

... ${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[*]}).


2

Я дополняющий на @ Икегах (принято) и @ 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, поэтому вы можете использовать его в любое время. На странице руководства говорится (в разделе « Расширение параметров» ):

  • ${parameter:offset}

  • ${parameter:offset:length}

    ... Если параметр - это имя индексированного массива с индексом @или *, результатом будут элементы длины массива, начинающиеся с ${parameter[offset]}. Отрицательное смещение берется относительно единицы, превышающей максимальный индекс указанного массива. Это ошибка раскрытия, если значение length меньше нуля.

Это пример, предоставленный @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, так что это небезопасный подход. Смотрите мой ответ .
dimo414

1

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

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

0

Интересная нестыковка; это позволяет вам определить что-то, что «не считается установленным», но отображается в выводе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

Спасибо, @ dimo414, в следующий раз предложу отредактировать его вместо голосования против. Кстати, если бы вы попробовали echo $arr[@]себя, вы бы увидели, что сообщение об ошибке другое.
MarcH

-2

Наиболее простой и совместимый способ выглядит так:

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

1
Сами ОП показали, что это не работает. Он заменяется пустой строкой вместо ничего.
ikegami

Верно, так что это нормально для строковой интерполяции, но не для цикла.
Craig Ringer
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.