Perulangan

Perulangan

Bitnesia • Aug 30, 2026 79 •EN

Bab 6 sudah membekali kita dengan kemampuan mengambil keputusan lewat percabangan, tapi banyak pekerjaan sehari-hari Sysadmin dan User justru berulang dalam jumlah besar: memeriksa puluhan service satu per satu, mengganti nama ratusan file foto liburan, atau membaca ribuan baris log untuk mencari pola tertentu. Menulis perintah yang sama berkali-kali secara manual jelas tidak realistis. Bab ini membahas perulangan (loop), struktur yang membuat Bash mengeksekusi satu blok perintah berulang kali tanpa kita perlu menuliskannya berkali-kali. Kita akan mulai dari for loop untuk iterasi list, range angka, dan array, lanjut ke while dan until loop yang mengandalkan kondisi, masuk ke break dan continue untuk mengendalikan alur di dalam loop, dan ditutup dengan studi kasus memproses banyak file sekaligus, pola yang akan terus kita pakai di bab-bab praktik selanjutnya.

7.1 for Loop: Iterasi List, Range, dan Array

Sub-bab ini membahas for loop, bentuk perulangan yang paling sering dipakai ketika jumlah item yang akan diproses sudah diketahui atau bisa dihasilkan sebelum loop berjalan, baik itu daftar kata, rentang angka, maupun isi array.

7.1.1 Sintaks Dasar for...in

Bentuk paling dasar for loop mengambil setiap item dari sebuah list (daftar kata yang dipisah spasi) satu per satu, menampungnya ke sebuah variabel, lalu menjalankan blok perintah untuk tiap item itu.

for service in nginx mysql redis; do
    echo "Memeriksa status $service..."
    systemctl is-active "$service"
done

Kata kunci do menandai awal blok perintah, dan done menutupnya, mirip peran then dan fi pada if yang sudah dibahas di Bab 6.1.1. Variabel service pada contoh di atas berganti nilai setiap putaran, mulai dari nginx, lalu mysql, dan terakhir redis, dan blok do...done dijalankan penuh sebanyak jumlah item dalam list.

for loop juga bisa langsung menerima hasil command substitution ($(), sudah dibahas di Bab 3.4) sebagai list-nya, berguna saat daftar item sebenarnya berasal dari perintah lain, bukan ditulis manual.

for user in $(cut -d: -f1 /etc/passwd); do
    echo "User terdaftar: $user"
done

7.1.2 Iterasi Range dengan Brace Expansion dan seq

Untuk iterasi rentang angka, Bash menyediakan brace expansion, fitur shell yang mengembangkan pola {awal..akhir} menjadi deretan angka sebelum perintah benar-benar dijalankan.

for angka in {1..5}; do
    echo "Percobaan ke-$angka"
done

Brace expansion mendukung langkah kenaikan (step) lewat sintaks {awal..akhir..langkah}, tapi fitur step ini baru tersedia sejak Bash 4.0, jadi pastikan versi Bash target sudah memenuhi syarat (cek dengan bash --version, dibahas di Bab 1.3) sebelum memakainya di script yang mungkin dijalankan di lingkungan lama.

for genap in {0..10..2}; do
    echo "Angka genap: $genap"
done

Brace expansion juga bisa menghasilkan angka dengan zero-padding otomatis jika salah satu batasnya ditulis dengan angka nol di depan, berguna saat penomoran file butuh format seragam, misalnya backup_01.log sampai backup_10.log.

for nomor in {01..10}; do
    echo "backup_${nomor}.log"
done

Satu keterbatasan penting yang wajib diketahui sejak awal: brace expansion diproses shell sebelum ekspansi variabel, jadi batas rentangnya tidak bisa diisi dari sebuah variabel secara langsung.

batas=5
for i in {1..$batas}; do
    echo "$i"
done

Script di atas tidak menghasilkan rentang angka 1 sampai 5 seperti yang diharapkan. Karena brace expansion sudah selesai diproses sebelum $batas sempat diekspansi, Bash memperlakukan {1..$batas} sebagai string literal karena polanya tidak valid sebagai rentang angka, sehingga loop hanya berjalan satu kali dengan nilai i berupa teks {1..5} apa adanya. Untuk rentang yang batasnya berasal dari variabel, gunakan perintah eksternal seq yang memang dievaluasi saat runtime lewat command substitution, atau C-style for loop yang dibahas di sub-bab berikutnya.

