Misteri ekspansi penjepit bersarang di Bash

19

Ini:

$ echo {{a..c},{1..3}}

menghasilkan ini:

a b c 1 2 3

Yang bagus, tetapi sulit untuk dijelaskan mengingat itu

$ echo {a..c},{1..3}

memberi

a,1 a,2 a,3 b,1 b,2 b,3 c,1 c,2 c,3

Apakah ini didokumentasikan di suatu tempat? The Bash Referensi tidak menyebutkan itu (meskipun memiliki contoh menggunakannya).

xenoid
sumber

Jawaban:

18

Yah, itu terurai satu lapisan pada satu waktu:

X{{a..c},{1..3}}Y

didokumentasikan sebagai yang diperluas untuk X{a..c}Y X{1..3}Y(yang ini X{A,B}Ydiperluas untuk XA XBdengan Amenjadi {a..c}dan Bmenjadi {1..3}), diri mereka didokumentasikan sebagai diperluas ke XaY XbY XcY X1Y X2Y X3Y.

Yang mungkin patut didokumentasikan adalah bahwa mereka dapat disarangkan (bahwa yang pertama }tidak menutup yang pertama {di sana misalnya).

Saya kira cangkang bisa memilih untuk menyelesaikan kawat gigi bagian dalam terlebih dahulu, seperti dengan bertindak pada setiap penutupan }pada gilirannya:

  1. X{{a..c},{1..3}}
  2. X{a,{1..3}}Y X{b,{1..3}}Y X{c,{1..3}}Y

    (yang A{a..c}Bdiperluas untuk AaB AbB AcB, di mana Aadalah X{dan Badalah ,{1..3}Y)

  3. X{a,1}Y X{a,2}Y X{a,3}Y X{b,1}Y X{b,2}Y X{b,3}Y X{c,1}Y X{c,2}Y X{c,3}Y

  4. XaY X1Y XaY Xa2...

Tetapi saya tidak menemukan bahwa secara khusus lebih intuitif atau berguna (lihat contoh Kevin dalam komentar misalnya), masih akan ada beberapa ambiguitas mengenai urutan di mana ekspansi akan dilakukan, dan itu tidak seberapa csh(shell yang memperkenalkan brace ekspansi di akhir 70-an, sementara {1..3}formulir datang kemudian (1995) dari zshdan {a..c}kemudian (2004) dari bash) melakukannya.

Perhatikan bahwa csh(dari awal, lihat halaman manual 2BSD (1979) ) mendokumentasikan fakta bahwa ekspansi brace dapat disarangkan, meskipun tidak secara eksplisit mengatakan bagaimana ekspansi brace bersarang akan diperluas. Tetapi Anda dapat melihat cshkode dari tahun 1979 untuk melihat bagaimana itu dilakukan saat itu. Lihat bagaimana memang menangani sarang secara eksplisit, dan bagaimana hal itu diselesaikan mulai dari kawat gigi luar.

Dalam hal apapun, saya tidak benar-benar melihat bagaimana ekspansi {a..c},{1..3}dapat memiliki pengaruh. Di sana, ,ini bukan operator dari ekspansi brace (karena tidak ada di dalam kurung kurawal), jadi diperlakukan seperti karakter biasa.

Stéphane Chazelas
sumber
Sepertinya aneh bagi saya bahwa kawat gigi luar seharusnya diselesaikan sebelum yang dalam.
Hauke ​​Laging
@ stéphane-chazelas Ada dua cara yang jelas bahwa ungkapan ini mungkin diuraikan. Mengapa itu diurai satu arah dan bukan yang lain? Komentar Anda sepertinya tidak memberikan penjelasan.
igal
Jadi, penjelasan itu masuk akal, tetapi jika ini "didokumentasikan sebagai diperluas ke ..." apakah ada URL?
xenoid
@ xenoid Lihat solusi saya yang diperbarui.
igal
1
@ (semua orang): Pertimbangkan ekspansi /dev/{h,s}d{a..d}{1..4,}. Sekarang anggaplah Anda ingin memperluasnya juga termasuk /dev/nulldan /dev/zero. Jika ekspansi brace bekerja dari dalam ke luar, ekspansi itu akan sangat mengganggu untuk dibangun. Tetapi karena itu bekerja dari luar, itu cukup sepele:/dev/{null,zero,{h,s}d{a..d}{1..4,}}
Kevin
7

Inilah jawaban singkatnya. Dalam ekspresi pertama koma digunakan sebagai pemisah, jadi ekspansi brace hanyalah gabungan dari dua subekspresi bersarang. Dalam ekspresi kedua koma tersebut sendiri diperlakukan sebagai subexpression karakter tunggal, sehingga ekspresi produk yang terbentuk.

Apa yang Anda lewatkan adalah definisi tentang bagaimana brace-ekspansi dilakukan. Berikut adalah tiga referensi:

Penjelasan lebih rinci berikut.


Anda membandingkan hasil dari ungkapan ini:

$ echo {{a..c},{1..3}}
a b c 1 2 3

ke hasil ungkapan ini:

$ echo {a..c},{1..3}
a,1 a,2 a,3 b,1 b,2 b,3 c,1 c,2 c,3

Anda mengatakan bahwa ini sulit untuk dijelaskan, yaitu bahwa ini kontra-intuitif. Apa yang hilang adalah definisi formal tentang bagaimana brace-ekspansi diproses. Anda perhatikan bahwa Bash Manual tidak memberikan definisi penuh.

Saya mencari sedikit tetapi saya tidak dapat menemukan definisi yang hilang (lengkap, formal) juga. Jadi saya pergi ke kode sumber:

Sumber berisi beberapa komentar yang bermanfaat. Pertama adalah gambaran umum tingkat tinggi dari algoritma ekspansi brace:

Basic idea:

Segregate the text into 3 sections: preamble (stuff before an open brace),
postamble (stuff after the matching close brace) and amble (stuff after
preamble, and before postamble).  Expand amble, and then tack on the
expansions to preamble.  Expand postamble, and tack on the expansions to
the result so far.

Jadi format token ekspansi-ekspansi adalah sebagai berikut:

<PREAMBLE><AMBLE><POSTAMBLE>

Titik masuk utama ke ekspansi adalah fungsi yang disebut brace_expandyang dijelaskan sebagai berikut:

Return an array of strings; the brace expansion of TEXT.

Jadi brace_expandfungsi mengambil string yang mewakili ekspresi ekspansi penjepit dan mengembalikan array string yang diperluas.

Menggabungkan dua pengamatan ini kita melihat bahwa amble diperluas ke daftar string, yang masing-masing digabungkan ke pembukaan. Pembukaan pos kemudian diperluas ke daftar string, dan setiap string dalam daftar postamble digabungkan ke setiap string dalam daftar pembukaan / amble (yaitu produk dari dua daftar dibentuk). Tetapi ini tidak menjelaskan bagaimana amble dan postamble diproses. Untungnya ada komentar yang menggambarkan hal itu juga. Amble diproses oleh fungsi yang disebut expand_ambledefinisi yang didahului oleh komentar berikut:

Expand the text found inside of braces.  We simply try to split the
text at BRACE_ARG_SEPARATORs into separate strings.  We then brace
expand each slot which needs it, until there are no more slots which
need it.

Di tempat lain dalam kode kita melihat bahwa BRACE_ARG_SEPARATOR didefinisikan sebagai koma. Ini memperjelas bahwa amble adalah daftar string yang dipisahkan koma, beberapa di antaranya mungkin juga merupakan ekspresi brace-expansion. String ini kemudian membentuk satu array. Akhirnya, kita juga bisa melihat bahwa setelah expand_ambledipanggil brace_expandfungsi kemudian dipanggil secara rekursif pada postamble. Ini memberi kita deskripsi lengkap tentang algoritma.

Ada beberapa referensi (tidak resmi) lain yang menguatkan temuan ini.

Untuk satu referensi, lihat Bash Hackers Wiki . Bagian tentang menggabungkan dan bersarang tidak cukup mengatasi masalah Anda, tetapi halaman tersebut memberikan sintaksis / tata bahasa dari ekspansi brace, yang menurut saya menjawab pertanyaan Anda. Sintaks diberikan oleh pola-pola berikut:

{string1,string2,...,stringN}

{<START>..<END>}

<PREAMBLE>{........}

{........}<POSTSCRIPT>

<PREAMBLE>{........}<POSTSCRIPT>

Dan penguraian dijelaskan sebagai berikut:

Ekspansi Brace digunakan untuk menghasilkan string acak. String yang ditentukan digunakan untuk menghasilkan semua kombinasi yang mungkin dengan preamble dan postscript opsional.

Untuk referensi lain, lihat Panduan Pemula Bash , yang memiliki kata-kata berikut ini:

Brace expansion is a mechanism by which arbitrary strings may be generated. Patterns to be brace-expanded take the form of an optional PREAMBLE, followed by a series of comma-separated strings between a pair of braces, followed by an optional POSTSCRIPT. The preamble is prefixed to each string contained within the braces, and the postscript is then appended to each resulting string, expanding left to right.

Jadi untuk mengurai ekspresi brace-expansion, kita belok kiri-ke-kanan, memperluas setiap ekspresi dan membentuk produk yang berurutan (berkenaan dengan operasi string-concatenation).

Sekarang mari kita perhatikan ungkapan pertama Anda:

{{a..c},{1..3}}

Dalam bahasa Wiki Hacker Bash, ini cocok dengan bentuk pertama:

{string1,string2,...,stringN}

Di mana N=2, string1={a..c}dan string2={1..3}- ekspansi penjepit di dalam dilakukan pertama kali dan masing-masing dari mereka dalam bentuk {<START>..<END>}. Sebagai alternatif, kita dapat mengatakan bahwa ini adalah ekspresi brace-expansion yang hanya terdiri dari amble (tidak ada pembukaan atau postamble). Amble adalah daftar yang dipisahkan koma, jadi kami memeriksa slot satu demi satu, dan melakukan ekspansi tambahan jika diperlukan. Tidak ada produk yang terbentuk karena tidak ada ekspresi yang berdekatan (koma digunakan sebagai pemisah).

Selanjutnya mari kita lihat ekspresi kedua Anda:

{a..c},{1..3}

Dalam bahasa Wiki Bash Hacker, ungkapan ini cocok dengan bentuk:

{........}<POSTSCRIPT>

di mana postscript adalah sub-ekspresi ,{1..3}. Atau, kita dapat mengatakan bahwa ungkapan ini memiliki amble ( {a..c}) dan postamble ( ,{1..3}). Amble diperluas ke daftar a b cdan kemudian masing-masing digabungkan dengan masing-masing string dalam ekspansi postamble. Pembukaan pos diproses secara rekursif: memiliki pembukaan ,dan amble dari {1..3}. Ini diperluas ke daftar ,1 ,2 ,3. Dua daftar a b cdan ,1 ,2 ,3kemudian digabungkan untuk membentuk daftar produk a,1 a,2 a,3 b,1 b,2 b,3 c,1 c,2 c,3.

Mungkin membantu untuk memberikan deskripsi psuedo-aljabar tentang bagaimana ekspresi ini diuraikan, di mana tanda kurung "[]" menunjukkan array, "+" menunjukkan penggabungan array, dan "*" menunjukkan produk Cartesian (berkenaan dengan penggabungan).

Inilah cara ekspresi pertama diperluas (satu langkah per baris):

{{a..c},{1..3}}
{a..c} + {1..3}
[a b c] + [1 2 3]
a b c 1 2 3

Dan inilah cara ekspresi kedua diperluas:

{a..c},{1..3}
{a..c} * ,{1..3}
[a b c] * [,1 ,2 ,3]
a,1 a,2 a,3 b,1 b,2 b,3 c,1 c,2 c,3
igal
sumber
2

Pemahaman saya adalah ini:

Kawat gigi bagian dalam diselesaikan terlebih dahulu (seperti biasa) yang berputar

{{a..c},{1..3}}

ke

{a,b,c,1,2,3}

Karena ,ada di dalam kurung kurawal itu hanya memisahkan elemen kurung.

Tetapi dalam kasus

{a..c},{1..3}

yang ,tidak dalam kurung yaitu itu adalah karakter biasa menyebabkan permutasi penjepit di kedua sisi.

Hauke ​​Laging
sumber
Jadi {a..c}apakah itu tergantung a,b,catau a b ctergantung pada kelembaban dan Dow Jones? Rapi.
kubanczyk
Ini agak membingungkan. Jika {{a..c},{1..3}}sama dengan {a,b,c,1,2,3}, maka seharusnya tidak {{a..c}.{1..3}}sama dengan {a,b,c.1,2,3}? Tentu saja bukan ini masalahnya.
ilkkachu
@ilkkachu Kenapa harus sama? ,adalah karakter pemisahan ekspansi brace, .bukan. Mengapa karakter biasa harus mengarah ke hasil yang sama seperti yang spesial? c.1adalah elemen penjepit. Namun dalam {a..c}.{1..3}yang .adalah jangkar untuk ekspansi penjepit di sebelah kiri dan kanan. Dengan ,kawat gigi luar digunakan untuk ekspansi brace karena konten mereka memiliki format ekspansi brace, dengan .mereka bukan karena konten mereka tidak memiliki format itu.
Hauke ​​Laging
@ HaukeLaging, well, jika {{a..c},{1..3}}berubah menjadi {a,b,c,1,2,3}maka beberapa koma hanya muncul di antara a, bdan c. Mengapa mereka tidak muncul dengan cara yang sama {a..c}.{1..3}? Komentar oleh @kubanczyk adalah tentang hal yang sama, jika koma muncul di sana seperti itu, bagaimana kita tahu kapan ekspansi menghasilkan koma dan kapan tidak? Jawabannya tentu saja, bahwa tidak pernah menghasilkan koma dengan sendirinya, itu menghasilkan daftar kata-kata. Jadi tidak ada yang berubah menjadi {a,b,c,1,2,3}atau {a,b,c.1,2,3}.
ilkkachu
@kubanczyk Anda seharusnya tidak mengolok-olok jawaban yang tidak Anda mengerti.
Hauke ​​Laging