Bash perluasan array kosong dengan `set -u`

104

Saya sedang menulis skrip bash yang memiliki set -u, dan saya memiliki masalah dengan ekspansi array kosong: bash tampaknya memperlakukan array kosong sebagai variabel yang tidak disetel selama ekspansi:

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

( declare -a arrtidak membantu.)

Solusi umum untuk ini adalah dengan menggunakan ${arr[@]-}sebagai gantinya, dengan demikian menggantikan string kosong sebagai ganti array kosong ("tidak ditentukan"). Namun ini bukan solusi yang baik, karena sekarang Anda tidak dapat membedakan antara sebuah array dengan satu string kosong di dalamnya dan sebuah array kosong. (@ -expansion istimewa dalam bash, ia berkembang "${arr[@]}"menjadi "${arr[0]}" "${arr[1]}" …, yang menjadikannya alat yang sempurna untuk membuat baris perintah.)

$ 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

Jadi apakah ada cara untuk mengatasi masalah itu, selain memeriksa panjang array dalam if(lihat contoh kode di bawah), atau mematikan -upengaturan untuk potongan pendek itu?

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

Pembaruan:bugs Tag dihapus karena penjelasan oleh ikegami.

Ivan Tarasov
sumber

Jawaban:

18

Satu- satunya idiom yang aman adalah${arr[@]+"${arr[@]}"}

Ini sudah menjadi rekomendasi dalam jawaban ikegami , tetapi ada banyak informasi yang salah dan tebakan di utas ini. Pola lain, seperti ${arr[@]-}atau ${arr[@]:0}, tidak aman di semua versi utama Bash.