batas=5
for i in $(seq 1 "$batas"); do
    echo "$i"
done

seq menerima satu, dua, atau tiga argumen: seq akhir, seq awal akhir, atau seq awal langkah akhir, mirip fleksibilitas brace expansion tapi bisa menerima nilai dinamis dari variabel karena dievaluasi sebagai perintah biasa, bukan sintaks yang di-expand lebih dulu oleh shell.

7.1.3 C-style for Loop

Selain bentuk for...in, Bash juga menyediakan C-style for loop, sintaks yang mirip perulangan pada bahasa C, Java, atau JavaScript, dengan tiga bagian yang dipisah titik koma di dalam tanda kurung ganda: inisialisasi, kondisi, dan langkah kenaikan.

for (( i=1; i<=5; i++ )); do
    echo "Percobaan ke-$i"
done

Ketiga bagian itu memakai arithmetic expansion yang sama seperti (( )) yang sudah disinggung di Bab 3.3 dan Bab 6.2.1, jadi variabel di dalamnya tidak perlu diawali tanda dolar ($) dan mendukung operator matematika biasa. Karena batasnya dievaluasi sebagai ekspresi aritmatika, bukan brace expansion, C-style for loop bebas memakai variabel sebagai batas rentang tanpa jebakan yang dibahas di sub-bab sebelumnya.

batas=5
for (( i=1; i<=batas; i++ )); do
    echo "$i"
done

Bentuk ini bukan bagian dari standar POSIX shell, sama seperti [[ ]] yang sudah dibahas di Bab 6.1.4, jadi hanya bisa dipakai di script yang memang khusus ditulis untuk Bash (atau shell lain yang mendukungnya seperti ksh dan zsh), bukan di script dengan shebang #!/bin/sh yang menyasar shell POSIX murni. Bagian mana pun dari ketiga bagian itu boleh dikosongkan untuk membuat loop tanpa batas, sebuah pola yang umum dipakai untuk membuat infinite loop, yaitu loop yang sengaja tidak diberi kondisi berhenti dan dikendalikan manual dengan break di dalam bloknya (dibahas di sub-bab 7.3).

for (( ; ; )); do
    echo "Loop tanpa henti, keluar manual dengan Ctrl+C"
done

7.1.4 Iterasi Array

Array yang sudah dibahas tuntas di Bab 4 sangat sering dipadukan dengan for loop untuk memproses tiap elemennya satu per satu. Pola paling aman memakai ekspansi "${nama_array[@]}" yang selalu dikutip, persis alasan yang sama seperti "$@" di Bab 5: tanpa kutip, elemen yang mengandung spasi bisa pecah menjadi beberapa argumen terpisah akibat word splitting.

daftar_service=("nginx" "mysql" "redis" "docker")

for service in "${daftar_service[@]}"; do
    echo "Memeriksa status $service..."
done

Jika indeks elemen juga dibutuhkan, misalnya untuk menampilkan nomor urut atau mengakses array lain dengan indeks yang sama, gunakan ekspansi "${!nama_array[@]}" yang menghasilkan daftar indeksnya, bukan nilainya.

daftar_service=("nginx" "mysql" "redis" "docker")

for i in "${!daftar_service[@]}"; do
    echo "Service ke-$i: ${daftar_service[$i]}"
done

Pola ini juga berlaku untuk associative array (declare -A, Bab 4.2), dengan catatan penting bahwa "${!nama_array[@]}" pada associative array menghasilkan daftar key, bukan indeks angka, dan urutannya tidak dijamin sama seperti urutan saat elemen dimasukkan.

declare -A status_server=(
    [web1]="aktif"
    [web2]="mati"
    [db1]="aktif"
)

for nama_server in "${!status_server[@]}"; do
    echo "$nama_server: ${status_server[$nama_server]}"
done

7.1.5 Troubleshooting: Word Splitting dan Glob Tanpa Match

