Di Bash script saya ingin membagi sebuah garis menjadi potongan-potongan dan menyimpannya dalam array.
Garis:
Paris, France, Europe
Saya ingin memiliki mereka dalam array seperti ini:
array[0] = Paris
array[1] = France
array[2] = Europe
Saya ingin menggunakan kode sederhana, perintah's kecepatan doesn't peduli. Bagaimana saya bisa melakukannya?
IFS=', ' read -r -a array <<< "$string"
Catatan bahwa karakter-karakter di $IFS
diperlakukan secara individual sebagai pemisah sehingga dalam hal ini bidang-bidang yang dapat dipisahkan oleh either koma atau spasi bukan urutan dari dua karakter. Menariknya meskipun, bidang kosong aren't dibuat saat koma-ruang muncul di input karena ruang yang diperlakukan khusus.
Untuk mengakses sebuah elemen individu:
echo "${array[0]}"
Untuk iterate atas unsur-unsur:
for element in "${array[@]}"
do
echo "$element"
done
Untuk mendapatkan kedua indeks dan nilai:
for index in "${!array[@]}"
do
echo "$index ${array[index]}"
done
Contoh terakhir adalah berguna karena Bash array jarang. Dengan kata lain, anda dapat menghapus sebuah elemen atau menambahkan elemen dan kemudian indeks-indeks yang tidak bersebelahan.
unset "array[1]"
array[42]=Earth
Untuk mendapatkan jumlah elemen dalam array:
echo "${#array[@]}"
Seperti disebutkan di atas, array dapat jarang sehingga anda tidak't menggunakan panjang untuk sampai elemen terakhir. Berikut ini's bagaimana anda dapat di Bash 4.2 dan yang lebih baru:
echo "${array[-1]}"
dalam versi Bash (dari suatu tempat setelah 2.05 b):
echo "${array[@]: -1:1}"
Negatif yang besar offset pilih jauh dari akhir array. Perhatikan spasi sebelum tanda minus dalam bentuk yang lebih tua. Hal ini diperlukan.
IFS=', ' read -r -a array <<< "$string"
1: Ini adalah penyalahgunaan $IFS
. Nilai $IFS
variabel tidak diambil sebagai satu variabel-panjang string pemisah, melainkan diambil sebagai set dari single-karakter string pemisah, di mana masing-masing bidang yang membaca
membagi off dari input line dapat dihentikan oleh setiap karakter dalam set (koma atau ruang, dalam contoh ini).
Sebenarnya, untuk real ngotot di luar sana, penuh makna $IFS
sedikit lebih terlibat. Dari bash manual:
shell memperlakukan masing-masing karakter SEANDAINYA sebagai pembatas, dan membagi hasil lainnya ekspansi ke kata-kata yang menggunakan karakter ini sebagai bidang terminator. Jika SEANDAINYA adalah unset, atau nilainya persis <ruang><tab><newline>, default, maka urutan <ruang>, <tab>, dan <newline> di awal dan akhir dari hasil ekspansi sebelumnya diabaikan, dan setiap urutan SEANDAINYA karakter bukan di awal atau di akhir berfungsi untuk membatasi kata-kata. Jika SEANDAINYA memiliki nilai lain selain default, maka urutan-urutan dari karakter spasi putih <ruang>, <tab>, dan <newline> diabaikan pada awal dan akhir kata, selama spasi karakter adalah nilai SEANDAINYA (an SEANDAINYA karakter spasi). Setiap karakter di SEANDAINYA yang tidak SEANDAINYA spasi, bersama dengan berdekatan SEANDAINYA karakter spasi putih, menetapkan limit lapangan. Urutan SEANDAINYA karakter spasi putih juga diperlakukan sebagai pembatas. Jika nilai SEANDAINYA null, tidak ada kata yang membelah terjadi. Pada dasarnya, untuk non-default non-null-nilai
$IFS
, bidang dapat dipisahkan dengan baik (1) urutan satu atau lebih karakter yang semua dari set "IFS spasi karakter" (artinya, apapun <ruang>, <tab>, dan <newline> ("baru" yang berarti line feed (LF)) yang hadir di mana saja di$IFS
), atau (2) non-"IFS karakter spasi" yang's hadir di$IFS
bersama dengan apa pun yang "IFS spasi karakter" surround pada baris input. Untuk OP, it's mungkin bahwa kedua mode pemisahan yang saya jelaskan di paragraf sebelumnya adalah persis apa yang dia inginkan untuk masukan string, tapi kita dapat cukup yakin bahwa yang pertama pemisahan mode yang saya jelaskan adalah tidak benar sama sekali. Misalnya, bagaimana jika input string'Los Angeles, Amerika Serikat, Amerika Utara'
?
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 single-karakter pemisah (seperti koma dengan sendirinya, itu adalah, dengan tidak mengikuti ruang atau bawaan lainnya), jika nilai $string
variabel terjadi untuk mengandung LFs, maka baca
akan menghentikan proses setelah pertemuan pertama LF. Membaca
builtin hanya proses satu baris untuk setiap doa. Hal ini berlaku bahkan jika anda adalah pipa atau mengarahkan input hanya untuk membaca
pernyataan, seperti yang kita lakukan dalam contoh ini dengan di sini-string mekanisme, dan dengan demikian diproses input dijamin akan hilang. Kode yang kekuatan baca
builtin tidak memiliki pengetahuan tentang data yang mengalir di dalam nya mengandung struktur komando.
Anda bisa berpendapat bahwa hal ini tidak menyebabkan masalah, tapi tetap saja, itu's halus bahaya yang harus dihindari jika mungkin. Hal ini disebabkan oleh fakta bahwa membaca
builtin sebenarnya dua tingkat input membelah: pertama ke garis, kemudian ke ladang. Karena OP hanya ingin satu tingkat membelah, penggunaan baca
builtin tidak tepat, dan kita harus menghindarinya.
3: non-jelas potensi masalah dengan solusi ini adalah bahwa membaca
selalu turun trailing lapangan jika kosong, meskipun itu mempertahankan bidang-bidang kosong sebaliknya. Berikut ini's 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]="")
membaca
, karena saya akan menunjukkan nanti. string="1:2:3:4:5"
set -f # avoid globbing (expansion of *).
array=(${string//:/ })
t="one,two,three"
a=($(echo $t | tr ',' "\n"))
(Catatan: saya menambahkan hilang tanda kurung di sekitar perintah substitusi yang penjawab tampaknya telah dihilangkan.) Ide serupa:
string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
baca
, kata umum membelah juga menggunakan $IFS
variabel khusus, meskipun dalam hal ini tersirat bahwa hal ini diatur ke nilai default <ruang><tab><newline>, dan oleh karena itu setiap urutan satu atau lebih IFS karakter (yang semua karakter spasi putih sekarang) ini dianggap sebagai pembatas lapangan.
Ini memecahkan masalah dari dua tingkat pemisahan yang dilakukan oleh baca
, karena kata yang membelah dengan sendirinya merupakan hanya satu tingkat pemisahan. Tapi sama seperti sebelumnya, masalahnya di sini adalah bahwa masing-masing kolom dalam input string dapat berisi $IFS
karakter, dan dengan demikian mereka akan benar berpisah selama word membelah operasi. Hal ini terjadi untuk tidak menjadi kasus untuk setiap sampel input string yang disediakan oleh penjawab (bagaimana nyaman...), tapi tentu saja itu doesn't mengubah fakta bahwa setiap basis kode yang digunakan idiom ini kemudian akan menjalankan risiko meledakkan jika asumsi ini pernah dilanggar di beberapa titik di bawah garis. Sekali lagi, pertimbangkan saya counterexample dari 'Los Angeles, Amerika Serikat, Amerika Utara'
(atau 'Los Angeles:Amerika Serikat:Amerika Utara'
).
Juga, kata membelah biasanya diikuti oleh nama expansion (alias pathname ekspansi alias globbing), yang, jika dilakukan, akan berpotensi korup kata-kata yang mengandung karakter *
, ?
, atau [
diikuti oleh ]
(dan, jika extglob
adalah set, tanda kurung fragmen didahului oleh?
, *
, +
, @
, atau !
) dengan pencocokan mereka terhadap objek sistem berkas dan memperluas kata-kata ("gumpalan") yang sesuai. Yang pertama dari tiga penjawab telah cerdik melemahkan masalah ini dengan menjalankan set-f
terlebih dahulu untuk menonaktifkan globbing. Secara teknis ini bekerja (meskipun anda mungkin harus menambahkan set +f
setelah itu untuk mengaktifkan kembali globbing untuk kode berikutnya yang mungkin tergantung pada hal itu), tapi itu's tidak diinginkan harus main-main dengan global shell pengaturan dalam rangka untuk hack string dasar-untuk-para parsing operasi di kode lokal.
Masalah lain dengan jawaban ini adalah bahwa semua bidang kosong akan hilang. Hal ini mungkin atau mungkin tidak menjadi masalah, tergantung pada aplikasi.
Catatan: Jika anda're akan menggunakan solusi ini, it's baik untuk menggunakan ${string//:/ }
"pola substitusi" bentuk parameter expansion, daripada pergi ke kesulitan menerapkan perintah substitusi (yang forks shell), memulai sebuah pipa, dan menjalankan aplikasi eksternal yang dapat dieksekusi (tr
atau sed
), sejak parameter ekspansi adalah murni shell-operasi internal. (Juga, untuk tr
dan sed
solusi, masukan variabel harus double-dikutip dalam perintah substitusi; jika tidak, kata membelah akan berlaku di echo
perintah dan berpotensi main-main dengan nilai-nilai bidang. Juga, $ ( ... )
bentuk perintah substitusi adalah lebih baik untuk tua `...
` bentuk karena menyederhanakan bersarang perintah substitusi dan memungkinkan untuk lebih baik sintaks dengan teks editor.) str="a, b, c, d" # assuming there is a space after ',' as in Q
arr=(${str//,/}) # delete all occurrences of ','
$IFS
, dan yang lainnya tidak. Dia telah diselesaikan ini bukan kasus tertentu dengan menghilangkan non-IFS-mewakili karakter menggunakan pola substitusi ekspansi dan kemudian menggunakan kata membelah untuk membagi bidang pada yang masih hidup IFS-mewakili karakter pembatas.
Hal ini tidak sangat generik solusi. Selain itu, dapat dikatakan bahwa koma adalah benar-benar "dasar" pembatas karakter di sini, dan bahwa pengupasan dan kemudian tergantung pada karakter ruang untuk bidang membelah hanya salah. Sekali lagi, pertimbangkan saya counterexample: 'Los Angeles, Amerika Serikat, Amerika Utara'
.
Juga, sekali lagi, nama file, ekspansi bisa merusak memperluas kata-kata, tetapi hal ini dapat dicegah dengan menonaktifkan sementara globbing untuk penugasan 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. 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"
$IFS
untuk hanya berisi karakter tunggal bidang pembatas hadir di input string. Itu harus diulang bahwa ini tidak bekerja untuk multi karakter pembatas lapangan seperti OP's koma-pembatas ruang. Tapi untuk yang single-karakter pembatas seperti LF digunakan dalam contoh ini, itu benar-benar datang dekat untuk menjadi sempurna. Bidang yang tidak dapat tidak sengaja perpecahan di tengah seperti yang kita lihat sebelumnya, jawaban yang salah, dan hanya ada satu tingkat membelah, seperti yang diperlukan.
Satu masalah adalah bahwa filename ekspansi akan merusak terpengaruh kata-kata seperti yang dijelaskan sebelumnya, meskipun sekali lagi ini dapat diselesaikan dengan membungkus pernyataan kritis di set-f
dan set +f
.
Potensi masalah lain adalah bahwa, karena JIKA memenuhi syarat sebagai "IFS karakter spasi" sebagaimana didefinisikan sebelumnya, semua bidang kosong akan hilang, seperti di #2 dan #3. Hal ini akan tentu tidak menjadi masalah jika pembatas yang terjadi untuk menjadi non-"IFS karakter spasi", dan tergantung pada aplikasi ini mungkin tidak masalah sih, tapi itu tidak melemahkan sifat umum dari solusi.
Jadi, untuk meringkas, dengan asumsi anda memiliki satu karakter pembatas, dan itu adalah baik non-"IFS karakter spasi" atau anda don't peduli tentang bidang-bidang kosong, dan anda membungkus pernyataan kritis di set-f
dan set +f
, maka solusi ini bekerja, tetapi sebaliknya tidak.
(Juga, untuk informasi's sake, menetapkan LF untuk variabel dalam bash dapat dilakukan dengan lebih mudah dengan$'...'
sintaks, misal IFS=$'\n';
.) countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"
IFS=', ' eval 'array=($string)'
Solusi ini secara efektif persilangan antara #1 (di set $IFS
untuk koma-spasi) dan #2-4 (dalam hal ini menggunakan kata membelah untuk membagi string menjadi ladang). Karena ini, ia menderita sebagian besar masalah yang menimpa semua atas jawaban yang salah, seperti yang terburuk dari semua dunia.
Juga, mengenai varian kedua, ini mungkin tampak seperti eval
call adalah benar-benar tidak perlu, karena argumen yang single-quoted string literal, dan oleh karena itu statis dikenal. Tapi ada's benar-benar sangat non-manfaat yang jelas untuk menggunakan eval
dengan cara ini. Biasanya, ketika anda menjalankan perintah sederhana yang terdiri dari sebuah variabel penugasan hanya, yang berarti tanpa sebuah perintah yang sebenarnya kata-kata berikut ini, tugas berlaku di shell lingkungan:
IFS=', '; ## changes $IFS in the shell environment
Hal ini berlaku bahkan jika perintah sederhana melibatkan *beberapa variabel tugas; lagi, asalkan ada's tidak ada kata perintah, semua tugas-tugas variabel mempengaruhi shell lingkungan:
IFS=', ' array=($countries); ## changes both $IFS and $array in the shell environment
Tapi, jika variabel tugas yang melekat pada nama perintah (saya suka menyebutnya "awalan tugas") maka tidak tidak mempengaruhi shell lingkungan, dan bukan hanya mempengaruhi lingkungan dieksekusi perintah, terlepas apakah itu adalah bawaan 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
Terkait kutipan dari bash manual:
Jika tidak ada perintah nama hasil, variabel yang mempengaruhi tugas-tugas saat ini shell lingkungan. Sebaliknya, variabel-variabel yang ditambahkan ke lingkungan dieksekusi perintah dan tidak mempengaruhi arus shell lingkungan. Hal ini dimungkinkan untuk memanfaatkan fitur ini variabel tugas untuk mengubah
$IFS
hanya sementara, yang memungkinkan kita untuk menghindari seluruh menyimpan dan mengembalikan gambit seperti yang sedang dilakukan dengan$OIFS
variabel dalam varian pertama. Tapi tantangan yang kita hadapi di sini adalah bahwa perintah yang kita butuhkan untuk menjalankan sendiri hanya variabel tugas, dan karena itu tidak akan melibatkan kata perintah untuk membuat$IFS
tugas sementara. Anda mungkin berpikir untuk diri sendiri, mengapa tidak hanya menambahkan no-op kata perintah untuk pernyataan seperti: builtin
untuk membuat$IFS
tugas sementara? Ini tidak bekerja karena itu maka akan membuat$array
tugas sementara juga:
IFS=', ' array=($countries) :; ## fails; new $array value never escapes the : command
Jadi, kita're efektif pada jalan buntu, sedikit catch-22. Tapi, ketika eval
menjalankan kode ini berjalan pada shell lingkungan, seperti apakah itu normal, statis kode sumber, dan oleh karena itu kita dapat menjalankan $array
tugas dalam eval
argumen untuk mengambil efek di shell lingkungan, sedangkan $IFS
awalan tugas yang diawali dengan eval
perintah tidak akan hidup lebih lama eval
perintah. Ini adalah persis trik yang digunakan di varian kedua solusi ini:
IFS=', ' eval 'array=($string)'; ## $IFS does not outlive the eval command, but $array does
eval
; hanya berhati-hati untuk single-quote argumen string untuk waspada terhadap ancaman keamanan.
Tapi sekali lagi, karena "terburuk dari semua dunia" aglomerasi dari masalah ini masih menjadi salah satu jawaban untuk OP's kebutuhan. IFS=', '; array=(Paris, France, Europe)
IFS=' ';declare -a array=(Paris France Europe)
Um... apa? OP memiliki sebuah variabel string yang perlu diurai ke dalam sebuah array. Ini "menjawab" dimulai dengan verbatim isi dari input string yang disisipkan ke dalam sebuah array literal. Saya kira bahwa's salah satu cara untuk melakukan itu.
Sepertinya penjawab mungkin telah mengasumsikan bahwa $IFS
variabel mempengaruhi semua parsing bash dalam semua konteks, yang tidak benar. Dari bash manual:
SEANDAINYA Internal Bidang Pemisah yang digunakan untuk kata yang membelah setelah ekspansi dan untuk membagi garis menjadi kata-kata dengan baca perintah bawaan. Nilai default adalah <ruang><tab><newline>. Jadi
$IFS
khusus variabel adalah benar-benar hanya digunakan dalam dua konteks: (1) kata pemisahan yang dilakukan setelah ekspansi (artinya tidak ketika parsing bash kode sumber) dan (2) untuk membelah jalur input ke dalam kata-kata denganmembaca
builtin. Biarkan saya mencoba untuk membuat ini lebih jelas. Saya pikir mungkin lebih baik untuk menarik perbedaan antara parsing dan pelaksanaan. Bash harus terlebih dahulu mengurai kode sumber, yang jelas adalah parsing acara, dan kemudian itu menjalankan kode, yang ketika ekspansi datang ke dalam gambar. Ekspansi adalah benar-benar pelaksanaan acara tersebut. Selain itu, saya mengambil masalah dengan deskripsi$IFS
variabel yang saya kutip di atas; bukan mengatakan bahwa kata pemisahan dilakukan setelah ekspansi, aku akan mengatakan bahwa kata pemisahan dilakukan selama ekspansi, atau, bahkan mungkin lebih tepatnya, kata membelah bagian proses ekspansi. Kalimat "kata membelah" hanya mengacu pada langkah ini ekspansi; itu tidak boleh digunakan untuk merujuk kepada parsing bash source code, meskipun sayangnya docs tampaknya untuk membuang seluruh kata-kata "split" dan "kata-kata" banyak. Berikut ini's yang relevan kutipan dari linux.die.net versi dari bash manual: Ekspansi ini dilakukan pada baris perintah setelah hal itu telah dibagi ke dalam kata-kata. Ada tujuh jenis ekspansi yang dilakukan: penjepit ekspansi, tilde ekspansi, parameter dan variabel ekspansi, perintah substitusi, aritmatika ekspansi, kata membelah, dan pathname ekspansi.urutan ekspansi adalah: brace ekspansi; tilde ekspansi, parameter dan variabel ekspansi, aritmatika ekspansi, dan perintah substitusi (dilakukan di kiri-ke-kanan fashion); kata yang membelah; dan path ekspansi. Anda bisa berpendapat GRATIS version manual memang sedikit lebih baik, karena itu memilih kata "token" bukan "kata-kata" dalam kalimat pertama dari Perluasan bagian: Ekspansi ini dilakukan pada baris perintah setelah hal itu telah dibagi ke dalam token. Yang penting adalah,
$IFS
tidak mengubah cara bash parse kode sumber. Parsing bash source code sebenarnya adalah sebuah proses yang sangat kompleks yang melibatkan pengakuan dari berbagai elemen shell tata bahasa, seperti perintah-perintah, perintah daftar, pipa, parameter ekspansi, aritmatika substitusi, dan perintah substitusi. Untuk sebagian besar, parsing bash proses tidak dapat diubah oleh pengguna-tingkat tindakan-tindakan seperti variabel tugas (sebenarnya, ada beberapa minor pengecualian untuk aturan ini; misalnya, melihat berbagaicompatxx
shell settings, yang dapat mengubah aspek-aspek tertentu dari penguraian perilaku on-the-fly). Hulu "kata-kata"/"token" yang dihasilkan dari kompleks ini menguraikan proses yang kemudian diperluas sesuai dengan proses umum "ekspansi" sebagai rusak di atas dokumen kutipan, di mana kata yang membelah diperluas (expanding?) teks ke hilir kata-kata adalah hanya satu langkah dari proses itu. Kata membelah hanya menyentuh teks yang telah dimuntahkan dari sebelumnya ekspansi langkah; itu tidak mempengaruhi literal teks yang diambil langsung dari sumber bytestream.
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're kembali menggunakan baca
. Didn't saya katakan sebelumnya bahwa membaca
adalah tidak pantas karena ia melakukan dua tingkat membelah, ketika kita hanya perlu satu? Kuncinya di sini adalah bahwa anda dapat memanggil baca
sedemikian rupa sehingga secara efektif tidak hanya satu tingkat membelah, khususnya dengan membelah off hanya satu bidang per doa, yang memerlukan biaya untuk panggilan itu berulang kali dalam satu lingkaran. It's sedikit sulap, tapi itu bekerja.
Tapi ada masalah. Pertama: Ketika anda memberikan setidaknya satu NAMA argumen untuk membaca
, maka secara otomatis mengabaikan terkemuka dan trailing whitespace di masing-masing bidang yang memisahkan diri dari string masukan. Hal ini terjadi apakah $IFS
diatur ke nilai default-nya atau tidak, seperti yang dijelaskan sebelumnya pada posting ini. Sekarang, OP mungkin tidak peduli tentang hal ini untuk penggunaan tertentu-kasus, dan pada kenyataannya, ini mungkin sebuah fitur yang diinginkan dari penguraian perilaku. Tapi tidak semua orang yang ingin mengurai string ke dalam bidang ini. Ada solusi, namun: agak non-jelas penggunaan baca
adalah untuk lulus nol NAMA argumen. Dalam hal ini, membaca
akan menyimpan seluruh input line yang mendapat dari input stream dalam variabel bernama $REPLY
, dan, sebagai bonus, itu tidak tidak strip terkemuka dan trailing whitespace dari nilai. Ini adalah sangat kuat penggunaan membaca
yang saya've sering dimanfaatkan dalam pemrograman shell karir. Berikut ini's demonstrasi perbedaan dalam 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
Kedua masalah dengan solusi ini adalah bahwa hal itu tidak benar-benar menangani kasus custom field separator, seperti OP's koma-spasi. Seperti sebelumnya, multi karakter pemisah yang tidak didukung, yang disayangkan batasan dari solusi ini. Kita bisa mencoba untuk setidaknya split pada koma dengan menentukan pemisah untuk -d
pilihan, tapi 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")
Bisa ditebak, terhitung sekitar spasi punya ditarik ke bidang nilai-nilai, dan karenanya ini harus diperbaiki selanjutnya melalui pemangkasan operasi (hal ini juga bisa dilakukan secara langsung di saat-loop). Tapi ada's lain kesalahan yang jelas: Eropa hilang! Apa yang terjadi dengan itu? Jawabannya adalah bahwa membaca
kembali gagal kembali kode jika itu adalah end-of-file (dalam hal ini kita dapat menyebutnya end-of-string) tanpa menemui akhir bidang terminator pada akhir lapangan. Hal ini menyebabkan saat-loop untuk istirahat sebelum waktunya dan kita kehilangan akhir lapangan.
Secara teknis kesalahan yang sama menderita dalam contoh-contoh sebelumnya juga; perbedaan yang ada adalah bahwa bidang pemisah diambil untuk menjadi LF, yang merupakan default ketika anda don't menentukan -d
, dan <<<
("di sini-string") mekanisme secara otomatis menambahkan LF untuk string sebelum itu feed sebagai input untuk perintah. Oleh karena itu, dalam kasus-kasus, kita semacam sengaja memecahkan masalah yang turun akhir lapangan dengan sengaja menambahkan tambahan dummy terminator untuk input. Let's call solusi ini "dummy-terminator" solusi. Kita dapat menerapkan boneka-terminator solusi secara manual untuk setiap kustom pembatas dengan menggabungkan terhadap input string diri kita sendiri ketika instantiating itu di sini-string:
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,"; declare -p a;
declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")
Di sana, masalah diselesaikan. Solusi lain adalah dengan hanya istirahat sementara loop jika keduanya (1) membaca
kembali kegagalan dan (2) $REPLY
adalah kosong, yang berarti membaca
tidak mampu membaca setiap karakter sebelum memukul end-of-file. 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')
<<<
pengalihan operator. Hal ini tentu saja bisa menanggalkan terpisah melalui eksplisit pemangkasan operasi seperti yang dijelaskan beberapa saat yang lalu, tapi jelas manual boneka-terminator pendekatan memecahkan itu secara langsung, jadi kita hanya bisa pergi dengan itu. Manual boneka-terminator solusinya sebenarnya cukup mudah dalam hal itu memecahkan kedua masalah ini dua (turun-final-bidang masalah dan ditambahkan-LF masalah) dalam satu pergi.
Jadi, secara keseluruhan, ini adalah solusi yang kuat. It's-satunya kelemahan adalah kurangnya dukungan untuk multi karakter pembatas, yang saya akan membahas nanti. string='first line
second line
third line'
readarray -t lines <<<"$string"
readarray
builtin, yang merupakan sinonim untuk mapfile
, sangat ideal. It's builtin perintah yang mem-parsing bytestream ke variabel array dalam satu tembakan; tidak bermain-main dengan loop, conditional, substitusi, atau apa pun. Dan itu doesn't diam-diam strip setiap spasi dari string masukan. Dan (jika -O
tidak diberikan) itu terletak membersihkan array sasaran sebelum menugaskan untuk itu. Tapi itu's masih belum sempurna, oleh karena itu saya kritik itu sebagai "jawaban yang salah".
Pertama, hanya untuk mendapatkan ini keluar dari jalan, perhatikan bahwa, seperti perilaku baca
ketika melakukan bidang-parsing, readarray
tetes trailing lapangan jika kosong. Sekali lagi, ini mungkin tidak menjadi perhatian untuk OP, tapi itu bisa untuk beberapa kasus. I'akan datang kembali ke ini dalam sekejap.
Kedua, seperti sebelumnya, tidak mendukung multi karakter pembatas. I'll memberikan untuk memperbaiki ini dalam sekejap juga.
Ketiga, solusi seperti yang tertulis tidak mengurai OP's string masukan, dan pada kenyataannya, hal ini tidak dapat digunakan untuk mengurai itu. I'll memperluas ini sejenak juga.
Untuk alasan di atas, saya masih menganggap hal ini menjadi sebuah "jawaban yang salah" untuk OP's pertanyaan. Di bawah ini saya'll memberikan apa yang saya anggap menjadi jawaban yang tepat. Jawaban yang benar
Berikut ini's a naïve mencoba untuk membuat #8 bekerja dengan hanya menyebutkan -d
pilihan:
string='Paris, France, Europe';
readarray -td, a <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')
Kita lihat hasilnya sama dengan hasil yang kita dapatkan dari ganda bersyarat pendekatan perulangan baca
solusi yang dibahas dalam #7. Kita bisa hampir memecahkan masalah ini dengan manual boneka-terminator trik:
readarray -td, a <<<"$string,"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe" [3]=$'\n')
Masalahnya di sini adalah bahwa readarray
diawetkan trailing lapangan, sejak <<<
pengalihan operator ditambahkan LF untuk input string, dan oleh karena itu trailing lapangan adalah tidak kosong (jika tidak maka akan've telah turun). Kita bisa mengurus hal ini dengan secara eksplisit unsetting akhir elemen array setelah-the-fakta:
readarray -td, a <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")
Hanya dua masalah yang tetap, yang benar-benar terkait, adalah (1) asing spasi yang perlu dipangkas, dan (2) kurangnya dukungan untuk multi karakter pembatas. Spasi tentu saja bisa dipangkas sesudahnya (untuk contoh, lihat https://stackoverflow.com/questions/369758/how-to-trim-whitespace-from-a-bash-variable). Tapi jika kita bisa hack multi karakter pembatas, maka itu akan memecahkan kedua masalah dalam satu tembakan. Sayangnya, ada's tidak langsung * cara untuk mendapatkan multi karakter pembatas untuk bekerja. Solusi terbaik saya've berpikir adalah untuk preprocess string masukan untuk menggantikan multi karakter pembatas dengan single-karakter pembatas yang akan dijamin untuk tidak bertabrakan dengan isi dari string masukan. Satu-satunya karakter yang memiliki jaminan ini adalah NUL byte. Hal ini karena, di bash (meskipun tidak di zsh, kebetulan), variabel tidak mengandung NUL byte. Langkah preprocessing dapat dilakukan inline dalam proses substitusi. Berikut ini's bagaimana untuk melakukannya dengan 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")
Pemangkasan solusi
Terakhir, saya ingin menunjukkan saya sendiri cukup rumit pemangkasan solusi menggunakan mengaburkan -C callback
pilihan readarray
. Sayangnya, saya've berjalan keluar dari ruang terhadap Stack Overflow's kejam 30,000 karakter pos batas, jadi saya tidak't dapat menjelaskan hal itu. I'll meninggalkan itu sebagai latihan bagi para 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")
Berikut ini adalah cara tanpa pengaturan 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 adalah menggunakan string pengganti:
${string//substring/replacement}
untuk mengganti semua pertandingan dari $substring dengan ruang putih dan kemudian menggunakan diganti string untuk inisialisasi array:
(element1 element2 ... elementN)
Catatan: jawaban ini membuat penggunaan split+glob operator. Dengan demikian, untuk mencegah perluasan dari beberapa karakter (seperti *
) itu adalah ide yang baik untuk berhenti sejenak globbing untuk script ini.
Kadang-kadang hal itu terjadi kepada saya bahwa metode yang dijelaskan dalam jawaban yang diterima tidak't bekerja, terutama jika separator adalah suatu carriage return.
Dalam kasus-kasus aku diselesaikan 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
Jawaban yang diterima bekerja untuk nilai-nilai dalam satu baris.
Jika variabel memiliki beberapa baris:
string='first line
second line
third line'
Kita perlu sangat perintah yang berbeda untuk mendapatkan semua lini:
sambil baca -r line; apakah garis+=("$line"); selesai <<<"$string"
Atau yang lebih sederhana bash readarray:
readarray -t lines <<<"$string"
Cetak semua garis-garis ini sangat mudah mengambil keuntungan dari printf fitur:
printf ">[%s]\n" "${lines[@]}"
>[first line]
>[ second line]
>[ third line]
Kunci untuk memisahkan string menjadi array multi karakter pembatas dari ", "
. Solusi menggunakan SEANDAINYA
untuk multi karakter pembatas adalah salah sejak IFS adalah kumpulan dari karakter-karakter, bukan string.
Jika anda menetapkan IFS=", "
maka string akan istirahat di KEDUA "," ATAU", "
atau kombinasi dari mereka yang bukan representasi akurat dari dua karakter pembatas dari ", "
.
Anda dapat menggunakan awk
atau sed
untuk membagi string, dengan proses substitusi:
#!/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
Hal ini lebih efisien untuk 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 menjadi inheren lebih cepat.
Edit oleh bgoldst: Berikut ini adalah beberapa tolok ukur membandingkan saya readarray
solusi untuk dawg's regex solusi, dan saya juga termasuk baca
solusi untuk heck itu (catatan: saya sedikit dimodifikasi regex solusi untuk keharmonisan yang lebih besar dengan solusi saya) (juga lihat komentar saya di bawah posting):
## 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
##
Hal ini mirip dengan pendekatan dengan Jmoney38, tetapi menggunakan sed:
string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
echo ${array[0]}
Cetakan 1
Murni bash multi-karakter pembatas solusi.
Seperti orang lain telah menunjukkan di thread ini, OP's pertanyaan memberi contoh yang dipisahkan koma string yang akan dipecah menjadi array, tetapi tidak menunjukkan jika dia/dia hanya tertarik dalam koma pembatas, satu karakter pembatas, atau multi-karakter pembatas.
Sejak Google cenderung peringkat jawaban ini di atau di dekat bagian atas hasil pencarian, saya ingin memberikan pembaca dengan kuat untuk menjawab pertanyaan dari beberapa karakter pembatas, karena yang ini juga disebutkan dalam setidaknya satu respon.
Jika anda'kembali mencari solusi untuk multi-karakter pembatas masalah, saya sarankan meninjau Mallikarjun M's post, khususnya respon dari gniourf_gniourf yang menyediakan elegan ini murni BASH solusi menggunakan parameter ekspansi:
#!/bin/bash
str="LearnABCtoABCSplitABCaABCString"
delimiter=ABC
s=$str$delimiter
array=();
while [[ $s ]]; do
array+=( "${s%%"$delimiter"*}" );
s=${s#*"$delimiter"};
done;
declare -p array
Link ke dikutip komentar/direferensikan pos
Link ke dikutip pertanyaan: Howto split string pada multi-karakter pembatas di bash?
Coba ini
IFS=', '; array=(Paris, France, Europe)
for item in ${array[@]}; do echo $item; done
It's sederhana. Jika anda ingin, anda juga dapat menambahkan menyatakan (dan juga menghapus koma):
IFS=' ';declare -a array=(Paris France Europe)
IFS ditambahkan untuk membatalkan atas, tetapi ia bekerja tanpa itu di bash contoh
Saya menemukan posting ini ketika mencari untuk mengurai input seperti: word1,word2,...
di atas tidak ada yang membantu saya. diselesaikan 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
Ini bekerja untuk saya pada OSX:
bash string="1 2 3 4 5" mendeklarasikan sebuah array=($string)
Jika anda string telah yang berbeda pembatas, hanya 1 menggantikan mereka dengan space:
bash string="1,2,3,4,5" pembatas="," mendeklarasikan sebuah array=($(echo $string | tr "$delimiter" " "))
Sederhana :-)
Cara lain untuk melakukannya tanpa memodifikasi IFS:
read -r -a myarray <<< "${string//, /$IFS}"
Daripada mengubah IFS untuk pertandingan kami yang diinginkan pembatas, kita bisa mengganti semua kejadian yang kita inginkan pembatas ", "
dengan isi $IFS
melalui "${string//, /$IFS"
.
Mungkin ini akan menjadi lambat untuk sangat besar string meskipun?
Hal ini didasarkan pada Dennis Williamson's jawaban.
Kita dapat menggunakan tr perintah untuk membagi string menjadi array objek. Ini bekerja 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
Pilihan 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
Berikut's my hack!
Memecah string dengan string yang cukup membosankan hal yang harus dilakukan menggunakan bash. Apa yang terjadi adalah bahwa kami memiliki keterbatasan pendekatan yang hanya bekerja dalam beberapa kasus (split dengan ";", "/", "." dan sebagainya) atau kita memiliki berbagai efek samping pada output.
Pendekatan di bawah ini diperlukan sejumlah manuver, tapi saya yakin itu akan bekerja untuk sebagian besar dari 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.: https://dba.stackexchange.com/questions/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
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 sebuah array dengan empat senar. Ini doesn't membutuhkan berurusan IFS atau membaca atau hal-hal khusus maka jauh lebih sederhana dan langsung.