Fungsi

Fungsi

Bitnesia • Aug 30, 2026 65 •EN

Script kompres_log.sh yang kita susun di penutup Bab 7 sudah cukup panjang untuk satu tugas tunggal. Bayangkan kalau logika pengecekan file kosong, kompresi, dan pencatatan hasil itu perlu dipakai ulang di tiga script lain: script backup harian, script rotasi log mingguan, dan script maintenance yang dijalankan manual. Menyalin-tempel blok kode yang sama ke banyak file jelas bukan solusi yang baik, karena begitu ada bug atau perlu penyesuaian, kita harus memperbaikinya satu per satu di setiap salinan. Bab ini membahas fungsi (function), blok kode yang diberi nama dan bisa dipanggil ulang kapan pun dibutuhkan, baik dari bagian lain script yang sama maupun dari script lain lewat mekanisme source yang akan dibahas lebih lanjut di Bab 26. Kita akan mulai dari cara mendefinisikan dan memanggil fungsi, lanjut ke cara mengirim argumen ke dalamnya, masuk ke perbedaan antara return value dan output fungsi yang sering membingungkan pemula, dan ditutup dengan cakupan variabel local versus global di dalam fungsi, fondasi penting supaya fungsi yang kita tulis tidak diam-diam merusak variabel di bagian lain script.

8.1 Mendefinisikan dan Memanggil Fungsi

Sub-bab ini membahas dua sintaks yang tersedia di Bash untuk mendefinisikan fungsi, serta cara memanggilnya setelah didefinisikan.

8.1.1 Sintaks Dasar Deklarasi Fungsi

Bash menyediakan dua bentuk sintaks untuk mendefinisikan fungsi, dan keduanya menghasilkan perilaku yang identik. Bentuk pertama, dan yang direkomendasikan karena sesuai standar POSIX shell, menulis nama fungsi diikuti tanda kurung kosong, lalu blok perintah di dalam kurung kurawal.

nama_fungsi() {
    perintah_pertama
    perintah_kedua
}

Bentuk kedua memakai kata kunci function sebagai penanda eksplisit, fitur khusus Bash (dan beberapa shell turunan ksh) yang tidak dikenali oleh sh POSIX murni, sama seperti status [[ ]] yang sudah dibahas di Bab 6.1.4.

function nama_fungsi {
    perintah_pertama
    perintah_kedua
}

Kedua bentuk itu boleh digabung sekaligus (function nama_fungsi()), tapi gabungan ini murni redundan tanpa manfaat tambahan, jadi sebaiknya dihindari. Di lapangan, bentuk nama_fungsi() { ... } tanpa kata kunci function lebih umum dipakai karena portable ke sh, kecuali script memang sudah dipastikan hanya berjalan di Bash dan kata kunci function dianggap membuat maksud kode lebih eksplisit dibaca. Mari kita coba definisikan fungsi pertama yang menampilkan pesan sapaan sederhana.

sapa_pengguna() {
    echo "Halo, script maintenance dimulai"
}

Secara teknis, parser Bash sebenarnya sangat longgar soal karakter yang boleh dipakai untuk nama fungsi, bahkan mengizinkan nama yang diawali angka sekalipun, jauh lebih longgar dibanding aturan penamaan variabel yang sudah dibahas di Bab 3.1. Kelonggaran ini sebaiknya tidak dimanfaatkan begitu saja demi keterbacaan dan portabilitas. Konvensi yang berlaku luas di kalangan penulis script Bash adalah tetap mengikuti aturan yang sama seperti nama variabel: hanya huruf, angka, dan garis bawah, serta tidak diawali angka, supaya nama fungsi tetap mudah dibaca dan aman jika suatu saat script perlu diadaptasi ke sh POSIX yang aturan penamaannya lebih ketat. Di lapangan, konvensi snake_case huruf kecil seperti cek_status_service paling umum dipakai supaya konsisten dengan konvensi penamaan variabel itu.

8.1.2 Memanggil Fungsi

Setelah didefinisikan, fungsi dipanggil cukup dengan menuliskan namanya seperti memanggil perintah biasa, tanpa tanda kurung dan tanpa perlu awalan khusus apa pun.

sapa_pengguna() {
    echo "Halo, script maintenance dimulai"
}

sapa_pengguna

Fungsi bisa dipanggil berkali-kali dari mana pun di dalam script, termasuk dari dalam struktur percabangan (Bab 6) atau perulangan (Bab 7) yang sudah kita pelajari, persis manfaat utama fungsi: menulis logika sekali, pakai berulang kali.

cek_service() {
    echo "Memeriksa status service..."
    systemctl is-active nginx
}

for percobaan in 1 2 3; do
    cek_service
    sleep 5
done

Untuk memastikan sebuah fungsi benar-benar sudah dikenal oleh shell aktif sebelum dipanggil, terutama pada script panjang dengan banyak fungsi, builtin type bisa dipakai memeriksanya langsung dari terminal, dan declare -f untuk menampilkan isi definisinya secara lengkap.

type sapa_pengguna
declare -f sapa_pengguna

type sapa_pengguna menampilkan bahwa sapa_pengguna adalah sebuah fungsi lengkap dengan isi definisinya jika fungsi itu sudah dikenal, atau pesan bahwa perintah itu tidak ditemukan jika belum. Cara ini berguna untuk memverifikasi sebuah fungsi memang berhasil dimuat, terutama saat fungsi didefinisikan lewat source dari file lain (Bab 26), bukan langsung di script yang sama.

8.1.3 Troubleshooting: Fungsi Harus Dikenal Sebelum Dipanggil

Bash membaca dan mengeksekusi script secara berurutan dari atas ke bawah, dan definisi fungsi hanya "didaftarkan" ke shell begitu baris definisinya benar-benar dieksekusi, bukan di awal saat script pertama kali dibuka. Akibatnya, memanggil fungsi sebelum definisinya muncul dalam alur eksekusi akan gagal.

sapa_pengguna

sapa_pengguna() {
    echo "Halo, script maintenance dimulai"
}

Script di atas menghasilkan error sapa_pengguna: command not found, karena pada saat baris pertama dieksekusi, Bash belum pernah melihat definisi fungsi sapa_pengguna sama sekali. Ini beda dengan bahasa pemrograman lain yang melakukan hoisting deklarasi fungsi. Kebiasaan yang dianjurkan di lapangan adalah meletakkan seluruh definisi fungsi di bagian atas script, tepat setelah shebang dan variabel konfigurasi, baru diikuti bagian "utama" yang memanggil fungsi-fungsi itu di bagian bawah, pola yang akan kita pakai lagi saat menyusun script terstruktur di Bab 26.

#!/bin/bash

sapa_pengguna() {
    echo "Halo, script maintenance dimulai"
}

sapa_pengguna

8.2 Parameter dan Argumen Fungsi

Sub-bab ini membahas cara mengirim data ke dalam fungsi lewat argumen, memakai mekanisme positional parameter yang sama seperti argumen script ($1, $2, dst.), pola yang sempat muncul sekilas lewat ${1:-status} di studi kasus Bab 6.4.4 dan akan dibahas lebih dalam untuk konteks script di Bab 12.

8.2.1 Mengakses Argumen dengan $1, $2, dan Seterusnya

Argumen dikirim ke fungsi persis seperti memanggil perintah biasa, dipisah spasi setelah nama fungsi, tanpa tanda kurung. Di dalam fungsi, argumen itu diakses lewat variabel $1, $2, dan seterusnya sesuai urutan posisinya.

cek_service() {
    echo "Memeriksa status service: $1"
    systemctl is-active "$1"
}

cek_service "nginx"
cek_service "mysql"

Detail penting yang wajib dipahami: $1, $2, dan seterusnya di dalam fungsi adalah positional parameter milik fungsi itu sendiri, terpisah dari positional parameter script secara keseluruhan (yaitu argumen yang dikirim saat script dipanggil dari terminal, dibahas di Bab 12). Selama fungsi sedang berjalan, nilai $1 di dalamnya merujuk ke argumen pertama fungsi, bukan argumen pertama script, meski fungsi itu dipanggil dari dalam script yang juga menerima argumennya sendiri.

tampilkan_argumen_fungsi() {
    echo "Argumen fungsi: $1"
}

echo "Argumen script: $1"
tampilkan_argumen_fungsi "nilai_khusus_fungsi"
echo "Argumen script setelah fungsi selesai: $1"

Menjalankan script di atas dengan ./contoh.sh argumen_script menampilkan Argumen script: argumen_script, lalu Argumen fungsi: nilai_khusus_fungsi, dan terakhir Argumen script setelah fungsi selesai: argumen_script. Positional parameter script tidak tertimpa maupun hilang setelah fungsi selesai dieksekusi, Bash otomatis mengembalikannya begitu fungsi selesai berjalan.

8.2.2 $@ dan $# untuk Argumen yang Fleksibel

Sama seperti pada script, fungsi juga bisa menerima jumlah argumen yang tidak ditentukan sejak awal lewat $@ (seluruh argumen sebagai list terpisah) dan $# (jumlah total argumen). Ini berguna saat fungsi perlu bekerja dengan daftar item yang panjangnya bervariasi, misalnya memeriksa banyak service sekaligus dalam satu pemanggilan.

cek_banyak_service() {
    echo "Total service yang diperiksa: $#"
    for service in "$@"; do
        echo "Memeriksa: $service"
        systemctl is-active "$service"
    done
}

cek_banyak_service "nginx" "mysql" "redis"

Pola for item in "$@" di atas persis pola yang sudah kita pakai untuk mengiterasi array di Bab 7.1.4, dan memang keduanya bekerja dengan alasan yang sama: "$@" yang dikutip selalu memperlakukan tiap argumen sebagai satu item terpisah, bahkan jika argumen itu sendiri mengandung spasi di dalamnya.

8.2.3 $@ vs $*: Perbedaan yang Wajib Dikutip dengan Benar

Persis seperti pembahasan word splitting di Bab 3.4 dan Bab 7.1.5, $@ dan $* tampak serupa tapi berperilaku berbeda saat dikutip di dalam tanda kutip ganda. "$@" mengembangkan setiap argumen sebagai word terpisah, sedangkan "$*" menggabungkan seluruh argumen menjadi satu string tunggal, dipisah karakter pertama dari variabel IFS (Internal Field Separator, variabel penentu pemisah kata pada shell yang sudah dibahas di Bab 7.2.3), biasanya berupa spasi.

tampilkan_argumen() {
    echo "--- Memakai \"\$@\" ---"
    for a in "$@"; do
        echo "Item: $a"
    done

    echo "--- Memakai \"\$*\" ---"
    for a in "$*"; do
        echo "Item: $a"
    done
}

tampilkan_argumen "laporan januari.txt" "laporan februari.txt"

Loop pertama menampilkan dua baris Item, satu untuk tiap argumen, sesuai yang biasanya diharapkan. Loop kedua justru hanya menampilkan satu baris Item berisi kedua argumen yang sudah bergabung jadi satu string dipisah spasi, karena "$*" memang dirancang untuk kasus yang membutuhkan seluruh argumen sebagai satu representasi string tunggal, misalnya untuk ditampilkan di pesan log. Untuk kebutuhan mengiterasi tiap argumen secara terpisah, "$@" yang dikutip adalah pilihan yang hampir selalu benar.

Bentuk (Dikutip)Hasil EkspansiPaling Cocok Dipakai untuk
"$@"Tiap argumen sebagai word terpisahIterasi argumen satu per satu, misalnya dengan for
"$*"Seluruh argumen digabung jadi satu string, dipisah IFSMenampilkan seluruh argumen sebagai satu baris teks, misalnya pesan log

8.2.4 Troubleshooting: Argumen Kosong dan Validasi Jumlah Argumen

Fungsi yang mengakses $1 tanpa memeriksa dulu apakah argumennya benar-benar dikirim akan menghasilkan string kosong, bukan error, sebuah perilaku yang bisa menimbulkan bug tersembunyi jika tidak diantisipasi.

cek_service() {
    systemctl is-active "$1"
}

cek_service

Memanggil cek_service tanpa argumen di atas membuat $1 berisi string kosong, dan systemctl is-active "" dijalankan dengan argumen kosong, yang kemungkinan besar menghasilkan error dari systemctl sendiri, bukan dari Bash. Kebiasaan yang dianjurkan adalah memvalidasi jumlah argumen di baris pertama fungsi memakai $# dan operator kondisi dari Bab 6, lalu keluar lebih awal dengan pesan yang jelas jika argumen tidak lengkap.

