Semua jawaban untuk pertanyaan ini salah dalam satu atau lain cara.
Jawaban salah # 1
IFS=', ' read -r -a array <<< "$string"
1: Ini adalah penyalahgunaan $IFS
. Nilai dari $IFS
variabel tidak diambil sebagai variabel-panjang tunggal separator string, melainkan diambil sebagai set dari karakter tunggal pemisah tali, di mana masing-masing bidang yang read
perpecahan off dari garis masukan dapat dihentikan oleh setiap karakter dalam set (koma atau spasi, dalam contoh ini).
Sebenarnya, untuk stickler nyata di luar sana, makna penuh $IFS
sedikit lebih terlibat. Dari manual bash :
Shell memperlakukan setiap karakter IFS sebagai pembatas, dan membagi hasil ekspansi lainnya menjadi kata-kata menggunakan karakter ini sebagai terminator bidang. Jika IFS tidak disetel, atau nilainya tepat <spasi><tab> <newline> , default, lalu urutan <spasi> , <tab> , dan <newline> di awal dan akhir hasil ekspansi sebelumnya diabaikan, dan setiap urutan karakter IFS tidak di awal atau akhir berfungsi untuk membatasi kata-kata. Jika IFS memiliki nilai selain dari default, maka urutan karakter spasi <spasi> , <tab> , dan <diabaikan di awal dan akhir kata, selama karakter spasi putih adalah nilai IFS ( karakter spasi IFS ). Setiap karakter dalam IFS yang bukan spasi IFS , bersama dengan karakter spasi IFS yang berdekatan , membatasi bidang. Urutan karakter spasi putih IFS juga diperlakukan sebagai pembatas. Jika nilai IFS adalah nol, tidak ada pemisahan kata.
Pada dasarnya, untuk nilai non-null non-default $IFS
, bidang dapat dipisahkan dengan (1) urutan satu atau lebih karakter yang semuanya dari set "karakter spasi spasi IFS" (yaitu, yang mana dari <spasi> , <tab> , dan <newline> ("baris baru" umpan garis makna (LF) ) hadir di mana saja di $IFS
), atau (2) non- "karakter spasi IFS" yang hadir $IFS
bersama dengan "karakter spasi IFS" apa pun yang mengelilinginya pada baris input.
Untuk OP, ada kemungkinan bahwa mode pemisahan kedua yang saya jelaskan di paragraf sebelumnya adalah persis apa yang dia inginkan untuk string inputnya, tetapi kita dapat cukup yakin bahwa mode pemisahan pertama yang saya jelaskan tidak benar sama sekali. Misalnya, bagaimana jika string inputnya 'Los Angeles, United States, North America'
?
IFS=', ' read -ra a <<<'Los Angeles, United States, North America'; declare -p a;
## declare -a a=([0]="Los" [1]="Angeles" [2]="United" [3]="States" [4]="North" [5]="America")
2: Bahkan jika Anda menggunakan solusi ini dengan pemisah satu karakter (seperti koma dengan sendirinya, yaitu, tanpa ruang berikut atau bagasi lain), jika nilai $string
variabel kebetulan mengandung LF, maka read
akan berhenti memproses setelah bertemu LF pertama. The read
builtin hanya memproses satu baris per doa. Ini benar bahkan jika Anda memipihkan atau mengarahkan input hanya ke read
pernyataan, seperti yang kita lakukan dalam contoh ini dengan mekanisme di sini-string , dan dengan demikian input yang tidak diproses dijamin akan hilang. Kode yang mendukung read
builtin tidak memiliki pengetahuan tentang aliran data dalam struktur perintah yang mengandungnya.
Anda bisa berpendapat bahwa ini tidak mungkin menyebabkan masalah, tetapi tetap saja, itu adalah bahaya halus yang harus dihindari jika mungkin. Hal ini disebabkan oleh fakta bahwa read
builtin sebenarnya melakukan dua level pemisahan input: pertama menjadi garis, kemudian ke bidang. Karena OP hanya ingin satu tingkat pemisahan, penggunaan read
builtin ini tidak tepat, dan kita harus menghindarinya.
3: Masalah potensial yang tidak jelas dengan solusi ini adalah bahwa read
selalu menjatuhkan bidang trailing jika kosong, meskipun ia mempertahankan bidang kosong sebaliknya. Ini demo:
string=', , a, , b, c, , , '; IFS=', ' read -ra a <<<"$string"; declare -p a;
## declare -a a=([0]="" [1]="" [2]="a" [3]="" [4]="b" [5]="c" [6]="" [7]="")
Mungkin OP tidak akan peduli tentang ini, tapi masih ada batasan yang perlu diketahui. Ini mengurangi kekokohan dan generalisasi solusi.
Masalah ini dapat diatasi dengan menambahkan pembatas dummy trailing ke string input sesaat sebelum mengumpankannya read
, seperti yang akan saya tunjukkan nanti.
Jawaban salah # 2
string="1:2:3:4:5"
set -f # avoid globbing (expansion of *).
array=(${string//:/ })
Ide serupa:
t="one,two,three"
a=($(echo $t | tr ',' "\n"))
(Catatan: Saya menambahkan tanda kurung yang hilang di sekitar substitusi perintah yang tampaknya dihilangkan oleh penjawab.)
Ide serupa:
string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
Solusi ini memanfaatkan pemisahan kata dalam penugasan array untuk membagi string menjadi bidang. Lucunya, sama seperti read
, pemisahan kata umum juga menggunakan $IFS
variabel khusus, meskipun dalam hal ini tersirat bahwa itu diatur ke nilai default dari <spasi><tab> <newline> , dan oleh karena itu setiap urutan satu atau lebih IFS karakter (yang semuanya merupakan karakter spasi sekarang) dianggap sebagai pembatas bidang.
Ini memecahkan masalah dua tingkat pemisahan yang dilakukan oleh read
, karena pemisahan kata dengan sendirinya merupakan satu tingkat pemisahan. Tapi seperti sebelumnya, masalahnya di sini adalah bahwa masing-masing bidang dalam string input sudah dapat berisi $IFS
karakter, dan dengan demikian mereka akan terpecah secara tidak benar selama operasi pemisahan kata. Ini tidak terjadi pada salah satu string input sampel yang disediakan oleh penjawab ini (betapa nyamannya ...), tetapi tentu saja itu tidak mengubah fakta bahwa basis kode apa pun yang menggunakan idiom ini kemudian akan berisiko. meledak jika asumsi ini pernah dilanggar di beberapa titik di telepon. Sekali lagi, pertimbangkan sampel tandingan saya dari 'Los Angeles, United States, North America'
(atau'Los Angeles:United States:North America'
).
Juga, kata membelah biasanya diikuti dengan ekspansi nama file ( alias ekspansi pathname alias globbing), yang, jika dilakukan, akan kata-kata yang berpotensi korup yang berisi karakter *
, ?
atau [
diikuti oleh ]
(dan, jika extglob
diatur, fragmen kurung didahului oleh ?
, *
, +
, @
, atau !
) dengan mencocokkannya dengan objek sistem file dan memperluas kata-kata ("gumpalan") sesuai. Yang pertama dari tiga penjawab ini telah secara cerdik melemahkan masalah ini dengan menjalankan set -f
sebelumnya untuk menonaktifkan globbing. Secara teknis ini berfungsi (walaupun Anda mungkin harus menambahkanset +f
setelah itu untuk mengaktifkan kembali globbing untuk kode selanjutnya yang mungkin bergantung padanya), tetapi tidak diinginkan untuk mengacaukan pengaturan global shell untuk meretas operasi parsing string-to-array dasar dalam kode lokal.
Masalah lain dengan jawaban ini adalah bahwa semua bidang kosong akan hilang. Ini mungkin atau mungkin tidak menjadi masalah, tergantung pada aplikasi.
Catatan: Jika Anda akan menggunakan solusi ini, lebih baik menggunakan ${string//:/ }
bentuk " parameter substitusi" dari ekspansi parameter , daripada pergi ke kesulitan menerapkan substitusi perintah (yang bercabang shell), memulai pipa, dan menjalankan executable eksternal ( tr
atau sed
), karena ekspansi parameter adalah murni operasi shell-internal. (Juga, untuk tr
dan sed
solusi, variabel input harus dikutip ganda di dalam substitusi perintah; jika tidak, pemisahan kata akan berpengaruh pada echo
perintah dan berpotensi mengacaukan nilai-nilai bidang. Juga, $(...)
bentuk substitusi perintah lebih disukai daripada yang lama).`...`
formulir karena menyederhanakan bersarangnya penggantian perintah dan memungkinkan penyorotan sintaksis yang lebih baik oleh editor teks.)
Jawaban salah # 3
str="a, b, c, d" # assuming there is a space after ',' as in Q
arr=(${str//,/}) # delete all occurrences of ','
Jawaban ini hampir sama dengan # 2 . Perbedaannya adalah bahwa penjawab telah membuat asumsi bahwa bidang dibatasi oleh dua karakter, yang salah diwakili dalam default $IFS
, dan yang lainnya tidak. Dia telah memecahkan kasus yang agak spesifik ini dengan menghapus karakter yang diwakili non-IFS menggunakan ekspansi substitusi pola dan kemudian menggunakan pemisahan kata untuk membagi bidang pada karakter pembatas yang diwakili IFS yang masih hidup.
Ini bukan solusi yang sangat umum. Lebih lanjut, dapat diperdebatkan bahwa koma benar-benar karakter pembatas "primer" di sini, dan melepasnya lalu bergantung pada karakter spasi untuk pemisahan bidang adalah salah. Sekali lagi, pertimbangkan counterexample saya: 'Los Angeles, United States, North America'
.
Juga, sekali lagi, ekspansi nama file dapat merusak kata-kata yang diperluas, tetapi ini dapat dicegah dengan menonaktifkan sementara penggumpalan untuk tugas dengan set -f
dan kemudian set +f
.
Juga, sekali lagi, semua bidang kosong akan hilang, yang mungkin atau mungkin tidak menjadi masalah tergantung pada aplikasi.
Jawaban salah # 4
string='first line
second line
third line'
oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"
Ini mirip dengan # 2 dan # 3 karena menggunakan pemisahan kata untuk menyelesaikan pekerjaan, hanya sekarang kode secara eksplisit mengatur $IFS
untuk berisi hanya pembatas bidang karakter tunggal yang ada dalam string input. Harus diulangi bahwa ini tidak dapat berfungsi untuk pembatas bidang multicharacter seperti pembatas koma-ruang OP. Tetapi untuk pembatas satu karakter seperti LF yang digunakan dalam contoh ini, sebenarnya mendekati sempurna. Kolom tidak dapat dibagi secara tidak sengaja di tengah seperti yang kita lihat dengan jawaban yang salah sebelumnya, dan hanya ada satu tingkat pemisahan, seperti yang diperlukan.
Satu masalah adalah bahwa ekspansi nama file akan merusak kata-kata yang terpengaruh seperti yang dijelaskan sebelumnya, meskipun sekali lagi ini dapat diselesaikan dengan membungkus pernyataan kritis di set -f
dan set +f
.
Masalah potensial lainnya adalah bahwa, karena LF memenuhi syarat sebagai "karakter spasi IFS" sebagaimana didefinisikan sebelumnya, semua bidang kosong akan hilang, seperti pada # 2 dan # 3 . Ini tentu saja tidak akan menjadi masalah jika pembatas kebetulan bukan "ruang karakter spasi IFS", dan tergantung pada aplikasi itu mungkin tidak masalah, tapi itu merusak generalisasi dari solusi.
Jadi, singkatnya, dengan asumsi Anda memiliki pembatas satu karakter, dan itu adalah non-"karakter spasi putih IFS" atau Anda tidak peduli dengan bidang kosong, dan Anda membungkus pernyataan kritis set -f
dan set +f
, maka solusi ini berfungsi , tetapi sebaliknya tidak.
(Juga, demi informasi, menugaskan LF ke variabel dalam bash dapat dilakukan dengan lebih mudah dengan $'...'
sintaks, misalnya IFS=$'\n';
.)
Jawaban salah # 5
countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"
Ide serupa:
IFS=', ' eval 'array=($string)'
Solusi ini secara efektif merupakan persilangan antara # 1 (dalam hal ini ditetapkan $IFS
ke koma-ruang) dan # 2-4 (dalam hal ini menggunakan pemisahan kata untuk membagi string menjadi bidang). Karena itu, ia menderita sebagian besar masalah yang menimpa semua jawaban yang salah di atas, semacam yang terburuk dari semua dunia.
Juga, mengenai varian kedua, sepertinya eval
panggilan itu sama sekali tidak perlu, karena argumennya adalah string literal yang dikutip tunggal, dan oleh karena itu diketahui secara statis. Tetapi sebenarnya ada manfaat yang sangat tidak jelas untuk digunakan eval
dengan cara ini. Biasanya, ketika Anda menjalankan perintah sederhana yang terdiri dari variabel tugas hanya , yang berarti tanpa kata perintah yang sebenarnya berikut, tugas tersebut berlaku dalam lingkungan shell:
IFS=', '; ## changes $IFS in the shell environment
Ini benar bahkan jika perintah sederhana melibatkan banyak penugasan variabel; lagi, selama tidak ada kata perintah, semua tugas variabel mempengaruhi lingkungan shell:
IFS=', ' array=($countries); ## changes both $IFS and $array in the shell environment
Tapi, jika penugasan variabel dilampirkan ke nama perintah (saya suka menyebutnya "penugasan awalan") maka itu tidak mempengaruhi lingkungan shell, dan sebaliknya hanya mempengaruhi lingkungan dari perintah yang dieksekusi, terlepas apakah itu adalah builtin atau eksternal:
IFS=', ' :; ## : is a builtin command, the $IFS assignment does not outlive it
IFS=', ' env; ## env is an external command, the $IFS assignment does not outlive it
Kutipan yang relevan dari manual bash :
Jika tidak ada nama perintah yang dihasilkan, penugasan variabel memengaruhi lingkungan shell saat ini. Jika tidak, variabel ditambahkan ke lingkungan perintah yang dieksekusi dan tidak mempengaruhi lingkungan shell saat ini.
Dimungkinkan untuk mengeksploitasi fitur penugasan variabel ini untuk mengubah $IFS
hanya sementara, yang memungkinkan kita untuk menghindari keseluruhan save-and-restore gambit seperti yang sedang dilakukan dengan $OIFS
variabel dalam varian pertama. Tetapi tantangan yang kita hadapi di sini adalah bahwa perintah yang perlu kita jalankan itu sendiri hanyalah tugas variabel, dan karenanya tidak akan melibatkan kata perintah untuk membuat $IFS
penugasan sementara. Anda mungkin berpikir sendiri, mengapa tidak menambahkan kata perintah no-op pada pernyataan seperti : builtin
membuat $IFS
tugas sementara? Ini tidak berfungsi karena itu akan membuat $array
tugas sementara juga:
IFS=', ' array=($countries) :; ## fails; new $array value never escapes the : command
Jadi, kita secara efektif menemui jalan buntu, sedikit tangkapan-22. Tapi, ketika eval
menjalankan kodenya, ia menjalankannya di lingkungan shell, seolah-olah itu normal, kode sumber statis, dan oleh karena itu kita dapat menjalankan $array
tugas di dalam eval
argumen untuk membuatnya berlaku di lingkungan shell, sementara $IFS
tugas awalan yang diawali dengan eval
perintah tidak akan hidup lebih lama dari eval
perintah. Ini persis trik yang sedang digunakan dalam varian kedua dari solusi ini:
IFS=', ' eval 'array=($string)'; ## $IFS does not outlive the eval command, but $array does
Jadi, seperti yang Anda lihat, itu sebenarnya trik yang cukup pintar, dan menyelesaikan apa yang diperlukan (setidaknya berkenaan dengan efek penugasan) dengan cara yang agak tidak jelas. Saya sebenarnya tidak menentang trik ini secara umum, meskipun ada keterlibatan eval
; hanya berhati-hatilah untuk mengutip argumen string untuk menjaga terhadap ancaman keamanan.
Tetapi sekali lagi, karena aglomerasi masalah "terburuk dari semua dunia", ini masih merupakan jawaban yang salah terhadap persyaratan OP.
Jawaban salah # 6
IFS=', '; array=(Paris, France, Europe)
IFS=' ';declare -a array=(Paris France Europe)
Um ... apa? OP memiliki variabel string yang perlu diuraikan menjadi array. "Jawaban" ini dimulai dengan konten kata demi kata dari string input yang disisipkan ke dalam array literal. Saya kira itu salah satu cara untuk melakukannya.
Sepertinya penjawab mungkin berasumsi bahwa $IFS
variabel mempengaruhi semua parsing bash dalam semua konteks, yang tidak benar. Dari manual bash:
IFS Pemisah Bidang Internal yang digunakan untuk pemisahan kata setelah ekspansi dan untuk memecah baris menjadi kata-kata dengan perintah baca bawaan. Nilai standarnya adalah <spasi><tab> <newline> .
Jadi $IFS
variabel khusus sebenarnya hanya digunakan dalam dua konteks: (1) pemisahan kata yang dilakukan setelah ekspansi (artinya tidak ketika mengurai kode sumber bash) dan (2) untuk memisahkan jalur input menjadi kata-kata oleh read
builtin.
Biarkan saya mencoba membuat ini lebih jelas. Saya pikir mungkin ada baiknya untuk membedakan antara parsing dan eksekusi . Bash pertama-tama harus mem - parsing kode sumber, yang jelas merupakan peristiwa parsing , dan kemudian mengeksekusi kode tersebut, yaitu ketika ekspansi muncul di dalam gambar. Ekspansi benar-benar acara eksekusi . Selanjutnya, saya mengambil masalah dengan deskripsi $IFS
variabel yang baru saja saya kutip di atas; Daripada mengatakan bahwa pemisahan kata dilakukan setelah ekspansi , saya akan mengatakan bahwa pemisahan kata dilakukan selama ekspansi, atau, mungkin bahkan lebih tepatnya, pemisahan kata adalah bagian dariproses ekspansi. Frasa "pemisahan kata" hanya merujuk pada langkah ekspansi ini; itu tidak boleh digunakan untuk merujuk pada parsing dari kode sumber bash, meskipun sayangnya dokumen tampaknya melemparkan sekitar kata "split" dan "kata" banyak. Berikut kutipan yang relevan dari versi bash manual linux.die.net :
Ekspansi dilakukan pada baris perintah setelah dipecah menjadi kata-kata. Ada jenis tujuh ekspansi yang dilakukan: ekspansi brace , tilde ekspansi , parameter dan ekspansi variabel , substitusi perintah , ekspansi aritmatika , kata membelah , dan ekspansi pathname .
Urutan ekspansi adalah: ekspansi brace; ekspansi tilde, ekspansi parameter dan variabel, ekspansi aritmatika, dan penggantian perintah (dilakukan dengan cara kiri-ke-kanan); pemisahan kata; dan perluasan pathname.
Anda bisa berpendapat bahwa versi manual GNU sedikit lebih baik, karena ia memilih kata "token" daripada "kata" di kalimat pertama bagian Ekspansi:
Ekspansi dilakukan pada baris perintah setelah dipecah menjadi token.
Poin pentingnya adalah, $IFS
jangan mengubah cara bash mem-parsing kode sumber. Parsing kode sumber bash sebenarnya adalah proses yang sangat kompleks yang melibatkan pengenalan berbagai elemen tata bahasa shell, seperti urutan perintah, daftar perintah, pipa, ekspansi parameter, penggantian aritmatika, dan penggantian perintah. Untuk sebagian besar, proses parsing bash tidak dapat diubah oleh tindakan tingkat pengguna seperti tugas variabel (sebenarnya, ada beberapa pengecualian kecil untuk aturan ini; misalnya, lihat berbagai compatxx
pengaturan shell, yang dapat mengubah aspek tertentu dari perilaku parsing on-the-fly). "Kata" / "token" hulu yang dihasilkan dari proses penguraian kompleks ini kemudian diperluas sesuai dengan proses umum "ekspansi" sebagaimana dirinci dalam kutipan dokumentasi di atas, di mana pemisahan kata dari teks yang diperluas (yang diperluas?) Ke dalam hilir kata-kata hanyalah satu langkah dari proses itu. Pemisahan kata hanya menyentuh teks yang telah dimuntahkan dari langkah ekspansi sebelumnya; itu tidak mempengaruhi teks literal yang diuraikan langsung dari sumber bytestream.
Jawaban salah # 7
string='first line
second line
third line'
while read -r line; do lines+=("$line"); done <<<"$string"
Ini adalah salah satu solusi terbaik. Perhatikan bahwa kita kembali menggunakan read
. Bukankah saya katakan sebelumnya bahwa read
itu tidak tepat karena melakukan dua tingkat pemisahan, ketika kita hanya membutuhkan satu? Kuncinya di sini adalah bahwa Anda dapat memanggil read
sedemikian rupa sehingga secara efektif hanya melakukan satu tingkat pemisahan, khususnya dengan memisahkan hanya satu bidang per doa, yang mengharuskan biaya harus memanggilnya berulang kali dalam satu lingkaran. Ini sedikit sulap, tapi berhasil.
Tapi ada masalah. Pertama: Ketika Anda memberikan setidaknya satu argumen NAME untuk read
, secara otomatis mengabaikan spasi spasi awal dan akhir di setiap bidang yang terpisah dari string input. Ini terjadi apakah $IFS
diatur ke nilai default atau tidak, seperti dijelaskan sebelumnya dalam posting ini. Sekarang, OP mungkin tidak peduli dengan kasus penggunaan spesifiknya, dan pada kenyataannya, ini mungkin fitur yang diinginkan dari perilaku parsing. Tetapi tidak semua orang yang ingin mengurai string ke bidang akan menginginkan ini. Namun ada solusinya: Penggunaan yang agak tidak jelas read
adalah untuk meloloskan nol argumen NAMA . Dalam hal ini, read
akan menyimpan seluruh jalur input yang didapat dari aliran input dalam variabel bernama $REPLY
, dan, sebagai bonus, itu tidakstrip whitespace terkemuka dan tertinggal dari nilai. Ini adalah penggunaan yang sangat kuat read
yang sering saya manfaatkan dalam karier pemrograman shell saya. Inilah demonstrasi perbedaan perilaku:
string=$' a b \n c d \n e f '; ## input string
a=(); while read -r line; do a+=("$line"); done <<<"$string"; declare -p a;
## declare -a a=([0]="a b" [1]="c d" [2]="e f") ## read trimmed surrounding whitespace
a=(); while read -r; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]=" a b " [1]=" c d " [2]=" e f ") ## no trimming
Masalah kedua dengan solusi ini adalah tidak benar-benar mengatasi kasus pemisah bidang khusus, seperti koma-ruang OP. Seperti sebelumnya, pemisah multicharacter tidak didukung, yang merupakan batasan yang disayangkan dari solusi ini. Kami dapat mencoba setidaknya membagi pada koma dengan menentukan pemisah untuk -d
opsi, tetapi lihat apa yang terjadi:
string='Paris, France, Europe';
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France")
Dapat diprediksi, spasi putih di sekitarnya yang tidak terhitung dapat ditarik ke dalam nilai-nilai lapangan, dan karenanya ini harus diperbaiki selanjutnya melalui operasi pemangkasan (ini juga bisa dilakukan langsung dalam loop-sementara). Tapi ada kesalahan lain yang jelas: Eropa hilang! Apa yang terjadi dengannya? Jawabannya adalah read
mengembalikan kode pengembalian yang gagal jika hits akhir file (dalam hal ini kita dapat menyebutnya end-of-string) tanpa menemui terminator bidang terakhir pada bidang terakhir. Hal ini menyebabkan loop sementara rusak sebelum waktunya dan kami kehilangan bidang terakhir.
Secara teknis kesalahan yang sama ini juga menimpa contoh-contoh sebelumnya; perbedaannya adalah bahwa pemisah bidang dianggap LF, yang merupakan default ketika Anda tidak menentukan -d
opsi, dan <<<
mekanisme ("di sini-string") secara otomatis menambahkan LF ke string tepat sebelum ia memasukkannya sebagai masukan ke perintah. Oleh karena itu, dalam kasus tersebut, kami semacam secara tidak sengaja memecahkan masalah bidang akhir yang dijatuhkan dengan tanpa sengaja menambahkan terminator dummy tambahan ke input. Sebut solusi ini sebagai solusi "dummy-terminator". Kita dapat menerapkan solusi dummy-terminator secara manual untuk setiap pembatas khusus dengan menggabungkannya sendiri dengan string input ketika membuat instance dalam string di sini:
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,"; declare -p a;
declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")
Di sana, masalah terpecahkan. Solusi lain adalah dengan hanya mematahkan while-loop jika kedua (1) read
kembali gagal dan (2) $REPLY
kosong, artinya read
tidak dapat membaca karakter apa pun sebelum memukul file akhir. Demo:
a=(); while read -rd,|| [[ -n "$REPLY" ]]; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')
Pendekatan ini juga mengungkapkan LF rahasia yang secara otomatis ditambahkan ke string di sini oleh <<<
operator pengalihan. Tentu saja bisa dilucuti secara terpisah melalui operasi pemangkasan eksplisit seperti yang dijelaskan beberapa saat yang lalu, tetapi jelas pendekatan dummy-terminator manual menyelesaikannya secara langsung, jadi kita bisa langsung melakukannya. Solusi dummy-terminator manual sebenarnya cukup nyaman karena dapat menyelesaikan kedua masalah ini (masalah field-final yang dijatuhkan dan masalah LF yang ditambahkan) dalam sekali jalan.
Jadi, secara keseluruhan, ini adalah solusi yang sangat kuat. Hanya saja kelemahan yang tersisa adalah kurangnya dukungan untuk pembatas multicharacter, yang akan saya bahas nanti.
Jawaban salah # 8
string='first line
second line
third line'
readarray -t lines <<<"$string"
(Ini sebenarnya dari pos yang sama dengan # 7 ; penjawab menyediakan dua solusi di pos yang sama.)
The readarray
builtin, yang merupakan sinonim untuk mapfile
, sangat ideal. Ini adalah perintah bawaan yang mem-parsing bytestream menjadi variabel array dalam satu shot; tidak main-main dengan loop, conditional, substitusi, atau apa pun. Dan itu tidak secara diam-diam menghapus spasi putih dari string input. Dan (jika -O
tidak diberikan) itu dengan mudah menghapus array target sebelum menetapkan untuk itu. Tapi itu masih belum sempurna, karenanya kritik saya tentang itu sebagai "jawaban yang salah".
Pertama, hanya untuk menghilangkan hal ini, perhatikan bahwa, sama seperti perilaku read
ketika melakukan field-parsing, readarray
turunkan trailing field jika kosong. Sekali lagi, ini mungkin bukan masalah bagi OP, tetapi bisa untuk beberapa kasus penggunaan. Saya akan kembali ke sini sebentar lagi.
Kedua, seperti sebelumnya, itu tidak mendukung pembatas multicharacter. Saya akan memberikan perbaikan untuk ini sebentar lagi.
Ketiga, solusi seperti yang tertulis tidak menguraikan string input OP, dan pada kenyataannya, itu tidak dapat digunakan apa adanya untuk menguraikannya. Saya akan memperluas ini sebentar juga.
Untuk alasan di atas, saya masih menganggap ini sebagai "jawaban yang salah" untuk pertanyaan OP. Di bawah ini saya akan memberikan apa yang saya anggap sebagai jawaban yang tepat.
Jawaban benar
Berikut ini adalah upaya naif untuk membuat # 8 berfungsi dengan hanya menentukan -d
opsi:
string='Paris, France, Europe';
readarray -td, a <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')
Kami melihat hasilnya identik dengan hasil yang kami dapatkan dari pendekatan kondisional ganda dari read
solusi looping yang dibahas dalam # 7 . Kita hampir dapat menyelesaikan ini dengan trik dummy-terminator manual:
readarray -td, a <<<"$string,"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe" [3]=$'\n')
Masalahnya di sini adalah bahwa readarray
mempertahankan bidang trailing, karena <<<
operator pengalihan menambahkan LF ke string input, dan oleh karena itu bidang trailing tidak kosong (jika tidak maka akan dijatuhkan). Kita dapat mengatasinya dengan secara eksplisit membatalkan elemen array akhir setelah fakta:
readarray -td, a <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")
Hanya dua masalah yang tersisa, yang sebenarnya terkait, adalah (1) ruang putih asing yang perlu dipangkas, dan (2) kurangnya dukungan untuk pembatas multicharacter.
Ruang kosong tentu saja dapat dipangkas sesudahnya (misalnya, lihat Cara memangkas ruang kosong dari variabel Bash? ). Tetapi jika kita dapat meretas pembatas multicharacter, maka itu akan menyelesaikan kedua masalah dalam satu kesempatan.
Sayangnya, tidak ada cara langsung untuk membuat pembatas multicharacter berfungsi. Solusi terbaik yang saya pikirkan adalah preprocess string input untuk menggantikan pembatas multicharacter dengan pembatas karakter tunggal yang akan dijamin tidak akan bertabrakan dengan isi dari string input. Satu-satunya karakter yang memiliki jaminan ini adalah byte NUL . Ini karena, dalam bash (meskipun tidak dalam zsh, kebetulan), variabel tidak dapat berisi byte NUL. Langkah preprocessing ini dapat dilakukan secara inline dalam proses substitusi. Berikut cara melakukannya menggunakan awk :
readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; }' <<<"$string, "); unset 'a[-1]';
declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")
Akhirnya! Solusi ini tidak akan secara keliru membagi bidang di tengah, tidak akan memotong sebelum waktunya, tidak akan menjatuhkan bidang kosong, tidak akan merusak dirinya sendiri pada ekspansi nama file, tidak akan secara otomatis menghapus spasi spasi awal dan akhir, tidak akan meninggalkan LF penumpang gelap pada akhirnya, tidak memerlukan loop, dan tidak puas dengan pembatas satu karakter.
Solusi pemangkasan
Terakhir, saya ingin menunjukkan solusi pemangkasan saya sendiri yang cukup rumit dengan menggunakan -C callback
opsi yang tidak jelas readarray
. Sayangnya, saya sudah kehabisan ruang melawan batas posting 30.000 karakter Stack Overflow, jadi saya tidak akan bisa menjelaskannya. Saya akan meninggalkan itu sebagai latihan untuk pembaca.
function mfcb { local val="$4"; "$1"; eval "$2[$3]=\$val;"; };
function val_ltrim { if [[ "$val" =~ ^[[:space:]]+ ]]; then val="${val:${#BASH_REMATCH[0]}}"; fi; };
function val_rtrim { if [[ "$val" =~ [[:space:]]+$ ]]; then val="${val:0:${#val}-${#BASH_REMATCH[0]}}"; fi; };
function val_trim { val_ltrim; val_rtrim; };
readarray -c1 -C 'mfcb val_trim a' -td, <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")
,
(koma-ruang) dan bukan karakter tunggal seperti koma. Jika Anda hanya tertarik pada yang terakhir, jawaban di sini lebih mudah diikuti: stackoverflow.com/questions/918886/…