Dua jebakan berikut ini paling sering ditemui saat memakai for loop di lapangan. Jebakan pertama masih berkaitan dengan word splitting yang sudah disinggung berkali-kali sejak Bab 3.4: memakai variabel string biasa (bukan array) yang tidak dikutip sebagai list, padahal isinya mengandung item dengan spasi di dalamnya.

daftar_file="laporan januari.txt laporan februari.txt"

for f in $daftar_file; do
    echo "Memproses: $f"
done

Loop di atas berjalan empat kali, bukan dua kali seperti yang diharapkan, karena $daftar_file yang tidak dikutip mengalami word splitting berdasarkan spasi, memecah laporan januari.txt menjadi dua item terpisah. Solusi yang tepat adalah menyimpan daftar file sebagai array sejak awal, bukan sebagai satu string panjang, persis rekomendasi yang sudah dibahas di Bab 4.4.

daftar_file=("laporan januari.txt" "laporan februari.txt")

for f in "${daftar_file[@]}"; do
    echo "Memproses: $f"
done

Jebakan kedua muncul saat memakai glob pattern (pola wildcard seperti *.log, sudah disinggung di Bab 6.3.2) sebagai list, tapi tidak ada satu pun file yang cocok dengan pola itu di direktori aktif.

for f in *.log; do
    echo "Memproses file: $f"
done

Secara default, jika sebuah glob pattern tidak menemukan file yang cocok, Bash tidak menghapusnya begitu saja, melainkan meneruskan pola itu apa adanya sebagai string literal. Akibatnya, di direktori yang tidak punya file .log sama sekali, loop di atas tetap berjalan satu kali dengan nilai f berupa teks harfiah *.log, bukan nol kali seperti yang mungkin diasumsikan banyak orang. Ini bisa berakibat fatal jika blok loop langsung memproses $f sebagai nama file yang dianggap benar-benar ada. Bash menyediakan opsi shopt -s nullglob untuk mengubah perilaku ini, membuat glob pattern yang tidak menemukan kecocokan diekspansi menjadi tidak ada item sama sekali, sehingga loop di atasnya otomatis dilewati.

shopt -s nullglob

for f in *.log; do
    echo "Memproses file: $f"
done

shopt -u nullglob

Di lapangan, kebiasaan mengaktifkan nullglob di awal script yang mengandalkan glob pattern untuk for loop, lalu menonaktifkannya kembali setelah tidak dibutuhkan, cukup umum dipakai Sysadmin untuk mencegah script salah memproses nama pola sebagai nama file sungguhan.

7.2 while dan until Loop

Berbeda dengan for yang cocok untuk daftar item yang sudah diketahui, sub-bab ini membahas while dan until, dua bentuk perulangan yang bergantung pada kondisi dan terus berjalan selama (atau sampai) kondisi itu berubah, cocok dipakai saat jumlah iterasi tidak diketahui sejak awal.

7.2.1 Sintaks Dasar while

while menjalankan blok perintah berulang kali selama kondisinya masih bernilai sukses (exit status 0, konsep yang sudah dibahas tuntas di Bab 6.4.1). Begitu kondisi bernilai gagal, loop berhenti dan eksekusi lanjut ke baris setelah done.

percobaan=1

while [ "$percobaan" -le 3 ]; do
    echo "Percobaan ke-$percobaan"
    percobaan=$((percobaan + 1))
done

Kondisi pada while memakai bentuk yang sama seperti kondisi if, boleh memakai [ ], [[ ]], (( )), atau bahkan perintah biasa yang dinilai dari exit status-nya. Pola while true atau while : (memakai builtin : yang selalu mengembalikan exit status 0 tanpa melakukan apa pun) sangat umum dipakai untuk membuat infinite loop yang sengaja dikendalikan dari dalam blok, misalnya script monitoring yang terus memeriksa status service tiap beberapa detik sampai dihentikan manual atau menemui kondisi tertentu.

while true; do
    if ! systemctl is-active --quiet nginx; then
        echo "nginx tidak aktif, mencoba restart..."
        systemctl restart nginx
    fi
    sleep 10
done

7.2.2 Sintaks Dasar until

until adalah kebalikan logis dari while: blok perintahnya dijalankan berulang kali sampai kondisinya bernilai sukses, bukan selama kondisinya sukses. Artinya, until justru terus berjalan selama kondisinya masih gagal.

percobaan=1