cek_service() {
    if [ "$#" -lt 1 ]; then
        echo "Error: cek_service butuh 1 argumen (nama service)" >&2
        return 1
    fi
    systemctl is-active "$1"
}

Statement return yang dipakai di atas akan dibahas tuntas pada sub-bab berikutnya. Pola validasi argumen seperti ini sangat umum ditemukan Sysadmin pada fungsi-fungsi utilitas yang dipakai berulang kali di berbagai script, karena kegagalan lebih baik terdeteksi lebih awal dengan pesan yang jelas, daripada menyebabkan error membingungkan di tengah eksekusi.

8.3 Return Value vs Output Fungsi

Sub-bab ini membahas satu sumber kebingungan paling umum bagi pemula Bash: fungsi di Bash tidak "mengembalikan nilai" seperti fungsi pada bahasa pemrograman lain (Python, JavaScript, dan semacamnya). Ada dua mekanisme terpisah yang sering tertukar, yaitu return value lewat statement return dan output fungsi lewat echo atau printf.

8.3.1 return dan Exit Status Fungsi

Statement return mengatur exit status fungsi, konsep yang sama persis dengan exit status perintah biasa yang sudah dibahas tuntas di Bab 6.4.1, bukan mengembalikan sembarang nilai data. Sama seperti exit, angka yang dikirim return berada di rentang 0 sampai 255, dan nilai di luar rentang itu otomatis terpotong modulo 256, persis perilaku exit status pada umumnya (Bab 6.4.1). return juga hanya valid dipanggil dari dalam fungsi (atau script yang di-source, dibahas di Bab 26).

service_aktif() {
    if systemctl is-active --quiet "$1"; then
        return 0
    else
        return 1
    fi
}

if service_aktif "nginx"; then
    echo "nginx sedang berjalan"
else
    echo "nginx tidak aktif"
fi

Perhatikan bahwa fungsi service_aktif di atas langsung dipakai sebagai kondisi if, sama seperti kita menguji exit status perintah biasa semacam [ -f file ] di Bab 6. Ini karena if memang menguji exit status apa pun perintah yang dijalankannya, termasuk pemanggilan fungsi. Jika return dipanggil tanpa argumen angka, fungsi mengembalikan exit status dari perintah terakhir yang dijalankan di dalam tubuhnya, mirip perilaku default sebuah script Bash yang tidak memanggil exit secara eksplisit.

service_aktif() {
    systemctl is-active --quiet "$1"
}

Bentuk singkat di atas berperilaku identik dengan versi sebelumnya yang memakai if dan return eksplisit, karena exit status systemctl is-active --quiet sendiri sudah bernilai 0 saat service aktif dan bukan 0 saat tidak aktif, dan nilai itulah yang otomatis diteruskan sebagai exit status fungsi.

8.3.2 Mengembalikan Data lewat Command Substitution

Karena return terbatas pada angka 0 sampai 255 dan hanya cocok untuk menyatakan status sukses/gagal, fungsi yang perlu "mengembalikan" data sesungguhnya (string, hasil perhitungan, path file, dan semacamnya) memakai pola berbeda: fungsi menampilkan data itu ke stdout lewat echo atau printf, lalu kode pemanggil menangkap output itu memakai command substitution $() yang sudah dibahas di Bab 3.4.

ambil_waktu_server() {
    date "+%Y-%m-%d %H:%M:%S"
}

waktu_sekarang=$(ambil_waktu_server)
echo "Waktu server saat ini: $waktu_sekarang"

Pola ini bisa dipakai untuk data yang lebih kompleks, misalnya mengembalikan nama file hasil proses tertentu, yang kemudian bisa langsung dipakai untuk perintah lain di kode pemanggil.

buat_nama_backup() {
    local prefix="$1"
    echo "${prefix}_$(date +%Y%m%d).tar.gz"
}

nama_file=$(buat_nama_backup "app")
echo "Nama file backup: $nama_file"
tar -czf "$nama_file" /var/www/app

