Kodeloka
  1. Kodeloka
  2. Kelas Linux & Shell
  3. Materi

3 · Shell · Modul 6 dari 8

Menulis Skrip Bash yang Tidak Berbahaya

Skrip otomatisasi sering berjalan sebagai root di produksi — beberapa kebiasaan sederhana mencegahnya menjadi bencana.

Kepala Skrip yang Wajib

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
OpsiEfek
-eBerhenti begitu ada perintah yang gagal
-uError bila memakai variabel yang belum diset
-o pipefailPipeline dianggap gagal bila bagian mana pun gagal
🚫 Tanpa ini, skrip melanjutkan setelah kegagalan
#!/bin/bash
cd /srv/aplikasi        # ← bila direktori ini tidak ada, cd gagal
rm -rf ./data/*         # ← tapi skrip TETAP LANJUT, menghapus di direktori
                        #    tempat skrip dijalankan. Bisa jadi $HOME.

Ini bukan skenario teoretis — pola persis ini pernah menghapus data produksi di banyak organisasi. set -e menghentikan skrip pada baris cd, dan -u menangkap variabel kosong yang menjadi penyebab umum lainnya.

Tanda Kutip: Sumber Bug Nomor Satu

# ❌ Nama berkas berspasi akan pecah menjadi beberapa argumen
for f in $(ls); do rm $f; done

# ✅
for f in *; do rm -- "$f"; done

# ❌ Variabel kosong membuat perintah menjadi berbahaya
rm -rf $DIR/*          # bila DIR kosong → rm -rf /*

# ✅ Selalu kutip, dan beri nilai bawaan
rm -rf "${DIR:?DIR wajib diisi}"/*
📌 Aturan praktis

Kutip setiap ekspansi variabel kecuali Anda punya alasan spesifik untuk tidak melakukannya. "$var", "$@", "${arr[@]}". Sintaks ${VAR:?pesan} membuat skrip berhenti dengan pesan jelas bila variabel kosong — jauh lebih baik daripada menjalankan perintah destruktif dengan path kosong.

Kerangka Skrip yang Baik

#!/usr/bin/env bash
set -euo pipefail

readonly SKRIP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly LOG_TAG="pencadangan"

log()   { printf '[%s] %s\n' "$(date -Is)" "$*" >&2; }
gagal() { log "GAGAL: $*"; exit 1; }

bersihkan() {
    local kode=$?
    [[ -n "${TMPDIR_SAYA:-}" ]] && rm -rf "$TMPDIR_SAYA"
    exit "$kode"
}
trap bersihkan EXIT

periksa_syarat() {
    command -v pg_dump >/dev/null || gagal "pg_dump tidak ditemukan"
    [[ -n "${DATABASE_URL:-}" ]] || gagal "DATABASE_URL belum diset"
}

utama() {
    periksa_syarat
    TMPDIR_SAYA="$(mktemp -d)"
    log "mulai mencadangkan"

    pg_dump "$DATABASE_URL" > "$TMPDIR_SAYA/dump.sql" \
        || gagal "pg_dump gagal"

    log "selesai: $(du -h "$TMPDIR_SAYA/dump.sql" | cut -f1)"
}

utama "$@"
💡 trap ... EXIT menjamin pembersihan

Fungsi yang dipasang lewat trap EXIT berjalan apa pun cara skrip berakhir — selesai normal, gagal karena set -e, atau dihentikan sinyal. Ini cara paling andal memastikan direktori sementara terhapus dan kunci dilepas.

Perbandingan & Kondisi

# Gunakan [[ ]], bukan [ ]
[[ -f "$berkas" ]]        # berkas ada
[[ -d "$dir" ]]           # direktori ada
[[ -z "$var" ]]           # kosong
[[ -n "$var" ]]           # tidak kosong
[[ "$a" == "$b" ]]        # perbandingan teks
[[ "$a" =~ ^[0-9]+$ ]]    # regex
(( angka > 10 ))          # aritmetika

# Nilai bawaan
nama="${1:-bawaan}"
: "${WAJIB:?variabel ini harus diisi}"

Menjalankan Sesuatu Sekali Saja

# Mencegah dua salinan skrip berjalan bersamaan
exec 9>/var/lock/pencadangan.lock
flock -n 9 || { echo "sudah berjalan"; exit 0; }

# Alternatif
flock -n /var/lock/tugas.lock /usr/local/bin/tugas.sh
⚠️ Tugas terjadwal yang tumpang tindih

Sebuah tugas yang biasanya selesai 3 menit tetapi suatu hari melambat menjadi 20 menit akan berjalan bersamaan dengan jadwal berikutnya. Bila tugas itu menyentuh basis data atau berkas yang sama, hasilnya bisa rusak. Penguncian berkas adalah pengaman yang murah dan hampir selalu layak dipasang.

ShellCheck

shellcheck skrip.sh
📌 Jalankan di CI

ShellCheck menangkap variabel tak berkutip, perbandingan yang keliru, dan kesalahan kuoting sebelum skrip pernah dijalankan. Menjadikannya bagian pipeline CI mencegah seluruh kelas bug yang biasanya baru ditemukan saat skrip berjalan di produksi pada jam dua pagi.