Расширение пустого массива в 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 в этой сущности .

dimo414
источник
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...
ijs
источник
Больше не имеет значения. 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
агг3л
источник
Могу подтвердить, с этим 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
Jayen
источник
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) Ваш пробег может отличаться.

Kevinarpe
источник
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[*]}).

don311
источник
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, не вызывая «ошибку несвязанной переменной».

init_js
источник
К сожалению, :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