Fungsi

Fungsi shell: argumen, nilai balik, variabel lokal, dan pemakaian ulang logika di dalam skrip.

Daftar isi

Fungsi membungkus satu blok logika menjadi satu nama yang dapat dipanggil berulang kali, sama seperti perintah pada umumnya. Topik ini membahas cara mendefinisikan fungsi shell, cara argumen diteruskan ke dalamnya, dua makna nilai balik, penggunaan variabel lokal, serta pemanfaatan fungsi untuk merapikan skrip yang berisi logika yang digunakan di banyak tempat. Bagian skrip yang berulang dapat dikemas menjadi fungsi yang mudah diperiksa.

Mendefinisikan dan memanggil fungsi#

Definisi fungsi ditulis dengan nama diikuti sepasang tanda kurung dan blok perintah di antara kurung kurawal, misalnya sapa() { perintah; }. bash juga menerima kata kunci function di depan nama, tetapi bentuk dengan tanda kurung saja lebih lazim karena dipahami oleh shell POSIX sekalipun. Fungsi dipanggil dengan menuliskan namanya seperti mengetik perintah, tanpa tanda kurung.

bash
# definisi fungsi harus berada sebelum pemanggilannya
sapa_belanja() {
    printf 'Menyiapkan daftar belanja.\n'
}

sapa_belanja
text Keluaran
Menyiapkan daftar belanja.

Definisi hanya mendaftarkan nama ke dalam shell saat itu dan tidak mencetak apa pun, sehingga keluaran muncul tepat satu kali dari baris pemanggilan. Urutan baris menentukan perilakunya, karena bash membaca skrip dari atas ke bawah dan fungsi hanya dapat dipanggil setelah definisinya dibaca. Definisi yang ditulis setelah pemanggilan menyebabkan bash menganggap namanya belum ada.

Kebiasaan yang lazim adalah menyusun seluruh definisi fungsi di bagian atas skrip, tepat setelah baris shebang dan set -euo pipefail, lalu memanggilnya di bagian bawah. Susunan ini membuat bagian bawah skrip terbaca seperti urutan langkah yang singkat, sementara rinciannya tersusun rapi di atas. Nama fungsi mengikuti aturan nama variabel, yaitu huruf, angka, dan garis bawah, dan sebaiknya menggambarkan pekerjaannya, misalnya hitung_biaya atau tulis_laporan, karena fungsi dengan nama yang jelas membuat skrip panjang tetap mudah ditelusuri.

Argumen fungsi#

Fungsi menerima argumen dengan cara yang sama seperti perintah. Di dalam blok, argumen pertama tersimpan pada $1, argumen kedua pada $2, dan seterusnya, sedangkan $# berisi jumlah argumen yang masuk. Nomor argumen mengikuti panggilan fungsi yang sedang berjalan, bukan argumen skrip, sehingga setiap pemanggilan menyuplai nilainya sendiri.

bash
# fungsi menerima argumen seperti perintah
hitung_biaya() {
    printf 'Barang %s seharga Rp%d, sebanyak %d buah\n' "$1" "$2" "$3"
}

hitung_biaya "susu formula" 8500 2
hitung_biaya "telur" 2900 12
text Keluaran
Barang susu formula seharga Rp8500, sebanyak 2 buah
Barang telur seharga Rp2900, sebanyak 12 buah

Dua pemanggilan menghasilkan keluaran yang berbeda karena setiap pemanggilan membawa argumennya sendiri. Argumen yang ditulis dengan kutip ganda tetap utuh meskipun memuat spasi, sesuai aturan quoting. Nomor argumen juga diperbarui pada setiap pemanggilan, sehingga fungsi yang sama dapat digunakan berulang kali untuk data yang berbeda tanpa menyimpan keadaan apa pun di dalamnya. Jumlah argumen yang masuk dapat diuji dengan $# untuk melindungi fungsi dari pemanggilan yang kurang lengkap, sebagaimana ditunjukkan fungsi berikut yang menuntut dua argumen.

bash
# jumlah argumen diperiksa dengan $#
perkenalkan() {
    if [[ $# -lt 2 ]]; then
        printf 'butuh nama dan kota\n'
        return 1
    fi
    printf '%s dari %s\n' "$1" "$2"
}

perkenalkan "Ali"
perkenalkan "Ali" "Bandung"
text Keluaran
butuh nama dan kota
Ali dari Bandung

Pemanggilan pertama hanya membawa satu argumen, sehingga pesan petunjuk dicetak dan return 1 meninggalkan kode keluar 1 sebagai tanda kegagalan. Pemanggilan kedua lengkap, sehingga blok setelah pemeriksaan dijalankan dan kode keluarnya 0. Pemeriksaan argumen seperti ini adalah bentuk paling sederhana dari penanganan error di dalam fungsi.

Nilai balik#

Nilai balik fungsi memiliki dua jalur yang berbeda. Jalur pertama adalah kode keluar melalui return, yaitu angka 0 sampai 255 yang menyatakan keberhasilan, dengan 0 sebagai tanda berhasil. Jalur kedua adalah data yang dicetak ke keluaran standar, yang ditangkap pemanggil melalui substitusi perintah. Fungsi berikut memperlihatkan jalur pertama untuk menguji keberadaan berkas.

bash
# siapkan berkas untuk pengujian keberadaan
printf 'catatan belanja minggu ini\n' > catatan.txt
bash
# fungsi validasi mengembalikan kode keluar
ujian_file() {
    if [[ -f "$1" ]]; then
        return 0
    fi
    return 1
}

ujian_file catatan.txt && printf 'catatan.txt ada\n'
ujian_file tidak-ada.txt || printf 'tidak-ada.txt tidak ditemukan\n'
text Keluaran
catatan.txt ada
tidak-ada.txt tidak ditemukan

Kode keluar inilah yang membuat fungsi dapat digunakan di dalam kondisi if, pasangan && dan ||, maupun di dalam perulangan, persis seperti perintah biasa. Untuk mengirim data keluar dari fungsi, fungsi mencetaknya ke keluaran standar dan pemanggil menangkapnya dengan substitusi perintah.

bash
# hasil perhitungan dikembalikan lewat keluaran standar
jumlahkan() {
    printf '%d\n' $(( $1 + $2 ))
}

hasil=$(jumlahkan 8500 2900)
printf 'total sementara: %s\n' "$hasil"
text Keluaran
total sementara: 11400

Pemisahan peran ini penting untuk dijaga, yaitu return untuk keberhasilan dan keluaran standar untuk data. Kata exit tidak boleh digunakan di dalam fungsi untuk tujuan return, karena exit mengakhiri seluruh skrip atau shell yang berjalan, bukan hanya fungsi yang memanggilnya.

Variabel lokal#

Variabel yang ditugaskan di dalam fungsi tanpa kata local menjadi variabel global, sehingga menimpa nilai yang sudah ada di luar fungsi. Perilaku ini sulit dilacak pada skrip panjang karena perubahan terjadi jauh dari tempat asalnya. Demonstrasi berikut menunjukkan penimpaan yang terjadi tanpa local.

bash
# tanpa local, variabel di dalam fungsi menimpa nilai di luar
nama="pencatat"

catat() {
    nama="fungsi"
    printf 'di dalam fungsi: %s\n' "$nama"
}

catat
printf 'di luar fungsi: %s\n' "$nama"
text Keluaran
di dalam fungsi: fungsi
di luar fungsi: fungsi

Nilai nama berubah menjadi fungsi di luar fungsi karena kedua tempat menunjuk variabel yang sama. Penambahan kata local di depan penugasan membuat variabel itu hidup hanya selama fungsi berjalan, dan nilai di luar fungsi tetap utuh.

bash
# dengan local, perubahan terbatas pada fungsi
nama="pencatat"

catat_aman() {
    local nama="fungsi"
    printf 'di dalam fungsi: %s\n' "$nama"
}

catat_aman
printf 'di luar fungsi: %s\n' "$nama"
text Keluaran
di dalam fungsi: fungsi
di luar fungsi: pencatat

Aturan kerjanya sederhana, yaitu setiap variabel yang hanya dibutuhkan di dalam fungsi ditulis dengan local. Argumen seperti $1 sudah bersifat lokal secara bawaan, sehingga dua fungsi dapat menggunakan $1 pada pemanggilan yang berbeda tanpa saling mengganggu. Kebocoran variabel global terasa ringan pada skrip pendek, tetapi menjadi sumber error yang sulit dilacak pada skrip panjang, karena nilai yang hilang atau berubah harus ditelusuri ke seluruh fungsi yang pernah berjalan. Dengan local pada setiap variabel kerja, dampak fungsi terhadap skrip terbatas pada keluaran dan kode keluarnya, dan itulah yang membuat fungsi aman dipindahkan antarskrip.

Pemakaian ulang pada skrip#

Skrip berikut merangkum peran fungsi dalam merapikan logika, yaitu menghitung rata-rata nilai dan menentukan predikatnya. Perhitungan yang panjang tersembunyi di dalam fungsi, sedangkan bagian bawah skrip terbaca seperti urutan langkah yang jelas. Fungsi hitung_rata mengembalikan hasilnya melalui keluaran standar dan mengikuti kebiasaan variabel lokal.

bash rekap-nilai.sh
#!/usr/bin/env bash
# rekap-nilai.sh: merangkum nilai ujian menjadi rata-rata dan predikat

set -euo pipefail

hitung_rata() {
    # rata-rata dihitung dari seluruh argumen angka
    local total=0
    local banyak=0
    local nilai
    for nilai in "$@"; do
        total=$((total + nilai))
        banyak=$((banyak + 1))
    done
    printf '%d\n' "$((total / banyak))"
}

tentukan_predikat() {
    if [[ $1 -ge 85 ]]; then
        printf 'A'
    elif [[ $1 -ge 70 ]]; then
        printf 'B'
    else
        printf 'C'
    fi
}

rata=$(hitung_rata 87 76 74)
predikat=$(tentukan_predikat "$rata")

printf 'Rata-rata: %d\n' "$rata"
printf 'Predikat : %s\n' "$predikat"
bash
# jalankan skrip rekap nilai
bash rekap-nilai.sh
text Keluaran
Rata-rata: 79
Predikat : B

Tiga nilai ujian diproses oleh hitung_rata yang menjumlahkannya lalu membaginya, dan hasil pembagiannya memperoleh predikat B dari tentukan_predikat. Perubahan aturan predikat cukup dilakukan di satu tempat, yaitu di dalam fungsinya, tanpa menyentuh bagian skrip yang memanggilnya. Inilah keuntungan mengemas logika ke dalam fungsi, yakni satu tempat yang jelas untuk setiap aturan.

Peran kedua fungsi dalam skrip ini sengaja dipisahkan, yaitu perhitungan dan klasifikasi, karena keduanya berubah dengan alasan yang berbeda. Aturan predikat dapat berubah mengikuti kebijakan penilaian, sedangkan cara menghitung rata-rata jarang berubah. Fungsi yang menangani satu hal dengan jelas juga mudah diuji secara terpisah, misalnya hitung_rata 100 0 yang seharusnya menghasilkan 50. Pola yang sama dapat diterapkan pada pekerjaan apa pun yang muncul di lebih dari satu tempat, sehingga fungsi menjadi unit pemakaian ulang yang paling kecil dalam skrip shell.

Kesalahan umum#

Memanggil fungsi sebelum definisinya dibaca. Bash mengeksekusi skrip dari atas ke bawah, sehingga pemanggilan yang berada di atas definisi berakhir dengan pesan command not found. Kesalahan ini mudah muncul ketika fungsi ditambahkan di akhir berkas padahal pemanggilannya sudah tertulis di atas. Susun definisi fungsi di bagian atas skrip, tepat setelah baris set -euo pipefail, sebelum baris pemanggilan pertama.

Mengira return mengirim data. return hanya menerima angka 0 sampai 255 sebagai kode keluar, sehingga return "$hasil" dengan angka besar gagal atau memberi makna yang salah. Data dikirim melalui keluaran standar dan ditangkap pemanggil dengan substitusi perintah, sebagaimana diperlihatkan oleh fungsi jumlahkan. Kata return juga hanya sah di dalam fungsi atau berkas yang dimuat melalui sourcing, dan penggunaannya di tempat lain memicu pesan error bash.

Variabel di dalam fungsi ditulis tanpa local. Tanpa local, penugasan di dalam fungsi menimpa variabel global dengan nama sama, dan efeknya muncul jauh setelah fungsi selesai. Tulis local pada setiap variabel kerja fungsi, termasuk variabel yang digunakan sebagai penghitung perulangan.

Ringkasan#

  • Fungsi didefinisikan dengan nama() { }, mendaftar pada shell saat definisi dibaca, dan dipanggil seperti perintah biasa.
  • Argumen masuk melalui $1, $2, dan $#, dengan nomor yang mengikuti panggilan fungsi masing-masing.
  • Kode keluar melalui return menyatakan keberhasilan, sedangkan data dikirim lewat keluaran standar dan ditangkap dengan substitusi perintah.
  • Kata local menjaga variabel kerja fungsi agar tidak menimpa nilai global, dan pengemasan logika ke dalam fungsi memberi satu tempat yang jelas untuk setiap aturan.

Referensi#