Bagaimana cara membagi satu string menjadi beberapa string yang dipisahkan oleh setidaknya satu ruang dalam bash shell?

224

Saya memiliki string yang berisi banyak kata dengan setidaknya satu spasi di antara keduanya. Bagaimana saya bisa membagi string menjadi kata-kata individual sehingga saya bisa memutarnya?

String dilewatkan sebagai argumen. Misalnya ${2} == "cat cat file". Bagaimana saya bisa memutarnya?

Juga, bagaimana saya bisa memeriksa apakah string berisi spasi?

derrdji
sumber
1
Shell jenis apa? Bash, cmd.exe, PowerShell ...?
Alexey Sviridov
Apakah Anda hanya perlu mengulang (mis. Jalankan perintah untuk setiap kata)? Atau apakah Anda perlu menyimpan daftar kata untuk digunakan nanti?
DVK

Jawaban:

281

Apakah Anda mencoba meneruskan variabel string ke forloop? Bash, misalnya, akan terpecah pada spasi putih secara otomatis.

sentence="This is   a sentence."
for word in $sentence
do
    echo $word
done

 

This
is
a
sentence.
massa
sumber
1
@ MobRule - satu-satunya kelemahan dari ini adalah bahwa Anda tidak dapat dengan mudah menangkap (setidaknya saya tidak ingat cara) output untuk diproses lebih lanjut. Lihat solusi "tr" saya di bawah untuk sesuatu yang mengirim barang ke STDOUT
DVK
4
Anda bisa menambahkan ke sebuah variabel: A=${A}${word}).
Lucas Jones
1
set $ text [ini akan memasukkan kata-kata ke dalam $ 1, $ 2, $ 3 ... dll]
Rajesh
32
Sebenarnya trik ini bukan hanya solusi yang salah, tetapi juga sangat berbahaya karena shell globbing. touch NOPE; var='* a *'; for a in $var; do echo "[$a]"; doneoutput [NOPE] [a] [NOPE]bukan yang diharapkan [*] [a] [*](LF digantikan oleh SPC untuk dibaca).
Tino
@ob apa yang harus saya lakukan jika saya ingin membagi string berdasarkan beberapa string tertentu? contoh pemisah ".xlsx" .
296

Saya suka konversi ke array, untuk dapat mengakses elemen individual:

sentence="this is a story"
stringarray=($sentence)

sekarang Anda dapat mengakses setiap elemen secara langsung (dimulai dengan 0):

echo ${stringarray[0]}

atau konversi kembali ke string untuk mengulang:

for i in "${stringarray[@]}"
do
  :
  # do whatever on $i
done

Tentu saja perulangan melalui string langsung dijawab sebelumnya, tetapi jawaban itu memiliki kelemahan untuk tidak melacak elemen individu untuk digunakan nanti:

for i in $sentence
do
  :
  # do whatever on $i
done

Lihat juga Referensi Bash Array .

Angin kencang
sumber
26
Sayangnya tidak cukup sempurna, karena shell-globbing: touch NOPE; var='* a *'; arr=($var); set | grep ^arr=output arr=([0]="NOPE" [1]="a" [2]="NOPE")bukannya yang diharapkanarr=([0]="*" [1]="a" [2]="*")
Tino
@Tino: jika Anda tidak ingin globbing mengganggu, cukup matikan. Solusinya kemudian akan bekerja dengan baik dengan wildcard juga. Ini adalah pendekatan terbaik menurut saya.
Alexandros
3
@Alexandros Pendekatan saya hanya menggunakan pola, yang aman secara default dan bekerja dalam setiap konteks dengan sempurna. Persyaratan untuk mengganti shell-globbing untuk mendapatkan solusi yang aman lebih dari sekadar jalur yang sangat berbahaya, itu sudah merupakan sisi gelap. Jadi saran saya adalah jangan pernah terbiasa menggunakan pola seperti ini di sini, karena cepat atau lambat Anda akan melupakan beberapa detail, dan kemudian seseorang mengeksploitasi bug Anda. Anda dapat menemukan bukti untuk eksploitasi tersebut di media. Setiap. Tunggal. Hari.
Tino
86

Cukup gunakan shell "set" built-in. Sebagai contoh,

set $ text

Setelah itu, kata-kata individual dalam $ text akan menjadi $ 1, $ 2, $ 3, dll. Untuk ketahanan, biasanya satu

set - junk $ text
bergeser

untuk menangani case di mana $ text kosong atau mulai dengan tanda hubung. Sebagai contoh:

text = "Ini adalah ujian"
set - junk $ text
bergeser
untuk kata; melakukan
  gema "[$ word]"
selesai

Ini mencetak

[Ini]
[adalah]
[Sebuah]
[uji]
Idelic
sumber
5
Ini adalah cara terbaik untuk membagi var sehingga setiap bagian dapat diakses secara langsung. +1; memecahkan masalah saya
Cheekysoft
Saya akan menyarankan menggunakan awktetapi setjauh lebih mudah. Saya sekarang seorang setfanboy. Terima kasih @ Idelic!
Yzmir Ramirez
22
Perlu diketahui shell globbing jika Anda melakukan hal-hal seperti itu: touch NOPE; var='* a *'; set -- $var; for a; do echo "[$a]"; doneoutput [NOPE] [a] [NOPE]bukannya yang diharapkan [*] [a] [*]. Hanya gunakan itu jika Anda yakin 101% bahwa tidak ada metakarakter SHELL dalam string yang terpecah!
Tino
4
@Tino: Masalah itu berlaku di mana-mana, tidak hanya di sini, tetapi dalam hal ini Anda bisa set -fsebelum set -- $vardan set +fsesudahnya untuk menonaktifkan globbing.
Idelic
3
@ Idelic: Tangkapan bagus. Dengan set -fsolusi Anda juga aman. Tetapi set +fini adalah default dari masing-masing shell, jadi ini adalah detail penting, yang harus dicatat, karena yang lain mungkin tidak menyadarinya (seperti saya juga).
Tino
81

Cara yang mungkin paling mudah dan paling aman di BASH 3 dan di atas adalah:

var="string    to  split"
read -ra arr <<<"$var"

(di mana arrarray yang mengambil bagian-bagian yang terpisah dari string) atau, jika mungkin ada baris baru di input dan Anda ingin lebih dari sekadar baris pertama:

var="string    to  split"
read -ra arr -d '' <<<"$var"

(harap perhatikan spasi di -d '', tidak dapat ditinggalkan), tetapi ini mungkin memberi Anda baris baru yang tidak terduga <<<"$var"(karena ini secara implisit menambahkan LF pada akhirnya).

Contoh:

touch NOPE
var="* a  *"
read -ra arr <<<"$var"
for a in "${arr[@]}"; do echo "[$a]"; done

Output yang diharapkan

[*]
[a]
[*]

karena solusi ini (berbeda dengan semua solusi sebelumnya di sini) tidak rentan terhadap globbing shell yang tak terduga dan sering tidak terkendali.

Juga ini memberi Anda kekuatan penuh IFS seperti yang Anda inginkan:

Contoh:

IFS=: read -ra arr < <(grep "^$USER:" /etc/passwd)
for a in "${arr[@]}"; do echo "[$a]"; done

Menghasilkan sesuatu seperti:

[tino]
[x]
[1000]
[1000]
[Valentin Hilbig]
[/home/tino]
[/bin/bash]

Seperti yang Anda lihat, ruang juga dapat dipertahankan dengan cara ini:

IFS=: read -ra arr <<<' split  :   this    '
for a in "${arr[@]}"; do echo "[$a]"; done

output

[ split  ]
[   this    ]

Harap perhatikan bahwa penanganan IFSdalam BASH adalah subjek tersendiri, jadi lakukan pengujian Anda, beberapa topik menarik tentang ini:

  • unset IFS: Mengabaikan menjalankan SPC, TAB, NL dan on line dimulai dan berakhir
  • IFS='': Tidak ada pemisahan bidang, hanya membaca semuanya
  • IFS=' ': Jalankan SPC (dan hanya SPC)

Beberapa contoh terakhir

var=$'\n\nthis is\n\n\na test\n\n'
IFS=$'\n' read -ra arr -d '' <<<"$var"
i=0; for a in "${arr[@]}"; do let i++; echo "$i [$a]"; done

output

1 [this is]
2 [a test]

sementara

unset IFS
var=$'\n\nthis is\n\n\na test\n\n'
read -ra arr -d '' <<<"$var"
i=0; for a in "${arr[@]}"; do let i++; echo "$i [$a]"; done

output

1 [this]
2 [is]
3 [a]
4 [test]

BTW:

  • Jika Anda tidak $'ANSI-ESCAPED-STRING'terbiasa dengan itu, itu adalah penghemat waktu.

  • Jika Anda tidak memasukkan -r(seperti dalam read -a arr <<<"$var") maka baca apakah backslash lolos. Ini dibiarkan sebagai latihan untuk pembaca.


Untuk pertanyaan kedua:

Untuk menguji sesuatu dalam string saya biasanya tetap berpegang pada case, karena ini dapat memeriksa beberapa kasus sekaligus (catatan: case hanya mengeksekusi pertandingan pertama, jika Anda perlu fallthrough menggunakan casepernyataan multiplce ), dan kebutuhan ini cukup sering terjadi case (pun dimaksudkan):

case "$var" in
'')                empty_var;;                # variable is empty
*' '*)             have_space "$var";;        # have SPC
*[[:space:]]*)     have_whitespace "$var";;   # have whitespaces like TAB
*[^-+.,A-Za-z0-9]*) have_nonalnum "$var";;    # non-alphanum-chars found
*[-+.,]*)          have_punctuation "$var";;  # some punctuation chars found
*)                 default_case "$var";;      # if all above does not match
esac

Jadi, Anda dapat mengatur nilai balik untuk memeriksa SPC seperti ini:

case "$var" in (*' '*) true;; (*) false;; esac

Mengapa case? Karena biasanya sedikit lebih mudah dibaca daripada urutan regex, dan berkat karakter meta Shell ia menangani 99% dari semua kebutuhan dengan sangat baik.

Tino
sumber
2
Jawaban ini layak mendapat lebih banyak suara, karena masalah-masalah yang menggelembung disorot, dan kelengkapannya
Brian Agnew
@ Brian Terima kasih. Harap dicatat bahwa Anda dapat menggunakan set -fatau set -o noglobuntuk beralih globbing, sehingga karakter meta shell tidak lagi membahayakan dalam konteks ini. Tapi saya bukan benar-benar teman itu, karena ini meninggalkan banyak kekuatan shell / sangat rentan untuk beralih bolak-balik pengaturan ini.
Tino
2
Jawaban yang bagus, memang layak mendapat lebih banyak suara. Catatan samping pada kasus jatuh melalui - Anda dapat menggunakan ;&mencapainya. Tidak yakin versi bash mana yang muncul. Saya pengguna 4.3
Sergiy Kolodyazhnyy
2
@Serg, terima kasih atas perhatiannya, karena saya belum tahu ini! Jadi saya mencarinya, muncul di Bash4 . ;&adalah fallthrough yang dipaksakan tanpa pemeriksaan pola seperti di C. Dan ada juga ;;&yang terus melakukan pemeriksaan pola lebih lanjut. Begitu ;;juga if ..; then ..; else if ..dan ;;&itu seperti if ..; then ..; fi; if .., di mana ;&seperti m=false; if ..; then ..; m=:; fi; if $m || ..; then ..- seseorang tidak pernah berhenti belajar (dari orang lain);)
Tino
@Tino Itu benar sekali - belajar adalah proses yang berkelanjutan. Bahkan, saya tidak tahu ;;&sebelum Anda berkomentar: D Terima kasih, dan mungkin cangkangnya bersamamu;)
Sergiy Kolodyazhnyy
43
$ echo "This is   a sentence." | tr -s " " "\012"
This
is
a
sentence.

Untuk memeriksa spasi, gunakan grep:

$ echo "This is   a sentence." | grep " " > /dev/null
$ echo $?
0
$ echo "Thisisasentence." | grep " " > /dev/null     
$ echo $?
1
DVK
sumber
1
Dalam BASH echo "X" |biasanya dapat digantikan oleh <<<"X", seperti ini: grep -s " " <<<"This contains SPC". Anda dapat melihat perbedaannya jika Anda melakukan sesuatu yang echo X | read varberbeda dengan read var <<< X. Hanya yang terakhir mengimpor variabel varke shell saat ini, sementara untuk mengaksesnya di varian pertama Anda harus mengelompokkan seperti ini:echo X | { read var; handle "$var"; }
Tino
17

(A) Untuk membagi kalimat menjadi kata-katanya (dipisahkan spasi) Anda cukup menggunakan IFS default dengan menggunakan

array=( $string )


Contoh menjalankan cuplikan berikut

#!/bin/bash

sentence="this is the \"sentence\"   'you' want to split"
words=( $sentence )

len="${#words[@]}"
echo "words counted: $len"

printf "%s\n" "${words[@]}" ## print array

akan menampilkan

words counted: 8
this
is
the
"sentence"
'you'
want
to
split

Seperti yang Anda lihat, Anda dapat menggunakan tanda kutip tunggal atau ganda juga tanpa masalah.

Catatan:
- ini pada dasarnya sama dengan jawaban mob , tetapi dengan cara ini Anda menyimpan array untuk keperluan lebih lanjut. Jika Anda hanya memerlukan satu loop, Anda dapat menggunakan jawabannya, yang merupakan satu baris lebih pendek :)
- silakan lihat pertanyaan ini untuk metode alternatif untuk membagi string berdasarkan pembatas.


(B) Untuk memeriksa karakter dalam string, Anda juga dapat menggunakan pencocokan ekspresi reguler.
Contoh untuk memeriksa keberadaan karakter spasi yang dapat Anda gunakan:

regex='\s{1,}'
if [[ "$sentence" =~ $regex ]]
    then
        echo "Space here!";
fi
Luca Borrione
sumber
Untuk petunjuk regex (B) a +1, tetapi -1 untuk solusi yang salah (A) karena ini rawan kesalahan shell globbing. ;)
Tino
6

Untuk memeriksa ruang hanya dengan bash:

[[ "$str" = "${str% *}" ]] && echo "no spaces" || echo "has spaces"
glenn jackman
sumber
1
echo $WORDS | xargs -n1 echo

Ini menghasilkan setiap kata, Anda dapat memproses daftar itu sesuai keinginan sesudahnya.

Álex
sumber