Jadikan skrip kalian profesional: fungsi logging dengan timestamp dan level, output berwarna dengan ANSI escape codes yang aman untuk non-TTY, hingga spinner dan progress bar. Disertai praktik skrip lengkap dan jebakan warna yang mencemari log.

Setelah di episode 23 sebelumnya kita membahas interaksi dengan system tools & external APIs — bagaimana skrip memanggil HTTP API dengan curl, memproses JSON dengan jq, dan mengirim notifikasi — pada episode kali ini kita membahas sesuatu yang mungkin terdengar sepele tapi justru membedakan skrip amateur dari skrip production-grade: logging, colorizing output, dan terminal UX.
Bayangkan kalian mewarisi skrip dari rekan kerja yang sudah pindah perusahaan. Skripnya berjalan, tapi di dalamnya hanya ada echo "done" dan echo "error" tanpa keterangan apa pun. Ketika skrip gagal di tengah malam, tidak ada jejak yang bisa kalian telusuri: kapan gagal, di langkah mana, dan apa yang terjadi sebelumnya. Ini seperti pesawat tanpa black box — semua orang tahu ada masalah, tidak ada yang tahu penyebabnya. Logging adalah black box-nya skrip kalian.
Di sisi lain, bayangkan menjalankan skrip yang outputnya seperti dinding teks abu-abu: tidak ada yang bisa dibedakan, error tenggelam di antara seratus baris INFO. Bandingkan dengan skrip yang mencetak pesan sukses hijau, peringatan kuning, dan error merah tebal. Mata kalian langsung tertuju pada masalahnya. Warna bukan dekorasi — ia adalah alat navigasi untuk siapa pun yang membaca output, termasuk kalian sendiri enam bulan ke depan.
Dan ada satu elemen terakhir yang sering dilupakan: pengalaman saat menunggu. Skrip yang menekan tombol Enter lalu diam tanpa kabar selama dua menit membuat orang bertanya-tanya apakah masih hidup. Spinner dan progress bar menjawab pertanyaan itu. Pada episode ini, kita akan membangun ketiganya: fungsi logging yang andal, warna yang aman, dan UX terminal yang manusiawi.
Skrip yang baik mencatat apa yang terjadi, kapan, dan dengan hasil apa. Tiga elemen wajib dari sebuah baris log adalah timestamp, level, dan pesan. Timestamp menjawab "kapan", level menjawab "seberapa penting", dan pesan menjawab "apa".
Level log yang lazim digunakan:
| Level | Warna | Makna |
|---|---|---|
INFO | Hijau/Biru | Proses normal berjalan, informasi umum |
WARN | Kuning | Ada anomali, tapi skrip bisa lanjut |
ERROR | Merah | Ada kegagalan, butuh perhatian |
DEBUG | Abu-abu | Detail untuk melacak masalah |
Mari kita bangun fungsi logging yang dipakai bersama oleh semua skrip kalian. Fungsi ini membaca level lewat parameter, dan mencetak timestamp dalam format ISO-8601 yang sortable — penting karena format log harus bisa diurutkan secara leksikal (ingat pitfall timestamp dengan spasi di akhir episode ini):
log_info() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*"
}
log_warn() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] $*" >&2
}
log_error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2
}Perhatikan dua detail penting:
log_warn dan log_error mencetak ke stderr (>&2), bukan stdout. Ini bukan kebiasaan — ini disiplin Unix: data lewat stdout, diagnosa lewat stderr. Ketika output skrip dialihkan ke file (script.sh > result.txt), pesan error tidak ikut merusak file data. Pesan log info tetap di stdout.Dengan fungsi ini, skrip kalian berubah dari echo "gagal" menjadi jejak yang bisa digali: [2026-08-02 14:31:08] [ERROR] Backup gagal: disk penuh.
Skrip yang berjalan di cron (ingat episode 22 dari series Linux) tidak memiliki terminal untuk membaca outputnya. Solusinya: tulis log ke file, sambil tetap menampilkannya ke layar saat dijalankan manual. Perintah ajaibnya adalah tee -a:
LOG_FILE="/var/log/myapp/deploy.log"
mkdir -p "$(dirname "$LOG_FILE")"
log_info() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*"
echo "$msg" | tee -a "$LOG_FILE"
}tee -a menerima stdout dan menyalinnya ke dua tujuan sekaligus: layar dan file (-a = append, tidak menimpa log lama). Sekarang satu baris fungsi menghasilkan dua manfaat: operator yang menjalankan skrip langsung melihat progresnya, dan file log menyimpan jejak permanen untuk audit.
Important
Selalu gunakan mkdir -p "$(dirname "$LOG_FILE")" sebelum menulis log ke path bertingkat. tee (dan redirect >) akan gagal jika direktori induknya belum ada. Satu perintah mkdir -p di awal skrip menghindari error yang membingungkan di tengah eksekusi — dan pastikan pengguna skrip memiliki izin tulis pada direktori tersebut.
Warna di terminal Linux dihasilkan oleh ANSI escape codes — urutan karakter tak terlihat yang memberi tahu terminal cara menampilkan teks berikutnya. Polanya selalu: \033[<kode>m, dan diakhiri dengan reset \033[0m. Kode yang paling sering dipakai:
| Kode | Efek |
|---|---|
\033[0m | Reset semua gaya |
\033[1m | Tebal (bold) |
\033[31m | Merah (error) |
\033[32m | Hijau (sukses) |
\033[33m | Kuning (warn) |
\033[34m | Biru (info) |
\033[36m | Cyan |
Contoh paling sederhana:
echo -e "\033[31mGagal: koneksi ditolak\033[0m"
echo -e "\033[32mSukses: deploy selesai\033[0m"-e memberi tahu echo untuk menafsirkan escape sequence. Gaya bisa digabung dengan titik koma — \033[1;31m adalah merah tebal. Trik kecil yang menyenangkan: letakkan \033[31m sebelum teks dan \033[0m setelahnya; lupa menutup dengan reset adalah penyebab klasik seluruh output terminal berubah merah.
Agar tidak menulis kode mentah di setiap pesan, bungkus dalam fungsi warna:
RED='\033[31m'; GREEN='\033[32m'; YELLOW='\033[33m'; BLUE='\033[34m'; RESET='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${RESET} $*"; }
log_success() { echo -e "${GREEN}[OK]${RESET} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
log_error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }Sekarang skrip kalian berbicara dengan tiga bahasa sekaligus: warna untuk mata, level untuk pembaca, dan timestamp untuk arsip.
Inilah bagian paling penting di episode ini, dan yang paling sering diabaikan. Escape code bukanlah teks — ia adalah instruksi untuk terminal. Jika output skrip diarahkan ke file log atau pipa (misalnya saat dijalankan cron), escape code tersebut ikut tertulis sebagai karakter mentah: [31m akan memenuhi file log kalian dan merusak parsing.
Solusinya: hanya terapkan warna ketika stdout benar-benar terminal interaktif. Tesnya sederhana — [ -t 1 ] bernilai benar jika file descriptor 1 (stdout) terhubung ke terminal:
if [ -t 1 ]; then
RED='\033[31m'; GREEN='\033[32m'; YELLOW='\033[33m'; RESET='\033[0m'
else
RED=''; GREEN=''; YELLOW=''; RESET=''
fiDengan pola ini, skrip yang sama otomatis berwarna di terminal dan polos di log/cron. Ditambah pengecekan $TERM untuk menghormati lingkungan tanpa warna:
if [ -t 1 ] && [ "$TERM" != "dumb" ]; then
color="yes"
fiCaution
Jangan pernah mewarnai output tanpa pengecekan TTY. Skrip yang mencetak \033[31m ke dalam file log menciptakan dua masalah sekaligus: log menjadi sulit dibaca/diparsing, dan alat pengolah log (seperti grep atau monitoring) ikut kacau karena barisnya penuh karakter kontrol. Warna hanya untuk manusia di terminal; mesin dan file cukup teks polos.
Skrip yang diam membuat pengguna cemas — apalagi jika tugasnya memakan waktu menit. Solusi klasiknya adalah spinner: animasi kecil berputar di satu baris yang memberi tahu "masih bekerja". Tekniknya: cetak frame berurutan dengan printf '\r' (carriage return) yang mengembalikan kursor ke awal baris tanpa pindah ke baris baru.
spinner() {
local frames='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
local pid=$1 i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r[%s] bekerja..." "${frames:i++%${#frames}:1}"
sleep 0.1
done
printf "\r[✓] selesai \n"
}
long_task &
spinner $!Perhatikan alurnya: long_task & menjalankan tugas di latar belakang dan memberikan PID-nya; spinner berputar selama proses tersebut masih hidup (kill -0 hanya mengecek keberadaan proses, tidak membunuhnya); begitu selesai, baris diganti dengan tanda selesai. Karena spinner memakai \r, ia tidak meninggalkan jejak seratus baris animasi — hanya satu baris yang terus diperbarui.
\rVariasi lain yang lebih informatif: progress bar dengan persentase. Untuk pekerjaan dengan jumlah item yang diketahui (misalnya memproses 100 file), baris yang menunjukkan (42/100) 42% jauh lebih menenangkan daripada layar diam:
total=100
for i in $(seq 1 "$total"); do
sleep 0.05
printf "\rMemproses... %3d%%" "$((i * 100 / total))"
done
printf "\rSelesai. \n"printf "%3d%%" mencetak angka rata kanan minimal 3 digit dan tanda persen — % di printf butuh ditulis ganda (%%) agar tidak dianggap specifier. Sekali lagi, \r mengembalikan kursor, sehingga layar tidak terisi puluhan baris.
Tip
Batasi animasi hanya untuk terminal interaktif. Seperti warna, spinner dan \r adalah alat untuk layar. Jika skrip dijalankan dari cron dan outputnya ditangkap ke file, \r hanya akan menciptakan tumpukan karakter aneh. Bungkus spinner/progress dalam kondisi [ -t 1 ], sama seperti warna — pengguna cron cukup mendapat baris log biasa.
Saatnya menggabungkan semuanya menjadi satu skrip kohesif — pola ini bisa kalian salin ke semua skrip berikutnya:
#!/usr/bin/env bash
set -euo pipefail
LOG_FILE="${LOG_FILE:-/var/log/myapp/run.log}"
mkdir -p "$(dirname "$LOG_FILE")"
if [ -t 1 ] && [ "$TERM" != "dumb" ]; then
RED='\033[31m'; GREEN='\033[32m'; YELLOW='\033[33m'; BLUE='\033[34m'; RESET='\033[0m'
else
RED=''; GREEN=''; YELLOW=''; BLUE=''; RESET=''
fi
log() {
local level=$1 color=$2; shift 2
echo -e "${color}[$(date '+%Y-%m-%d %H:%M:%S')] [$level]${RESET} $*" | tee -a "$LOG_FILE"
}
info() { log INFO "$BLUE" "$@"; }
success() { log SUCCESS "$GREEN" "$@"; }
warn() { log WARN "$YELLOW" "$@" >&2; }
error() { log ERROR "$RED" "$@" >&2; }
info "Skrip dimulai"
if command -v curl >/dev/null; then
success "curl tersedia"
else
error "curl tidak terpasang"
exit 1
fiMari bedah apa yang terjadi:
echo -e tetap aman — escape kosong tidak mencetak apa-apa.log() menerima level dan warna sebagai argumen, lalu menyeragamkan format. tee -a menulis ke file dan layar sekaligus.error mengarahkan stderr (>&2) setelah pipeline tee, sehingga pesan error tetap masuk file log tapi perginya ke stderr.Hasilnya: di terminal, skrip ini hidup dengan warna; di file log dan cron, ia polos dan rapi. Inilah standar yang kalian targetkan untuk skrip produksi.
1. Escape code mencemari file log. Ini jebakan nomor satu — sudah dibahas panjang: selalu uji -t 1 sebelum mewarnai, karena cron dan redirection tidak menampilkan warna, mereka merekam karakter mentahnya.
2. Timestamp dengan spasi memecah parsing. date '+%Y-%m-%d %H:%M:%S' mudah dibaca manusia, tapi log parser yang memisah kolom dengan spasi akan terpecah. Jika log kalian akan diproses otomatis, gunakan format tanpa spasi (+%Y-%m-%dT%H:%M:%S%z) atau pastikan parser menyadari timestamp berkelompok. Pilih format konsisten sejak awal — mengganti format log di produksi adalah kerjaan yang menyakitkan.
3. Lupa reset warna (\033[0m). Satu pesan error tanpa reset membuat seluruh output setelahnya berwarna merah — termasuk prompt terminal kalian. Selalu tutup setiap gaya yang dibuka.
4. echo -e vs printf. Perilaku -e berbeda antar shell (di dash, echo -e justru mencetak literal -e). printf lebih portabel dan lebih aman untuk format kompleks — biasakan memakai printf untuk output yang butuh kontrol presisi.
5. tee -a di dalam fungsi yang dipanggil banyak kali. File log dibuka ulang setiap pemanggilan — ini normal dan efisien, tapi pastikan tee tidak digabung dengan stderr secara keliru: echo ... | tee -a file mengalihkan stdout ke tee, sehingga >&2 di akhir hanya berlaku untuk stderr.
Pada episode 24 ini, kalian telah mengubah skrip BASH dari "teks yang menjalankan perintah" menjadi "produk dengan pengalaman pengguna". Kita membangun fungsi logging berlevel (INFO, WARN, ERROR) dengan timestamp, menulis log ke layar dan file sekaligus dengan tee -a, mewarnai output dengan ANSI escape codes (\033[31m untuk merah, \033[0m untuk reset), mendeteksi TTY dengan -t 1 agar warna tidak mencemari file log, serta menambahkan spinner dan progress bar dengan printf '\r'. Kita merakit semuanya menjadi skrip template yang bisa dipakai ulang, dan menutup dengan jebakan yang paling sering merusak log produksi.
Poin kunci yang perlu kalian bawa:
[ -t 1 ] dan $TERM != dumb.\033[0m, dan hindari echo -e untuk portabilitas.tee -a memberikan jejak permanen sekaligus umpan balik langsung.Sekarang skrip kalian rapi dan enak dibaca, tetapi bagaimana kita memastikan skrip itu benar? Di episode 25 selanjutnya kita akan membahas static analysis & automated testing: menangkap bug sebelum dieksekusi dengan ShellCheck, dan membangun pengaman otomatis dengan framework pengujian Bats-core. Ini langkah terakhir sebelum episode penutup yang merakit semuanya. Sampai jumpa!