Розширення порожнього масиву в Bash за допомогою `set -u`


103

Я пишу скрипт 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.

Відповіді:


17

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

Це вже рекомендація у відповіді ikegami , але в цій темі є багато дезінформації та здогадок. Інші моделі, такі як ${arr[@]-}або ${arr[@]:0}, не є безпечними для всіх основних версій Bash.

Як видно з таблиці нижче, єдиним надійним розширенням для всіх сучасних версій Bash є ${arr[@]+"${arr[@]}"}(стовпець +"). Зауважимо, що кілька інших розширень зазнали невдачі в Bash 4.2, включаючи (на жаль) коротшу ${arr[@]:0}ідіому, яка не просто дає неправильний результат, а насправді не вдається. Якщо вам потрібна підтримка версій до 4.4, зокрема 4.2, це єдина робоча ідіома.

Знімок екрана різних ідіом у різних версіях

На жаль, інші +розширення, які, на перший погляд, виглядають однаково, справді мають різну поведінку. :+розширення не є безпечним, оскільки :-expansion обробляє масив з одним порожнім елементом (('') ) як "null" і, отже, не (послідовно) розширюється до того самого результату.

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

Ви можете побачити код, який згенерував ці дані, а також результати для кількох додаткових версій bash у цій суті .


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

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

Зовсім. Незважаючи на приємний вигляд (наприклад, форматування), зайві пробіли - це велике зло bash, яке доставляє багато клопоту
agg3l

81

Згідно з документацією,

Змінна масиву вважається встановленою, якщо індексу присвоєно значення. Нульовий рядок є допустимим значенням.

Жодному індексу не присвоєно значення, тому масив не встановлено.

Але хоча в документації передбачається, що помилка тут доречна, це вже не так з 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.


4
Хтось може пояснити, як і чому це працює? Мене бентежить, що [@]+насправді робить і чому другий ${arr[@]}не спричинить незв’язаної помилки.
Martin von Wittich

2
${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-yuri

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

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це закінчиться єдиним порожнім рядком, коли масив не визначений / визначений-як-порожній, де, як вам може знадобитися тіло циклу не запускатись, якщо масив не визначений.
Еш Берлін-Тейлор

дякую @AshBerlin, я додав цикл for до своєї відповіді, щоб читачі були в курсі
Jayen

-1 до цього підходу, це просто неправильно. Це замінює порожній масив одним порожнім рядком, що не є однаковим. Шаблон, запропонований у прийнятій відповіді ${arr[@]+"${arr[@]}"},, правильно зберігає стан порожнього масиву.
dimo414

Див. Також мою відповідь, що показує ситуації, коли це розширення припиняється.
dimo414

це неправильно. він прямо говорить, що він дасть порожній рядок, і є навіть два приклади, коли ви можете побачити порожній рядок.
Jayen

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 дуже прощає.

То чому Баш так ускладнив обробку крайової справи масивів? Зітхнути. Я не можу гарантувати, що версія дозволить подібне зловживання оператором зрізу масиву, але це працює для мене надзвичайно.

Застереження: я використовую GNU bash, version 3.2.25(1)-release (x86_64-redhat-linux-gnu) Ваш пробіг може відрізнятися.


9
Спочатку ikegami це мав, але видалив, оскільки це ненадійно, як теоретично (немає жодної причини, чому це мало б спрацювати), так і на практиці (версія баш-програми OP не приймала).

@hvd: Дякуємо за оновлення. Читачі: Будь ласка, додайте коментар, якщо знайдете версії bash, де наведений вище код не працює.
kevinarpe

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

Одна річ, яка повинна працювати в різних версіях, - це встановити значення масиву за замовчуванням arr=("_dummy_")і використовувати розширення ${arr[@]:1}скрізь. Про це йдеться в інших відповідях, посилаючись на вартові вартості.
init_js 07.03.18

1
@init_js: Вашу редакцію на жаль відхилено. Пропоную додати як окрему відповідь. (Посилання: stackoverflow.com/review/suggest-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, IMO, ми могли б сказати, що помилка є у самому визначенні ("набору") та / або в тому, що вона застосовується непослідовно. У попередньому абзаці на довідковій сторінці сказано

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

Я доповнюю @ ikegami's (прийнято) та @ 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]}. Від’ємне зміщення приймається відносно одного, що перевищує максимальний індекс вказаного масиву. Помилка розширення, якщо довжина визначається числом менше нуля.

Це приклад, наданий @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

Так, це нормально для інтерполяції рядків, але не для циклу.
Крейг Рінгер,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.