Amalan Terbaik Skrip Bash: Panduan Pantas untuk Skrip Bash Boleh Dipercayai dan Berprestasi

Amalan Terbaik Skrip Bash: Panduan Pantas untuk Skrip Bash Boleh Dipercayai dan Berprestasi
Kertas dinding cangkang oleh manapi

Menyahpepijat skrip bash adalah seperti mencari jarum dalam timbunan jerami, terutamanya apabila penambahan baharu muncul dalam pangkalan kod sedia ada tanpa pertimbangan tepat pada masanya mengenai isu struktur, pembalakan dan kebolehpercayaan. Anda boleh mendapati diri anda dalam situasi sedemikian sama ada disebabkan oleh kesilapan anda sendiri atau semasa menguruskan timbunan skrip yang kompleks.

Pasukan Penyelesaian Awan Mail.ru menterjemah artikel dengan cadangan yang akan membantu anda menulis, nyahpepijat dan mengekalkan skrip anda dengan lebih baik. Percaya atau tidak, tiada apa yang mengalahkan kepuasan menulis kod bash yang bersih dan sedia untuk digunakan yang berfungsi setiap masa.

Dalam artikel itu, pengarang berkongsi apa yang telah dipelajarinya sejak beberapa tahun kebelakangan ini, serta beberapa kesilapan biasa yang menyebabkan dia tidak sedarkan diri. Ini penting kerana setiap pembangun perisian, pada satu ketika dalam kerjaya mereka, berfungsi dengan skrip untuk mengautomasikan tugas kerja rutin.

Pengendali perangkap

Kebanyakan skrip bash yang saya temui tidak pernah menggunakan mekanisme pembersihan yang berkesan apabila sesuatu yang tidak dijangka berlaku semasa pelaksanaan skrip.

Kejutan boleh timbul dari luar, seperti menerima isyarat dari teras. Mengendalikan kes sedemikian adalah amat penting untuk memastikan bahawa skrip cukup dipercayai untuk dijalankan pada sistem pengeluaran. Saya sering menggunakan pengendali keluar untuk bertindak balas kepada senario 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 ialah arahan terbina dalam shell yang membantu anda mendaftarkan fungsi pembersihan yang dipanggil sekiranya terdapat sebarang isyarat. Walau bagaimanapun, penjagaan khas harus diambil dengan pengendali seperti SIGINT, yang menyebabkan skrip dibatalkan.

Di samping itu, dalam kebanyakan kes anda hanya perlu menangkap EXIT, tetapi ideanya ialah anda sebenarnya boleh menyesuaikan tingkah laku skrip untuk setiap isyarat individu.

Fungsi set terbina dalam - penamatan pantas apabila ralat

Adalah sangat penting untuk bertindak balas terhadap ralat sebaik sahaja ia berlaku dan menghentikan pelaksanaan dengan cepat. Tiada yang lebih buruk daripada terus menjalankan arahan seperti ini:

rm -rf ${directory_name}/*

Sila ambil perhatian bahawa pembolehubah directory_name tidak ditentukan.

Adalah penting untuk menggunakan fungsi terbina dalam untuk mengendalikan senario sedemikian setseperti set -o errexit, set -o pipefail atau set -o nounset pada permulaan skrip. Fungsi-fungsi ini memastikan bahawa skrip anda akan keluar sebaik sahaja ia menemui sebarang kod keluar bukan sifar, penggunaan pembolehubah tidak ditentukan, arahan tidak sah yang dihantar melalui paip 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

Nota: fungsi terbina dalam seperti set -o errexit, akan keluar dari skrip sebaik sahaja terdapat kod pulangan "mentah" (selain sifar). Oleh itu adalah lebih baik untuk memperkenalkan pengendalian ralat tersuai, sebagai contoh:

#!/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 tentang kelakuan semua arahan dalam skrip dan menjangkakan kemungkinan ralat sebelum ia mengejutkan anda.

ShellCheck untuk mengesan ralat semasa pembangunan

Ia bernilai menyepadukan sesuatu seperti Pemeriksaan Shell ke dalam saluran pembangunan dan ujian anda untuk menyemak kod bash anda terhadap amalan terbaik.

Saya menggunakannya dalam persekitaran pembangunan tempatan saya untuk mendapatkan laporan tentang sintaks, semantik dan beberapa ralat dalam kod yang mungkin saya terlepas semasa membangun. Ini adalah alat analisis statik untuk skrip bash anda dan saya sangat mengesyorkan menggunakannya.

Menggunakan kod keluar anda sendiri

Kod pulangan dalam POSIX bukan hanya sifar atau satu, tetapi nilai sifar atau bukan sifar. Gunakan ciri ini untuk mengembalikan kod ralat tersuai (antara 201-254) untuk pelbagai kes ralat.

Maklumat ini kemudiannya boleh digunakan oleh skrip lain yang membungkus skrip anda untuk memahami dengan tepat jenis ralat yang berlaku dan bertindak balas dengan sewajarnya:

#!/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
}

Nota: sila berhati-hati terutamanya dengan nama pembolehubah yang anda tentukan untuk mengelakkan pembolehubah persekitaran yang tidak sengaja mengatasi.

Fungsi pembalakan

Pengelogan yang cantik dan berstruktur adalah penting untuk memahami hasil skrip anda dengan mudah. Seperti bahasa pengaturcaraan peringkat tinggi yang lain, saya sentiasa menggunakan fungsi pengelogan asli dalam skrip bash saya, seperti __msg_info, __msg_error dan sebagainya.

Ini membantu menyediakan struktur pembalakan piawai dengan membuat perubahan di satu tempat sahaja:

#!/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 cuba mempunyai beberapa jenis mekanisme dalam skrip saya __init, di mana pembolehubah logger tersebut dan pembolehubah sistem lain dimulakan atau ditetapkan kepada nilai lalai. Pembolehubah ini juga boleh ditetapkan daripada pilihan baris arahan semasa penyerahan skrip.

Sebagai contoh, sesuatu seperti:

$ ./run-script.sh --debug

Apabila skrip sedemikian dilaksanakan, ia memastikan tetapan seluruh sistem ditetapkan kepada nilai lalai jika ia diperlukan, atau sekurang-kurangnya dimulakan kepada sesuatu yang sesuai jika perlu.

Saya biasanya mendasarkan pilihan perkara yang perlu dimulakan dan perkara yang tidak boleh dilakukan pada pertukaran antara antara muka pengguna dan butiran konfigurasi yang pengguna boleh/harus menyelidikinya.

Seni bina untuk kegunaan semula dan keadaan sistem bersih

Kod modular/boleh guna semula

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

Saya menyimpan repositori berasingan yang boleh saya gunakan untuk memulakan skrip projek/bash baharu yang ingin saya bangunkan. Apa-apa sahaja yang boleh digunakan semula boleh disimpan dalam repositori dan diambil oleh projek lain yang ingin menggunakan fungsi tersebut. Mengatur projek dengan cara ini mengurangkan saiz skrip lain dengan ketara dan juga memastikan bahawa asas kod adalah kecil dan mudah untuk diuji.

Seperti contoh di atas, semua fungsi logging seperti __msg_info, __msg_error dan lain-lain, seperti laporan Slack, terkandung secara berasingan dalam common/* dan menyambung secara dinamik dalam senario lain seperti daily_database_operation.sh.

Tinggalkan sistem yang bersih

Jika anda memuatkan sebarang sumber semasa skrip berjalan, adalah disyorkan untuk menyimpan semua data tersebut dalam direktori kongsi dengan nama rawak, mis. /tmp/AlRhYbD97/*. Anda boleh menggunakan penjana teks rawak untuk memilih nama direktori:

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

Selepas selesai kerja, pembersihan direktori tersebut boleh disediakan dalam pengendali cangkuk yang dibincangkan di atas. Jika direktori sementara tidak dijaga, ia terkumpul dan pada beberapa peringkat menyebabkan masalah yang tidak dijangka pada hos, seperti cakera penuh.

Menggunakan fail kunci

Selalunya anda perlu memastikan bahawa hanya satu contoh skrip berjalan pada hos pada bila-bila masa. Ini boleh dilakukan menggunakan fail kunci.

Saya biasanya membuat fail kunci masuk /tmp/project_name/*.lock dan semak kehadiran mereka pada permulaan skrip. Ini membantu skrip ditamatkan dengan anggun dan mengelakkan perubahan yang tidak dijangka pada keadaan sistem oleh skrip lain yang dijalankan secara selari. Fail kunci tidak diperlukan jika anda memerlukan skrip yang sama untuk dilaksanakan secara selari pada hos tertentu.

Ukur dan perbaiki

Kami selalunya perlu bekerja dengan skrip yang berjalan dalam jangka masa yang lama, seperti operasi pangkalan data harian. Operasi sedemikian biasanya melibatkan urutan langkah: memuatkan data, menyemak anomali, mengimport data, menghantar laporan status dan sebagainya.

Dalam kes sedemikian, saya sentiasa cuba memecahkan skrip menjadi skrip kecil yang berasingan dan melaporkan status dan masa pelaksanaannya menggunakan:

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

Kemudian saya boleh melihat masa pelaksanaan dengan:

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

Ini membantu saya mengenal pasti kawasan masalah/lambat dalam skrip yang memerlukan pengoptimuman.

Good luck!

Apa lagi yang perlu dibaca:

  1. Pergi dan cache GPU.
  2. Contoh aplikasi dipacu peristiwa berdasarkan webhooks dalam storan objek S3 Penyelesaian Awan Mail.ru.
  3. Saluran telegram kami tentang transformasi digital.

Sumber: www.habr.com

Tambah komen