Satu jebakan yang wajib diperhatikan: apa pun yang ditampilkan fungsi ke stdout (termasuk lewat echo debug yang lupa dihapus) akan ikut tertangkap oleh command substitution di kode pemanggil, bukan hanya baris output yang dimaksud sebagai "hasil". Untuk pesan debug atau progres yang tidak seharusnya ikut tertangkap, arahkan ke stderr memakai redirection >&2 yang sudah dibahas di Bab 5.2 dan Bab 5.3.

buat_nama_backup() {
    local prefix="$1"
    echo "Menyiapkan nama file backup..." >&2
    echo "${prefix}_$(date +%Y%m%d).tar.gz"
}

nama_file=$(buat_nama_backup "app")
echo "Nama file backup: $nama_file"

Karena pesan Menyiapkan nama file backup... diarahkan ke stderr, ia tetap tampil di terminal saat script dijalankan, tapi tidak ikut masuk ke variabel nama_file, yang isinya tetap bersih hanya berupa nama file hasil echo terakhir.

8.3.3 Kapan Memakai return, Kapan Memakai Output

Di lapangan, aturan praktisnya sederhana: pakai return ketika fungsi hanya perlu menyatakan sukses atau gagal (dipakai sebagai kondisi if, while, atau digabung operator &&/|| dari Bab 6.4.2), dan pakai echo plus command substitution ketika fungsi perlu menghasilkan data yang akan dipakai lebih lanjut oleh kode pemanggil. Kedua pola ini juga bisa digabung dalam satu fungsi yang sama, exit status menandakan berhasil tidaknya proses, sementara output berisi data hasilnya.

AspekreturnOutput (echo/printf)
Jenis nilaiAngka 0-255 (exit status)String/data bebas apa pun
Cara diambil pemanggil$?, atau langsung sebagai kondisi if/whileCommand substitution $()
Kegunaan utamaMenyatakan sukses/gagalMengembalikan hasil pengolahan data
ambil_ip_server() {
    local hasil
    hasil=$(dig +short "$1" 2>/dev/null)

    if [ -z "$hasil" ]; then
        return 1
    fi

    echo "$hasil"
    return 0
}

if ip=$(ambil_ip_server "example.com"); then
    echo "IP ditemukan: $ip"
else
    echo "Gagal menemukan IP untuk domain tersebut" >&2
fi

Perhatikan bahwa pemanggilan ip=$(ambil_ip_server "example.com") di atas tetap bisa langsung dipakai sebagai kondisi if, karena exit status command substitution mewarisi exit status dari perintah (dalam hal ini fungsi) yang dijalankan di dalamnya. Pola gabungan status dan output seperti ini akan sering kita pakai lagi saat menyusun fungsi utilitas untuk validasi input di Bab 12 dan penanganan error yang lebih matang di Bab 25.

8.4 Local vs Global Variable dalam Fungsi

Sub-bab penutup ini membahas cakupan variabel (variable scope) di dalam fungsi Bash, topik yang sering jadi sumber bug tersembunyi jika tidak dipahami sejak awal, karena perilaku default Bash berbeda dari kebanyakan bahasa pemrograman modern.

8.4.1 Variabel Bersifat Global Secara Default

Berbeda dengan bahasa seperti Python atau JavaScript yang secara default membuat variabel di dalam fungsi bersifat lokal, Bash memperlakukan setiap variabel yang dibuat di dalam fungsi sebagai variabel global kecuali dinyatakan lain secara eksplisit. Artinya, variabel yang diisi di dalam fungsi tetap ada dan bisa diakses (bahkan tertimpa) di luar fungsi setelah fungsi itu selesai dijalankan.

hitung_total() {
    total=100
    echo "Total dihitung di dalam fungsi: $total"
}

total=5
hitung_total
echo "Total setelah fungsi dipanggil: $total"

Output baris terakhir menampilkan Total setelah fungsi dipanggil: 100, bukan 5 seperti yang mungkin diharapkan pemula yang terbiasa dengan bahasa lain. Variabel total yang diisi di dalam hitung_total menimpa begitu saja variabel total yang sudah ada di luar fungsi, karena keduanya sebenarnya adalah variabel yang sama persis, bukan dua variabel terpisah dengan nama kebetulan sama.

8.4.2 Mendeklarasikan Variabel Local

