Praktik terbaik untuk pembuatan skrip bash: panduan cepat untuk pembuatan skrip bash yang andal dan berkinerja



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:



  1. Pergi 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.



All Articles