Bash սկրիպտների վրիպազերծումը նման է խոտի դեզում ասեղ փնտրելուն, հատկապես, երբ նոր հավելումներ են հայտնվում գոյություն ունեցող կոդերի բազայում՝ առանց կառուցվածքի, հատումների և հուսալիության հարցերը ժամանակին հաշվի առնելու: Նման իրավիճակներում կարող եք հայտնվել կա՛մ սեփական սխալների պատճառով, կա՛մ սցենարների բարդ կույտեր կառավարելիս:
Թիմ Mail.ru Cloud Solutions թարգմանել է հոդված՝ առաջարկություններով, որոնք կօգնեն ձեզ ավելի լավ գրել, կարգաբերել և պահպանել ձեր սցենարները: Հավատացեք դրան, թե ոչ, ոչինչ չի գերազանցում մաքուր, օգտագործման համար պատրաստ bash կոդը, որն ամեն անգամ աշխատում է:
Հոդվածում հեղինակը կիսվում է այն ամենով, ինչ սովորել է վերջին մի քանի տարիների ընթացքում, ինչպես նաև որոշ տարածված սխալներով, որոնք իրեն անսպասելի են դարձրել: Սա կարևոր է, քանի որ յուրաքանչյուր ծրագրաշար մշակող, իր կարիերայի ինչ-որ պահի, աշխատում է սկրիպտներով՝ սովորական աշխատանքային առաջադրանքները ավտոմատացնելու համար:
Ծուղակ մշակողներ
Շատ bash սկրիպտներ, որոնք ես հանդիպել եմ, երբեք չեն օգտագործում արդյունավետ մաքրման մեխանիզմ, երբ սցենարի կատարման ժամանակ անսպասելի բան է տեղի ունենում:
Անակնկալներ կարող են առաջանալ դրսից, օրինակ՝ միջուկից ազդանշան ստանալը։ Նման դեպքերի հետ վարվելը չափազանց կարևոր է ապահովելու համար, որ սցենարները բավականաչափ հուսալի են արտադրական համակարգերում աշխատելու համար: Ես հաճախ օգտագործում եմ ելքի մշակիչներ՝ արձագանքելու նման սցենարներին.
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 կճեպով ներկառուցված հրաման է, որն օգնում է ձեզ գրանցել մաքրման ֆունկցիա, որը կանչվում է ցանկացած ազդանշանի դեպքում: Այնուամենայնիվ, պետք է հատուկ խնամք ցուցաբերել այնպիսի կարգավորիչների հետ, ինչպիսիք են SIGINT, ինչը հանգեցնում է սցենարի ընդհատմանը:
Բացի այդ, շատ դեպքերում դուք պետք է միայն բռնեք EXIT, բայց գաղափարն այն է, որ դուք կարող եք իրականում հարմարեցնել սցենարի վարքագիծը յուրաքանչյուր առանձին ազդանշանի համար:
Ներկառուցված հավաքածուի գործառույթներ - արագ դադարեցում սխալի դեպքում
Շատ կարևոր է սխալներին արձագանքել դրանց առաջացմանն պես և արագ դադարեցնել կատարումը: Ոչինչ չի կարող ավելի վատ լինել, քան շարունակել գործարկել այսպիսի հրամանը.
rm -rf ${directory_name}/*
Խնդրում ենք նկատի ունենալ, որ փոփոխականը directory_name որոշված չէ.
Նման սցենարները կարգավորելու համար կարևոր է օգտագործել ներկառուցված գործառույթները setԻնչպես, օրինակ, set -o errexit, set -o pipefail կամ set -o nounset սցենարի սկզբում։ Այս գործառույթները ապահովում են, որ ձեր սցենարը դուրս կգա, հենց որ հանդիպի որևէ ոչ զրոյական ելքի կոդի, չսահմանված փոփոխականների օգտագործման, խողովակի վրայով անցած անվավեր հրամանների և այլնի հետ՝
#!/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
Նշում: ներկառուցված գործառույթներ, ինչպիսիք են set -o errexit, դուրս կգա սկրիպտից, հենց որ լինի «հում» վերադարձի կոդը (բացի զրոյից): Հետևաբար, ավելի լավ է ներմուծել սովորական սխալների մշակում, օրինակ.
#!/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
Այս կերպ սկրիպտներ գրելը ստիպում է ձեզ ավելի զգույշ լինել սկրիպտի բոլոր հրամանների վարքագծի նկատմամբ և կանխատեսել սխալի հավանականությունը, նախքան դա ձեզ զարմացնի:
ShellCheck՝ մշակման ընթացքում սխալները հայտնաբերելու համար
Արժե ինտեգրվել նման բան ShellCheck- ը ձեր մշակման և փորձարկման խողովակաշարերի մեջ՝ ստուգելու ձեր bash կոդը լավագույն փորձի դեմ:
Ես օգտագործում եմ այն իմ տեղական զարգացման միջավայրում՝ շարահյուսության, իմաստաբանության և կոդի որոշ սխալների վերաբերյալ հաշվետվություններ ստանալու համար, որոնք կարող էի բաց թողնել մշակելիս: Սա ստատիկ վերլուծության գործիք է ձեր bash սցենարների համար, և ես խորհուրդ եմ տալիս օգտագործել այն:
Օգտագործելով ձեր սեփական ելքի կոդերը
POSIX-ում վերադարձի կոդերը ոչ միայն զրո կամ մեկ են, այլ զրո կամ ոչ զրոյական արժեք: Օգտագործեք այս հնարավորությունները՝ տարբեր սխալների դեպքերի համար հատուկ սխալի կոդերը վերադարձնելու համար (201-254-ի միջև):
Այնուհետև այս տեղեկատվությունը կարող է օգտագործվել այլ սկրիպտների կողմից, որոնք փաթաթում են ձերը, որպեսզի հասկանաք, թե ինչ տեսակի սխալ է տեղի ունեցել և համապատասխանաբար արձագանքել.
#!/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
}
Նշում: խնդրում եմ հատկապես զգույշ եղեք ձեր սահմանած փոփոխականների անունների հետ, որպեսզի խուսափեք շրջակա միջավայրի փոփոխականների պատահական գերակայությունից:
Գրանցման գործառույթներ
Գեղեցիկ և կառուցվածքային գրանցումը կարևոր է ձեր սցենարի արդյունքները հեշտությամբ հասկանալու համար: Ինչպես և բարձր մակարդակի ծրագրավորման լեզուների դեպքում, ես միշտ օգտագործում եմ բնագիր գրանցման գործառույթներ իմ bash սկրիպտներում, ինչպիսիք են. __msg_info, __msg_error եւ այլն:
Սա օգնում է ապահովել ստանդարտացված անտառահատումների կառուցվածք՝ փոփոխություններ կատարելով միայն մեկ վայրում.
#!/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"
Ես սովորաբար փորձում եմ ինչ-որ մեխանիզմ ունենալ իմ սցենարներում __init, որտեղ նման լոգերի փոփոխականները և համակարգի այլ փոփոխականները նախնականացված են կամ դրված են լռելյայն արժեքների վրա: Այս փոփոխականները կարող են սահմանվել նաև հրամանի տողի ընտրանքներից՝ սկրիպտի կանչի ժամանակ:
Օրինակ, նման մի բան.
$ ./run-script.sh --debug
Երբ այդպիսի սկրիպտը կատարվում է, այն ապահովում է, որ ամբողջ համակարգի կարգավորումները դրված են լռելյայն արժեքների վրա, եթե դրանք պահանջվում են, կամ առնվազն սկզբնավորվում են համապատասխան ինչ-որ բանի, եթե անհրաժեշտ է:
Ես սովորաբար ընտրում եմ, թե ինչ պետք է սկզբնավորել և ինչ չանել, փոխզիջման հիման վրա օգտագործողի միջերեսի և կոնֆիգուրացիաների մանրամասների վրա, որոնց մեջ օգտագործողը կարող է/պետք է խորանա:
Վերօգտագործման և մաքուր համակարգի վիճակի ճարտարապետություն
Ես պահում եմ առանձին պահոց, որը կարող եմ օգտագործել նոր նախագիծ/bash սցենարը սկզբնավորելու համար, որը ես ցանկանում եմ մշակել: Այն ամենը, ինչ հնարավոր է նորից օգտագործել, կարող է պահվել պահեստում և ետ բերել այլ նախագծերի կողմից, որոնք ցանկանում են օգտագործել այդ գործառույթը: Նախագծերի այս ձևով կազմակերպումը զգալիորեն նվազեցնում է այլ սկրիպտների չափերը և նաև ապահովում է, որ կոդերի բազան փոքր է և հեշտ փորձարկվող:
Ինչպես վերը նշված օրինակում, բոլոր գրանցման գործառույթները, ինչպիսիք են __msg_info, __msg_error և մյուսները, ինչպիսիք են Slack-ի հաշվետվությունները, ներառված են առանձին common/* և դինամիկ միացեք այլ սցենարներում, ինչպիսիք են daily_database_operation.sh.
Թողեք մաքուր համակարգ
Եթե դուք բեռնում եք որևէ ռեսուրս, մինչ սկրիպտը աշխատում է, խորհուրդ է տրվում պահել բոլոր այդպիսի տվյալները պատահական անունով համօգտագործվող գրացուցակում, օրինակ. /tmp/AlRhYbD97/*. Գրացուցակի անունը ընտրելու համար կարող եք օգտագործել պատահական տեքստի գեներատորներ.
Աշխատանքի ավարտից հետո նման դիրեկտորիաների մաքրումը կարող է իրականացվել վերը քննարկված կեռիկներում: Եթե ժամանակավոր գրացուցակները չեն պահպանվում, դրանք կուտակվում են և ինչ-որ փուլում անսպասելի խնդիրներ են առաջացնում հոսթի վրա, օրինակ՝ լրիվ սկավառակը:
Օգտագործելով կողպեքի ֆայլերը
Հաճախ դուք պետք է համոզվեք, որ սկրիպտի միայն մեկ օրինակ է աշխատում հոսթի վրա ցանկացած պահի: Դա կարելի է անել կողպեքի ֆայլերի միջոցով:
Ես սովորաբար ստեղծում եմ կողպեքի ֆայլեր /tmp/project_name/*.lock և ստուգեք նրանց ներկայությունը սցենարի սկզբում: Սա օգնում է սկրիպտը նրբագեղորեն դադարեցնել և խուսափել համակարգի վիճակի անսպասելի փոփոխություններից զուգահեռ աշխատող մեկ այլ սցենարի միջոցով: Կողպեք ֆայլերը անհրաժեշտ չեն, եթե ձեզ անհրաժեշտ է, որ նույն սկրիպտը կատարվի տվյալ հոսթի վրա զուգահեռ:
Չափել և կատարելագործել
Մենք հաճախ պետք է աշխատենք երկար ժամանակ գործող սկրիպտների հետ, ինչպիսիք են տվյալների բազայի ամենօրյա գործողությունները: Նման գործողությունները սովորաբար ներառում են քայլերի հաջորդականություն՝ տվյալների բեռնում, անոմալիաների ստուգում, տվյալների ներմուծում, կարգավիճակի մասին հաշվետվությունների ուղարկում և այլն:
Նման դեպքերում ես միշտ փորձում եմ սկրիպտը բաժանել առանձին փոքր սցենարների և զեկուցել դրանց կարգավիճակի և կատարման ժամանակի մասին՝ օգտագործելով.
time source "${filepath}" "${args}">> "${LOG_DIR}/RUN_LOG" 2>&1
Հետագայում ես կարող եմ տեսնել կատարման ժամանակը հետևյալով.
tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"
Սա օգնում է ինձ բացահայտել խնդրահարույց/դանդաղ տարածքները սցենարներում, որոնք օպտիմալացման կարիք ունեն: