Perulangan

for dan while, serta iterasi atas berkas dan baris.

Daftar isi

Perulangan menjalankan sekumpulan perintah berkali-kali atas nilai yang berbeda atau sampai sebuah syarat berubah, sehingga pekerjaan berulang tidak perlu ditulis ulang. Topik ini membahas for untuk iterasi atas sebuah daftar, while untuk pengulangan bersyarat, serta pola membaca berkas baris demi baris yang paling sering digunakan pada skrip pemrosesan data. Ketiganya dieksekusi langsung pada folder kerja yang isinya dapat diperiksa ulang.

for atas daftar nilai#

Perulangan for mengambil setiap kata dari sebuah daftar, menyimpannya ke variabel perulangan, lalu menjalankan isi badannya untuk kata tersebut. Struktur pembukanya adalah for nama in daftar, badannya dibuka dengan do dan ditutup dengan done. Variabel perulangan berlaku seperti variabel biasa di dalam badannya.

bash
# folder kerja latihan
mkdir -p /tmp/kerja-perulangan
# menyalin tiga skrip yang digunakan topik ini
cp hitung-baris.sh hitung-mundur.sh belanja.sh /tmp/kerja-perulangan/
cd /tmp/kerja-perulangan

Skrip berikut membuat tiga berkas catatan, lalu memeriksa jumlah baris masing-masing dengan for:

bash hitung-baris.sh
#!/bin/bash
# membuat tiga berkas catatan lalu menghitung barisnya

printf 'baris satu\nbaris dua\n' > catatan-a.txt
printf 'baris satu\n' > catatan-b.txt
printf 'baris satu\nbaris dua\nbaris tiga\n' > catatan-c.txt

for berkas in catatan-a.txt catatan-b.txt catatan-c.txt; do
    echo "$(wc -l < "$berkas") baris: $berkas"
done
text Keluaran
2 baris: catatan-a.txt
1 baris: catatan-b.txt
3 baris: catatan-c.txt

Daftarnya ditulis eksplisit, yaitu tiga nama berkas yang dipisah spasi, sehingga badan perulangan berjalan tiga kali. Substitusi perintah $(wc -l < "$berkas") menghitung jumlah baris berkas saat itu juga, dan tanda kutip pada "$berkas" menjaga nama berkas tetap satu kesatuan. Tiga berkas catatan yang dibuatnya tetap tersisa di folder kerja dan digunakan pada bagian berikutnya.

for atas hasil glob dan rentang#

Daftar for tidak harus ditulis manual, dan inilah bentuk yang paling sering digunakan pada skrip shell. Pola nama berkas (glob) seperti catatan-*.txt diperluas bash menjadi seluruh nama berkas yang cocok pada folder saat ini, sehingga perulangannya otomatis mengikuti isi folder:

bash
# for atas hasil glob pada folder kerja
for berkas in catatan-*.txt; do
    echo "$berkas berisi $(wc -l < "$berkas") baris"
done
text Keluaran
catatan-a.txt berisi 2 baris
catatan-b.txt berisi 1 baris
catatan-c.txt berisi 3 baris

Ketiga nama pada daftar ini adalah hasil perluasan pola, bukan penulisan manual, sehingga skrip yang sama tetap bekerja ketika jumlah berkasnya berubah. Apabila folder kerja tidak memuat satu berkas catatan pun, pola tidak diperluas dan diteruskan apa adanya, sehingga perulangan tetap berjalan satu kali dengan nilai literal catatan-*.txt. Perilaku bawaan ini penting diingat ketika perulangan ditulis untuk folder yang mungkin kosong. Untuk deretan angka, bash menyediakan rentang brace seperti {1..3} yang diperluas menjadi 1 2 3:

bash
# rentang brace menghasilkan deretan angka
for angka in {1..3}; do
    echo "Percobaan ke-$angka"
done
text Keluaran
Percobaan ke-1
Percobaan ke-2
Percobaan ke-3

Rentang brace dinilai sebelum perulangan berjalan, sehingga cocok untuk jumlah putaran yang sudah pasti. Nilai putarannya tidak dapat diubah dari dalam badan perulangan karena daftarnya sudah diperluas sejak awal. Kombinasi glob untuk berkas dan rentang angka untuk penghitung menutup sebagian besar kebutuhan for sehari-hari.

while untuk pengulangan bersyarat#

Perulangan while menjalankan badannya selama perintah syaratnya masih berhasil, yaitu selama status keluarnya nol. Struktur ini cocok untuk pengulangan yang berhentinya ditentukan sebuah syarat, bukan oleh daftar yang sudah pasti. Penghitung pada contoh berikut dikurangi manual di dalam badan perulangannya:

