Boş dizi genişletmesini `set -u` ile Bash


108

İçerdiği bir bash betiği yazıyorum set -uve boş dizi genişletmesiyle ilgili bir sorunum var: bash, genişletme sırasında boş bir diziyi ayarlanmamış bir değişken olarak ele alıyor görünüyor:

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

( declare -a arrda yardımcı olmuyor.)

Bunun yaygın bir çözümü, ${arr[@]-}bunun yerine kullanmaktır , böylece boş diziyi ("tanımsız") boş dizi yerine boş bir dizge kullanmaktır. Ancak bu iyi bir çözüm değildir, çünkü artık içinde tek bir boş dizge olan bir dizi ile boş bir dizi arasında ayrım yapamazsınız. (@ -expansion, bash için özeldir, "${arr[@]}"içine genişler "${arr[0]}" "${arr[1]}" …, bu da onu komut satırları oluşturmak için mükemmel bir araç yapar.)

$ 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

Öyleyse, bir dizideki bir dizinin uzunluğunu kontrol etmekten if(aşağıdaki kod örneğine bakın) veya -ubu kısa parça için ayarı kapatmaktan başka bir yolu var mı?

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

Güncelleme:bugs İkegami'nin açıklaması nedeniyle kaldırılan etiket.

Yanıtlar:


24

Sadece güvenli deyim olduğunu${arr[@]+"${arr[@]}"}

Bu zaten ikegami'nin cevabındaki öneridir , ancak bu başlıkta çok fazla yanlış bilgi ve tahmin vardır. Gibi diğer desenler, ${arr[@]-}ya ${arr[@]:0}vardır değil Bash tüm ana sürümleri arasında güvenli.