Seperti yang ditunjukkan tabel di bawah ini, satu-satunya perluasan yang dapat diandalkan di semua versi Bash modern adalah ${arr[@]+"${arr[@]}"}(kolom +"). Sebagai catatan, beberapa ekspansi lain gagal di Bash 4.2, termasuk (sayangnya) ${arr[@]:0}idiom yang lebih pendek , yang tidak hanya menghasilkan hasil yang salah tetapi sebenarnya gagal. Jika Anda perlu mendukung versi sebelum 4.4, dan khususnya 4.2, ini adalah satu-satunya idiom yang berfungsi.

Tangkapan layar berbagai idiom di berbagai versi

Sayangnya +ekspansi lain yang sekilas terlihat sama memang mengeluarkan perilaku yang berbeda. :+ekspansi tidak aman, karena :-expansion memperlakukan array dengan satu elemen kosong ( ('')) sebagai "null" dan karenanya tidak (secara konsisten) meluas ke hasil yang sama.

Mengutip ekspansi penuh alih-alih array bersarang ( "${arr[@]+${arr[@]}}"), yang saya harapkan kira-kira setara, juga tidak aman di 4.2.

Anda dapat melihat kode yang menghasilkan data ini beserta hasil untuk beberapa versi tambahan bash di inti ini .

dimo414
sumber
1
Saya tidak melihat Anda menguji "${arr[@]}". Apakah saya melewatkan sesuatu? Dari apa yang saya lihat setidaknya berhasil 5.x.
x-yuri
1
@ x-yuri ya, Bash 4.4 memperbaiki situasi; Anda tidak perlu menggunakan pola ini jika Anda tahu skrip Anda hanya akan berjalan di 4.4+, tetapi banyak sistem masih menggunakan versi sebelumnya.
dimo414
Benar. Meskipun terlihat bagus (misalnya pemformatan), spasi ekstra adalah kejahatan besar dari pesta, menyebabkan banyak masalah
agg3l
82

Menurut dokumentasi,

Variabel array dianggap disetel jika subskrip telah diberi nilai. String nol adalah nilai yang valid.

Tidak ada subskrip yang diberi nilai, jadi lariknya tidak disetel.

Tetapi sementara dokumentasi menyarankan kesalahan sesuai di sini, ini tidak lagi menjadi masalah sejak 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: ''

Ada syarat yang dapat Anda gunakan sebaris untuk mencapai apa yang Anda inginkan di versi yang lebih lama: Gunakan ${arr[@]+"${arr[@]}"}sebagai ganti "${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

Diuji dengan bash 4.2.25 dan 4.3.11.

ikegami
sumber
4
Adakah yang bisa menjelaskan bagaimana dan mengapa ini berhasil? Saya bingung tentang apa yang [@]+sebenarnya terjadi dan mengapa yang kedua ${arr[@]}tidak menyebabkan kesalahan tak terikat.
Martin von Wittich
3
${parameter+word}hanya meluas wordjika parametertidak disetel.
ikegami
2
${arr+"${arr[@]}"}lebih pendek dan tampaknya berfungsi dengan baik.
Per Cederberg
3
@ Per Cerderberg, Tidak berfungsi. unset arr, arr[1]=a, args ${arr+"${arr[@]}"}Vsargs ${arr[@]+"${arr[@]}"}
ikegami
1
Tepatnya, dalam kasus di mana +ekspansi tidak terjadi (yaitu, array kosong), ekspansi diganti dengan apa - apa , yang merupakan perluasan dari array kosong. :+tidak aman karena juga memperlakukan ('')larik elemen tunggal sebagai tidak disetel dan juga meluas menjadi tidak ada, kehilangan nilainya.
dimo414
23

Jawaban yang diterima @ ikegami agak salah! Mantra yang benar adalah ${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
sumber
Tidak lagi membuat perbedaan. bash-4.4.23: arr=('') && countArgs "${arr[@]:+${arr[@]}}"menghasilkan 1. Tetapi ${arr[@]+"${arr[@]}"}bentuk memungkinkan untuk membedakan antara nilai kosong / tidak kosong dengan menambahkan / tidak menambahkan titik dua.
x-yuri
arr=('') && countArgs ${arr[@]:+"${arr[@]}"}-> 0, arr=('') && countArgs ${arr[@]+"${arr[@]}"}-> 1.
x-yuri
1
Ini telah diperbaiki dalam jawaban saya sejak lama. (Sebenarnya, saya yakin sebelumnya saya telah meninggalkan komentar tentang jawaban ini untuk efek itu ?!)
ikegami
16

Ternyata penanganan array telah diubah dalam rilis baru-baru ini (2016/09/16) bash 4.4 (tersedia di bentangan Debian, misalnya).

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

Sekarang ekspansi array kosong tidak mengeluarkan peringatan

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

$ # everything is fine
agg3l
sumber
Saya bisa konfirmasi, dengan bash-4.4.12 "${arr[@]}"sudah cukup.
x-yuri
14

ini mungkin opsi lain bagi mereka yang memilih untuk tidak menduplikasi arr [@] dan boleh saja memiliki string kosong

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

untuk menguji:

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
sumber
10
Ini akan berfungsi jika Anda hanya menginterpolasi variabel, tetapi jika Anda ingin menggunakan array dalam a forini akan berakhir dengan satu string kosong ketika array tidak ditentukan / ditentukan-sebagai-kosong, di mana Anda mungkin menginginkan badan perulangan untuk tidak berjalan jika array tidak ditentukan.
Ash Berlin-Taylor
terima kasih @AshBerlin, saya menambahkan loop for ke jawaban saya sehingga pembaca sadar
Jayen
-1 untuk pendekatan ini, itu tidak benar. Ini menggantikan larik kosong dengan satu string kosong, yang tidak sama. Pola yang disarankan dalam jawaban yang diterima ${arr[@]+"${arr[@]}"},, dengan benar mempertahankan status array kosong.
dimo414
Lihat juga jawaban saya yang menunjukkan situasi di mana perluasan ini rusak.
dimo414
itu tidak salah. itu secara eksplisit mengatakan itu akan memberikan string kosong, dan bahkan ada dua contoh di mana Anda dapat melihat string kosong.
Jayen
7

Jawaban @ ikegami benar, tetapi saya menganggap sintaksnya ${arr[@]+"${arr[@]}"} mengerikan. Jika Anda menggunakan nama variabel array yang panjang, itu mulai terlihat spaghetti-ish lebih cepat dari biasanya.

Coba ini sebagai gantinya:

$ 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

Sepertinya operator irisan array Bash sangat pemaaf.

Jadi mengapa Bash membuat penanganan kasus tepi array begitu sulit? Mendesah. Saya tidak dapat menjamin versi Anda akan mengizinkan penyalahgunaan operator irisan array, tetapi berfungsi dengan baik untuk saya.

Peringatan: Saya menggunakan GNU bash, version 3.2.25(1)-release (x86_64-redhat-linux-gnu) jarak tempuh Anda mungkin berbeda-beda.

kevinarpe
sumber
9
ikegami awalnya memiliki ini, tetapi menghapusnya karena tidak dapat diandalkan, baik secara teori (tidak ada alasan mengapa ini harus berhasil) dan dalam praktik (versi bash OP tidak menerimanya).
@hvd: Terima kasih atas pembaruannya. Pembaca: Silakan tambahkan komentar jika Anda menemukan versi bash di mana kode di atas tidak berfungsi.
kevinarpe
hvp sudah melakukannya, dan saya akan memberi tahu Anda juga: "${arr[@]:0}"memberi -bash: arr[@]: unbound variable.
ikegami
Satu hal yang seharusnya berfungsi di semua versi adalah menyetel nilai array default ke arr=("_dummy_"), dan menggunakan perluasan di ${arr[@]:1}mana saja. Ini disebutkan dalam jawaban lain, mengacu pada nilai sentinel.
init_js
1
@init_js: Sayangnya hasil edit Anda ditolak. Saya sarankan Anda menambahkan sebagai jawaban terpisah. (Ref: stackoverflow.com/review/suggested-edits/19027379 )
kevinarpe
6

Inkonsistensi yang "menarik" memang.

Selanjutnya,

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

Meskipun saya setuju bahwa perilaku saat ini mungkin bukan bug dalam arti yang dijelaskan @ikegami, IMO kami dapat mengatakan bug ada dalam definisi (dari "set") itu sendiri, dan / atau fakta bahwa itu diterapkan secara tidak konsisten. Paragraf sebelumnya di halaman manual mengatakan

... ${name[@]}memperluas setiap elemen nama menjadi kata terpisah. Jika tidak ada anggota array, lakukan ${name[@]}ekspansi ke nol.

yang sepenuhnya konsisten dengan apa yang dikatakannya tentang perluasan parameter posisi di "$@". Bukan berarti tidak ada ketidakkonsistenan lain dalam perilaku array dan parameter posisi ... tetapi bagi saya tidak ada petunjuk bahwa detail ini tidak konsisten di antara keduanya.

Melanjutkan,

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

Jadi arr[]bukankah begitu tidak terikat sehingga kita tidak bisa mendapatkan hitungan elemennya (0), atau daftar (kosong) kuncinya? Bagi saya ini masuk akal, dan berguna - satu-satunya pencilan tampaknya menjadi ${arr[@]}(dan ${arr[*]}) ekspansi.

don311
sumber
2

Saya melengkapi jawaban @ ikegami (diterima) dan @ kevinarpe (juga bagus).

Anda dapat melakukannya "${arr[@]:+${arr[@]}}"untuk mengatasi masalah tersebut. Sisi kanan (yaitu, setelah :+) memberikan ekspresi yang akan digunakan jika sisi kiri tidak ditentukan / null.

Sintaksnya misterius. Perhatikan bahwa sisi kanan ekspresi akan mengalami perluasan parameter, jadi perhatian ekstra harus diberikan untuk mendapatkan kutipan yang konsisten.

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

Seperti yang disebutkan @kevinarpe, sintaks yang kurang misterius adalah menggunakan notasi irisan array ${arr[@]:0}(pada versi Bash >= 4.4), yang meluas ke semua parameter, mulai dari indeks 0. Ini juga tidak memerlukan banyak pengulangan. Perluasan ini berfungsi apa pun set -u, jadi Anda dapat menggunakannya setiap saat. Halaman manual mengatakan (di bawah Ekspansi Parameter ):

  • ${parameter:offset}

  • ${parameter:offset:length}

    ... Jika parameter adalah nama array terindeks yang dilanggan oleh @atau *, hasilnya adalah panjang anggota array yang diawali dengan ${parameter[offset]}. Offset negatif diambil relatif terhadap yang lebih besar dari indeks maksimum dari larik yang ditentukan. Ini adalah kesalahan ekspansi jika panjang mengevaluasi ke angka kurang dari nol.

Ini adalah contoh yang disediakan oleh @kevinarpe, dengan format alternatif untuk menempatkan keluaran sebagai bukti:

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"

Perilaku ini bervariasi dengan versi Bash. Anda mungkin juga telah memperhatikan bahwa operator panjang ${#arr[@]}akan selalu mengevaluasi ke 0untuk larik kosong, terlepas dari set -u, tanpa menyebabkan 'kesalahan variabel tak terikat'.

init_js
sumber
Sayangnya :0idiom gagal di Bash 4.2, jadi ini bukan pendekatan yang aman. Lihat jawabanku .
dimo414
1

Berikut adalah beberapa cara untuk melakukan sesuatu seperti ini, satu menggunakan sentinel dan yang lainnya menggunakan tambahan bersyarat:

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

Inkonsistensi yang menarik; ini memungkinkan Anda menentukan sesuatu yang "tidak dianggap set" namun muncul di keluarandeclare -p

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

PEMBARUAN: seperti yang disebutkan orang lain, diperbaiki di 4.4 dirilis setelah jawaban ini diposting.

Maret
sumber
Itu hanya sintaks array yang salah; yang Anda butuhkan echo ${arr[@]}(tetapi sebelum Bash 4.4 Anda masih akan melihat kesalahan).
dimo414
Terima kasih @ dimo414, lain kali sarankan edit, bukan downvoting. BTW jika Anda mencoba echo $arr[@]sendiri, Anda akan melihat bahwa pesan kesalahan berbeda.
MarcH
-2

Cara yang paling sederhana dan kompatibel tampaknya:

$ set -u
$ arr=()
$ echo "foo: '${arr[@]-}'"
nikolay
sumber
1
OP sendiri menunjukkan bahwa ini tidak berhasil. Ini meluas menjadi string kosong, bukan tidak ada.
ikegami
Benar, jadi tidak masalah untuk interpolasi string tetapi tidak melakukan perulangan.
Craig Ringer