bash hitung-mundur.sh
#!/bin/bash
# menghitung mundur dari tiga ke satu

sisa=3
while [ "$sisa" -gt 0 ]; do
    echo "Sisa percobaan: $sisa"
    sisa=$((sisa - 1))
done

echo "Selesai"
text Keluaran
Sisa percobaan: 3
Sisa percobaan: 2
Sisa percobaan: 1
Selesai

Syarat [ "$sisa" -gt 0 ] diperiksa ulang pada setiap putaran, sedangkan baris sisa=$((sisa - 1)) mengurangi nilainya satu per satu. Keluaran Selesai tercetak setelah perulangan berakhir karena letaknya di luar badan while. Apabila syaratnya tidak pernah berubah, perulangan tidak akan pernah berhenti, sehingga setiap while perlu memiliki jalur keluar yang jelas.

Membaca berkas baris demi baris#

Pola perulangan yang paling sering digunakan pada skrip pemrosesan data adalah while read, yaitu membaca sebuah berkas satu baris per putaran. Perintah read menarik satu baris masukan ke sebuah variabel, dan pengalihan done < berkas menyuplai isi berkas ke perulangan tersebut. Berkas berukuran besar tetap ringan diproses karena hanya satu baris yang berada di memori pada setiap putaran:

bash belanja.sh
#!/bin/bash
# membaca daftar belanja baris demi baris

printf 'teh hitam\nkopi robusta\ngula pasir\n' > daftar-belanja.txt

nomor=1
while IFS= read -r item; do
    echo "$nomor. $item"
    nomor=$((nomor + 1))
done < daftar-belanja.txt
text Keluaran
1. teh hitam
2. kopi robusta
3. gula pasir

Tulisan IFS= read -r adalah bentuk yang disarankan. IFS= mencegah read membuang spasi di awal dan akhir baris, sedangkan opsi -r membuat garis miring terbalik diperlakukan sebagai teks biasa, bukan karakter escape. Tanpa keduanya, baris yang mengandung spasi berlebih atau teks teknis dapat terbaca berbeda dari aslinya.

Pola yang sama dapat menyuplai perulangan dari aliran apa pun, bukan hanya berkas. Contoh berikut membaca dari heredoc, yaitu blok teks yang ditulis langsung di dalam perintah:

bash
# pola while read juga dapat membaca dari heredoc
while IFS= read -r baris; do
    echo "-> $baris"
done <<'EOF'
baris pertama
baris kedua
EOF
text Keluaran
-> baris pertama
-> baris kedua

Kesalahan umum#

  • Mengisi daftar for dari keluaran ls, misalnya for berkas in $(ls *.txt). Keluaran ls dipecah pada spasi sehingga nama berkas yang mengandung spasi terbelah, sedangkan glob langsung seperti for berkas in *.txt tidak punya masalah itu.
  • Lupa mengutip variabel perulangan. echo $berkas pada berkas bernama laporan bulanan.txt mencetak dua kata terpisah dan merusak perintah yang menggunakannya, sedangkan "$berkas" menjaganya tetap satu.
  • Menulis cat berkas | while read lalu mengharapkan penghitung di dalam perulangan tetap terisi sesudahnya. Pipe memindahkan perulangan ke subshell, sehingga gunakan done < berkas.
  • Mengharapkan baris terakhir terbaca padahal berkasnya tidak diakhiri baris baru. read melaporkan kegagalan sebelum isi baris terakhir terpakai sehingga baris itu terlewat, dan perbaikannya adalah menambahkan pemeriksaan seperti || [ -n "$item" ] pada syarat while.
  • Membuat while dengan syarat yang tidak pernah berubah, misalnya while true tanpa jalur keluar. Perulangan seperti ini berjalan terus sampai dihentikan paksa dengan Ctrl-C.

Ringkasan#

  • for mengulang badannya untuk setiap kata pada daftar, dan daftar itu dapat berupa penulisan manual, hasil glob, atau rentang brace.
  • while mengulang badannya selama perintah syaratnya berhasil, dan penghitungnya dikelola manual dengan penugasan aritmetika.
  • Pola while IFS= read -r dengan done < berkas adalah cara baku membaca berkas baris demi baris.
  • Kutip variabel perulangan dan hindari pemecahan keluaran ls sebagai daftar agar perulangan tidak rusak oleh nama berkas yang mengandung spasi.

Referensi#