until [ "$percobaan" -gt 3 ]; do
    echo "Percobaan ke-$percobaan"
    percobaan=$((percobaan + 1))
done

Contoh di atas menghasilkan output yang identik dengan contoh while pada sub-bab sebelumnya, karena kondisi [ "$percobaan" -gt 3 ] pada until secara logis setara dengan kebalikan dari kondisi [ "$percobaan" -le 3 ] pada while. Di lapangan, until lebih jarang dipakai dibanding while, tapi cukup berguna untuk kasus yang secara alami lebih mudah dinyatakan sebagai "tunggu sampai kondisi X terpenuhi", misalnya menunggu sebuah service benar-benar siap menerima koneksi sebelum melanjutkan proses deploy.

until curl -s -o /dev/null "http://localhost:8080/health"; do
    echo "Service belum siap, menunggu..."
    sleep 2
done

echo "Service sudah siap menerima koneksi"

Pola menunggu seperti ini umum ditemukan Sysadmin saat menyusun script deployment yang harus memastikan satu service benar-benar aktif sebelum service berikutnya dijalankan, topik yang dibahas lebih lanjut di Bab 21.

7.2.3 Membaca File Baris per Baris dengan while read

Kombinasi while dengan builtin read (sudah disinggung di Bab 5.1) adalah cara paling umum dan paling aman untuk membaca file baris demi baris di Bash, jauh lebih andal dibanding memakai for loop yang rentan terhadap word splitting pada setiap kata dalam baris, bukan hanya setiap baris.

while IFS= read -r baris; do
    echo "Baris: $baris"
done < /etc/hosts

Dua detail pada baris while di atas sengaja ditulis untuk menghindari jebakan yang sering luput dari pemula. Opsi -r pada read mencegah backslash di dalam baris ditafsirkan sebagai karakter escape, sehingga baris seperti path Windows C:\Users\budi tetap terbaca apa adanya, bukan dianggap menyambung ke baris berikutnya. Sementara itu, mengosongkan IFS (Internal Field Separator, variabel yang menentukan pemisah kata bagi shell) sebelum read mencegah spasi atau tab di awal dan akhir tiap baris ikut terpotong, karena secara default read memakai IFS untuk membuang whitespace di kedua ujung input.

Redirection < /etc/hosts di akhir baris done mengarahkan isi file itu sebagai stdin bagi keseluruhan loop, konsep yang sudah dibahas di Bab 5.3. Pola ini bisa dipadukan dengan struktur percabangan dari Bab 6 untuk memproses baris secara selektif, misalnya melewati baris komentar pada file konfigurasi.