Kata kunci local, hanya bisa dipakai di dalam fungsi, mendeklarasikan sebuah variabel yang cakupannya terbatas pada fungsi itu (beserta fungsi lain yang dipanggil dari dalamnya, dibahas di sub-bab berikutnya), dan otomatis "lenyap" begitu fungsi selesai dieksekusi, tanpa mempengaruhi variabel dengan nama yang sama di luar fungsi.

hitung_total() {
    local total=100
    echo "Total dihitung di dalam fungsi: $total"
}

total=5
hitung_total
echo "Total setelah fungsi dipanggil: $total"

Setelah local ditambahkan, output baris terakhir kini menampilkan Total setelah fungsi dipanggil: 5, nilai aslinya tidak berubah sama sekali, karena total di dalam fungsi dan total di luar fungsi sekarang benar-benar dua variabel yang terpisah. local bisa dipakai bersamaan dengan pemberian nilai awal dalam satu baris seperti contoh di atas, atau dideklarasikan lebih dulu baru diisi belakangan, dan juga bisa mendeklarasikan beberapa variabel sekaligus dalam satu baris yang sama.

proses_data() {
    local nama status
    nama="$1"
    status="aktif"
    echo "$nama: $status"
}

local juga bisa digabung dengan opsi declare yang relevan, misalnya local -a untuk array lokal (Bab 4.1) atau local -i untuk variabel yang selalu dievaluasi sebagai ekspresi aritmatika (Bab 3.3), memberi kontrol tipe yang lebih ketat pada variabel yang memang hanya relevan di dalam fungsi itu.

daftar_service_gagal() {
    local -a gagal=()
    for s in "$@"; do
        if ! systemctl is-active --quiet "$s"; then
            gagal+=("$s")
        fi
    done
    echo "${gagal[@]}"
}

Praktik terbaik yang berlaku luas di lapangan, dan sangat dianjurkan diikuti sejak awal belajar: selalu deklarasikan variabel yang memang hanya dipakai di dalam fungsi sebagai local, kecuali memang ada alasan spesifik untuk membuatnya global. Kebiasaan ini mencegah fungsi secara tidak sengaja "membocorkan" atau menimpa variabel di bagian lain script, terutama pada script besar dengan banyak fungsi yang ditulis atau dimodifikasi oleh lebih dari satu orang.

8.4.3 Troubleshooting: Local dan Cakupan Dinamis

Satu detail teknis yang sering luput dari pemahaman: cakupan local di Bash bersifat dinamis (dynamic scope), bukan lexical scope seperti kebanyakan bahasa modern. Artinya, variabel local tidak hanya terbatas pada fungsi tempat ia dideklarasikan, tapi juga tetap terlihat oleh fungsi lain mana pun yang dipanggil dari dalam fungsi itu, selama pemanggilan itu masih berlangsung.

fungsi_dalam() {
    echo "Nilai pesan di fungsi_dalam: $pesan"
}

fungsi_luar() {
    local pesan="halo dari fungsi_luar"
    fungsi_dalam
}

fungsi_luar

Meski pesan dideklarasikan local di dalam fungsi_luar dan fungsi_dalam sama sekali tidak mendefinisikan variabel bernama pesan, pemanggilan fungsi_dalam di atas tetap menampilkan Nilai pesan di fungsi_dalam: halo dari fungsi_luar, karena fungsi_dalam dipanggil dari "dalam" cakupan fungsi_luar yang variabel local-nya masih aktif. Begitu fungsi_luar selesai dieksekusi sepenuhnya, variabel pesan itu baru benar-benar lenyap, dan pemanggilan fungsi_dalam secara terpisah di luar fungsi_luar tidak akan melihat variabel pesan sama sekali.

Perilaku ini di lapangan jarang jadi masalah, tapi penting dipahami saat kita menyusun banyak fungsi kecil yang saling memanggil, sesuatu yang akan makin sering kita lakukan mulai Bab 9 dan seterusnya saat fungsi dipadukan dengan pemrosesan teks, penanganan argumen, dan struktur script yang lebih besar. Kebiasaan menamai variabel local dengan nama yang cukup spesifik (misalnya local nama_service_aktif, bukan sekadar local hasil) membantu mengurangi risiko tabrakan nama yang tidak disengaja antar fungsi yang saling memanggil.