Aşağıdaki tabloda gösterildiği gibi, tüm modern-ish Bash sürümlerinde güvenilir olan tek genişletme ${arr[@]+"${arr[@]}"}(sütun +") ' dur . Not olarak, Bash 4.2'de (maalesef) kısa ${arr[@]:0}deyim de dahil olmak üzere birkaç başka genişletme başarısız olur , bu sadece yanlış bir sonuç üretmez, aynı zamanda başarısız olur. 4.4'ten önceki sürümleri ve özellikle 4.2'yi desteklemeniz gerekiyorsa, bu çalışan tek deyimdir.

Sürümler arasında farklı deyimlerin ekran görüntüsü

Ne yazık +ki, bir bakışta aynı görünen diğer açılımlar aslında farklı davranışlar sergiliyor. :+genişletme güvenli değildir , çünkü :-expansion tek bir boş elemanı ( ('')) olan bir diziyi "null" olarak değerlendirir ve bu nedenle (tutarlı bir şekilde) aynı sonuca genişlemez.

"${arr[@]+${arr[@]}}"Kabaca eşdeğer olmasını beklediğim iç içe geçmiş dizi ( ) yerine tam genişletmeyi alıntılamak, 4.2'de benzer şekilde güvenli değildir.

Bu ana başlıkta birkaç ek bash sürümünün sonuçları ile birlikte bu verileri oluşturan kodu görebilirsiniz .


1
Test ettiğini görmüyorum "${arr[@]}". Bir şey mi kaçırıyorum? Görebildiğim kadarıyla en azından içinde çalışıyor 5.x.
x-yuri

1
@ x-yuri evet, Bash 4.4 durumu düzeltti; betiğinizin yalnızca 4.4+ üzerinde çalışacağını biliyorsanız, bu kalıbı kullanmanız gerekmez, ancak birçok sistem hala önceki sürümlerde bulunmaktadır.
dimo414

Kesinlikle. Güzel görünmesine rağmen (ör. Biçimlendirme), fazladan boşluklar bash için büyük bir kötülüktür ve birçok
soruna

4.4.20 (1) bash'ta bu amaçlandığı gibi çalışmaz. bu yanıttaki değişken genişletme alıntı , bir dizideki öğe sayısını saymaz . Daha kötüsü, değişkeni tırnaksız bırakacaktır.
inetknght

@inetknght gözlemlediklerinizle ilgili bir MCVE paylaşabilir misiniz? Garip sözdizimine rağmen, bu doğru bir şekilde alıntılanmıştır. Dış (sorgulanmamış) genişletme, dizi boş olmadığında dahili (alıntılanan) genişletmeye genişler.
dimo414

84

Belgelere göre,

Bir alt simgeye bir değer atanmışsa, dizi değişkeni küme olarak kabul edilir. Boş dizge geçerli bir değerdir.

Hiçbir alt simgeye bir değer atanmadığı için dizi ayarlanmadı.

Ancak dokümantasyon burada bir hatanın uygun olduğunu öne sürse de, 4.4'ten beri durum artık böyle değildir .

$ 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: ''

Eski sürümlerde istediğinizi elde etmek için satır içi kullanabileceğiniz bir koşul vardır: ${arr[@]+"${arr[@]}"}Bunun yerine kullanın "${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 ve 4.3.11 ile test edilmiştir.


4
Bunun nasıl ve neden çalıştığını kimse açıklayabilir mi? Gerçekte ne [@]+yaptığı ve neden ikincinin ${arr[@]}bağlanmamış bir hataya neden olmayacağı konusunda kafam karıştı .
Martin von Wittich

3
${parameter+word}Sadece genişletir wordeğer parametertanımsız değildir.
ikegami

2
${arr+"${arr[@]}"}daha kısa ve aynı şekilde çalışıyor gibi görünüyor.
Per Cederberg

3
@Per Cerderberg, çalışmıyor. unset arr, arr[1]=a, args ${arr+"${arr[@]}"}Vsargs ${arr[@]+"${arr[@]}"}
Ikegami

1
Kesin olmak gerekirse, +genişlemenin gerçekleşmediği durumlarda (yani boş bir dizi), genişletme hiçbir şeyle değiştirilir , bu da tam olarak boş bir dizinin genişleyeceği şeydir. :+güvensizdir çünkü aynı zamanda tek öğeli bir ('')diziyi ayarlanmamış olarak kabul eder ve benzer şekilde hiçbir şeye genişleyerek değeri kaybederek genişler.
dimo414

24

@ ikegami'nin kabul ettiği cevap ince bir şekilde yanlış! Doğru büyülü söz ${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...

Artık bir fark yaratmıyor. bash-4.4.23: arr=('') && countArgs "${arr[@]:+${arr[@]}}"üretir 1. Ancak ${arr[@]+"${arr[@]}"}form, iki nokta üst üste ekleyerek / eklemeyerek boş / boş olmayan değer arasında ayrım yapmaya izin verir.
x-yuri

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

1
Bu, cevabımda uzun zaman önce düzeltildi. (Aslında, bu yanıta daha önce bu etkiyle ilgili bir yorum yaptığıma eminim ?!)
ikegami

16

Dizi işlemenin yakın zamanda yayımlanan (2016/09/16) bash 4.4'te (örneğin Debian genişlemesinde mevcuttur) değiştirildiği ortaya çıktı.

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

Artık boş diziler genişletmesi uyarı vermiyor

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

$ # everything is fine

Onaylayabilirim, bash-4.4.12 "${arr[@]}"yeterli olur.
x-yuri

14

bu, arr [@] 'yi kopyalamamayı tercih edenler için başka bir seçenek olabilir ve boş bir dizeye sahip olmakta sorun yoktur

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

test etmek için:

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
Bu, yalnızca değişkeni enterpolasyon yapıyorsanız işe yarar, ancak diziyi bir dizide kullanmak istiyorsanız for, dizi tanımsız / boş olarak tanımlandığında tek bir boş dizeyle sonuçlanır, burada döngü gövdesini istediğiniz gibi dizi tanımlı değilse çalışmaz.
Ash Berlin-Taylor

teşekkürler @AshBerlin, cevabıma bir for döngüsü ekledim, böylece okuyucuların farkında olsun
Jayen

Bu yaklaşıma -1, tamamen yanlış. Bu, boş bir diziyi, aynı olmayan tek bir boş dizeyle değiştirir. Kabul edilen yanıtta önerilen kalıp ${arr[@]+"${arr[@]}"}, boş dizi durumunu doğru bir şekilde korur.
dimo414

Ayrıca bkz cevabım bu genişleme çöker durumları gösteren.
dimo414

yanlış değil. açıkça boş bir dizge vereceğini söylüyor ve boş dizgeyi görebileceğiniz iki örnek bile var.
Jayen

7

@ ikegami'nin cevabı doğru, ancak sözdiziminin ${arr[@]+"${arr[@]}"}korkunç olduğunu düşünüyorum . Uzun dizi değişken adları kullanırsanız, normalden daha hızlı görünmeye başlar.

Bunun yerine şunu deneyin:

$ 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 dizi dilim operatörü çok bağışlayıcı gibi görünüyor.

Öyleyse Bash dizilerin uç durumunun üstesinden gelmeyi neden bu kadar zorlaştırdı? İç çekmek. Sürümün dizi dilim operatörünün böyle kötüye kullanılmasına izin vereceğini garanti edemem, ancak benim için mükemmel çalışıyor.

Uyarı: Kullandığım GNU bash, version 3.2.25(1)-release (x86_64-redhat-linux-gnu) kilometre değişebilir.


9
ikegami başlangıçta buna sahipti, ancak hem teoride (bunun çalışması için bir neden yok) hem de pratikte (OP'nin bash versiyonu bunu kabul etmedi) güvenilmez olduğu için kaldırdı.

@hvd: Güncelleme için teşekkürler. Okuyucular: Yukarıdaki kodun çalışmadığı bash sürümlerini bulursanız lütfen bir yorum ekleyin.
kevinarpe

hvp zaten yaptı ve size de söyleyeceğim: "${arr[@]:0}"verir -bash: arr[@]: unbound variable.
ikegami

Sürümler arasında çalışması gereken bir şey, varsayılan bir dizi değerini ayarlamak arr=("_dummy_")ve genişletmeyi ${arr[@]:1}her yerde kullanmaktır . Bu, sentinel değerlere atıfta bulunarak diğer yanıtlarda belirtilmiştir.
init_js

1
@init_js: Düzenlemeniz maalesef reddedildi. Ayrı bir cevap olarak eklemenizi öneririm. (Ref: stackoverflow.com/review/suggested-edits/19027379 )
kevinarpe

6

Gerçekten de "ilginç" tutarsızlık.

Ayrıca,

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

Mevcut davranışın @ikegami'nin açıkladığı anlamda bir hata olmayabileceğini kabul etsem de, IMO hatanın kendisinin tanımında ("set") olduğunu ve / veya tutarsız bir şekilde uygulandığını söyleyebiliriz. Man sayfasındaki önceki paragraf diyor ki

... ${name[@]}adın her bir öğesini ayrı bir kelimeye genişletir. Dizi üyesi olmadığında, hiçbir ${name[@]}şeye genişler.

Bu, içindeki konumsal parametrelerin genişlemesi hakkında söyledikleriyle tamamen tutarlıdır "$@". Dizilerin davranışlarında ve konumsal parametrelerde başka tutarsızlıklar olmadığından değil ... ama bana göre bu detayın ikisi arasında tutarsız olması gerektiğine dair bir ipucu yok.

Devam ediyor,

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

Öyleyse arr[], öğelerinin sayısını (0) veya anahtarlarının (boş) bir listesini alamayacağımız kadar bağlı değil mi? Bana göre bunlar mantıklı ve kullanışlıdır - tek aykırı değer ${arr[@]}(ve ${arr[*]}) genişleme gibi görünüyor .


2

Üzerinde tamamlayıcı am Ikegami @ (kabul) ve @ kevinarpe en (aynı zamanda iyi) cevapları.

Yapabileceğin "${arr[@]:+${arr[@]}}"sorunu geçmenin. Sağ taraf (yani, sonra :+), sol tarafın tanımlanmamış / boş olmaması durumunda kullanılacak bir ifade sağlar.

Sözdizimi gizlidir. İfadenin sağ tarafının parametre genişlemesine maruz kalacağına dikkat edin, bu nedenle tutarlı alıntılar yapmaya ekstra dikkat gösterilmelidir.

: 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'nin bahsettiği gibi, daha az gizli bir sözdizimi, dizin 0'dan başlayarak tüm parametrelere genişleyen dizi dilim gösterimini ${arr[@]:0}(Bash sürümlerinde >= 4.4) kullanmaktır. Aynı zamanda çok fazla tekrar gerektirmez. Bu genişletme set -u, ne olursa olsun çalışır , böylece bunu her zaman kullanabilirsiniz. Man sayfasında ( Parametre Genişletme altında ):

  • ${parameter:offset}

  • ${parameter:offset:length}

    ... parametresi ile indislenmiş dizinlenmiş bir dizi ismi ise @veya *sonuç ile dizi başından uzunluğu üyeler ${parameter[offset]}. Belirtilen dizinin maksimum indeksinden büyük olana göre negatif bir uzaklık alınır. Uzunluk, sıfırdan küçük bir sayı olarak değerlendirilirse, bu bir genişletme hatasıdır.

Bu, çıktıyı kanıta yerleştirmek için alternatif biçimlendirme ile @kevinarpe tarafından sağlanan örnektir:

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"

Bu davranış, Bash sürümlerine göre değişir. Ayrıca uzunluk operatörünün boş diziler için, bir 'bağlanmamış değişken hatası'na neden olmadan ${#arr[@]}her zaman olarak değerlendirileceğini fark etmiş olabilirsiniz .0set -u


Ne yazık ki :0deyim Bash 4.2'de başarısız oluyor, bu yüzden bu güvenli bir yaklaşım değil. Cevabımı gör .
dimo414

1

İşte böyle bir şeyi yapmanın birkaç yolu, biri nöbetçi kullanarak ve diğeri koşullu ekler kullanarak:

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

İlginç tutarsızlık; bu, "küme olarak kabul edilmeyen" bir şeyi tanımlamanıza izin verir, ancak yine de çıktıdadeclare -p

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

GÜNCELLEME: Diğerlerinin de belirtildiği gibi, bu yanıt gönderildikten sonra yayınlanan 4.4'te düzeltildi.


Bu sadece yanlış dizi sözdizimi; ihtiyacınız var echo ${arr[@]}(ancak Bash 4.4'ten önce hala bir hata göreceksiniz).
dimo414

Teşekkürler @ dimo414, bir dahaki sefere olumsuz oy kullanmak yerine bir düzenleme önerin. BTW eğer echo $arr[@]kendinizi denediyseniz , hata mesajının farklı olduğunu görürdünüz.
MarcH

-2

En basit ve uyumlu yol şöyle görünüyor:

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

1
OP'nin kendisi bunun işe yaramadığını gösterdi. Hiçbir şey yerine boş bir dizeye genişler.
ikegami

Doğru, dize enterpolasyonu için sorun değil ama döngü değil.
Craig Ringer
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.