Rozbij pustą tablicę za pomocą „set -u”


108

Piszę skrypt basha, który ma set -ui mam problem z rozszerzeniem pustej tablicy: bash wydaje się traktować pustą tablicę jako zmienną nieustawioną podczas rozwijania:

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

( declare -a arrteż nie pomaga.)

Typowym rozwiązaniem jest użycie ${arr[@]-}zamiast tego zastępowania pustego ciągu zamiast („niezdefiniowanej”) pustej tablicy. Nie jest to jednak dobre rozwiązanie, ponieważ teraz nie można odróżnić tablicy zawierającej pojedynczy pusty ciąg znaków od pustej tablicy. (@ -expansion jest specjalne w bash, rozszerza się "${arr[@]}"do "${arr[0]}" "${arr[1]}" …, co czyni go idealnym narzędziem do budowania linii poleceń.)

$ 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

Czy jest więc sposób obejścia tego problemu, inny niż sprawdzenie długości tablicy w if(zobacz przykład kodu poniżej) lub wyłączenie -uustawienia dla tego krótkiego fragmentu?

if [ "${#arr[@]}" = 0 ]; then
   veryLongCommandLine
else
   veryLongCommandLine "${arr[@]}"
fi

Aktualizacja: Usunięto bugstag z powodu wyjaśnienia przez ikegami.

Odpowiedzi:


24

Tylko bezpieczne jest idiom${arr[@]+"${arr[@]}"}

To jest już zalecenie w odpowiedzi ikegami , ale w tym wątku jest wiele dezinformacji i domysłów. Inne wzorce, takie jak ${arr[@]-}lub ${arr[@]:0}, nie są bezpieczne we wszystkich głównych wersjach Bash.

