Pisahkan string ke dalam array di Bash


641

Dalam skrip Bash saya ingin membagi satu baris menjadi beberapa bagian dan menyimpannya dalam sebuah array.

Garis:

Paris, France, Europe

Saya ingin memilikinya dalam array seperti ini:

array[0] = Paris
array[1] = France
array[2] = Europe

Saya ingin menggunakan kode sederhana, kecepatan perintah tidak masalah. Bagaimana saya bisa melakukannya?


22
Ini adalah hit Google # 1 tetapi ada kontroversi dalam jawabannya karena pertanyaan sayangnya menanyakan tentang pembatasan pada , (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/…
antak

Jika Anda ingin membuat string dan tidak peduli memilikinya sebagai array, cutadalah perintah bash yang berguna untuk diingat juga. Pemisah dapat didefinisikan en.wikibooks.org/wiki/Cut Anda juga dapat mengekstrak data dari struktur catatan lebar tetap juga. en.wikipedia.org/wiki/Cut_(Unix) computerhope.com/unix/ucut.htm
JGFMK

Jawaban:


1090
IFS=', ' read -r -a array <<< "$string"

Perhatikan bahwa karakter dalam $IFSdiperlakukan secara individual sebagai pemisah sehingga dalam hal ini bidang dapat dipisahkan dengan baik koma atau spasi daripada urutan dua karakter. Menariknya, bidang kosong tidak dibuat ketika koma-ruang muncul di input karena ruang diperlakukan secara khusus.

Untuk mengakses elemen individual:

echo "${array[0]}"

Untuk beralih ke elemen:

for element in "${array[@]}"
do
    echo "$element"
done

Untuk mendapatkan indeks dan nilainya:

for index in "${!array[@]}"
do
    echo "$index ${array[index]}"
done

Contoh terakhir berguna karena array Bash jarang. Dengan kata lain, Anda dapat menghapus elemen atau menambahkan elemen dan indeksnya tidak bersebelahan.

unset "array[1]"
array[42]=Earth

Untuk mendapatkan jumlah elemen dalam array:

echo "${#array[@]}"

Seperti disebutkan di atas, array bisa jadi jarang sehingga Anda tidak harus menggunakan panjang untuk mendapatkan elemen terakhir. Begini caranya di Bash 4.2 dan yang lebih baru:

echo "${array[-1]}"

di semua versi Bash (dari suatu tempat setelah 2.05b):

echo "${array[@]: -1:1}"

Offset negatif yang lebih besar pilih lebih jauh dari ujung array. Catat spasi sebelum tanda minus dalam formulir yang lebih lama. Itu wajib.


15
Cukup gunakan IFS=', ', maka Anda tidak perlu menghapus spasi secara terpisah. Tes:IFS=', ' read -a array <<< "Paris, France, Europe"; echo "${array[@]}"
l0b0

4
@ l0b0: Terima kasih. Saya tidak tahu apa yang saya pikirkan. declare -p arrayNgomong-ngomong, saya suka menggunakan untuk hasil tes.
Dijeda sampai pemberitahuan lebih lanjut.

1
Ini sepertinya tidak menghargai kutipan. Misalnya France, Europe, "Congo, The Democratic Republic of the"ini akan terpecah setelah congo.
Yisrael Dov

2
@YisraelDov: Bash tidak memiliki cara untuk menangani CSV dengan sendirinya. Itu tidak bisa membedakan antara koma di dalam tanda kutip dan yang di luar mereka. Anda perlu menggunakan alat yang memahami CSV seperti lib dalam bahasa tingkat yang lebih tinggi, misalnya modul csv dengan Python.
Dijeda sampai pemberitahuan lebih lanjut.

5
str="Paris, France, Europe, Los Angeles"; IFS=', ' read -r -a array <<< "$str"akan dibagi menjadi array=([0]="Paris" [1]="France" [2]="Europe" [3]="Los" [4]="Angeles")catatan. Jadi ini hanya berfungsi dengan bidang tanpa spasi karena IFS=', 'merupakan kumpulan karakter individu - bukan pembatas string.
dawg

