Bagaimana cara memisahkan string pada pembatas di Bash?

2043

Saya memiliki string ini disimpan dalam variabel:

IN="[email protected];[email protected]"

Sekarang saya ingin membagi string dengan ;pembatas sehingga saya memiliki:

ADDR1="[email protected]"
ADDR2="[email protected]"

Saya tidak perlu ADDR1dan ADDR2variabel. Jika mereka adalah elemen dari array itu bahkan lebih baik.


Setelah saran dari jawaban di bawah ini, saya berakhir dengan yang berikut yang saya cari:

#!/usr/bin/env bash

IN="[email protected];[email protected]"

mails=$(echo $IN | tr ";" "\n")

for addr in $mails
do
    echo "> [$addr]"
done

Keluaran:

> [bla@some.com]
> [john@home.com]

Ada solusi yang melibatkan pengaturan Internal_field_separator (IFS) ke ;. Saya tidak yakin apa yang terjadi dengan jawaban itu, bagaimana Anda mengatur ulang IFSkembali ke default?

RE: IFSsolusi, saya mencoba ini dan berhasil, saya menyimpan yang lama IFSdan mengembalikannya:

IN="[email protected];[email protected]"

OIFS=$IFS
IFS=';'
mails2=$IN
for x in $mails2
do
    echo "> [$x]"
done

IFS=$OIFS

BTW, ketika saya mencoba

mails2=($IN)

Saya hanya mendapatkan string pertama saat mencetaknya dalam lingkaran, tanpa tanda kurung di $INsekitarnya berfungsi.

Stefan
sumber
14
Sehubungan dengan "Edit2" Anda: Anda cukup "unset IFS" dan itu akan kembali ke keadaan default. Tidak perlu menyimpan dan mengembalikannya secara eksplisit kecuali Anda memiliki alasan untuk berharap bahwa itu sudah diatur ke nilai non-default. Selain itu, jika Anda melakukan ini di dalam suatu fungsi (dan, jika tidak, mengapa tidak?), Anda dapat mengatur IFS sebagai variabel lokal dan itu akan kembali ke nilai sebelumnya setelah Anda keluar dari fungsi.
Brooks Moses
19
@BrooksMoses: (a) +1 untuk digunakan local IFS=...jika memungkinkan; (b) -1 untuk unset IFS, ini tidak persis me-reset IFS ke nilai default, meskipun saya percaya IFS yang tidak disetel berperilaku sama dengan nilai default IFS ($ '\ t \ n'), namun tampaknya praktik yang buruk untuk mengasumsikan secara membabi buta bahwa kode Anda tidak akan pernah dipanggil dengan IFS diatur ke nilai khusus; (c) ide lain adalah meminta subshell: (IFS=$custom; ...)ketika subshell keluar IFS akan kembali ke apa pun awalnya.
dubiousjim
Saya hanya ingin melihat-lihat jalur untuk memutuskan di mana harus melempar executable, jadi saya terpaksa lari ruby -e "puts ENV.fetch('PATH').split(':')". Jika Anda ingin tetap bash murni tidak akan membantu tetapi menggunakan bahasa skrip apa pun yang memiliki pemisahan bawaan lebih mudah.
nicooga
4
for x in $(IFS=';';echo $IN); do echo "> [$x]"; done
user2037659
2
Untuk menyimpannya sebagai sebuah array saya harus menempatkan satu set tanda kurung dan mengubah \nhanya spasi. Jadi baris terakhir adalah mails=($(echo $IN | tr ";" " ")). Jadi sekarang saya dapat memeriksa elemen-elemen mailsdengan menggunakan notasi array mails[index]atau hanya mengulangi dalam satu lingkaran
afranques

Jawaban:

1236

Anda bisa mengatur variabel pemisah bidang internal (IFS), dan kemudian membiarkannya menguraikan menjadi array. Ketika ini terjadi dalam suatu perintah, maka penugasan untuk IFShanya terjadi pada lingkungan perintah tunggal (untuk read). Ini kemudian mem-parsing input sesuai dengan nilai IFSvariabel ke dalam array, yang kemudian dapat kita iterate.

IFS=';' read -ra ADDR <<< "$IN"
for i in "${ADDR[@]}"; do
    # process "$i"
done

Ini akan mengurai satu baris item yang dipisahkan oleh ;, mendorongnya ke dalam array. Hal-hal untuk memproses keseluruhan $IN, setiap kali satu baris input dipisahkan oleh ;:

 while IFS=';' read -ra ADDR; do
      for i in "${ADDR[@]}"; do
          # process "$i"
      done
 done <<< "$IN"
Johannes Schaub - litb
sumber
22
Ini mungkin cara terbaik. Berapa lama IFS akan bertahan dalam nilai saat ini, dapatkah itu mengacaukan kode saya dengan ditetapkan ketika seharusnya tidak, dan bagaimana saya bisa mengatur ulang ketika saya selesai dengan itu?
Chris Lutz
7
sekarang setelah perbaikan diterapkan, hanya dalam durasi perintah baca :)
Johannes Schaub - litb
14
Anda dapat membaca semuanya sekaligus tanpa menggunakan loop sementara: read -r -d '' -a addr <<< "$ in" # The -d '' adalah kunci di sini, ia memberitahu read untuk tidak berhenti pada baris baru pertama ( yang merupakan default -d) tetapi untuk melanjutkan sampai EOF atau byte NULL (yang hanya terjadi dalam data biner).
lhunath
55
@LucaBorrione Pengaturan IFSpada baris yang sama readdengan tanpa titik koma atau pemisah lainnya, sebagai lawan dalam perintah terpisah, lingkup ke perintah itu - sehingga selalu "dikembalikan"; Anda tidak perlu melakukan apa pun secara manual.
Charles Duffy
5
@imagineerThis Ada bug yang melibatkan herestrings dan perubahan lokal pada IFS yang perlu $INdikutip. Bug diperbaiki pada bash4.3.
chepner
973

Diambil dari array pemisah skrip Bash shell :

IN="[email protected];[email protected]"
arrIN=(${IN//;/ })

Penjelasan:

Konstruksi ini menggantikan semua kemunculan ';'(inisialisasi //global berarti) dalam string INdengan ' '(spasi tunggal), kemudian mengartikan string yang dibatasi ruang sebagai array (itulah yang dilakukan kurung di sekitarnya).

Sintaks yang digunakan di dalam kurung kurawal untuk mengganti setiap ';'karakter dengan ' 'karakter disebut Parameter Expansion .

Ada beberapa gotcha yang umum:

  1. Jika string asli memiliki spasi, Anda harus menggunakan IFS :
    • IFS=':'; arrIN=($IN); unset IFS;
  2. Jika string asli memiliki spasi dan pembatas adalah baris baru, Anda dapat mengatur IFS dengan:
    • IFS=$'\n'; arrIN=($IN); unset IFS;
palindrom
sumber
84
Saya hanya ingin menambahkan: ini adalah yang paling sederhana, Anda dapat mengakses elemen array dengan $ {arrIN [1]} (mulai dari nol tentu saja)
Oz123
26
Ditemukan: teknik memodifikasi variabel dalam $ {} dikenal sebagai 'ekspansi parameter'.
KomodoDave
23
Tidak, saya tidak berpikir ini berfungsi ketika ada juga spasi yang hadir ... itu mengkonversi ',' ke '' dan kemudian membangun array yang dipisahkan oleh ruang.
Ethan
12
Sangat ringkas, tetapi ada peringatan untuk penggunaan umum : shell menerapkan pemisahan kata dan ekspansi ke string, yang mungkin tidak diinginkan; coba saja dengan. IN="[email protected];[email protected];*;broken apart". Singkatnya: pendekatan ini akan pecah, jika token Anda berisi ruang dan / atau karakter yang disematkan. seperti *itu terjadi untuk membuat nama file token yang cocok di folder saat ini.
mklement0
53
Ini adalah pendekatan yang buruk karena alasan lain: Misalnya, jika string Anda berisi ;*;, maka *akan diperluas ke daftar nama file di direktori saat ini. -1
Charles Duffy
249

Jika Anda tidak keberatan memprosesnya segera, saya suka melakukan ini:

for i in $(echo $IN | tr ";" "\n")
do
  # process
done

Anda bisa menggunakan loop semacam ini untuk menginisialisasi array, tetapi mungkin ada cara yang lebih mudah untuk melakukannya. Semoga ini bisa membantu.

Chris Lutz
sumber
Anda seharusnya menyimpan jawaban IFS. Itu mengajari saya sesuatu yang saya tidak tahu, dan itu pasti membuat array, sedangkan ini hanya membuat pengganti yang murah.
Chris Lutz
Saya melihat. Ya saya menemukan melakukan eksperimen konyol ini, saya akan belajar hal-hal baru setiap kali saya mencoba menjawab sesuatu. Saya telah mengedit hal-hal berdasarkan umpan balik IRC #bash dan terhapus :)
Johannes Schaub - litb
33
-1, Anda jelas tidak menyadari peletakan kata-kata, karena itu memperkenalkan dua bug dalam kode Anda. satu adalah ketika Anda tidak mengutip $ IN dan yang lainnya adalah ketika Anda berpura-pura baris baru adalah satu-satunya pembatas yang digunakan dalam peletakan kata. Anda mengulangi setiap KATA dalam IN, tidak setiap baris, dan PASTI tidak setiap elemen dibatasi oleh tanda titik koma, meskipun itu mungkin tampak memiliki efek samping dari terlihat seperti itu berfungsi.
lhunath
3
Anda dapat mengubahnya ke gema "$ IN" | tr ';' '\ n' | saat membaca -r ADDY; lakukan # proses "$ ADDY"; selesai untuk membuatnya beruntung, saya pikir :) Perhatikan bahwa ini akan bercabang, dan Anda tidak dapat mengubah variabel luar dari dalam loop (itu sebabnya saya menggunakan sintaks <<< "$ IN") lalu
Johannes Schaub - litb
8
Untuk meringkas perdebatan dalam komentar: Peringatan untuk penggunaan umum : shell menerapkan pemisahan kata dan ekspansi ke string, yang mungkin tidak diinginkan; coba saja dengan. IN="[email protected];[email protected];*;broken apart". Singkatnya: pendekatan ini akan pecah, jika token Anda berisi ruang dan / atau karakter yang disematkan. seperti *itu terjadi untuk membuat nama file token yang cocok di folder saat ini.
mklement0
202

Jawaban yang kompatibel

Ada banyak cara berbeda untuk melakukan ini .

Namun, penting untuk diketahui terlebih dahulu yang bashmemiliki banyak fitur khusus (disebut bashism ) yang tidak akan berfungsi di fitur lainnya.

Secara khusus, array , array asosiatif , dan substitusi pola , yang digunakan dalam solusi dalam posting ini serta yang lain di utas, adalah bashism dan mungkin tidak berfungsi di bawah cangkang lain yang banyak digunakan orang.

Sebagai contoh: pada Debian GNU / Linux saya , ada shell standar yang disebut; Saya tahu banyak orang yang suka menggunakan shell lain yang disebut; dan ada juga alat khusus yang disebut dengan penerjemah shellnya sendiri ().

String yang diminta

String yang akan dibagi dalam pertanyaan di atas adalah:

IN="[email protected];[email protected]"

Saya akan menggunakan versi modifikasi dari string ini untuk memastikan bahwa solusi saya kuat untuk string yang berisi spasi putih, yang dapat memecahkan solusi lain:

IN="[email protected];[email protected];Full Name <[email protected]>"

Split string berdasarkan pembatas di (versi> = 4.2)

Secara murni bash , kita bisa membuat array dengan elemen yang dipisahkan oleh nilai sementara untuk IFS ( pemisah bidang input ). IFS, antara lain, memberi tahu bashkarakter mana yang harus diperlakukan sebagai pembatas antara elemen ketika mendefinisikan array:

IN="[email protected];[email protected];Full Name <[email protected]>"

# save original IFS value so we can restore it later
oIFS="$IFS"
IFS=";"
declare -a fields=($IN)
IFS="$oIFS"
unset oIFS

Dalam versi yang lebih baru dari bash, awalan perintah dengan definisi IFS mengubah IFS untuk perintah yang hanya dan me-reset ke nilai sebelumnya segera setelah itu. Ini berarti kita dapat melakukan hal di atas hanya dalam satu baris:

IFS=\; read -a fields <<<"$IN"
# after this command, the IFS resets back to its previous value (here, the default):
set | grep ^IFS=
# IFS=$' \t\n'

Kita dapat melihat bahwa string INtelah disimpan ke dalam array bernama fields, dipisah pada titik koma:

set | grep ^fields=\\\|^IN=
# fields=([0]="[email protected]" [1]="[email protected]" [2]="Full Name <[email protected]>")
# IN='[email protected];[email protected];Full Name <[email protected]>'

(Kami juga dapat menampilkan konten dari variabel-variabel ini menggunakan declare -p:)

declare -p IN fields
# declare -- IN="[email protected];[email protected];Full Name <[email protected]>"
# declare -a fields=([0]="[email protected]" [1]="[email protected]" [2]="Full Name <[email protected]>")

Perhatikan bahwa readini adalah cara tercepat untuk melakukan pemecahan karena tidak ada garpu atau sumber daya eksternal yang disebut.

Setelah array didefinisikan, Anda dapat menggunakan loop sederhana untuk memproses setiap bidang (atau, lebih tepatnya, setiap elemen dalam array yang sekarang telah Anda tetapkan):

# `"${fields[@]}"` expands to return every element of `fields` array as a separate argument
for x in "${fields[@]}" ;do
    echo "> [$x]"
    done
# > [[email protected]]
# > [[email protected]]
# > [Full Name <[email protected]>]

Atau Anda bisa menjatuhkan setiap bidang dari array setelah diproses menggunakan pendekatan pergeseran , yang saya suka:

while [ "$fields" ] ;do
    echo "> [$fields]"
    # slice the array 
    fields=("${fields[@]:1}")
    done
# > [[email protected]]
# > [[email protected]]
# > [Full Name <[email protected]>]

Dan jika Anda hanya menginginkan cetakan array yang sederhana, Anda bahkan tidak perlu mengulanginya:

printf "> [%s]\n" "${fields[@]}"
# > [[email protected]]
# > [[email protected]]
# > [Full Name <[email protected]>]

Perbarui: baru-baru ini > = 4.4

Di versi yang lebih baru bash, Anda juga dapat bermain dengan perintah mapfile:

mapfile -td \; fields < <(printf "%s\0" "$IN")

Sintaks ini mempertahankan karakter khusus, baris baru, dan bidang kosong!

Jika Anda tidak ingin memasukkan bidang kosong, Anda dapat melakukan hal berikut:

mapfile -td \; fields <<<"$IN"
fields=("${fields[@]%$'\n'}")   # drop '\n' added by '<<<'

Dengan mapfile, Anda juga dapat melewati mendeklarasikan array dan secara implisit "loop" di atas elemen yang dibatasi, memanggil fungsi pada masing-masing:

myPubliMail() {
    printf "Seq: %6d: Sending mail to '%s'..." $1 "$2"
    # mail -s "This is not a spam..." "$2" </path/to/body
    printf "\e[3D, done.\n"
}

mapfile < <(printf "%s\0" "$IN") -td \; -c 1 -C myPubliMail

(Catatan: \0di akhir string format tidak berguna jika Anda tidak peduli dengan bidang kosong di akhir string atau mereka tidak ada.)

mapfile < <(echo -n "$IN") -td \; -c 1 -C myPubliMail

# Seq:      0: Sending mail to '[email protected]', done.
# Seq:      1: Sending mail to '[email protected]', done.
# Seq:      2: Sending mail to 'Full Name <[email protected]>', done.

Atau Anda bisa menggunakan <<<, dan di badan fungsi menyertakan beberapa pemrosesan untuk menghapus baris baru yang ditambahkan:

myPubliMail() {
    local seq=$1 dest="${2%$'\n'}"
    printf "Seq: %6d: Sending mail to '%s'..." $seq "$dest"
    # mail -s "This is not a spam..." "$dest" </path/to/body
    printf "\e[3D, done.\n"
}

mapfile <<<"$IN" -td \; -c 1 -C myPubliMail

# Renders the same output:
# Seq:      0: Sending mail to '[email protected]', done.
# Seq:      1: Sending mail to '[email protected]', done.
# Seq:      2: Sending mail to 'Full Name <[email protected]>', done.

Split string berdasarkan pembatas di

Jika Anda tidak dapat menggunakan bash, atau jika Anda ingin menulis sesuatu yang dapat digunakan di banyak shell yang berbeda, Anda sering tidak dapat menggunakan bashism - dan ini termasuk array yang telah kami gunakan dalam solusi di atas.

Namun, kita tidak perlu menggunakan array untuk mengulang "elemen" string. Ada sintaks yang digunakan dalam banyak shell untuk menghapus substring dari string dari kemunculan pertama atau terakhir suatu pola. Perhatikan bahwa *wildcard yang mewakili nol atau lebih karakter:

(Kurangnya pendekatan ini dalam solusi yang diposting sejauh ini adalah alasan utama saya menulis jawaban ini;)

${var#*SubStr}  # drops substring from start of string up to first occurrence of `SubStr`
${var##*SubStr} # drops substring from start of string up to last occurrence of `SubStr`
${var%SubStr*}  # drops substring from last occurrence of `SubStr` to end of string
${var%%SubStr*} # drops substring from first occurrence of `SubStr` to end of string

Seperti yang dijelaskan oleh Score_Under :

#dan %hapus substring pencocokan yang sesingkat mungkin dari awal dan akhir string, dan

##dan %%hapus substring pencocokan yang terpanjang.

Menggunakan sintaks di atas, kita dapat membuat pendekatan di mana kita mengekstraksi "elemen" substring dari string dengan menghapus substring hingga atau setelah pembatas.

Kode kunci di bawah berfungsi dengan baik di (termasuk Mac OS bash),, , dan ini :

IN="[email protected];[email protected];Full Name <[email protected]>"
while [ "$IN" ] ;do
    # extract the substring from start of string up to delimiter.
    # this is the first "element" of the string.
    iter=${IN%%;*}
    echo "> [$iter]"
    # if there's only one element left, set `IN` to an empty string.
    # this causes us to exit this `while` loop.
    # else, we delete the first "element" of the string from IN, and move onto the next.
    [ "$IN" = "$iter" ] && \
        IN='' || \
        IN="${IN#*;}"
  done
# > [[email protected]]
# > [[email protected]]
# > [Full Name <[email protected]>]

Selamat bersenang-senang!

F. Hauri
sumber
15
The #, ##, %, dan %%substitusi memiliki apa yang IMO penjelasan lebih mudah untuk mengingat (untuk berapa banyak mereka menghapus): #dan %menghapus kemungkinan pencocokan string terpendek, dan ##dan %%menghapus terpanjang mungkin.
Score_Under
1
The IFS=\; read -a fields <<<"$var"gagal pada baris dan menambahkan baris baru trailing. Solusi lain menghilangkan bidang kosong yang tertinggal.
Isaac
Pembatas shell adalah jawaban yang paling elegan, titik.
Eric Chen
Bisakah alternatif terakhir digunakan dengan daftar pemisah bidang yang ditetapkan di tempat lain? Sebagai contoh, saya bermaksud menggunakan ini sebagai skrip shell, dan meneruskan daftar pemisah bidang sebagai parameter posisi.
sancho.s ReinstateMonicaCellio
Ya, dalam satu lingkaran:for sep in "#" "ł" "@" ; do ... var="${var#*$sep}" ...
F. Hauri
184

Saya telah melihat beberapa jawaban merujuk cutperintah, tetapi semuanya sudah dihapus. Agak aneh bahwa tidak ada yang menjelaskan hal itu, karena saya pikir itu adalah salah satu perintah yang lebih berguna untuk melakukan hal semacam ini, terutama untuk mem-parsing file log yang dibatasi.

Dalam hal memecah contoh spesifik ini menjadi array skrip bash, trmungkin lebih efisien, tetapi cutdapat digunakan, dan lebih efektif jika Anda ingin menarik bidang tertentu dari tengah.

Contoh:

$ echo "[email protected];[email protected]" | cut -d ";" -f 1
bla@some.com
$ echo "[email protected];[email protected]" | cut -d ";" -f 2
john@home.com

Anda jelas dapat memasukkannya ke dalam satu lingkaran, dan lakukan iterasi pada parameter -f untuk menarik setiap bidang secara independen.

Ini menjadi lebih berguna ketika Anda memiliki file log yang dibatasi dengan baris seperti ini:

2015-04-27|12345|some action|an attribute|meta data

cutsangat berguna untuk dapat catfile ini dan memilih bidang tertentu untuk diproses lebih lanjut.

DougW
sumber
6
Pujian untuk menggunakan cut, itu alat yang tepat untuk pekerjaan itu! Jauh lebih bersih dari semua peretas shell itu.
MisterMiyagi
4
Pendekatan ini hanya akan berfungsi jika Anda mengetahui jumlah elemen sebelumnya; Anda perlu memprogram logika lainnya di sekitarnya. Itu juga menjalankan alat eksternal untuk setiap elemen.
uli42
Benar-benar baik saya sedang mencari cara untuk menghindari string kosong di csv. Sekarang saya bisa menunjukkan nilai 'kolom' yang tepat juga. Bekerja dengan IFS sudah digunakan dalam satu lingkaran. Lebih baik dari yang diharapkan untuk situasi saya.
Louis Loudog Trottier
Sangat berguna untuk menarik ID dan PID juga yaitu
Milos Grujic
Jawaban ini layak
digulirkan
124

Ini bekerja untuk saya:

string="1;2"
echo $string | cut -d';' -f1 # output is 1
echo $string | cut -d';' -f2 # output is 2
Steven Lizarazo
sumber
1
Meskipun hanya bekerja dengan pembatas karakter tunggal, itulah yang dicari OP (catatan dibatasi oleh titik koma).
GuyPaddock
Dijawab sekitar empat tahun lalu oleh @Ashok , dan juga, lebih dari satu tahun yang lalu oleh @DougW , daripada jawaban Anda, dengan lebih banyak informasi. Silakan kirim solusi yang berbeda dari yang lain.
MAChitgarha
90

Bagaimana dengan pendekatan ini:

IN="[email protected];[email protected]" 
set -- "$IN" 
IFS=";"; declare -a Array=($*) 
echo "${Array[@]}" 
echo "${Array[0]}" 
echo "${Array[1]}" 

Sumber

Lepuh
sumber
7
+1 ... tapi saya tidak akan menyebut variabel "Array" ... pet peev kurasa. Solusi yang bagus
Yzmir Ramirez
14
+1 ... tetapi "set" dan menyatakan -a tidak perlu. Anda juga bisa menggunakan sajaIFS";" && Array=($IN)
ata
+1 Hanya catatan tambahan: bukankah sebaiknya menyimpan IFS lama dan mengembalikannya? (seperti yang ditunjukkan oleh stefanB dalam editnya3) orang-orang yang mendarat di sini (kadang-kadang hanya menyalin dan menempelkan solusi) mungkin tidak memikirkan hal ini
Luca Borrione
6
-1: Pertama, @ata benar bahwa sebagian besar perintah dalam hal ini tidak melakukan apa pun. Kedua, ia menggunakan pemisahan kata untuk membentuk array, dan tidak melakukan apa pun untuk menghambat glob-ekspansi ketika melakukannya (jadi jika Anda memiliki karakter glob di salah satu elemen array, elemen-elemen tersebut diganti dengan nama file yang cocok).
Charles Duffy
1
Menyarankan penggunaan $'...': IN=$'[email protected];[email protected];bet <d@\ns* kl.com>'. Kemudian echo "${Array[2]}"akan mencetak string dengan baris baru. set -- "$IN"juga diperlukan dalam kasus ini. Ya, untuk mencegah ekspansi gumpal, solusinya harus mencakup set -f.
John_West
79

Saya pikir AWK adalah perintah terbaik dan efisien untuk menyelesaikan masalah Anda. AWK disertakan secara default di hampir setiap distribusi Linux.

echo "[email protected];[email protected]" | awk -F';' '{print $1,$2}'

akan memberi

bla@some.com john@home.com

Tentu saja Anda dapat menyimpan setiap alamat email dengan mendefinisikan kembali bidang cetak awk.

Tong
sumber
3
Atau bahkan lebih sederhana: echo "[email protected]; [email protected]" | awk 'BEGIN {RS = ";"} {print}'
Jaro
@ Joaro Ini bekerja sempurna untuk saya ketika saya memiliki string dengan koma dan perlu memformatnya menjadi garis. Terima kasih.
Aquarelle
Ini berhasil dalam skenario ini -> "echo" $ SPLIT_0 "| awk -F 'inode =' '{print $ 1}'"! Saya mengalami masalah ketika mencoba menggunakan atrings ("inode ="), bukan karakter (";"). $ 1, $ 2, $ 3, $ 4 ditetapkan sebagai posisi dalam array! Jika ada cara mengatur array ... lebih baik! Terima kasih!
Eduardo Lucio
@EduardoLucio, apa yang saya pikirkan adalah mungkin Anda pertama dapat mengganti pembatas Anda inode=ke dalam ;misalnya dengan sed -i 's/inode\=/\;/g' your_file_to_process, kemudian menentukan -F';'ketika menerapkan awk, harapan yang dapat membantu Anda.
Tong
66
echo "[email protected];[email protected]" | sed -e 's/;/\n/g'
bla@some.com
john@home.com
lothar
sumber
4
-1 bagaimana jika string berisi spasi? misalnya IN="this is first line; this is second line" arrIN=( $( echo "$IN" | sed -e 's/;/\n/g' ) )akan menghasilkan array 8 elemen dalam hal ini (elemen untuk setiap ruang kata yang dipisahkan), bukan 2 (elemen untuk setiap baris yang dipisahkan titik koma)
Luca Borrione
3
@ Luca Tidak skrip sed membuat persis dua baris. Apa yang membuat banyak entri untuk Anda adalah ketika Anda memasukkannya ke dalam bash array (yang terbagi pada ruang putih secara default)
lothar
Itulah intinya: OP perlu menyimpan entri ke dalam array untuk mengulanginya, seperti yang dapat Anda lihat dalam suntingannya. Saya pikir jawaban Anda (baik) terlewatkan untuk digunakan arrIN=( $( echo "$IN" | sed -e 's/;/\n/g' ) )untuk mencapai itu, dan untuk saran untuk mengubah IFS IFS=$'\n'bagi mereka yang mendarat di sini di masa depan dan perlu membagi string yang berisi spasi. (dan mengembalikannya setelah itu). :)
Luca Borrione
1
@Luca Poin bagus. Namun tugas array tidak ada dalam pertanyaan awal ketika saya menulis jawaban itu.
lothar
65

Ini juga berfungsi:

IN="[email protected];[email protected]"
echo ADD1=`echo $IN | cut -d \; -f 1`
echo ADD2=`echo $IN | cut -d \; -f 2`

Hati-hati, solusi ini tidak selalu benar. Jika Anda melewati "[email protected]" saja, itu akan menetapkannya untuk ADD1 dan ADD2.

Ashok
sumber
1
Anda dapat menggunakan -s untuk menghindari masalah yang disebutkan: superuser.com/questions/896800/... "-f, --fields = LIST pilih hanya bidang-bidang ini; cetak juga setiap baris yang tidak mengandung karakter pembatas, kecuali opsi -s adalah ditentukan "
fersarr
34

Pandangan berbeda dari jawaban Darron , ini adalah bagaimana saya melakukannya:

IN="[email protected];[email protected]"
read ADDR1 ADDR2 <<<$(IFS=";"; echo $IN)
nickjb
sumber
Saya kira begitu! Jalankan perintah di atas dan kemudian "echo $ ADDR1 ... $ ADDR2" dan saya mendapatkan "[email protected] ... [email protected]" output
nickjb
1
Ini benar-benar bekerja dengan baik bagi saya ... Saya menggunakannya untuk beralih di atas serangkaian string yang berisi data DB, SERVER, PORT yang dipisahkan koma untuk menggunakan mysqldump.
Nick
5
Diagnosis: IFS=";"tugas hanya ada di $(...; echo $IN)subkulit; inilah mengapa sebagian pembaca (termasuk saya) pada awalnya berpikir itu tidak akan berhasil. Saya berasumsi bahwa semua $ IN disedot oleh ADDR1. Tapi nickjb benar; itu berhasil. Alasannya adalah bahwa echo $INperintah mem-parsing argumennya menggunakan nilai saat ini dari $ IFS, tetapi kemudian menggemakannya ke stdout menggunakan pembatas ruang, terlepas dari pengaturan $ IFS. Jadi efek bersihnya adalah seolah-olah seseorang telah memanggil read ADDR1 ADDR2 <<< "[email protected] [email protected]"(perhatikan input dipisahkan oleh spasi bukan; -disendiri).
dubiousjim
1
Ini gagal pada spasi dan baris baru, dan juga memperluas wildcard *di echo $INdengan ekspansi variabel yang tidak dikutip.
Isaac
Saya sangat suka solusi ini. Deskripsi mengapa ini bekerja akan sangat berguna dan menjadikannya sebagai jawaban keseluruhan yang lebih baik.
Michael Gaskill
32

Di Bash, cara anti peluru, itu akan berfungsi bahkan jika variabel Anda berisi baris baru:

IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")

Lihat:

$ in=$'one;two three;*;there is\na newline\nin this field'
$ IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")
$ declare -p array
declare -a array='([0]="one" [1]="two three" [2]="*" [3]="there is
a newline
in this field")'

Trik untuk ini bekerja adalah dengan menggunakan -dopsi read(pembatas) dengan pembatas kosong, sehingga readdipaksa untuk membaca semua yang diberi makan. Dan kami memberi makan readdengan tepat isi variabel in, tanpa garis belakang baru, terima kasih printf. Perhatikan bahwa kami juga meletakkan pembatas printfuntuk memastikan bahwa string yang dilewati readmemiliki pembatas trailing. Tanpa itu, readakan memangkas potensi trailing bidang kosong:

$ in='one;two;three;'    # there's an empty field
$ IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")
$ declare -p array
declare -a array='([0]="one" [1]="two" [2]="three" [3]="")'

bidang kosong yang tertinggal dipertahankan.


Perbarui untuk Bash≥4.4

Sejak Bash 4.4, builtin mapfile(alias readarray) mendukung -dopsi untuk menentukan pembatas. Karenanya cara kanonik lainnya adalah:

mapfile -d ';' -t array < <(printf '%s;' "$in")
gniourf_gniourf
sumber
5
Saya menemukannya sebagai solusi langka pada daftar yang berfungsi dengan benar \n, spasi, dan *secara bersamaan. Juga, tidak ada loop; variabel array dapat diakses di shell setelah eksekusi (bertentangan dengan jawaban tertinggi yang dipilih). Catatan,, in=$'...'itu tidak bekerja dengan tanda kutip ganda. Saya pikir, perlu lebih banyak upvotes.
John_West
28

Bagaimana dengan liner satu ini, jika Anda tidak menggunakan array:

IFS=';' read ADDR1 ADDR2 <<<$IN
Darron
sumber
Pertimbangkan read -r ...untuk menggunakan untuk memastikan bahwa, misalnya, dua karakter "\ t" di input berakhir sebagai dua karakter yang sama dalam variabel Anda (bukan karakter tab tunggal).
dubiousjim
-1 Ini tidak berfungsi di sini (ubuntu 12.04). Menambahkan echo "ADDR1 $ADDR1"\n echo "ADDR2 $ADDR2"ke cuplikan Anda akan menampilkan ADDR1 [email protected] [email protected]\nADDR2(\ n adalah baris baru)
Luca Borrione
Ini mungkin karena bug yang melibatkan IFSdan di sini string yang diperbaiki di bash4.3. Mengutip $INharus memperbaikinya. (Secara teori, $INtidak tunduk pada pemisahan kata atau penggumpalan setelah diperluas, artinya tanda kutip tidak perlu. Bahkan dalam 4.3, meskipun, setidaknya ada satu bug yang tersisa - dilaporkan dan dijadwalkan diperbaiki - sehingga mengutip tetap bagus ide.)
chepner
Ini pecah jika $ in berisi baris baru bahkan jika $ IN dikutip. Dan menambahkan baris baru.
Isaac
Masalah dengan ini, dan banyak solusi lainnya adalah mengasumsikan ada PERSIS DUA elemen dalam $ IN - ATAU bahwa Anda ingin item kedua dan selanjutnya dihancurkan bersama di ADDR2. Saya mengerti ini memenuhi permintaan, tapi ini bom waktu.
Steven the Easily Amused
23

Tanpa mengatur IFS

Jika Anda hanya memiliki satu titik dua, Anda dapat melakukannya:

a="foo:bar"
b=${a%:*}
c=${a##*:}

kamu akan mendapatkan:

b = foo
c = bar
Emilien Brigand
sumber
20

Ini adalah 3-liner bersih:

in="foo@bar;bizz@buzz;fizz@buzz;buzz@woof"
IFS=';' list=($in)
for item in "${list[@]}"; do echo $item; done

di mana IFSkata-kata pembatas berdasarkan pada pemisah dan ()digunakan untuk membuat array . Kemudian [@]digunakan untuk mengembalikan setiap item sebagai kata yang terpisah.

Jika Anda memiliki kode setelah itu, Anda juga harus mengembalikan $IFS, mis unset IFS.

kenorb
sumber
5
Penggunaan tanda $inkutip memungkinkan wildcard diperluas.
Isaac
10

Fungsi Bash / zsh berikut membagi argumen pertamanya pada pembatas yang diberikan oleh argumen kedua:

split() {
    local string="$1"
    local delimiter="$2"
    if [ -n "$string" ]; then
        local part
        while read -d "$delimiter" part; do
            echo $part
        done <<< "$string"
        echo $part
    fi
}

Misalnya, perintahnya

$ split 'a;b;c' ';'

hasil panen

a
b
c

Output ini dapat, misalnya, disalurkan ke perintah lain. Contoh:

$ split 'a;b;c' ';' | cat -n
1   a
2   b
3   c

Dibandingkan dengan solusi lain yang diberikan, yang ini memiliki keunggulan sebagai berikut:

  • IFStidak diganti: Karena pelingkupan dinamis dari variabel lokal, penimpaan IFSatas loop menyebabkan nilai baru bocor ke panggilan fungsi yang dilakukan dari dalam loop.

  • Array tidak digunakan: Membaca string ke dalam array menggunakan readmembutuhkan flag -adi Bash dan -Adi zsh.

Jika diinginkan, fungsi dapat dimasukkan ke dalam skrip sebagai berikut:

#!/usr/bin/env bash

split() {
    # ...
}

split "$@"
Halle Knast
sumber
Tampaknya tidak berfungsi dengan pembatas lebih dari 1 karakter: split = $ (split "$ content" "file: //")
madprops
Benar - dari help read:-d delim continue until the first character of DELIM is read, rather than newline
Halle Knast
7

Ada cara sederhana dan cerdas seperti ini:

echo "add:sfff" | xargs -d: -i  echo {}

Tetapi Anda harus menggunakan gnu xargs, BSD xargs tidak dapat mendukung -d delim. Jika Anda menggunakan apple mac seperti saya. Anda dapat menginstal gnu xargs:

brew install findutils

kemudian

echo "add:sfff" | gxargs -d: -i  echo {}
Victor Choy
sumber
4

Ini adalah cara paling sederhana untuk melakukannya.

spo='one;two;three'
OIFS=$IFS
IFS=';'
spo_array=($spo)
IFS=$OIFS
echo ${spo_array[*]}
James
sumber
4

Ada beberapa jawaban keren di sini (errator esp.), Tetapi untuk sesuatu yang analog untuk dipecah dalam bahasa lain - yang saya maksud dengan pertanyaan aslinya - saya memutuskan untuk ini:

IN="[email protected];[email protected]"
declare -a a="(${IN/;/ })";

Sekarang ${a[0]}, ${a[1]}dll, seperti yang Anda harapkan. Gunakan ${#a[*]}untuk sejumlah istilah. Atau untuk beralih, tentu saja:

for i in ${a[*]}; do echo $i; done

CATATAN PENTING:

Ini bekerja dalam kasus di mana tidak ada ruang untuk dikhawatirkan, yang memecahkan masalah saya, tetapi mungkin tidak menyelesaikan masalah Anda. Pergilah dengan $IFSsolusi dalam hal itu.

eukras
sumber
Tidak berfungsi saat INberisi lebih dari dua alamat email. Silakan merujuk ke ide yang sama (tetapi tetap) pada jawaban palindrom
olibre
Penggunaan yang lebih baik ${IN//;/ }(double slash) untuk membuatnya juga berfungsi dengan lebih dari dua nilai. Berhati-hatilah karena setiap wildcard ( *?[) akan diperluas. Dan bidang kosong yang tertinggal akan dibuang.
Isaac
3
IN="[email protected];[email protected]"
IFS=';'
read -a IN_arr <<< "${IN}"
for entry in "${IN_arr[@]}"
do
    echo $entry
done

Keluaran

bla@some.com
john@home.com

Sistem: Ubuntu 12.04.1

rashok
sumber
IFS tidak diatur dalam konteks spesifik di readsini dan karenanya dapat mengacaukan sisa kode, jika ada.
codeforester
2

Gunakan setbuilt-in untuk memuat $@array:

IN="[email protected];[email protected]"
IFS=';'; set $IN; IFS=$' \t\n'

Lalu, biarkan pesta dimulai:

echo $#
for a; do echo $a; done
ADDR1=$1 ADDR2=$2
Jeberle
sumber
Lebih baik digunakan set -- $INuntuk menghindari beberapa masalah dengan "$ IN" dimulai dengan tanda hubung. Namun, ekspansi tanda kutip yang tidak dikutip $INakan memperluas wildcard ( *?[).
Isaac
2

Dua alternatif bourne-ish di mana tidak memerlukan array bash:

Kasus 1 : Tetap bagus dan sederhana: Gunakan NewLine sebagai Pemisah-Rekam ... mis.

IN="[email protected]
[email protected]"

while read i; do
  # process "$i" ... eg.
    echo "[email:$i]"
done <<< "$IN"

Catatan: dalam kasus pertama ini tidak ada sub-proses yang bercabang dua untuk membantu dengan manipulasi daftar.

Ide: Mungkin perlu menggunakan NL secara internal , dan hanya mengonversi ke RS yang berbeda saat menghasilkan hasil akhir secara eksternal .

Kasus 2 : Menggunakan ";" sebagai pemisah rekaman ... mis.

NL="
" IRS=";" ORS=";"

conv_IRS() {
  exec tr "$1" "$NL"
}

conv_ORS() {
  exec tr "$NL" "$1"
}

IN="[email protected];[email protected]"
IN="$(conv_IRS ";" <<< "$IN")"

while read i; do
  # process "$i" ... eg.
    echo -n "[email:$i]$ORS"
done <<< "$IN"

Dalam kedua kasus, sub-daftar dapat dikomposisikan dalam loop tetap-menerus setelah loop selesai. Ini berguna saat memanipulasi daftar dalam memori, alih-alih menyimpan daftar dalam file. {ps tetap tenang dan lanjutkan B-)}

NevilleDNZ
sumber
2

Terlepas dari jawaban fantastis yang sudah disediakan, jika itu hanya masalah mencetak data yang dapat Anda pertimbangkan untuk menggunakan awk:

awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "$IN"

Ini mengatur pemisah bidang ke ;, sehingga dapat mengulangi bidang dengan forlingkaran dan mencetak sesuai.

Uji

$ IN="[email protected];[email protected]"
$ awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "$IN"
> [bla@some.com]
> [john@home.com]

Dengan input lain:

$ awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "a;b;c   d;e_;f"
> [a]
> [b]
> [c   d]
> [e_]
> [f]
fedorqui 'SO berhenti merugikan'
sumber
2

Di shell Android, sebagian besar metode yang diusulkan tidak berfungsi:

$ IFS=':' read -ra ADDR <<<"$PATH"                             
/system/bin/sh: can't create temporary file /sqlite_stmt_journals/mksh.EbNoR10629: No such file or directory

Apa yang berhasil adalah:

$ for i in ${PATH//:/ }; do echo $i; done
/sbin
/vendor/bin
/system/sbin
/system/bin
/system/xbin

di mana //berarti penggantian global.

18446744073709551615
sumber
1
Gagal jika ada bagian dari $ PATH berisi spasi (atau baris baru). Juga memperluas wildcard (tanda bintang *, tanda tanya? Dan kawat gigi [...]).
Isaac
2
IN='[email protected];[email protected];Charlie Brown <[email protected];!"#$%&/()[]{}*? are no problem;simple is beautiful :-)'
set -f
oldifs="$IFS"
IFS=';'; arrayIN=($IN)
IFS="$oldifs"
for i in "${arrayIN[@]}"; do
echo "$i"
done
set +f

Keluaran:

bla@some.com
john@home.com
Charlie Brown <cbrown@acme.com
!"#$%&/()[]{}*? are no problem
simple is beautiful :-)

Penjelasan: Penugasan sederhana menggunakan tanda kurung () mengubah daftar yang dipisahkan titik koma ke dalam array asalkan Anda memiliki IFS yang benar saat melakukan itu. Loop FOR standar menangani masing-masing item dalam array itu seperti biasa. Perhatikan bahwa daftar yang diberikan untuk variabel IN harus "keras" dikutip, yaitu dengan kutu tunggal.

IFS harus disimpan dan dipulihkan karena Bash tidak memperlakukan penugasan dengan cara yang sama seperti perintah. Solusi alternatif adalah untuk membungkus tugas di dalam suatu fungsi dan memanggil fungsi itu dengan IFS yang dimodifikasi. Dalam hal itu tidak diperlukan penyimpanan / pemulihan IFS secara terpisah. Terima kasih untuk "Bize" karena menunjukkannya.

ajaaskel
sumber
!"#$%&/()[]{}*? are no problembaik ... tidak cukup: []*?adalah karakter glob. Jadi bagaimana dengan membuat direktori dan file ini: `mkdir '!" # $% &'; Touch '! "# $% & / () [] {} Membuat Anda hahahaha - tidak ada masalah' dan menjalankan perintah Anda? Sederhana mungkin indah, tetapi ketika rusak, itu rusak.
gniourf_gniourf
@gniourf_gniourf String disimpan dalam variabel. Silakan lihat pertanyaan aslinya.
ajaaskel
1
@ajaaskel Anda tidak sepenuhnya memahami komentar saya. Pergi dalam direktori awal dan mengeluarkan perintah ini: mkdir '!"#$%&'; touch '!"#$%&/()[]{} got you hahahaha - are no problem'. Mereka hanya akan membuat direktori dan file, dengan nama yang tampak aneh, harus saya akui. Kemudian jalankan perintah Anda dengan tepat INyang Anda berikan: IN='[email protected];[email protected];Charlie Brown <[email protected];!"#$%&/()[]{}*? are no problem;simple is beautiful :-)'. Anda akan melihat bahwa Anda tidak akan mendapatkan output yang Anda harapkan. Karena Anda menggunakan subjek metode untuk ekspansi pathname untuk membagi string Anda.
gniourf_gniourf
Hal ini untuk menunjukkan bahwa karakter *, ?, [...]dan bahkan, jika extglobdiatur, !(...), @(...), ?(...), +(...) yang masalah dengan metode ini!
gniourf_gniourf
1
@ gniourf_gniourf Terima kasih atas komentar terperinci tentang globbing. Saya menyesuaikan kode untuk melakukan globbing. Maksud saya adalah hanya untuk menunjukkan bahwa tugas yang agak sederhana dapat melakukan pekerjaan pemisahan.
ajaaskel
1

Oke teman-teman!

Inilah jawaban saya!

DELIMITER_VAL='='

read -d '' F_ABOUT_DISTRO_R <<"EOF"
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION="Ubuntu 14.04.4 LTS"
NAME="Ubuntu"
VERSION="14.04.4 LTS, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04.4 LTS"
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
EOF

SPLIT_NOW=$(awk -F$DELIMITER_VAL '{for(i=1;i<=NF;i++){printf "%s\n", $i}}' <<<"${F_ABOUT_DISTRO_R}")
while read -r line; do
   SPLIT+=("$line")
done <<< "$SPLIT_NOW"
for i in "${SPLIT[@]}"; do
    echo "$i"
done

Mengapa pendekatan ini "yang terbaik" bagi saya?

Karena dua alasan:

  1. Anda tidak perlu melarikan diri dari pembatas;
  2. Anda tidak akan memiliki masalah dengan ruang kosong . Nilai akan dipisahkan dengan benar dalam array!

[]

Eduardo Lucio
sumber
FYI, /etc/os-releasedan /etc/lsb-releasedimaksudkan untuk bersumber, dan tidak diuraikan. Jadi metode Anda benar-benar salah. Selain itu, Anda tidak cukup menjawab pertanyaan tentang memiringkan string pada pembatas.
gniourf_gniourf
0

Satu baris untuk memisahkan string yang dipisahkan oleh ';' menjadi sebuah array adalah:

IN="[email protected];[email protected]"
ADDRS=( $(IFS=";" echo "$IN") )
echo ${ADDRS[0]}
echo ${ADDRS[1]}

Ini hanya menetapkan IFS dalam sebuah subkulit, sehingga Anda tidak perlu khawatir tentang menyimpan dan mengembalikan nilainya.

Michael Hale
sumber
-1 ini tidak berfungsi di sini (ubuntu 12.04). hanya mencetak gema pertama dengan semua nilai $ IN di dalamnya, sedangkan yang kedua kosong. Anda dapat melihatnya jika Anda memasukkan echo "0:" $ {ADDRS [0]} \ n echo "1:" $ {ADDRS [1]} hasilnya adalah 0: [email protected];[email protected]\n 1:(\ n adalah baris baru)
Luca Borrione
1
silakan merujuk ke jawaban nickjb di untuk alternatif kerja untuk ide ini stackoverflow.com/a/6583589/1032370
Luca Borrione
1
-1, 1. IFS tidak diatur dalam subkulit itu (itu diteruskan ke lingkungan "gema", yang merupakan builtin, jadi tidak ada yang terjadi). 2. $INdikutip sehingga tidak mengalami pemisahan IFS. 3. Substitusi proses dipisahkan oleh spasi, tetapi ini dapat merusak data asli.
Score_Under
0

Mungkin bukan solusi yang paling elegan, tetapi bekerja dengan *dan spasi:

IN="bla@so me.com;*;[email protected]"
for i in `delims=${IN//[^;]}; seq 1 $((${#delims} + 1))`
do
   echo "> [`echo $IN | cut -d';' -f$i`]"
done

Keluaran

> [bla@so me.com]
> [*]
> [john@home.com]

Contoh lain (pembatas di awal dan akhir):

IN=";bla@so me.com;*;[email protected];"
> []
> [bla@so me.com]
> [*]
> [john@home.com]
> []

Pada dasarnya ia menghilangkan setiap karakter selain dari ;membuat delimsmis. ;;;. Maka itu forloop dari 1ke yang number-of-delimitersdihitung oleh ${#delims}. Langkah terakhir adalah mendapatkan $ibagian yang aman menggunakancut .

Petr Újezdský
sumber