Jak pokazuje poniższa tabela, jedynym rozszerzeniem, które jest niezawodne we wszystkich nowoczesnych wersjach Bash, jest ${arr[@]+"${arr[@]}"}(kolumna +"). Warto zauważyć, że kilka innych rozszerzeń zawodzi w Bash 4.2, w tym (niestety) krótszy ${arr[@]:0}idiom, który nie tylko daje niepoprawny wynik, ale w rzeczywistości zawodzi. Jeśli potrzebujesz obsługiwać wersje wcześniejsze niż 4.4, aw szczególności 4.2, jest to jedyny działający idiom.

Zrzut ekranu przedstawiający różne idiomy w różnych wersjach

Niestety inne +rozszerzenia, które na pierwszy rzut oka wyglądają tak samo, faktycznie emitują inne zachowanie. :+rozwijanie nie jest bezpieczne, ponieważ :-expansion traktuje tablicę z jednym pustym elementem ( ('')) jako „null”, a zatem nie rozwija się (konsekwentnie) do tego samego wyniku.

Cytowanie pełnego rozwinięcia zamiast zagnieżdżonej tablicy ( "${arr[@]+${arr[@]}}"), która, jak spodziewałbym się, będzie z grubsza równoważna, jest podobnie niebezpieczne w 4.2.

Można zobaczyć kod wygenerowany te dane wraz z wynikami dla kilku dodatkowych wersji bash w tym GIST .


1
Nie widzę, żebyś testował "${arr[@]}". Czy coś mi brakuje? Z tego, co widzę, działa przynajmniej w 5.x.
x-yuri

1
@ x-yuri tak, Bash 4.4 naprawił sytuację; nie musisz używać tego wzorca, jeśli wiesz, że Twój skrypt będzie działał tylko w wersji 4.4+, ale wiele systemów nadal działa na wcześniejszych wersjach.
dimo414

Absolutnie. Pomimo ładnego wyglądu (np. Formatowanie), dodatkowe spacje są wielkim złem w bash, powodując wiele problemów
agg3l

W bashu 4.4.20 (1) nie działa to zgodnie z przeznaczeniem. cudzysłów rozwinięcia zmiennej w tej odpowiedzi nie liczy liczby elementów w tablicy . Co gorsza, pozostawi zmienną bez cytowania.
inetknght

@inetknght czy możesz udostępnić MCVE tego, co obserwujesz? Pomimo dziwnej składni jest to poprawnie cytowane. Rozszerzenie zewnętrzne (niewypełnione) rozwija się do rozwinięcia wewnętrznego (cytowanego), gdy tablica nie jest pusta.
dimo414

84

Zgodnie z dokumentacją

Zmienna tablicowa jest uznawana za ustawioną, jeśli indeksowi przypisano wartość. Ciąg pusty jest prawidłową wartością.

Żaden indeks nie ma przypisanej wartości, więc tablica nie jest ustawiona.

Ale chociaż dokumentacja sugeruje, że błąd jest tutaj odpowiedni, nie ma to już miejsca od 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: ''

Istnieje warunek, którego możesz użyć w tekście, aby osiągnąć to, czego chcesz w starszych wersjach: Użyj ${arr[@]+"${arr[@]}"}zamiast "${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

Testowane w bash 4.2.25 i 4.3.11.


4
Czy ktoś może wyjaśnić, jak i dlaczego to działa? Nie wiem, co [@]+tak naprawdę robi i dlaczego druga ${arr[@]}nie spowoduje niezwiązanego błędu.
Martin von Wittich,

3
${parameter+word}rozwija się tylko wordwtedy, gdy parameternie jest nieustawiona.
ikegami

2
${arr+"${arr[@]}"}jest krótszy i wydaje się działać równie dobrze.
Per Cederberg

3
@Per Cerderberg, nie działa. unset arr, arr[1]=a, args ${arr+"${arr[@]}"}Vsargs ${arr[@]+"${arr[@]}"}
Ikegami

1
Mówiąc dokładniej, w przypadkach, gdy +rozwinięcie nie występuje (a mianowicie pusta tablica), interpretacja jest zastępowana niczym , co jest dokładnie tym, do czego rozwija się pusta tablica. :+jest niebezpieczny, ponieważ traktuje również ('')tablicę jednoelementową jako nieustawioną i podobnie rozwija się do niczego, tracąc wartość.
dimo414

24

Zaakceptowana odpowiedź @ ikegami jest subtelnie błędna! Prawidłowa inkantacja to ${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...

Już nie ma znaczenia. bash-4.4.23: arr=('') && countArgs "${arr[@]:+${arr[@]}}"produkuje 1. Ale ${arr[@]+"${arr[@]}"}forma pozwala na rozróżnienie między pustymi / niepustymi wartościami poprzez dodanie / nie dodanie dwukropka.
x-yuri

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

1
Zostało to naprawione w mojej odpowiedzi dawno temu. (Właściwie jestem pewien, że wcześniej zostawiłem komentarz na temat tej odpowiedzi ?!)
ikegami

16

Okazuje się, że obsługa tablic została zmieniona w niedawno wydanym (2016/09/16) bash 4.4 (dostępnym na przykład w Debianie stretch).

$ bash --version | head -n1
bash --version | head -n1
GNU bash, version 4.4.0(1)-release (x86_64-pc-linux-gnu)

Teraz rozszerzenie pustych tablic nie generuje ostrzeżenia

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

$ # everything is fine

Mogę potwierdzić, bash-4.4.12 "${arr[@]}"wystarczy.
x-yuri

14

może to być kolejna opcja dla tych, którzy wolą nie powielać arr [@] i mogą mieć pusty łańcuch

echo "foo: '${arr[@]:-}'"

testować:

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
To zadziała, jeśli tylko interpolujesz zmienną, ale jeśli chcesz użyć tablicy w a for, skończy się to pojedynczym pustym ciągiem, gdy tablica jest niezdefiniowana / zdefiniowana jako pusta, gdzie możesz chcieć treści pętli nie działać, jeśli tablica nie jest zdefiniowana.
Ash Berlin-Taylor

dzięki @AshBerlin, dodałem pętlę for do mojej odpowiedzi, aby czytelnicy byli świadomi
Jayen

-1 do tego podejścia, jest po prostu niepoprawne. Spowoduje to zastąpienie pustej tablicy jednym pustym łańcuchem, który nie jest tym samym. Wzorzec zasugerowany w zaakceptowanej odpowiedzi ${arr[@]+"${arr[@]}"}, poprawnie zachowuje stan pustej tablicy.
dimo414

Zobacz także moją odpowiedź pokazującą sytuacje, w których to rozszerzenie się psuje.
dimo414

to nie jest nieprawidłowe. wyraźnie mówi, że poda pusty łańcuch, a są nawet dwa przykłady, w których można zobaczyć pusty łańcuch.
Jayen

7

Odpowiedź @ ikegami jest poprawna, ale uważam składnię za okropną ${arr[@]+"${arr[@]}"}. Jeśli używasz długich nazw zmiennych tablicowych, zaczyna wyglądać jak spaghetti szybciej niż zwykle.

Spróbuj tego zamiast tego:

$ 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

Wygląda na to, że operator wycinka tablicy Bash jest bardzo wyrozumiały.

Dlaczego więc Bash tak utrudnił obsługę skrajnego przypadku tablic? Westchnienie. Nie mogę zagwarantować, że wersja pozwoli na takie nadużycie operatora wycinka tablicy, ale dla mnie działa elegancko.

Uwaga: używam GNU bash, version 3.2.25(1)-release (x86_64-redhat-linux-gnu) Twój przebieg może się różnić.


9
ikegami pierwotnie to miało, ale usunęło je, ponieważ jest niewiarygodne, zarówno w teorii (nie ma powodu, dla którego to powinno działać), jak iw praktyce (wersja bash w OP nie zaakceptowała tego).

@hvd: Dzięki za aktualizację. Czytelnicy: Dodaj komentarz, jeśli znajdziesz wersje basha, w których powyższy kod nie działa.
kevinarpe

hvp już to zrobił i powiem ci też: "${arr[@]:0}"daje -bash: arr[@]: unbound variable.
ikegami

Jedną rzeczą, która powinna działać w różnych wersjach, jest ustawienie domyślnej wartości tablicy na arr=("_dummy_")i używanie rozszerzenia ${arr[@]:1}wszędzie. Wspomina się o tym w innych odpowiedziach, odnosząc się do wartości wartowniczych.
init_js

1
@init_js: Twoja zmiana została niestety odrzucona. Proponuję dodać jako oddzielną odpowiedź. (Patrz: stackoverflow.com/review/suggested-edits/19027379 )
kevinarpe

6

Rzeczywiście „ciekawa” niespójność.

Ponadto,

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

Chociaż zgadzam się, że obecne zachowanie może nie być błędem w tym sensie, który wyjaśnia @ikegami, IMO możemy powiedzieć, że błąd znajduje się w samej definicji („zestawu”) i / lub w fakcie, że jest stosowany niespójnie. Poprzedni akapit na stronie podręcznika mówi

... ${name[@]}rozwija każdy element nazwy do osobnego słowa. Gdy nie ma elementów tablicy, ${name[@]}rozwija się do zera.

co jest całkowicie zgodne z tym, co mówi o rozszerzaniu parametrów pozycyjnych w "$@". Nie, że nie ma innych niespójności w zachowaniu tablic i parametrów pozycyjnych ... ale dla mnie nie ma żadnej wskazówki, że ten szczegół powinien być niespójny między nimi.

Kontynuacja,

$ arr=()
$ echo "${arr[@]}"
bash: arr[@]: unbound variable   # as we've observed.  BUT...
$ echo "${#arr[@]}"
0                                # no error
$ echo "${!arr[@]}" | cat -e
$                                # no error

Więc arr[]czy nie jest tak niezwiązany, że nie możemy uzyskać liczby jego elementów (0) lub (pustej) listy jego kluczy? Są dla mnie sensowne i użyteczne - jedyną wartością odstającą wydaje się być ${arr[@]}(i ${arr[*]}) ekspansja.


2

Uzupełniam odpowiedzi na pytania @ ikegami (zaakceptowane) i @ kevinarpe (również dobre).

Możesz zrobić, "${arr[@]:+${arr[@]}}"aby obejść problem. Prawa strona (tj. Po :+) zawiera wyrażenie, które będzie używane w przypadku, gdy lewa strona nie jest zdefiniowana / zerowa.

Składnia jest tajemnicza. Zwróć uwagę, że prawa strona wyrażenia będzie podlegać interpretacji parametrów, dlatego należy zwrócić szczególną uwagę na spójne cytowanie.

: 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"]

Jak wspomina @kevinarpe, mniej tajemną składnią jest użycie notacji wycinka tablicy ${arr[@]:0}(w wersjach Bash >= 4.4), która rozszerza się na wszystkie parametry, zaczynając od indeksu 0. Nie wymaga również tak dużej liczby powtórzeń. To rozszerzenie działa niezależnie od tego set -u, więc możesz z niego korzystać przez cały czas. Strona podręcznika mówi (pod rozszerzeniem parametrów ):

  • ${parameter:offset}

  • ${parameter:offset:length}

    ... Jeśli parametr jest nazwą tablicy indeksowanej z indeksem @lub *, wynikiem jest długość elementów tablicy zaczynająca się od ${parameter[offset]}. Ujemne przesunięcie jest brane względem jednego większego niż maksymalny indeks określonej tablicy. Jest to błąd rozszerzania, jeśli długość daje liczbę mniejszą od zera.

Oto przykład dostarczony przez @kevinarpe, z alternatywnym formatowaniem, aby umieścić dane wyjściowe jako dowód:

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"

To zachowanie różni się w zależności od wersji Bash. Być może zauważyłeś również, że operator długości ${#arr[@]}zawsze będzie obliczał 0dla pustych tablic, niezależnie od tego set -u, bez powodowania „błędu niezwiązanej zmiennej”.


Niestety :0idiom zawodzi w Bash 4.2, więc nie jest to bezpieczne podejście. Zobacz moją odpowiedź .
dimo414

1

Oto kilka sposobów na zrobienie czegoś takiego, jeden za pomocą wartowników, a drugi za pomocą dołączeń warunkowych:

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

Ciekawa niespójność; pozwala to zdefiniować coś, co nie jest uważane za ustawione, ale pojawia się na wyjściu programudeclare -p

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

AKTUALIZACJA: jak wspominali inni, naprawiono w 4.4 wydanym po opublikowaniu tej odpowiedzi.


To po prostu nieprawidłowa składnia tablicy; potrzebujesz echo ${arr[@]}(ale przed Bash 4.4 nadal będziesz widzieć błąd).
dimo414

Dzięki @ dimo414, następnym razem zasugeruj zmianę zamiast głosowania w dół. BTW, gdybyś próbował echo $arr[@]sam, zobaczyłbyś, że komunikat o błędzie jest inny.
MarcH

-2

Wydaje się, że najprostszym i najbardziej kompatybilnym sposobem jest:

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

1
Same OP pokazały, że to nie działa. Rozwija się do pustego łańcucha zamiast niczego.
ikegami

Tak, więc jest OK w przypadku interpolacji ciągów, ale nie w pętli.
Craig Ringer
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.