333

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 $IFSvariabel tidak diambil sebagai variabel-panjang tunggal separator string, melainkan diambil sebagai set dari karakter tunggal pemisah tali, di mana masing-masing bidang yang readperpecahan 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 $IFSsedikit 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 $IFSbersama 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 $stringvariabel kebetulan mengandung LF, maka readakan berhenti memproses setelah bertemu LF pertama. The readbuiltin hanya memproses satu baris per doa. Ini benar bahkan jika Anda memipihkan atau mengarahkan input hanya ke readpernyataan, 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 readbuiltin 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 readbuiltin sebenarnya melakukan dua level pemisahan input: pertama menjadi garis, kemudian ke bidang. Karena OP hanya ingin satu tingkat pemisahan, penggunaan readbuiltin ini tidak tepat, dan kita harus menghindarinya.

3: Masalah potensial yang tidak jelas dengan solusi ini adalah bahwa readselalu 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 $IFSvariabel 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 $IFSkarakter, 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 extglobdiatur, 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 -fsebelumnya 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 ( tratau sed), karena ekspansi parameter adalah murni operasi shell-internal. (Juga, untuk trdan sedsolusi, variabel input harus dikutip ganda di dalam substitusi perintah; jika tidak, pemisahan kata akan berpengaruh pada echoperintah 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 -fdan 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 $IFSuntuk 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 -fdan 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 -fdan 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 $IFSke 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 evalpanggilan 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 evaldengan 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 $IFShanya sementara, yang memungkinkan kita untuk menghindari keseluruhan save-and-restore gambit seperti yang sedang dilakukan dengan $OIFSvariabel 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 $IFSpenugasan sementara. Anda mungkin berpikir sendiri, mengapa tidak menambahkan kata perintah no-op pada pernyataan seperti : builtinmembuat $IFStugas sementara? Ini tidak berfungsi karena itu akan membuat $arraytugas 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 evalmenjalankan kodenya, ia menjalankannya di lingkungan shell, seolah-olah itu normal, kode sumber statis, dan oleh karena itu kita dapat menjalankan $arraytugas di dalam evalargumen untuk membuatnya berlaku di lingkungan shell, sementara $IFStugas awalan yang diawali dengan evalperintah tidak akan hidup lebih lama dari evalperintah. 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 $IFSvariabel 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 $IFSvariabel 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 readbuiltin.

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 $IFSvariabel 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, $IFSjangan 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 compatxxpengaturan 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 readitu tidak tepat karena melakukan dua tingkat pemisahan, ketika kita hanya membutuhkan satu? Kuncinya di sini adalah bahwa Anda dapat memanggil readsedemikian 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 $IFSdiatur 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 readadalah untuk meloloskan nol argumen NAMA . Dalam hal ini, readakan 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 readyang 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 -dopsi, 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 readmengembalikan 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 -dopsi, 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) readkembali gagal dan (2) $REPLYkosong, artinya readtidak 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 readarraybuiltin, 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 -Otidak 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 readketika melakukan field-parsing, readarrayturunkan 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 -dopsi:

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 readsolusi 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 readarraymempertahankan 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 callbackopsi 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")

8
Mungkin juga bermanfaat untuk mencatat (walaupun Anda tidak memiliki ruang untuk melakukannya), bahwa -dopsi untuk readarraypertama kali muncul di Bash 4.4.
fbicknel

2
Jawaban bagus (+1). Jika Anda mengubah pekerjaan Anda awk '{ gsub(/,[ ]+|$/,"\0"); print }'dan menghilangkan rangkaian final, ", " maka Anda tidak harus melalui senam untuk menghilangkan rekor akhir. Jadi: readarray -td '' a < <(awk '{ gsub(/,[ ]+/,"\0"); print; }' <<<"$string")pada Bash yang mendukung readarray. Perhatikan metode Anda adalah Bash 4.4+ Saya pikir karena -ddireadarray
dawg

3
@datUser Sangat disayangkan. Versi bash Anda harus terlalu tua untuk readarray. Dalam hal ini, Anda dapat menggunakan solusi terbaik kedua yang ada di dalamnya read. Saya mengacu pada ini: a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,";(dengan awksubstitusi jika Anda memerlukan dukungan multicharacter delimiter). Beri tahu saya jika Anda mengalami masalah; Saya cukup yakin solusi ini harus bekerja pada versi bash yang cukup lama, kembali ke versi 2-sesuatu, dirilis seperti dua dekade lalu.
bgoldst

1
Wow, jawaban yang sangat brilian! Hee hee, tanggapan saya: membuang skrip bash dan menyalakan python!
artfulrobot

