Wallpaper shell oleh manapi Skrip
debugging bash seperti mencari jarum di tumpukan jerami, terutama saat tambahan baru muncul di basis kode yang ada tanpa pertimbangan masalah struktur, logging, dan keandalan secara tepat waktu. Anda dapat menemukan diri Anda dalam situasi seperti itu baik karena kesalahan Anda sendiri maupun saat mengelola kumpulan skrip yang rumit.
Tim Solusi Cloud Mail.ru telahmenerjemahkan artikel dengan pedoman 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 digunakan yang berfungsi setiap saat.
Dalam artikel ini, penulis membagikan apa yang telah dia pelajari selama beberapa tahun terakhir, serta beberapa kesalahan umum yang membuatnya lengah. Ini penting karena setiap pengembang perangkat lunak, di beberapa titik dalam karier mereka, bekerja dengan skrip untuk mengotomatiskan tugas kerja rutin.
Penangan perangkap
Kebanyakan skrip bash yang saya temui tidak pernah menggunakan mekanisme pembersihan yang efisien ketika sesuatu yang tidak terduga terjadi selama eksekusi skrip.
Hal yang tidak terduga dapat muncul dari luar, misalnya menerima sinyal dari kernel. Penanganan kasus seperti itu sangat penting untuk memastikan bahwa skrip cukup kuat untuk dijalankan di sistem produksi. Saya sering menggunakan penangan keluar untuk menanggapi 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
trapAdalah perintah shell bawaan yang membantu Anda mendaftarkan fungsi pembersihan yang akan dipanggil jika ada sinyal. Namun, perhatian khusus harus diberikan dengan penangan seperti mereka SIGINTyang mengganggu skrip.
Juga, dalam banyak kasus, Anda seharusnya hanya menangkap
EXIT, tetapi idenya adalah bahwa Anda sebenarnya dapat menyesuaikan perilaku skrip untuk setiap sinyal individu.
Atur Fungsi Bawaan - Keluar Cepat saat Terjadi Kesalahan
Sangat penting untuk bereaksi terhadap kesalahan segera setelah terjadi dan menghentikan eksekusi dengan cepat. Tidak ada yang lebih buruk daripada melanjutkan perintah seperti ini:
rm -rf ${directory_name}/*
Harap dicatat bahwa variabel
directory_nametidak ditentukan.
Untuk menangani skenario seperti itu, penting untuk menggunakan fungsi
setbawaan seperti set -o errexit, set -o pipefailatau set -o nounsetdi awal skrip. Fungsi ini memastikan bahwa skrip Anda keluar segera setelah menemukan kode keluar bukan nol, variabel yang tidak ditentukan, perintah pipa yang tidak valid, 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 errexitakan keluar dari skrip segera setelah kode pengembalian "mentah" (selain nol) muncul. Oleh karena itu, lebih baik memperkenalkan penanganan kesalahan khusus seperti:
#!/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
Pembuatan skrip seperti ini memaksa Anda untuk lebih berhati-hati tentang perilaku semua perintah dalam skrip dan untuk mengantisipasi kemungkinan kesalahan yang terjadi sebelum dikejutkan.
ShellCheck untuk mendeteksi kesalahan selama pengembangan
Ada baiknya mengintegrasikan sesuatu seperti ShellCheck ke dalam pengembangan dan jalur pengujian Anda untuk memvalidasi kode bash Anda untuk praktik terbaik.
Saya menggunakannya di lingkungan pengembangan lokal saya untuk mendapatkan laporan tentang sintaksis, semantik, dan beberapa kesalahan kode yang mungkin saya lewatkan dalam pengembangan. Ini adalah alat analisis statis untuk skrip bash Anda dan saya sangat menyarankan untuk menggunakannya.
Menggunakan kode keluar Anda
Kode pengembalian POSIX tidak hanya nol atau satu, tetapi nol atau bukan nol. Gunakan fitur ini untuk mengembalikan kode kesalahan kustom (antara 201-254) untuk kasus kesalahan yang berbeda.
Informasi ini kemudian dapat digunakan oleh skrip lain yang membungkus Anda untuk memahami dengan tepat jenis kesalahan apa yang terjadi dan bereaksi sesuai:
#!/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 tentukan untuk menghindari penggantian variabel lingkungan secara tidak sengaja.
Fungsi pencatat
Logging yang bagus dan terstruktur penting untuk memudahkan memahami hasil eksekusi skrip Anda. Seperti bahasa pemrograman tingkat tinggi lainnya, saya selalu menggunakan fungsi pencatatan saya sendiri di skrip bash saya seperti
__msg_info, __msg_errordan seterusnya.
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 untuk memiliki beberapa jenis mekanisme dalam skrip saya di
__initmana variabel logger dan variabel sistem lainnya diinisialisasi atau disetel ke nilai default. Variabel ini juga dapat disetel dari parameter baris perintah selama pemanggilan skrip.
Misalnya, sesuatu seperti:
$ ./run-script.sh --debug
Ketika skrip seperti itu dijalankan, dijamin bahwa pengaturan seluruh sistem diatur ke defaultnya, jika diperlukan, atau setidaknya diinisialisasi dengan sesuatu yang sesuai, jika perlu.
Saya biasanya mendasarkan pilihan saya tentang apa yang akan diinisialisasi dan apa yang tidak menjadi trade-off antara antarmuka pengguna dan detail konfigurasi yang dapat / harus dipelajari pengguna.
Arsitektur untuk penggunaan kembali dan status 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 proyek / skrip bash baru yang ingin saya kembangkan. Apa pun yang dapat digunakan kembali dapat disimpan di repositori dan diambil di proyek lain yang ingin menggunakan fungsi ini. Organisasi proyek ini secara signifikan mengurangi ukuran skrip lain dan juga memastikan bahwa basis kode kecil dan mudah diuji.
Seperti pada contoh di atas, semua fungsi logging, seperti
__msg_info, __msg_errordan lainnya, seperti laporan oleh Slack, disimpan secara terpisah common/*dan secara dinamis terhubung ke skenario lain, seperti daily_database_operation.sh.
Tinggalkan sistem yang bersih
Jika Anda memuat beberapa sumber daya saat skrip berjalan, disarankan untuk menyimpan semua data tersebut dalam 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 disediakan di penangan kait yang dibahas di atas. Jika Anda tidak berhati-hati saat menghapus direktori sementara, direktori tersebut terakumulasi dan pada titik tertentu menyebabkan masalah tak terduga pada host, seperti disk penuh.
Menggunakan file kunci
Seringkali, Anda perlu memastikan bahwa hanya satu contoh skrip yang berjalan di host pada waktu tertentu. Ini dapat dilakukan dengan menggunakan file kunci.
Saya biasanya membuat file kunci
/tmp/project_name/*.lockdan memeriksa keberadaannya di awal skrip. Ini membantu untuk menghentikan skrip dengan benar dan menghindari perubahan status sistem yang tidak terduga oleh skrip lain yang berjalan secara paralel. File kunci tidak diperlukan jika Anda membutuhkan skrip yang sama untuk dijalankan secara paralel pada host tertentu.
Ukur dan tingkatkan
Kami sering harus bekerja dengan skrip yang berjalan dalam jangka waktu lama, seperti operasi basis data harian. Operasi semacam itu biasanya mencakup 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 dan waktu pelaksanaannya dengan:
time source "${filepath}" "${args}">> "${LOG_DIR}/RUN_LOG" 2>&1
Nanti saya bisa melihat runtime dengan:
tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"
Ini membantu saya mengidentifikasi masalah / area lambat dalam skrip yang memerlukan pengoptimalan.
Semoga berhasil!
Apa lagi yang harus dibaca: