Praktik Terbaik Bash Scripting: Panduan Singkat untuk Skrip Bash yang Andal dan Berkinerja

Praktik Terbaik Bash Scripting: Panduan Singkat untuk Skrip Bash yang Andal dan Berkinerja
Wallpaper cangkang oleh manapi

Men-debug skrip bash seperti mencari jarum di tumpukan jerami, terutama ketika tambahan baru muncul di basis kode yang ada tanpa mempertimbangkan masalah struktur, logging, dan keandalan secara tepat waktu. Anda dapat menemukan diri Anda dalam situasi seperti itu baik karena kesalahan Anda sendiri atau ketika mengelola tumpukan skrip yang rumit.

Tim Solusi Cloud Mail.ru menerjemahkan artikel dengan rekomendasi yang akan membantu Anda menulis, men-debug, dan memelihara skrip Anda dengan lebih baik. Percaya atau tidak, tidak ada yang mengalahkan kepuasan menulis kode bash yang bersih dan siap pakai yang berfungsi setiap saat.

Dalam artikel tersebut, penulis membagikan apa yang telah ia pelajari selama beberapa tahun terakhir, serta beberapa kesalahan umum yang membuatnya lengah. Hal ini penting karena setiap pengembang perangkat lunak, pada suatu saat dalam kariernya, bekerja dengan skrip untuk mengotomatisasi tugas kerja rutin.

Penangan perangkap

Kebanyakan skrip bash yang saya temui tidak pernah menggunakan mekanisme pembersihan yang efektif ketika sesuatu yang tidak terduga terjadi selama eksekusi skrip.

Kejutan bisa muncul dari luar, seperti menerima sinyal dari inti. Menangani kasus seperti ini sangatlah penting untuk memastikan bahwa skrip cukup andal untuk dijalankan pada sistem produksi. Saya sering menggunakan penangan keluar untuk merespons skenario seperti ini:

function handle_exit() {
  // Add cleanup code here
  // for eg. rm -f "/tmp/${lock_file}.lock"
  // exit with an appropriate status code
}
  
// trap <HANDLER_FXN> <LIST OF SIGNALS TO TRAP>
trap handle_exit 0 SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM

trap adalah perintah bawaan shell yang membantu Anda mendaftarkan fungsi pembersihan yang dipanggil jika ada sinyal. Namun, perhatian khusus harus diberikan pada penangan seperti SIGINT, yang menyebabkan skrip dibatalkan.

Selain itu, dalam banyak kasus, Anda sebaiknya hanya menangkap EXIT, namun idenya adalah Anda sebenarnya dapat menyesuaikan perilaku skrip untuk setiap sinyal individual.

Fungsi set bawaan - penghentian cepat jika terjadi kesalahan

Sangat penting untuk merespons kesalahan segera setelah terjadi dan menghentikan eksekusi dengan cepat. Tidak ada yang lebih buruk daripada terus menjalankan perintah seperti ini:

