Kebingungan tentang $ {array [*]} versus $ {array [@]} dalam konteks penyelesaian bash

90

Saya mencoba menulis penyelesaian bash untuk pertama kalinya, dan saya agak bingung tentang dua cara dereferensi bash array ( ${array[@]}dan ${array[*]}).

Berikut potongan kode yang relevan (ngomong-ngomong, berfungsi, tetapi saya ingin memahaminya dengan lebih baik):

_switch()
{
    local cur perls
    local ROOT=${PERLBREW_ROOT:-$HOME/perl5/perlbrew}
    COMPREPLY=()
    cur=${COMP_WORDS[COMP_CWORD]}
    perls=($ROOT/perls/perl-*)
    # remove all but the final part of the name
    perls=(${perls[*]##*/})

    COMPREPLY=( $( compgen -W "${perls[*]} /usr/bin/perl" -- ${cur} ) )
}

Dokumentasi bash mengatakan :

Setiap elemen dari sebuah array dapat direferensikan menggunakan $ {name [subscript]}. Kawat gigi diperlukan untuk menghindari konflik dengan operator perluasan nama file shell. Jika subskripnya adalah '@' atau '*', kata tersebut akan meluas ke semua anggota nama array. Subskrip ini hanya berbeda jika kata tersebut muncul dalam tanda kutip ganda. Jika kata tersebut dikutip ganda, $ {name [*]} diperluas menjadi satu kata dengan nilai setiap anggota array dipisahkan oleh karakter pertama variabel IFS, dan $ {name [@]} memperluas setiap elemen nama ke kata terpisah.

Sekarang saya pikir saya mengerti bahwa compgen -Wmengharapkan string yang berisi daftar kata dari kemungkinan alternatif, tetapi dalam konteks ini saya tidak mengerti apa arti "$ {name [@]} memperluas setiap elemen nama menjadi kata terpisah".

Singkat cerita: ${array[*]}karya; ${array[@]}tidak. Saya ingin tahu alasannya, dan saya ingin lebih memahami apa sebenarnya ${array[@]}perluasan itu.

Telemakus
sumber

Jawaban:

120

(Ini adalah perluasan dari komentar saya tentang jawaban Kaleb Pederson - lihat jawaban itu untuk perlakuan yang lebih umum tentang [@]vs. [*])

Ketika bash (atau shell serupa) mem-parsing baris perintah, ia membaginya menjadi serangkaian "kata" (yang akan saya sebut "shell-words" untuk menghindari kebingungan nanti). Umumnya, kata-shell dipisahkan oleh spasi (atau spasi lainnya), tetapi spasi dapat dimasukkan dalam kata-shell dengan meng-escape atau mengutipnya. Perbedaan antara array [@]dan [*]-expanded dalam tanda kutip ganda adalah yang "${myarray[@]}"menyebabkan setiap elemen array diperlakukan sebagai kata shell terpisah, sementara "${myarray[*]}"menghasilkan kata shell tunggal dengan semua elemen array dipisahkan oleh spasi (atau apapun karakter pertama IFS).

Biasanya, [@]perilaku itu yang Anda inginkan. Misalkan kita memiliki perls=(perl-one perl-two)dan menggunakan ls "${perls[*]}"- itu setara dengan ls "perl-one perl-two", yang akan mencari satu file bernama perl-one perl-two, yang mungkin bukan yang Anda inginkan. ls "${perls[@]}"setara dengan ls "perl-one" "perl-two", yang jauh lebih mungkin untuk melakukan sesuatu yang bermanfaat.

Memberikan daftar kata penyelesaian (yang akan saya sebut kata-kata untuk menghindari kebingungan dengan kata-kata-shell) compgenberbeda; yang -Wpilihan mengambil daftar comp-kata, tetapi harus dalam bentuk shell-kata dengan comp-kata dipisahkan oleh spasi. Perhatikan bahwa opsi perintah yang selalu mengambil argumen (setidaknya sejauh yang saya tahu) menggunakan satu kata shell - jika tidak, tidak akan ada cara untuk mengetahui kapan argumen ke opsi berakhir, dan argumen perintah reguler (/ other flag opsi) dimulai.

Lebih detail:

perls=(perl-one perl-two)
compgen -W "${perls[*]} /usr/bin/perl" -- ${cur}

setara dengan:

compgen -W "perl-one perl-two /usr/bin/perl" -- ${cur}

... yang melakukan apa yang Anda inginkan. Di samping itu,

perls=(perl-one perl-two)
compgen -W "${perls[@]} /usr/bin/perl" -- ${cur}

setara dengan:

compgen -W "perl-one" "perl-two /usr/bin/perl" -- ${cur}

... yang benar-benar tidak masuk akal: "perl-one" adalah satu-satunya comp-word yang dilampirkan ke flag -W, dan argumen nyata pertama - yang akan dianggap compgen sebagai string yang harus diselesaikan - adalah "perl-two / usr / bin / perl ". Saya berharap compgen akan mengeluh bahwa itu telah diberi argumen tambahan ("-" dan apa pun yang ada di $ cur), tetapi tampaknya itu hanya mengabaikannya.

Gordon Davisson
sumber
3
Ini luar biasa; Terima kasih. Saya benar-benar berharap itu meledak lebih keras, tetapi ini setidaknya menjelaskan mengapa itu tidak berhasil.
Telemakus
61

Judul Anda bertanya tentang ${array[@]}versus ${array[*]}tetapi kemudian Anda bertanya tentang $array[*]versus$array[@] yang agak membingungkan. Saya akan menjawab keduanya:

Saat Anda mengutip variabel array dan menggunakannya @sebagai subskrip, setiap elemen array akan diperluas ke konten penuhnya terlepas dari spasi (sebenarnya, salah satu $IFS) yang mungkin ada di dalam konten itu. Saat Anda menggunakan tanda bintang ( *) sebagai subskrip (terlepas dari apakah itu dikutip atau tidak), itu mungkin meluas ke konten baru yang dibuat dengan memecah setiap konten elemen array di $IFS.

Berikut contoh skripnya:

#!/bin/sh

myarray[0]="one"
myarray[1]="two"
myarray[3]="three four"

echo "with quotes around myarray[*]"
for x in "${myarray[*]}"; do
        echo "ARG[*]: '$x'"
done

echo "with quotes around myarray[@]"
for x in "${myarray[@]}"; do
        echo "ARG[@]: '$x'"
done

echo "without quotes around myarray[*]"
for x in ${myarray[*]}; do
        echo "ARG[*]: '$x'"
done

echo "without quotes around myarray[@]"
for x in ${myarray[@]}; do
        echo "ARG[@]: '$x'"
done

Dan inilah hasilnya:

with quotes around myarray[*]
ARG[*]: 'one two three four'
with quotes around myarray[@]
ARG[@]: 'one'
ARG[@]: 'two'
ARG[@]: 'three four'
without quotes around myarray[*]
ARG[*]: 'one'
ARG[*]: 'two'
ARG[*]: 'three'
ARG[*]: 'four'
without quotes around myarray[@]
ARG[@]: 'one'
ARG[@]: 'two'
ARG[@]: 'three'
ARG[@]: 'four'

Saya pribadi biasanya mau "${myarray[@]}". Sekarang, untuk menjawab bagian kedua dari pertanyaan Anda, ${array[@]}versus $array[@].

Mengutip dokumen bash, yang Anda kutip:

Kawat gigi diperlukan untuk menghindari konflik dengan operator perluasan nama file shell.

$ myarray=
$ myarray[0]="one"
$ myarray[1]="two"
$ echo ${myarray[@]}
one two

Tapi, ketika Anda melakukannya $myarray[@], tanda dolar terikat erat myarraysehingga dievaluasi sebelum [@]. Sebagai contoh:

$ ls $myarray[@]
ls: cannot access one[@]: No such file or directory

Tapi, seperti yang dicatat dalam dokumentasi, tanda kurung untuk perluasan nama file, jadi mari kita coba ini:

$ touch one@
$ ls $myarray[@]
one@

Sekarang kita dapat melihat bahwa perluasan nama file terjadi setelah $myarray perluasan.

Dan satu catatan lagi, $myarraytanpa subskrip akan meluas ke nilai pertama dari array:

$ myarray[0]="one four"
$ echo $myarray[5]
one four[5]
Kaleb Pederson
sumber
1
Juga lihat ini mengenai bagaimana IFSmempengaruhi keluaran secara berbeda tergantung pada @vs. *dan dikutip vs. tidak dikutip.
Dennis Williamson
Saya minta maaf, karena ini cukup penting dalam konteks ini, tetapi saya selalu bermaksud ${array[*]}atau ${array[@]}. Kurangnya kawat gigi hanyalah kecerobohan. Selain itu, dapatkah Anda menjelaskan apa yang ${array[*]}akan diperluas dalam compgenperintah? Artinya, dalam konteks itu apa yang dimaksud dengan memperluas array ke dalam setiap elemennya secara terpisah?
Telemakus
Dengan kata lain, Anda (seperti hampir semua sumber) mengatakan bahwa ${array[@]}biasanya itulah cara yang harus ditempuh. Apa yang saya coba pahami adalah mengapa dalam kasus ini hanya ${array[*]} berhasil.
Telemakus
1
Itu karena daftar kata yang disertakan dengan opsi -W harus diberikan sebagai kata tunggal (yang kemudian dipecah oleh compgen berdasarkan IFS). Jika dipecah menjadi kata-kata terpisah sebelum diserahkan ke compgen (yang dilakukan oleh [@]), compgen akan berpikir bahwa hanya yang pertama berjalan dengan -W, dan sisanya adalah argumen biasa (dan saya pikir itu hanya mengharapkan satu argumen, dan karena itu akan muntah).
Gordon Davisson
@ Gordon: Pindahkan itu ke jawaban, dan saya akan menerimanya. Itulah yang benar-benar ingin saya ketahui. Terima kasih. (Btw, itu tidak muntah dengan cara yang jelas. Ini diam-diam muntah - yang membuatnya sulit untuk mengetahui apa yang salah.)
Telemakus