while IFS= read -r baris; do
    if [[ "$baris" =~ ^#.*$ || -z "$baris" ]]; then
        continue
    fi
    echo "Baris konfigurasi: $baris"
done < /etc/app/app.conf

Satu jebakan halus yang perlu diwaspadai: jika file yang dibaca tidak diakhiri karakter newline pada baris terakhirnya (kasus yang cukup umum pada file yang diedit manual atau dihasilkan tool tertentu), read tetap berhasil menampung isi baris terakhir itu ke variabel, tapi mengembalikan exit status gagal karena tidak menemukan newline penutup, sehingga baris terakhir itu tidak ikut diproses oleh loop. Idiom yang umum dipakai untuk menangani kasus ini menambahkan pengecekan tambahan dengan || (Bab 6.4.2) yang memeriksa apakah variabelnya masih terisi.

while IFS= read -r baris || [ -n "$baris" ]; do
    echo "Baris: $baris"
done < /etc/app/app.conf

7.2.4 Troubleshooting: Subshell Trap pada Pipe ke while

Jebakan yang sangat sering menjebak pemula, bahkan yang sudah cukup berpengalaman, adalah menyalurkan output sebuah perintah ke while read lewat pipe (|, Bab 5.3), lalu berharap perubahan variabel di dalam loop itu masih terbaca setelah loop selesai.

jumlah_baris=0

grep "error" /var/log/app.log | while IFS= read -r baris; do
    jumlah_baris=$((jumlah_baris + 1))
done

echo "Total baris error: $jumlah_baris"

Script di atas selalu menampilkan Total baris error: 0, berapa pun sebenarnya jumlah baris yang mengandung kata "error", karena setiap elemen dalam sebuah pipeline di Bash dijalankan di dalam subshell terpisah, yaitu proses shell anak yang salinan variabelnya terpisah dari shell utama. Perubahan nilai jumlah_baris di dalam blok while hanya berlaku di subshell milik pipeline itu, dan lenyap begitu subshell-nya selesai, sehingga variabel jumlah_baris di shell utama tetap bernilai nol seperti sebelum loop dijalankan.

Solusi paling umum dan paling portable adalah mengganti pipe dengan process substitution (<(...)), fitur Bash yang menjalankan sebuah perintah dan menyediakan outputnya sebagai jika berasal dari sebuah file, tanpa memaksa loop-nya sendiri berjalan di subshell terpisah.

jumlah_baris=0

while IFS= read -r baris; do
    jumlah_baris=$((jumlah_baris + 1))
done < <(grep "error" /var/log/app.log)

echo "Total baris error: $jumlah_baris"

Perhatikan bentuk < <(...): redirection < pertama mengarahkan stdin loop seperti biasa, sedangkan <(...) adalah process substitution yang menghasilkan sebuah path file sementara (biasanya berupa named pipe di /dev/fd/) yang isinya adalah output grep. Karena while-nya sendiri tidak lagi menjadi bagian dari pipeline, ia tetap berjalan di shell utama, sehingga perubahan jumlah_baris di dalamnya benar-benar tersimpan.

Alternatif lain sejak Bash 4.2 adalah mengaktifkan opsi shopt -s lastpipe, yang membuat perintah terakhir dalam sebuah pipeline dijalankan di shell utama, bukan subshell, asalkan job control sedang tidak aktif (kondisi yang lazim terpenuhi di script non-interaktif, tapi tidak di sesi terminal interaktif biasa).

shopt -s lastpipe

jumlah_baris=0

grep "error" /var/log/app.log | while IFS= read -r baris; do
    jumlah_baris=$((jumlah_baris + 1))
done

echo "Total baris error: $jumlah_baris"

Karena syarat lastpipe bergantung pada job control yang perilakunya bisa berbeda antara script dan sesi interaktif, process substitution tetap jadi solusi yang lebih konsisten dan lebih dianjurkan untuk dipakai di script yang portable ke berbagai konteks eksekusi.

7.3 break dan continue

Sub-bab ini membahas dua builtin yang mengendalikan alur eksekusi dari dalam sebuah loop, baik itu for, while, maupun until, tanpa harus menunggu kondisi loop itu sendiri berubah secara alami.

7.3.1 Menghentikan Loop Lebih Awal dengan break

break langsung menghentikan loop yang sedang berjalan sepenuhnya, melompat keluar ke baris pertama setelah done, tanpa memproses sisa item atau memeriksa kondisi loop lagi. Berguna saat kita sudah menemukan apa yang dicari dan tidak perlu melanjutkan iterasi yang tersisa.

daftar_server=("web1" "web2" "web3" "db1")
server_bermasalah=""

for server in "${daftar_server[@]}"; do
    if ! ping -c 1 -W 2 "$server" > /dev/null 2>&1; then
        server_bermasalah="$server"
        break
    fi
done

if [ -n "$server_bermasalah" ]; then
    echo "Server pertama yang tidak terjangkau: $server_bermasalah"
else
    echo "Semua server terjangkau"
fi

Tanpa break, loop di atas tetap akan memeriksa seluruh server sampai selesai meski server pertama yang gagal sudah cukup untuk memicu peringatan, membuang waktu eksekusi yang sebenarnya tidak perlu, terutama jika daftar server-nya panjang dan tiap percobaan koneksi punya timeout beberapa detik.

7.3.2 Melewati Iterasi dengan continue

continue tidak menghentikan keseluruhan loop, melainkan hanya melompati sisa perintah pada iterasi yang sedang berjalan, lalu langsung melanjutkan ke iterasi berikutnya (kembali memeriksa kondisi loop untuk while/until, atau mengambil item berikutnya untuk for).

for file in /var/log/*.log; do
    if [ ! -s "$file" ]; then
        echo "Lewati $file, ukurannya nol"
        continue
    fi
    echo "Memproses $file..."
    gzip -k "$file"
done

Operator -s pada contoh di atas sudah dibahas di Bab 6.2.3, memeriksa apakah ukuran file lebih besar dari nol. File log yang kosong dilewati lebih awal lewat continue, tanpa perlu membungkus sisa blok perintah pemrosesan ke dalam satu if tambahan yang membuat kode lebih menjorok dan lebih sulit dibaca. Pola "periksa kondisi yang membuat item harus dilewati di awal loop, lalu continue" ini dikenal sebagai guard clause, kebiasaan yang membantu menjaga blok utama loop tetap rata dan fokus pada logika inti.

7.3.3 break N dan continue N pada Nested Loop

Baik break maupun continue menerima argumen angka opsional, menentukan berapa banyak level loop bersarang (nested loop) yang ingin dipengaruhi, bukan hanya loop yang paling dalam. Secara default, tanpa argumen, keduanya hanya mempengaruhi satu level loop terdekat, setara dengan break 1 atau continue 1.

daftar_server=("web1" "web2")
daftar_service=("nginx" "mysql" "redis")

for server in "${daftar_server[@]}"; do
    echo "Memeriksa server: $server"
    for service in "${daftar_service[@]}"; do
        echo "  Memeriksa service: $service"
        if [ "$service" = "mysql" ] && [ "$server" = "web2" ]; then
            echo "  Kondisi kritis ditemukan, hentikan seluruh pemeriksaan"
            break 2
        fi
    done
done

echo "Pemeriksaan selesai"

break 2 pada contoh di atas menghentikan bukan hanya loop service yang paling dalam, tapi juga loop server di luarnya sekaligus, langsung melompat ke baris echo "Pemeriksaan selesai". Tanpa angka 2, break polos hanya akan menghentikan loop service, dan loop server di luarnya tetap lanjut memproses server berikutnya, perilaku yang mungkin tidak diinginkan untuk skenario "hentikan semuanya begitu menemukan masalah kritis" semacam ini.

continue N bekerja dengan logika serupa: continue 2 di dalam loop bersarang dua tingkat akan melompati sisa iterasi loop dalam sekaligus langsung melanjutkan ke iterasi berikutnya pada loop luar, bukan cuma iterasi loop dalam saja. Fitur break N dan continue N ini bagian dari standar POSIX shell, jadi aman dipakai bahkan di script yang menyasar sh atau dash, berbeda dengan beberapa fitur lain di bab ini yang khusus Bash.

7.4 Studi Kasus: Memproses Banyak File Sekaligus

Sub-bab penutup ini merangkai for, while, break, continue, beserta operator pengujian file dari Bab 6.2.3 dalam satu script kecil yang mewakili pekerjaan nyata Sysadmin: mengompresi kumpulan file log lama secara batch, lengkap dengan penanganan error dan ringkasan hasil di akhir.

7.4.1 Skenario dan Kebutuhan

Server yang sudah berjalan lama biasanya menumpuk banyak file log di direktori seperti /var/log/app/, dan Sysadmin perlu mengompresi file-file lama itu secara berkala supaya tidak menghabiskan ruang disk, pekerjaan yang biasanya dijadwalkan lewat cron (dibahas tuntas di Bab 18). Script yang akan kita susun perlu memenuhi beberapa syarat sederhana: hanya memproses file yang benar-benar regular file dan belum terkompresi, melewati file yang kosong, mencatat berapa banyak file yang berhasil dan gagal diproses, serta berhenti lebih awal jika jumlah kegagalan sudah terlalu banyak, tanda ada masalah lain yang lebih mendasar (misalnya permission direktori) yang perlu diperiksa manual dulu sebelum melanjutkan.

7.4.2 Menyusun Script Langkah demi Langkah

Kita mulai dengan menyiapkan variabel konfigurasi di bagian atas script, sebuah kebiasaan baik supaya nilai-nilai penting mudah diubah tanpa perlu menelusuri seluruh isi script. Path direktori log sengaja ditulis dengan pola ${direktori_log:-/var/log/app}, bentuk parameter expansion yang sama seperti ${1:-status} di Bab 6.4.4: jika variabel direktori_log sudah diisi dari luar (misalnya lewat environment variable saat memanggil script), nilai itu yang dipakai, dan baru jatuh ke nilai default /var/log/app jika belum diisi sama sekali. Pola ini membuat script tetap mudah diuji coba di direktori lain tanpa perlu mengubah isi script-nya.

#!/bin/bash

direktori_log="${direktori_log:-/var/log/app}"
maks_gagal=3
jumlah_berhasil=0
jumlah_dilewati=0
jumlah_gagal=0

Selanjutnya kita aktifkan nullglob (dibahas di sub-bab 7.1.5) supaya for loop tidak salah memproses pola literal *.log jika direktorinya ternyata kosong, lalu susun loop utama yang memakai continue untuk melewati file yang tidak memenuhi syarat dan break untuk menghentikan proses lebih awal saat kegagalan sudah melewati ambang batas.

shopt -s nullglob

for file_log in "$direktori_log"/*.log; do
    if [ ! -f "$file_log" ]; then
        continue
    fi

    if [ ! -s "$file_log" ]; then
        echo "Lewati $file_log: ukuran file nol"
        jumlah_dilewati=$((jumlah_dilewati + 1))
        continue
    fi

    echo "Mengompresi $file_log..."
    if gzip "$file_log"; then
        jumlah_berhasil=$((jumlah_berhasil + 1))
    else
        echo "Gagal mengompresi $file_log" >&2
        jumlah_gagal=$((jumlah_gagal + 1))

        if [ "$jumlah_gagal" -ge "$maks_gagal" ]; then
            echo "Kegagalan mencapai $maks_gagal kali, hentikan proses" >&2
            break
        fi
    fi
done

shopt -u nullglob

Terakhir, tampilkan ringkasan hasil di akhir script, informasi yang berguna baik dibaca langsung oleh Sysadmin maupun tersimpan di log hasil eksekusi cron.

echo "----------------------------------------"
echo "Ringkasan proses kompresi log:"
echo "Berhasil : $jumlah_berhasil"
echo "Dilewati : $jumlah_dilewati"
echo "Gagal    : $jumlah_gagal"

if [ "$jumlah_gagal" -gt 0 ]; then
    exit 1
fi

exit 0

7.4.3 Verifikasi dan Penanganan Error

Simpan seluruh potongan di atas sebagai satu file kompres_log.sh, beri izin eksekusi, lalu jalankan.

chmod +x kompres_log.sh
./kompres_log.sh
echo "Exit status: $?"

Jalankan lebih dulu di direktori uji coba berisi beberapa file .log palsu (termasuk satu file kosong) sebelum mengarahkannya ke direktori log sungguhan, supaya perilaku script bisa diverifikasi tanpa risiko terhadap data produksi, kebiasaan yang berlaku untuk semua script yang sifatnya destruktif atau mengubah file secara permanen (dalam kasus ini, gzip tanpa opsi -k akan menghapus file log aslinya setelah dikompresi).

mkdir -p /tmp/uji_log
touch /tmp/uji_log/app1.log /tmp/uji_log/app3.log
echo "contoh baris log" > /tmp/uji_log/app2.log

direktori_log="/tmp/uji_log" ./kompres_log.sh

Hasil yang diharapkan: app1.log dan app3.log dilewati karena kosong, sementara app2.log berhasil dikompresi menjadi app2.log.gz. Periksa ringkasan di akhir output untuk memastikan angkanya sesuai. Jika script justru melaporkan lebih banyak file gagal dari yang diharapkan, penyebab paling umum di lapangan adalah permission direktori log yang tidak mengizinkan user yang menjalankan script menulis file baru di sana, karena gzip perlu menulis file .gz hasil kompresi ke direktori yang sama. Periksa dengan operator -w dari Bab 6.2.3 sebelum loop utama dijalankan jika kasus ini sering terjadi, supaya script bisa gagal lebih awal dengan pesan yang jelas alih-alih membiarkan tiap file gagal satu per satu di tengah loop. Pola gabungan for, pengecekan file, continue untuk melewati kasus yang tidak relevan, dan break untuk berhenti lebih awal saat ada masalah sistemik seperti ini akan terus kita pakai di bab-bab berikutnya, termasuk saat menulis fungsi yang bisa dipanggil ulang di Bab 8 dan automasi tugas harian di Bab 13.