rm -rf ${directory_name}/*

Harap dicatat bahwa variabel directory_name tidak ditentukan.

Penting untuk menggunakan fungsi bawaan untuk menangani skenario seperti itu set, Seperti set -o errexit, set -o pipefail ΠΈΠ»ΠΈ set -o nounset di awal naskah. Fungsi-fungsi ini memastikan bahwa skrip Anda akan keluar segera setelah menemukan kode keluar yang bukan nol, penggunaan variabel yang tidak ditentukan, perintah yang tidak valid melewati pipa, dan sebagainya:

#!/usr/bin/env bash

set -o errexit
set -o nounset
set -o pipefail

function print_var() {
  echo "${var_value}"
}

print_var

$ ./sample.sh
./sample.sh: line 8: var_value: unbound variable

Catatan: fungsi bawaan seperti set -o errexit, akan keluar dari skrip segera setelah ada kode pengembalian "mentah" (selain nol). Oleh karena itu lebih baik memperkenalkan penanganan kesalahan khusus, misalnya:

#!/bin/bash
error_exit() {
  line=$1
  shift 1
  echo "ERROR: non zero return code from line: $line -- $@"
  exit 1
}
a=0
let a++ || error_exit "$LINENO" "let operation returned non 0 code"
echo "you will never see me"
# run it, now we have useful debugging output
$ bash foo.sh
ERROR: non zero return code from line: 9 -- let operation returned non 0 code

Menulis skrip dengan cara ini memaksa Anda untuk lebih berhati-hati terhadap perilaku semua perintah dalam skrip dan mengantisipasi kemungkinan kesalahan sebelum hal itu mengejutkan Anda.

ShellCheck untuk mendeteksi kesalahan selama pengembangan

Ada baiknya mengintegrasikan sesuatu seperti Pemeriksaan Cangkang ke dalam jalur pengembangan dan pengujian Anda untuk memeriksa kode bash Anda terhadap praktik terbaik.

Saya menggunakannya di lingkungan pengembangan lokal saya untuk mendapatkan laporan tentang sintaksis, semantik, dan beberapa kesalahan dalam kode yang mungkin saya lewatkan saat mengembangkan. Ini adalah alat analisis statis untuk skrip bash Anda dan saya sangat merekomendasikan menggunakannya.

Menggunakan kode keluar Anda sendiri

Kode pengembalian di POSIX bukan hanya nol atau satu, tetapi nilai nol atau bukan nol. Gunakan fitur ini untuk mengembalikan kode kesalahan khusus (antara 201-254) untuk berbagai kasus kesalahan.

Informasi ini kemudian dapat digunakan oleh skrip lain yang membungkus skrip Anda untuk memahami dengan tepat jenis kesalahan apa yang terjadi dan bereaksi sesuai dengan itu:

#!/usr/bin/env bash

SUCCESS=0
FILE_NOT_FOUND=240
DOWNLOAD_FAILED=241

function read_file() {
  if ${file_not_found}; then
    return ${FILE_NOT_FOUND}
  fi
}

Catatan: harap berhati-hati dengan nama variabel yang Anda tetapkan untuk menghindari penggantian variabel lingkungan secara tidak sengaja.

Fungsi pencatatan

Pencatatan log yang indah dan terstruktur penting untuk memudahkan memahami hasil skrip Anda. Seperti bahasa pemrograman tingkat tinggi lainnya, saya selalu menggunakan fungsi logging asli di skrip bash saya, seperti __msg_info, __msg_error dan sebagainya.

Hal ini membantu menyediakan struktur logging standar dengan membuat perubahan hanya di satu tempat:

#!/usr/bin/env bash

function __msg_error() {
    [[ "${ERROR}" == "1" ]] && echo -e "[ERROR]: $*"
}

function __msg_debug() {
    [[ "${DEBUG}" == "1" ]] && echo -e "[DEBUG]: $*"
}

function __msg_info() {
    [[ "${INFO}" == "1" ]] && echo -e "[INFO]: $*"
}

__msg_error "File could not be found. Cannot proceed"

__msg_debug "Starting script execution with 276MB of available RAM"

Saya biasanya mencoba memiliki semacam mekanisme di skrip saya __init, di mana variabel logger dan variabel sistem lainnya diinisialisasi atau disetel ke nilai default. Variabel-variabel ini juga dapat diatur dari opsi baris perintah selama pemanggilan skrip.

Misalnya, sesuatu seperti:

$ ./run-script.sh --debug

Ketika skrip seperti itu dijalankan, ini memastikan bahwa pengaturan seluruh sistem disetel ke nilai default jika diperlukan, atau setidaknya diinisialisasi ke sesuatu yang sesuai jika perlu.

Saya biasanya mendasarkan pilihan apa yang harus diinisialisasi dan apa yang tidak boleh dilakukan pada trade-off antara antarmuka pengguna dan detail konfigurasi yang dapat/harus dipelajari oleh pengguna.

Arsitektur untuk penggunaan kembali dan keadaan sistem bersih

Kode modular/dapat digunakan kembali

β”œβ”€β”€ framework
β”‚   β”œβ”€β”€ common
β”‚   β”‚   β”œβ”€β”€ loggers.sh
β”‚   β”‚   β”œβ”€β”€ mail_reports.sh
β”‚   β”‚   └── slack_reports.sh
β”‚   └── daily_database_operation.sh

Saya menyimpan repositori terpisah yang dapat saya gunakan untuk menginisialisasi skrip proyek/bash baru yang ingin saya kembangkan. Segala sesuatu yang dapat digunakan kembali dapat disimpan dalam repositori dan diambil oleh proyek lain yang ingin menggunakan fungsi tersebut. Mengorganisir proyek dengan cara ini secara signifikan mengurangi ukuran skrip lain dan juga memastikan bahwa basis kodenya kecil dan mudah untuk diuji.

Seperti pada contoh di atas, semua fungsi logging seperti __msg_info, __msg_error dan lainnya, seperti laporan Slack, dimuat secara terpisah di common/* dan terhubung secara dinamis dalam skenario lain seperti daily_database_operation.sh.

Tinggalkan sistem yang bersih

Jika Anda memuat sumber daya apa pun saat skrip sedang berjalan, disarankan untuk menyimpan semua data tersebut di direktori bersama dengan nama acak, misalnya. /tmp/AlRhYbD97/*. Anda dapat menggunakan generator teks acak untuk memilih nama direktori:

rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"

Setelah pekerjaan selesai, pembersihan direktori tersebut dapat dilakukan di penangan kait yang dibahas di atas. Jika direktori sementara tidak ditangani, direktori tersebut akan terakumulasi dan pada tahap tertentu menyebabkan masalah yang tidak terduga pada host, seperti disk penuh.

Menggunakan file kunci

Seringkali Anda perlu memastikan bahwa hanya satu skrip yang berjalan di host pada waktu tertentu. Ini dapat dilakukan dengan menggunakan file kunci.

Saya biasanya membuat file kunci /tmp/project_name/*.lock dan periksa keberadaannya di awal skrip. Hal ini membantu skrip berakhir dengan baik dan menghindari perubahan tak terduga pada status sistem oleh skrip lain yang berjalan secara paralel. File kunci tidak diperlukan jika Anda memerlukan skrip yang sama untuk dieksekusi secara paralel pada host tertentu.

Ukur dan tingkatkan

Kita sering kali perlu bekerja dengan skrip yang dijalankan dalam jangka waktu lama, seperti operasi database harian. Operasi tersebut biasanya melibatkan serangkaian langkah: memuat data, memeriksa anomali, mengimpor data, mengirim laporan status, dan sebagainya.

Dalam kasus seperti itu, saya selalu mencoba memecah skrip menjadi skrip kecil yang terpisah dan melaporkan status serta waktu eksekusinya menggunakan:

time source "${filepath}" "${args}">> "${LOG_DIR}/RUN_LOG" 2>&1

Nanti saya bisa melihat waktu eksekusinya dengan:

tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"

Ini membantu saya mengidentifikasi masalah/area lambat dalam skrip yang memerlukan pengoptimalan.

Good luck!

Apa lagi yang harus dibaca:

  1. Buka dan cache GPU.
  2. Contoh aplikasi berbasis peristiwa berdasarkan webhook di penyimpanan objek S3 Mail.ru Cloud Solutions.
  3. Saluran telegram kami tentang transformasi digital.

Sumber: www.habr.com

Tambah komentar