1
@datUser bash di OSX masih macet di 3,2 (dirilis sekitar 2007); Saya telah menggunakan bash yang ditemukan di Homebrew untuk mendapatkan versi 4.X bash di OS X
JDS

222

Berikut ini cara tanpa menetapkan IFS:

string="1:2:3:4:5"
set -f                      # avoid globbing (expansion of *).
array=(${string//:/ })
for i in "${!array[@]}"
do
    echo "$i=>${array[i]}"
done

Idenya menggunakan penggantian string:

${string//substring/replacement}

untuk mengganti semua kecocokan $ substring dengan spasi putih dan kemudian menggunakan string yang diganti untuk menginisialisasi array:

(element1 element2 ... elementN)

Catatan: jawaban ini menggunakan operator split + glob . Dengan demikian, untuk mencegah perluasan beberapa karakter (seperti *), adalah ide yang bagus untuk menghentikan globbing untuk skrip ini.


1
Menggunakan pendekatan ini ... sampai saya menemukan string panjang untuk dipisah. 100% CPU selama lebih dari satu menit (lalu saya bunuh). Sayang sekali karena metode ini memungkinkan untuk dipisah oleh sebuah string, bukan beberapa karakter di IFS.
Werner Lehmann

100% waktu CPU untuk satu menit terdengar seperti ada sesuatu yang salah di suatu tempat. Berapa lama string itu, apakah itu berukuran MB atau GB? Saya pikir, biasanya, jika Anda hanya akan membutuhkan string split kecil, Anda ingin tetap di dalam Bash, tetapi jika itu adalah file besar, saya akan menjalankan sesuatu seperti Perl untuk melakukannya.

12
PERINGATAN: Baru saja mengalami masalah dengan pendekatan ini. Jika Anda memiliki elemen bernama *, Anda akan mendapatkan semua elemen cwd Anda juga. jadi string = "1: 2: 3: 4: *" akan memberikan beberapa hasil yang tidak terduga dan mungkin berbahaya tergantung pada implementasi Anda. Tidak mendapatkan kesalahan yang sama dengan (IFS = ',' baca -a array <<< "$ string") dan ini tampaknya aman untuk digunakan.
Dieter Gribnitz

4
mengutip ${string//:/ }mencegah ekspansi shell
Andrew White

1
Saya harus menggunakan yang berikut ini di OSX: array=(${string//:/ })
Mark Thomson

95
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

Mencetak tiga


8
Saya sebenarnya lebih suka pendekatan ini. Sederhana.
Terobosan

4
Saya menyalin dan menempel ini dan itu tidak bekerja dengan gema, tetapi berhasil ketika saya menggunakannya dalam for loop.
Ben

2
Ini tidak berfungsi seperti yang dinyatakan. @ Jmoney38 atau shrimpwagon jika Anda dapat menempel ini di terminal dan mendapatkan output yang diinginkan, silakan tempel hasilnya di sini.
abalter

2
@abalter Bekerja untuk saya a=($(echo $t | tr ',' "\n")). Hasil yang sama dengan a=($(echo $t | tr ',' ' ')).
daun

@procrastinator Aku hanya mencoba di VERSION="16.04.2 LTS (Xenial Xerus)"dalam bashshell, dan yang terakhir echohanya mencetak baris kosong. Apa versi Linux dan cangkang mana yang Anda gunakan? Sayangnya, tidak dapat menampilkan sesi terminal dalam komentar.
abalter

29

Kadang-kadang terjadi pada saya bahwa metode yang dijelaskan dalam jawaban yang diterima tidak bekerja, terutama jika pemisahnya adalah carriage return.
Dalam kasus-kasus itu saya memecahkan dengan cara ini:

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"

for line in "${lines[@]}"
    do
        echo "--> $line"
done

2
+1 Ini sepenuhnya berfungsi untuk saya. Saya perlu meletakkan beberapa string, dibagi dengan baris baru, ke dalam array, dan read -a arr <<< "$strings"tidak berfungsi IFS=$'\n'.
Stefan van den Akker


Ini tidak cukup menjawab pertanyaan awal.
Mike

29

Jawaban yang diterima berfungsi untuk nilai dalam satu baris.
Jika variabel memiliki beberapa baris:

string='first line
        second line
        third line'

Kami membutuhkan perintah yang sangat berbeda untuk mendapatkan semua baris:

while read -r line; do lines+=("$line"); done <<<"$string"

Atau bash readarray yang jauh lebih sederhana :

readarray -t lines <<<"$string"

Mencetak semua garis sangat mudah dengan memanfaatkan fitur printf:

printf ">[%s]\n" "${lines[@]}"

>[first line]
>[        second line]
>[        third line]

2
Meskipun tidak semua solusi berfungsi untuk setiap situasi, penyebutan readarray Anda ... mengganti dua jam terakhir saya dengan 5 menit ... Anda mendapatkan suara saya
Angry 84


6

Kunci untuk memisahkan string Anda menjadi array adalah pembatas multi karakter ", ". Setiap solusi menggunakanIFS untuk pembatas multi karakter secara inheren salah karena IFS adalah seperangkat karakter tersebut, bukan string.

Jika Anda menetapkan IFS=", "maka string akan patah pada BAIK ","ATAU " "atau kombinasi mereka yang bukan merupakan representasi akurat dari pembatas dua karakter dari", " .

Anda dapat menggunakan awkatau seduntuk memisahkan string, dengan substitusi proses:

#!/bin/bash

str="Paris, France, Europe"
array=()
while read -r -d $'\0' each; do   # use a NUL terminated field separator 
    array+=("$each")
done < <(printf "%s" "$str" | awk '{ gsub(/,[ ]+|$/,"\0"); print }')
declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output

Lebih efisien menggunakan regex Anda langsung di Bash:

#!/bin/bash

str="Paris, France, Europe"

array=()
while [[ $str =~ ([^,]+)(,[ ]+|$) ]]; do
    array+=("${BASH_REMATCH[1]}")   # capture the field
    i=${#BASH_REMATCH}              # length of field + delimiter
    str=${str:i}                    # advance the string by that length
done                                # the loop deletes $str, so make a copy if needed

declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output...

Dengan bentuk kedua, tidak ada sub shell dan itu akan secara inheren lebih cepat.


Sunting oleh bgoldst: Berikut adalah beberapa tolok ukur yang membandingkan readarraysolusi saya dengan solusi regex dawg, dan saya juga memasukkan readsolusi untuk hal itu (catatan: Saya sedikit mengubah solusi regex untuk keselarasan yang lebih besar dengan solusi saya) (juga lihat komentar saya di bawah ini pos):

## competitors
function c_readarray { readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); unset 'a[-1]'; };
function c_read { a=(); local REPLY=''; while read -r -d ''; do a+=("$REPLY"); done < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); };
function c_regex { a=(); local s="$1, "; while [[ $s =~ ([^,]+),\  ]]; do a+=("${BASH_REMATCH[1]}"); s=${s:${#BASH_REMATCH}}; done; };

## helper functions
function rep {
    local -i i=-1;
    for ((i = 0; i<$1; ++i)); do
        printf %s "$2";
    done;
}; ## end rep()

function testAll {
    local funcs=();
    local args=();
    local func='';
    local -i rc=-1;
    while [[ "$1" != ':' ]]; do
        func="$1";
        if [[ ! "$func" =~ ^[_a-zA-Z][_a-zA-Z0-9]*$ ]]; then
            echo "bad function name: $func" >&2;
            return 2;
        fi;
        funcs+=("$func");
        shift;
    done;
    shift;
    args=("$@");
    for func in "${funcs[@]}"; do
        echo -n "$func ";
        { time $func "${args[@]}" >/dev/null 2>&1; } 2>&1| tr '\n' '/';
        rc=${PIPESTATUS[0]}; if [[ $rc -ne 0 ]]; then echo "[$rc]"; else echo; fi;
    done| column -ts/;
}; ## end testAll()

function makeStringToSplit {
    local -i n=$1; ## number of fields
    if [[ $n -lt 0 ]]; then echo "bad field count: $n" >&2; return 2; fi;
    if [[ $n -eq 0 ]]; then
        echo;
    elif [[ $n -eq 1 ]]; then
        echo 'first field';
    elif [[ "$n" -eq 2 ]]; then
        echo 'first field, last field';
    else
        echo "first field, $(rep $[$1-2] 'mid field, ')last field";
    fi;
}; ## end makeStringToSplit()

function testAll_splitIntoArray {
    local -i n=$1; ## number of fields in input string
    local s='';
    echo "===== $n field$(if [[ $n -ne 1 ]]; then echo 's'; fi;) =====";
    s="$(makeStringToSplit "$n")";
    testAll c_readarray c_read c_regex : "$s";
}; ## end testAll_splitIntoArray()

## results
testAll_splitIntoArray 1;
## ===== 1 field =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.000s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 10;
## ===== 10 fields =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.001s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 100;
## ===== 100 fields =====
## c_readarray   real  0m0.069s   user 0m0.000s   sys  0m0.062s
## c_read        real  0m0.065s   user 0m0.000s   sys  0m0.046s
## c_regex       real  0m0.005s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 1000;
## ===== 1000 fields =====
## c_readarray   real  0m0.084s   user 0m0.031s   sys  0m0.077s
## c_read        real  0m0.092s   user 0m0.031s   sys  0m0.046s
## c_regex       real  0m0.125s   user 0m0.125s   sys  0m0.000s
##
testAll_splitIntoArray 10000;
## ===== 10000 fields =====
## c_readarray   real  0m0.209s   user 0m0.093s   sys  0m0.108s
## c_read        real  0m0.333s   user 0m0.234s   sys  0m0.109s
## c_regex       real  0m9.095s   user 0m9.078s   sys  0m0.000s
##
testAll_splitIntoArray 100000;
## ===== 100000 fields =====
## c_readarray   real  0m1.460s   user 0m0.326s   sys  0m1.124s
## c_read        real  0m2.780s   user 0m1.686s   sys  0m1.092s
## c_regex       real  17m38.208s   user 15m16.359s   sys  2m19.375s
##

Solusi yang sangat keren! Saya tidak pernah berpikir untuk menggunakan loop pada pertandingan regex, penggunaan bagus $BASH_REMATCH. Ini bekerja, dan memang menghindari subshell pemijahan. +1 dari saya. Namun, dengan kritik, regex itu sendiri sedikit tidak ideal, dalam hal ini Anda dipaksa untuk menggandakan bagian dari token pembatas (khususnya koma) untuk mengatasi kurangnya dukungan untuk pengganda yang tidak rakus. (juga lookarounds) dalam ERE (rasa regex "extended" yang dibangun menjadi bash). Ini membuatnya sedikit kurang generik dan kuat.
bgoldst

Kedua, saya melakukan benchmarking, dan meskipun kinerjanya lebih baik daripada solusi lain untuk string yang bertubuh kecil, itu memburuk secara eksponensial karena pembangunan kembali string yang berulang, menjadi bencana bagi string yang sangat besar. Lihat edit saya untuk jawaban Anda.
bgoldst

@ bgoldst: Sungguh patokan yang keren! Untuk mempertahankan regex, untuk 10 atau 100 dari ribuan bidang (apa yang dibagi regex) mungkin akan ada beberapa bentuk catatan (seperti \ngaris teks terbatas) yang terdiri dari bidang-bidang tersebut sehingga pelambatan bencana besar kemungkinan tidak akan terjadi. Jika Anda memiliki string dengan 100.000 bidang - mungkin Bash tidak ideal ;-) Terima kasih atas tolok ukurnya. Saya belajar satu atau dua hal.
dawg

4

Solusi pembatas multi-karakter bash murni.

Seperti yang telah ditunjukkan orang lain di utas ini, pertanyaan OP memberikan contoh string yang dibatasi koma untuk diuraikan menjadi array, tetapi tidak menunjukkan apakah ia hanya tertarik pada pembatas koma, pembatas karakter tunggal, atau multi-karakter pembatas.

Karena Google cenderung memberi peringkat jawaban ini pada atau di dekat bagian atas hasil pencarian, saya ingin memberikan jawaban yang kuat kepada pembaca tentang pertanyaan beberapa pembatas karakter, karena itu juga disebutkan dalam setidaknya satu tanggapan.

Jika Anda mencari solusi untuk masalah pembatas multi-karakter, saya sarankan meninjau posting Mallikarjun M , khususnya respons dari gniourf_gniourf yang menyediakan solusi BASH murni yang elegan ini menggunakan ekspansi parameter:

#!/bin/bash
str="LearnABCtoABCSplitABCaABCString"
delimiter=ABC
s=$str$delimiter
array=();
while [[ $s ]]; do
    array+=( "${s%%"$delimiter"*}" );
    s=${s#*"$delimiter"};
done;
declare -p array

Tautan ke komentar / posting referensi yang dikutip

Tautan ke pertanyaan yang dikutip: Bagaimana cara membagi string pada pembatas multi-karakter di bash?


1
Lihat komentar saya untuk pendekatan yang serupa tetapi lebih baik.
xebeche

3

Ini berfungsi untuk saya di OSX:

string="1 2 3 4 5"
declare -a array=($string)

Jika string Anda memiliki pembatas yang berbeda, ganti saja yang pertama dengan spasi:

string="1,2,3,4,5"
delimiter=","
declare -a array=($(echo $string | tr "$delimiter" " "))

Sederhana :-)


Bekerja untuk Bash dan Zsh yang merupakan nilai tambah!
Elijah W. Gagne

2

Cara lain untuk melakukannya tanpa memodifikasi IFS:

read -r -a myarray <<< "${string//, /$IFS}"

Daripada mengubah IFS agar sesuai dengan pembatas yang kita inginkan, kita dapat mengganti semua kejadian pembatas yang kita inginkan ", "dengan isi $IFSvia "${string//, /$IFS}".

Mungkin ini akan lambat untuk string yang sangat besar?

Ini didasarkan pada jawaban Dennis Williamson.


2

Saya menemukan posting ini ketika mencari untuk mengurai input seperti: word1, word2, ...

tidak ada di atas yang membantu saya. dipecahkan dengan menggunakan awk. Jika itu membantu seseorang:

STRING="value1,value2,value3"
array=`echo $STRING | awk -F ',' '{ s = $1; for (i = 2; i <= NF; i++) s = s "\n"$i; print s; }'`
for word in ${array}
do
        echo "This is the word $word"
done

1

Coba ini

IFS=', '; array=(Paris, France, Europe)
for item in ${array[@]}; do echo $item; done

Itu mudah. Jika ingin, Anda juga dapat menambahkan deklarasi (dan juga menghapus koma):

IFS=' ';declare -a array=(Paris France Europe)

IFS ditambahkan untuk membatalkan hal di atas tetapi berfungsi tanpa itu dalam contoh bash baru


1

Kita dapat menggunakan perintah tr untuk membagi string ke objek array. Ini berfungsi baik MacOS dan Linux

  #!/usr/bin/env bash
  currentVersion="1.0.0.140"
  arrayData=($(echo $currentVersion | tr "." "\n"))
  len=${#arrayData[@]}
  for (( i=0; i<=$((len-1)); i++ )); do 
       echo "index $i - value ${arrayData[$i]}"
  done

Opsi lain menggunakan perintah IFS

IFS='.' read -ra arrayData <<< "$currentVersion"
#It is the same as tr
arrayData=($(echo $currentVersion | tr "." "\n"))

#Print the split string
for i in "${arrayData[@]}"
do
    echo $i
done

0

Gunakan ini:

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

#${array[1]} == Paris
#${array[2]} == France
#${array[3]} == Europe

3
Buruk: tunduk pada pemisahan kata dan perluasan pathname Tolong jangan menghidupkan kembali pertanyaan lama dengan jawaban yang baik untuk memberikan jawaban yang buruk.
gniourf_gniourf

2
Ini mungkin jawaban yang buruk, tetapi masih merupakan jawaban yang valid. Penanda / pengulas: Untuk jawaban yang salah seperti ini, downvote, jangan hapus!
Scott Weldon

2
@ gniourf_gniourf Bisakah Anda jelaskan mengapa itu adalah jawaban yang buruk? Saya benar-benar tidak mengerti ketika gagal.
George Sovetov

3
@ GeorgeSovetov: Seperti yang saya katakan, itu tergantung pada pemisahan kata dan perluasan pathname. Lebih umum, membelah string ke dalam array sebagai array=( $string )adalah (sayangnya sangat umum) antipattern: Kata membelah terjadi: string='Prague, Czech Republic, Europe'; Perluasan pathname terjadi: string='foo[abcd],bar[efgh]'akan gagal jika Anda memiliki file bernama, misalnya, foodatau barfdi direktori Anda. Satu-satunya penggunaan konstruksi semacam itu yang valid adalah kapan stringadalah sebuah bola.
gniourf_gniourf

0

UPDATE: Jangan lakukan ini, karena masalah dengan eval.

Dengan upacara yang sedikit kurang:

IFS=', ' eval 'array=($string)'

misalnya

string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar

4
eval itu jahat! jangan lakukan ini.
caesarsol

1
Pfft. Tidak. Jika Anda menulis skrip yang cukup besar untuk masalah ini, Anda salah melakukannya. Dalam kode aplikasi, eval itu jahat. Dalam skrip shell, itu umum, perlu, dan tidak penting.
user1009908

2
letakkan $di variabel Anda dan Anda akan melihat ... Saya menulis banyak skrip dan saya tidak pernah harus menggunakan satueval
caesarsol

2
Anda benar, ini hanya dapat digunakan ketika input diketahui bersih. Bukan solusi yang kuat.
user1009908

Satu-satunya waktu saya harus menggunakan eval, adalah untuk aplikasi yang akan menghasilkan sendiri kode / modulnya ... DAN ini tidak pernah memiliki bentuk input pengguna ...
Angry 84

0

Ini hack saya!

Memisahkan string dengan string adalah hal yang cukup membosankan untuk dilakukan menggunakan bash. Apa yang terjadi adalah bahwa kami memiliki pendekatan terbatas yang hanya berfungsi dalam beberapa kasus (dipisah oleh ";", "/", "." Dan seterusnya) atau kami memiliki berbagai efek samping dalam output.

Pendekatan di bawah ini membutuhkan sejumlah manuver, tetapi saya yakin itu akan berhasil untuk sebagian besar kebutuhan kita!

#!/bin/bash

# --------------------------------------
# SPLIT FUNCTION
# ----------------

F_SPLIT_R=()
f_split() {
    : 'It does a "split" into a given string and returns an array.

    Args:
        TARGET_P (str): Target string to "split".
        DELIMITER_P (Optional[str]): Delimiter used to "split". If not 
    informed the split will be done by spaces.

    Returns:
        F_SPLIT_R (array): Array with the provided string separated by the 
    informed delimiter.
    '

    F_SPLIT_R=()
    TARGET_P=$1
    DELIMITER_P=$2
    if [ -z "$DELIMITER_P" ] ; then
        DELIMITER_P=" "
    fi

    REMOVE_N=1
    if [ "$DELIMITER_P" == "\n" ] ; then
        REMOVE_N=0
    fi

    # NOTE: This was the only parameter that has been a problem so far! 
    # By Questor
    # [Ref.: https://unix.stackexchange.com/a/390732/61742]
    if [ "$DELIMITER_P" == "./" ] ; then
        DELIMITER_P="[.]/"
    fi

    if [ ${REMOVE_N} -eq 1 ] ; then

        # NOTE: Due to bash limitations we have some problems getting the 
        # output of a split by awk inside an array and so we need to use 
        # "line break" (\n) to succeed. Seen this, we remove the line breaks 
        # momentarily afterwards we reintegrate them. The problem is that if 
        # there is a line break in the "string" informed, this line break will 
        # be lost, that is, it is erroneously removed in the output! 
        # By Questor
        TARGET_P=$(awk 'BEGIN {RS="dn"} {gsub("\n", "3F2C417D448C46918289218B7337FCAF"); printf $0}' <<< "${TARGET_P}")

    fi

    # NOTE: The replace of "\n" by "3F2C417D448C46918289218B7337FCAF" results 
    # in more occurrences of "3F2C417D448C46918289218B7337FCAF" than the 
    # amount of "\n" that there was originally in the string (one more 
    # occurrence at the end of the string)! We can not explain the reason for 
    # this side effect. The line below corrects this problem! By Questor
    TARGET_P=${TARGET_P%????????????????????????????????}

    SPLIT_NOW=$(awk -F"$DELIMITER_P" '{for(i=1; i<=NF; i++){printf "%s\n", $i}}' <<< "${TARGET_P}")

    while IFS= read -r LINE_NOW ; do
        if [ ${REMOVE_N} -eq 1 ] ; then

            # NOTE: We use "'" to prevent blank lines with no other characters 
            # in the sequence being erroneously removed! We do not know the 
            # reason for this side effect! By Questor
            LN_NOW_WITH_N=$(awk 'BEGIN {RS="dn"} {gsub("3F2C417D448C46918289218B7337FCAF", "\n"); printf $0}' <<< "'${LINE_NOW}'")

            # NOTE: We use the commands below to revert the intervention made 
            # immediately above! By Questor
            LN_NOW_WITH_N=${LN_NOW_WITH_N%?}
            LN_NOW_WITH_N=${LN_NOW_WITH_N#?}

            F_SPLIT_R+=("$LN_NOW_WITH_N")
        else
            F_SPLIT_R+=("$LINE_NOW")
        fi
    done <<< "$SPLIT_NOW"
}

# --------------------------------------
# HOW TO USE
# ----------------

STRING_TO_SPLIT="
 * How do I list all databases and tables using psql?

\"
sudo -u postgres /usr/pgsql-9.4/bin/psql -c \"\l\"
sudo -u postgres /usr/pgsql-9.4/bin/psql <DB_NAME> -c \"\dt\"
\"

\"
\list or \l: list all databases
\dt: list all tables in the current database
\"

[Ref.: /dba/1285/how-do-i-list-all-databases-and-tables-using-psql]


"

f_split "$STRING_TO_SPLIT" "bin/psql -c"

# --------------------------------------
# OUTPUT AND TEST
# ----------------

ARR_LENGTH=${#F_SPLIT_R[*]}
for (( i=0; i<=$(( $ARR_LENGTH -1 )); i++ )) ; do
    echo " > -----------------------------------------"
    echo "${F_SPLIT_R[$i]}"
    echo " < -----------------------------------------"
done

if [ "$STRING_TO_SPLIT" == "${F_SPLIT_R[0]}bin/psql -c${F_SPLIT_R[1]}" ] ; then
    echo " > -----------------------------------------"
    echo "The strings are the same!"
    echo " < -----------------------------------------"
fi

0

Untuk elemen multiline, mengapa tidak seperti itu

$ array=($(echo -e $'a a\nb b' | tr ' ' '§')) && array=("${array[@]//§/ }") && echo "${array[@]/%/ INTERELEMENT}"

a a INTERELEMENT b b INTERELEMENT

-1

Cara lain adalah:

string="Paris, France, Europe"
IFS=', ' arr=(${string})

Sekarang elemen Anda disimpan dalam array "arr". Untuk beralih melalui elemen:

for i in ${arr[@]}; do echo $i; done

1
Saya membahas ide ini dalam jawaban saya ; lihat Jawaban salah # 5 (Anda mungkin tertarik dengan diskusi saya tentang evaltrik ini). Solusi Anda membiarkan $IFSnilai koma ruang setelah fakta.
bgoldst

-1

Karena ada banyak cara untuk menyelesaikan ini, mari kita mulai dengan mendefinisikan apa yang ingin kita lihat dalam solusi kami.

  1. Bash menyediakan builtin readarray untuk tujuan ini. Mari kita gunakan.
  2. Hindari trik yang jelek dan tidak perlu seperti mengubah IFS, mengulang, dan menggunakaneval , atau menambahkan elemen tambahan lalu menghapusnya.
  3. Temukan pendekatan sederhana dan mudah dibaca yang dapat dengan mudah disesuaikan dengan masalah serupa.

The readarrayperintah paling mudah digunakan dengan baris baru sebagai pembatas. Dengan pembatas lainnya, ini dapat menambahkan elemen tambahan ke array. Pendekatan terbersih adalah pertama-tama menyesuaikan input kami ke dalam bentuk yang berfungsi baikreadarray sebelum meneruskannya.

Input dalam contoh ini tidak memiliki pembatas multicharacter. Jika kita menerapkan sedikit akal sehat, sebaiknya dipahami sebagai input yang dipisahkan koma yang setiap elemen mungkin perlu dipangkas. Solusi saya adalah dengan membagi input dengan koma menjadi beberapa baris, memotong setiap elemen, dan meneruskannya ke semua readarray.

string='  Paris,France  ,   All of Europe  '
readarray -t foo < <(tr ',' '\n' <<< "$string" |sed 's/^ *//' |sed 's/ *$//')
declare -p foo

# declare -a foo='([0]="Paris" [1]="France" [2]="All of Europe")'

-2

Pendekatan lain dapat:

str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

Setelah ini 'arr' adalah array dengan empat string. Ini tidak memerlukan berurusan IFS atau membaca atau hal-hal khusus lainnya karenanya lebih sederhana dan langsung.


Antipattern yang sama (sayangnya umum) dengan jawaban lain: tunduk pada pemisahan kata dan ekspansi nama file.
gniourf_gniourf
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.