Mga Pinakamahuhusay na Kasanayan sa Bash Scripting: Isang Mabilis na Gabay sa Maaasahan at Pagganap ng Mga Bash Script

Mga Pinakamahuhusay na Kasanayan sa Bash Scripting: Isang Mabilis na Gabay sa Maaasahan at Pagganap ng Mga Bash Script
Shell wallpaper ni manapi

Ang pag-debug sa mga script ng bash ay parang naghahanap ng karayom ​​sa isang haystack, lalo na kapag ang mga bagong karagdagan ay lumalabas sa umiiral na codebase nang walang napapanahong pagsasaalang-alang sa mga isyu ng istraktura, pag-log at pagiging maaasahan. Maaari mong makita ang iyong sarili sa mga ganitong sitwasyon dahil sa iyong sariling mga pagkakamali o kapag namamahala ng mga kumplikadong tambak ng mga script.

Koponan Mail.ru Cloud Solutions isinalin ang isang artikulo na may mga rekomendasyon na makakatulong sa iyong magsulat, mag-debug at mapanatili ang iyong mga script nang mas mahusay. Maniwala ka man o hindi, walang makakatalo sa kasiyahan ng pagsulat ng malinis, handa na gamitin na bash code na gumagana sa bawat oras.

Sa artikulo, ibinahagi ng may-akda kung ano ang natutunan niya sa nakalipas na ilang taon, pati na rin ang ilang karaniwang pagkakamali na naging dahilan ng kanyang pagkalito. Mahalaga ito dahil ang bawat developer ng software, sa isang punto ng kanilang karera, ay gumagana sa mga script upang i-automate ang mga nakagawiang gawain sa trabaho.

Mga humahawak ng bitag

Karamihan sa mga script ng bash na nakatagpo ko ay hindi kailanman gumagamit ng isang epektibong mekanismo ng paglilinis kapag may nangyaring hindi inaasahan sa panahon ng pagpapatupad ng script.

Maaaring lumitaw ang mga sorpresa mula sa labas, tulad ng pagtanggap ng signal mula sa core. Ang paghawak sa mga ganitong kaso ay lubhang mahalaga upang matiyak na ang mga script ay sapat na maaasahan upang tumakbo sa mga sistema ng produksyon. Madalas akong gumagamit ng mga exit handler upang tumugon sa mga sitwasyong tulad nito:

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 ay isang shell built-in na command na tumutulong sa iyong magrehistro ng isang function ng paglilinis na tinatawag sa kaso ng anumang mga signal. Gayunpaman, ang espesyal na pangangalaga ay dapat gawin sa mga humahawak tulad ng SIGINT, na nagiging sanhi ng pag-abort ng script.

Bilang karagdagan, sa karamihan ng mga kaso dapat mo lamang mahuli EXIT, ngunit ang ideya ay maaari mong aktwal na i-customize ang pag-uugali ng script para sa bawat indibidwal na signal.

Built-in na set function - mabilis na pagwawakas sa error

Napakahalaga na tumugon sa mga error sa sandaling mangyari ang mga ito at mabilis na ihinto ang pagpapatupad. Wala nang mas masahol pa kaysa sa patuloy na pagpapatakbo ng isang utos na tulad nito:

rm -rf ${directory_name}/*

Mangyaring tandaan na ang variable directory_name hindi determinado.

Mahalagang gumamit ng mga built-in na function para pangasiwaan ang mga ganitong sitwasyon set, Tulad set -o errexit, set -o pipefail o set -o nounset sa simula ng script. Tinitiyak ng mga function na ito na lalabas ang iyong script sa sandaling makatagpo ito ng anumang non-zero exit code, paggamit ng mga hindi natukoy na variable, mga di-wastong command na ipinasa sa isang pipe, at iba pa:

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

Tandaan: built-in na mga function tulad ng set -o errexit, ay lalabas sa script sa sandaling magkaroon ng "raw" return code (maliban sa zero). Samakatuwid, mas mainam na ipakilala ang custom na paghawak ng error, halimbawa:

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

Ang pagsusulat ng mga script sa ganitong paraan ay pinipilit kang maging mas maingat tungkol sa pag-uugali ng lahat ng mga utos sa script at asahan ang posibilidad ng isang error bago ka mabigla.

ShellCheck upang makita ang mga error sa panahon ng pag-develop

Ito ay nagkakahalaga ng pagsasama ng isang bagay tulad ng Shell Check sa iyong pagbuo at pagsubok ng mga pipeline upang suriin ang iyong bash code laban sa pinakamahuhusay na kagawian.

Ginagamit ko ito sa aking mga lokal na kapaligiran sa pag-unlad upang makakuha ng mga ulat sa syntax, semantics, at ilang mga error sa code na maaaring napalampas ko habang bumubuo. Ito ay isang static na tool sa pagsusuri para sa iyong mga script ng bash at lubos kong inirerekumenda ang paggamit nito.

Gamit ang sarili mong mga exit code

Ang mga return code sa POSIX ay hindi lamang zero o isa, ngunit zero o isang non-zero na halaga. Gamitin ang mga feature na ito para ibalik ang mga custom na error code (sa pagitan ng 201-254) para sa iba't ibang kaso ng error.

Ang impormasyong ito ay maaaring gamitin ng iba pang mga script na bumabalot sa iyo upang maunawaan kung anong uri ng error ang nangyari at tumugon nang naaayon:

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

Tandaan: mangyaring mag-ingat lalo na sa mga variable na pangalan na iyong tinukoy upang maiwasan ang aksidenteng pag-override sa mga variable ng kapaligiran.

Mga function ng pag-log

Ang maganda at structured na pag-log ay mahalaga para madaling maunawaan ang mga resulta ng iyong script. Tulad ng iba pang mga high-level na programming language, palagi akong gumagamit ng mga native logging function sa aking mga bash script, tulad ng __msg_info, __msg_error at iba pa.

Nakakatulong ito na magbigay ng standardized logging structure sa pamamagitan ng paggawa ng mga pagbabago sa isang lugar lamang:

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

Karaniwan kong sinusubukan na magkaroon ng ilang uri ng mekanismo sa aking mga script __init, kung saan ang mga variable na logger at iba pang mga variable ng system ay sinisimulan o itinakda sa mga default na halaga. Ang mga variable na ito ay maaari ding itakda mula sa mga opsyon sa command line sa panahon ng script invocation.

Halimbawa, tulad ng:

$ ./run-script.sh --debug

Kapag ang naturang script ay naisakatuparan, tinitiyak nito na ang mga setting sa buong system ay nakatakda sa mga default na halaga kung kinakailangan ang mga ito, o hindi bababa sa nasimulan sa isang bagay na naaangkop kung kinakailangan.

Karaniwan kong ibinabatay ang pagpili ng kung ano ang sisimulan at kung ano ang hindi dapat gawin sa isang trade-off sa pagitan ng user interface at ang mga detalye ng mga pagsasaayos na maaari/dapat suriin ng user.

Arkitektura para sa muling paggamit at malinis na estado ng system

Modular/magagamit muli ang code

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

Nag-iingat ako ng hiwalay na repositoryo na magagamit ko para makapagsimula ng bagong project/bash script na gusto kong bumuo. Anumang bagay na maaaring magamit muli ay maaaring maimbak sa isang repositoryo at makuha ng iba pang mga proyekto na gustong gumamit ng pagpapaandar na iyon. Ang pag-aayos ng mga proyekto sa ganitong paraan ay makabuluhang binabawasan ang laki ng iba pang mga script at tinitiyak din na ang code base ay maliit at madaling subukan.

Tulad ng sa halimbawa sa itaas, lahat ng pag-andar ng pag-log tulad ng __msg_info, __msg_error at iba pa, gaya ng mga ulat ng Slack, ay hiwalay na nakapaloob sa common/* at dynamic na kumonekta sa iba pang mga sitwasyon tulad ng daily_database_operation.sh.

Mag-iwan ng malinis na sistema

Kung naglo-load ka ng anumang mga mapagkukunan habang tumatakbo ang script, inirerekomendang iimbak ang lahat ng naturang data sa isang nakabahaging direktoryo na may random na pangalan, hal. /tmp/AlRhYbD97/*. Maaari kang gumamit ng mga random na text generator para piliin ang pangalan ng direktoryo:

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

Pagkatapos makumpleto ang trabaho, ang paglilinis ng naturang mga direktoryo ay maaaring ibigay sa mga tagapangasiwa ng kawit na tinalakay sa itaas. Kung ang mga pansamantalang direktoryo ay hindi inaalagaan, sila ay nag-iipon at sa ilang yugto ay nagdudulot ng mga hindi inaasahang problema sa host, tulad ng isang buong disk.

Gamit ang mga lock file

Kadalasan kailangan mong tiyakin na isang instance lang ng isang script ang tumatakbo sa isang host sa anumang oras. Magagawa ito gamit ang mga lock file.

Karaniwan akong gumagawa ng mga lock file sa /tmp/project_name/*.lock at suriin ang kanilang presensya sa simula ng script. Tinutulungan nito ang script na magwakas nang maganda at maiwasan ang mga hindi inaasahang pagbabago sa estado ng system sa pamamagitan ng isa pang script na tumatakbo nang magkatulad. Ang mga lock file ay hindi kailangan kung kailangan mo ng parehong script upang maisakatuparan sa isang naibigay na host.

Sukatin at pagbutihin

Kadalasan kailangan nating magtrabaho kasama ang mga script na tumatakbo sa mahabang panahon, gaya ng pang-araw-araw na pagpapatakbo ng database. Ang mga naturang operasyon ay karaniwang nagsasangkot ng isang pagkakasunud-sunod ng mga hakbang: pag-load ng data, pagsuri para sa mga anomalya, pag-import ng data, pagpapadala ng mga ulat sa katayuan, at iba pa.

Sa ganitong mga kaso, palagi kong sinusubukang hatiin ang script sa magkakahiwalay na maliliit na script at iulat ang kanilang katayuan at oras ng pagpapatupad gamit ang:

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

Sa ibang pagkakataon ay makikita ko ang oras ng pagpapatupad sa:

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

Nakakatulong ito sa akin na matukoy ang mga problema/mabagal na bahagi sa mga script na nangangailangan ng pag-optimize.

Good luck!

Ano pa ang dapat basahin:

  1. Pumunta at GPU cache.
  2. Isang halimbawa ng application na hinimok ng kaganapan batay sa mga webhook sa S3 object storage ng Mail.ru Cloud Solutions.
  3. Ang aming telegram channel tungkol sa digital transformation.

Pinagmulan: www.habr.com

Magdagdag ng komento