Apakah ada sesuatu seperti "split ()" JavaScript di shell?

18

Sangat mudah digunakan split()dalam JavaScript untuk memecah string menjadi sebuah array.

Bagaimana dengan skrip shell?

Katakanlah saya ingin melakukan ini:

$ script.sh var1_var2_var3

Ketika pengguna memberikan string tersebut var1_var2_var3ke script.sh, di dalam skrip itu akan mengubah string menjadi seperti array

array=( var1 var2 var3 )
for name in ${array[@]}; do
    # some code
done
AGamePlayer
sumber
1
apa shellyang Anda gunakan, dengan bashyang dapat Anda lakukanIFS='_' read -a array <<< "${string}"
gwillie
perldapat melakukannya juga. Ini bukan shell "murni", tetapi cukup umum.
Sobrique
@Obrique Saya juga tidak mengetahui definisi teknis dari shell "murni", tetapi ada node.js.
emory
Saya cenderung bekerja pada 'apakah mungkin diinstal pada kotak linux saya secara default' dan jangan khawatirkan hal-hal kecil :)
Sobrique

Jawaban:

24

Shell seperti Bourne / POSIX memiliki operator glob + split dan dipanggil setiap kali Anda meninggalkan ekspansi parameter ( $var, $-...), substitusi perintah ( $(...)), atau ekspansi aritmatika ( $((...))) tidak dikutip dalam konteks daftar.

Sebenarnya, Anda dipanggil oleh kesalahan ketika Anda melakukan for name in ${array[@]}bukan for name in "${array[@]}". (Sebenarnya, Anda harus berhati-hati bahwa memanggil operator seperti itu secara tidak sengaja merupakan sumber dari banyak bug dan kerentanan keamanan ).

Operator itu dikonfigurasikan dengan $IFSparameter khusus (untuk memberi tahu karakter apa yang harus dipisah (meskipun berhati-hatilah bahwa ruang, tab, dan baris baru menerima perlakuan khusus di sana)) dan -fopsi untuk menonaktifkan ( set -f) atau mengaktifkan ( set +f) globbagian.

Perhatikan juga bahwa sementara Sin $IFSawalnya (dalam cangkang Bourne dari mana $IFS) dari untuk Separator, dalam cangkang POSIX, karakter dalam $IFSseharusnya lebih dilihat sebagai pembatas atau terminator (lihat di bawah untuk contoh).

Jadi untuk dibagi _:

string='var1_var2_var3'
IFS=_ # delimit on _
set -f # disable the glob part
array=($string) # invoke the split+glob operator

for i in "${array[@]}"; do # loop over the array elements.

Untuk melihat perbedaan antara pemisah dan pembatas , coba:

string='var1_var2_'

Itu akan membaginya menjadi var1dan var2hanya (tidak ada elemen kosong tambahan).

Jadi, untuk membuatnya mirip dengan JavaScript split(), Anda perlu langkah ekstra:

string='var1_var2_var3'
IFS=_ # delimit on _
set -f # disable the glob part
temp=${string}_ # add an extra delimiter
array=($temp) # invoke the split+glob operator

(perhatikan bahwa itu akan membagi elemen kosong $stringmenjadi 1 (bukan 0 ), seperti JavaScript split()).

Untuk melihat tab perawatan khusus, ruang dan baris baru terima, bandingkan:

IFS=' '; string=' var1  var2  '

(di mana Anda dapatkan var1dan var2) dengan

IFS='_'; string='_var1__var2__'

di mana Anda mendapatkan: '', var1, '', var2, ''.

Perhatikan bahwa zshshell tidak memanggil operator glob + split yang secara implisit seperti itu kecuali dalam shatau kshemulasi. Di sana, Anda harus memohonnya dengan jelas. $=stringuntuk bagian split, $~stringuntuk bagian glob ( $=~stringuntuk keduanya), dan juga memiliki operator split di mana Anda dapat menentukan pemisah:

array=(${(s:_:)string})

atau untuk melestarikan elemen kosong:

array=("${(@s:_:)string}")

Perhatikan bahwa ada sadalah untuk membelah , bukan pembatasan (juga dengan $IFS, yang dikenal POSIX ketidaksesuaian dari zsh). Ini berbeda dari JavaScript split()karena string kosong dipecah menjadi elemen 0 (bukan 1).

Perbedaan penting dengan $IFS-sitting adalah bahwa ${(s:abc:)string}splits pada abcstring, sedangkan dengan IFS=abc, yang akan terpecah a, batau c.

Dengan zshdan ksh93, perlakuan khusus yang diterima ruang, tab, atau baris baru dapat dihapus dengan menggandakannya $IFS.

Sebagai catatan bersejarah, cangkang Bourne (leluhur atau cangkang POSIX modern) selalu menghilangkan elemen-elemen yang kosong. Itu juga memiliki sejumlah bug yang terkait dengan pemisahan dan perluasan $ @ dengan nilai-nilai non-default $IFS. Misalnya IFS=_; set -f; set -- $@tidak akan setara dengan IFS=_; set -f; set -- $1 $2 $3....

Berpisah pada regexps

Sekarang untuk sesuatu yang lebih dekat dengan JavaScript split()yang dapat terpecah pada ekspresi reguler, Anda harus bergantung pada utilitas eksternal.

Dalam peti alat POSIX, awkmemiliki splitoperator yang dapat membagi pada ekspresi reguler yang diperluas (yang lebih atau kurang merupakan subset dari ekspresi reguler Perl-seperti yang didukung oleh JavaScript).

split() {
  awk -v q="'" '
    function quote(s) {
      gsub(q, q "\\" q q, s)
      return q s q
    }
    BEGIN {
      n = split(ARGV[1], a, ARGV[2])
      for (i = 1; i <= n; i++) printf " %s", quote(a[i])
      exit
    }' "$@"
}
string=a__b_+c
eval "array=($(split "$string" '[_+]+'))"

The zshshell memiliki builtin dukungan untuk ekspresi Perl-kompatibel reguler (di-nya zsh/pcremodul), tetapi menggunakannya untuk membagi string, meskipun mungkin relatif rumit.

Stéphane Chazelas
sumber
Apakah ada alasan untuk perawatan khusus dengan tab, ruang dan baris baru?
cuonglm
1
@cuonglm, umumnya Anda ingin membagi kata-kata ketika pembatas yang kosong, dalam kasus pembatas non-kosong (seperti perpecahan $PATHdi :) sebaliknya, biasanya Anda ingin melestarikan elemen kosong. Perhatikan bahwa dalam cangkang Bourne, semua karakter menerima perlakuan khusus, kshmengubahnya agar hanya yang kosong (hanya spasi, tab, dan baris baru) yang diperlakukan secara khusus.
Stéphane Chazelas
Nah, catatan shell Bourne yang baru ditambahkan mengejutkan saya. Dan untuk penyelesaian, haruskah Anda menambahkan catatan untuk zshperawatan dengan string berisi 2 karakter atau lebih ${(s:string:)var}? Jika ditambahkan, saya dapat menghapus jawaban saya :)
cuonglm
1
Apa yang Anda maksud dengan "Perhatikan juga bahwa S dalam $ IFS adalah untuk Pembatas, bukan Pemisah."? Saya mengerti mekanika dan mengabaikan separator, tetapi Ssingkatan dari Separator , bukan pembatas . Paling tidak, itulah yang dikatakan manual bash saya.
terdon
@terdon, $IFSberasal dari shell Bourne di mana ia pemisah , ksh mengubah perilaku tanpa mengubah nama. Saya menyebutkan bahwa untuk menekankan bahwa split+glob(kecuali dalam zsh atau pdksh) tidak hanya terpecah lagi.
Stéphane Chazelas
7

Ya, gunakan IFSdan atur ke _. Kemudian gunakan read -auntuk menyimpan ke dalam array ( -rmematikan ekspansi backslash). Perhatikan bahwa ini khusus untuk bash; ksh dan zsh memiliki fitur serupa dengan sintaks yang sedikit berbeda, dan sh polos tidak memiliki variabel array sama sekali.

$ r="var1_var2_var3"
$ IFS='_' read -r -a array <<< "$r"
$ for name in "${array[@]}"; do echo "+ $name"; done
+ var1
+ var2
+ var3

Dari man bash:

Baca

-a AName

Kata-kata tersebut ditugaskan untuk indeks berurutan dari variabel array aname, mulai dari 0. aname tidak disetel sebelum nilai baru ditetapkan. Argumen nama lain diabaikan.

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 `` ''.

Perhatikan bahwa readberhenti di baris baru pertama. Lewati -d ''untuk readmenghindari itu, tetapi dalam hal itu, akan ada baris baru tambahan pada akhirnya karena <<<operator. Anda dapat menghapusnya secara manual:

IFS='_' read -r -d '' -a array <<< "$r"
array[$((${#array[@]}-1))]=${array[$((${#array[@]}-1))]%?}
fedorqui
sumber
Itu dengan asumsi $rtidak mengandung karakter baris baru atau garis miring terbalik. Perhatikan juga bahwa itu hanya akan berfungsi di versi bashshell terbaru.
Stéphane Chazelas
@ StéphaneChazelas poin bagus. Ya, ini adalah kasus "dasar" dari sebuah string. Selebihnya, semua orang harus mencari jawaban komprehensif Anda. Mengenai versi bash, read -adiperkenalkan di bash 4, kan?
fedorqui
1
maaf saya buruk, saya pikir <<<hanya ditambahkan baru-baru ini bashtetapi tampaknya sudah ada sejak 2.05b (2002). read -abahkan lebih tua dari itu. <<<berasal dari zshdan didukung oleh ksh93(dan mksh dan yash) juga tetapi read -aspesifik bash (ada -Adi ksh93, yash dan zsh).
Stéphane Chazelas
@ StéphaneChazelas apakah ada cara "mudah" untuk menemukan ketika perubahan ini terjadi? Saya katakan "mudah" untuk tidak menggali ke dalam file rilis, mungkin halaman yang menunjukkan semuanya.
fedorqui
1
Saya melihat log perubahan untuk itu. zsh juga memiliki repositori git dengan histori sejauh 3.1.5 dan milisnya digunakan untuk melacak perubahan juga.
Stéphane Chazelas