
การดีบักสคริปต์ทุบตีก็เหมือนกับการมองหาเข็มในกองหญ้า โดยเฉพาะอย่างยิ่งเมื่อมีการเพิ่มเติมใหม่ปรากฏในโค้ดเบสที่มีอยู่โดยไม่ต้องคำนึงถึงปัญหาด้านโครงสร้าง การบันทึก และความน่าเชื่อถืออย่างทันท่วงที คุณสามารถพบว่าตัวเองอยู่ในสถานการณ์ดังกล่าวเนื่องจากความผิดพลาดของคุณเองหรือเมื่อจัดการกองสคริปต์ที่ซับซ้อน
ทีม แปลบทความพร้อมคำแนะนำที่จะช่วยให้คุณเขียน แก้ไขข้อบกพร่อง และบำรุงรักษาสคริปต์ของคุณได้ดีขึ้น เชื่อหรือไม่ ไม่มีอะไรจะดีไปกว่าความพึงพอใจในการเขียนโค้ด 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 เพื่อตรวจจับข้อผิดพลาดระหว่างการพัฒนา
มันคุ้มค่าที่จะบูรณาการบางอย่างเช่น ในการพัฒนาและทดสอบไปป์ไลน์ของคุณเพื่อตรวจสอบโค้ดทุบตีของคุณกับแนวปฏิบัติที่ดีที่สุด
ฉันใช้มันในสภาพแวดล้อมการพัฒนาในพื้นที่ของฉันเพื่อรับรายงานเกี่ยวกับไวยากรณ์ ความหมาย และข้อผิดพลาดบางอย่างในโค้ดที่ฉันอาจพลาดไปขณะพัฒนา นี่เป็นเครื่องมือวิเคราะห์แบบคงที่สำหรับสคริปต์ทุบตีของคุณ และฉันขอแนะนำให้ใช้มันเป็นอย่างยิ่ง
ใช้รหัสทางออกของคุณเอง
รหัสส่งคืนใน 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
}
หมายเหตุ: โปรดใช้ความระมัดระวังเป็นพิเศษกับชื่อตัวแปรที่คุณกำหนด เพื่อหลีกเลี่ยงการลบล้างตัวแปรสภาพแวดล้อมโดยไม่ตั้งใจ
ฟังก์ชั่นการบันทึก
การบันทึกที่สวยงามและมีโครงสร้างเป็นสิ่งสำคัญในการทำความเข้าใจผลลัพธ์ของสคริปต์ของคุณได้อย่างง่ายดาย เช่นเดียวกับภาษาการเขียนโปรแกรมระดับสูงอื่นๆ ฉันมักจะใช้ฟังก์ชันการบันทึกแบบเนทีฟในสคริปต์ทุบตีของฉัน เช่น __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
เมื่อเรียกใช้สคริปต์ดังกล่าว จะช่วยให้แน่ใจว่าการตั้งค่าทั้งระบบได้รับการตั้งค่าเป็นค่าเริ่มต้นหากจำเป็น หรืออย่างน้อยก็เตรียมใช้งานสิ่งที่เหมาะสมหากจำเป็น
ฉันมักจะเลือกว่าอะไรจะเริ่มต้นและอะไรไม่ควรทำในการแลกเปลี่ยนระหว่างอินเทอร์เฟซผู้ใช้กับรายละเอียดของการกำหนดค่าที่ผู้ใช้สามารถ/ควรเจาะลึก
สถาปัตยกรรมสำหรับการใช้ซ้ำและสถานะระบบที่สะอาด
รหัสแบบแยกส่วน/แบบใช้ซ้ำได้
├── framework
│ ├── common
│ │ ├── loggers.sh
│ │ ├── mail_reports.sh
│ │ └── slack_reports.sh
│ └── daily_database_operation.sh
ฉันเก็บพื้นที่เก็บข้อมูลแยกต่างหากไว้ซึ่งสามารถใช้เพื่อเริ่มต้นโปรเจ็กต์/สคริปต์ทุบตีใหม่ที่ฉันต้องการพัฒนา ทุกสิ่งที่สามารถนำมาใช้ซ้ำได้สามารถจัดเก็บไว้ในพื้นที่เก็บข้อมูลและเรียกคืนโดยโปรเจ็กต์อื่นที่ต้องการใช้ฟังก์ชันนั้น การจัดระเบียบโปรเจ็กต์ด้วยวิธีนี้จะช่วยลดขนาดของสคริปต์อื่นๆ ลงอย่างมาก และยังช่วยให้แน่ใจว่าฐานโค้ดมีขนาดเล็กและง่ายต่อการทดสอบ
ดังตัวอย่างข้างต้น ฟังก์ชันการบันทึกทั้งหมด เช่น __msg_info, __msg_error และอื่นๆ เช่น รายงาน Slack มีแยกไว้ใน common/* และเชื่อมต่อแบบไดนามิกในสถานการณ์อื่นๆ เช่น daily_database_operation.sh.
ทิ้งระบบที่สะอาดไว้เบื้องหลัง
หากคุณกำลังโหลดทรัพยากรใดๆ ในขณะที่สคริปต์กำลังทำงานอยู่ ขอแนะนำให้จัดเก็บข้อมูลดังกล่าวทั้งหมดไว้ในไดเร็กทอรีที่ใช้ร่วมกันโดยใช้ชื่อแบบสุ่ม เช่น /tmp/AlRhYbD97/*. คุณสามารถใช้ตัวสร้างข้อความแบบสุ่มเพื่อเลือกชื่อไดเร็กทอรี:
rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"
หลังจากเสร็จสิ้นงาน สามารถจัดเตรียมการล้างข้อมูลไดเร็กทอรีดังกล่าวได้ในตัวจัดการฮุกที่กล่าวถึงข้างต้น หากไดเร็กทอรีชั่วคราวไม่ได้รับการดูแล ไดเร็กทอรีเหล่านั้นจะสะสมและในบางขั้นตอนทำให้เกิดปัญหาที่ไม่คาดคิดบนโฮสต์ เช่น ดิสก์เต็ม
การใช้ไฟล์ล็อค
บ่อยครั้งที่คุณต้องแน่ใจว่ามีสคริปต์เพียงอินสแตนซ์เดียวเท่านั้นที่ทำงานบนโฮสต์ในเวลาใดก็ตาม ซึ่งสามารถทำได้โดยใช้ไฟล์ล็อค
ฉันมักจะสร้างไฟล์ล็อคใน /tmp/project_name/*.lock และตรวจสอบการมีอยู่ของพวกเขาที่จุดเริ่มต้นของสคริปต์ ซึ่งจะช่วยให้สคริปต์สิ้นสุดอย่างสวยงามและหลีกเลี่ยงการเปลี่ยนแปลงสถานะระบบโดยไม่คาดคิดโดยสคริปต์อื่นที่ทำงานพร้อมกัน ไม่จำเป็นต้องใช้ไฟล์ล็อคหากคุณต้องการให้สคริปต์เดียวกันดำเนินการแบบขนานบนโฮสต์ที่กำหนด
วัดและปรับปรุง
เรามักจะต้องทำงานกับสคริปต์ที่ทำงานเป็นระยะเวลานาน เช่น การทำงานของฐานข้อมูลรายวัน โดยทั่วไปการดำเนินการดังกล่าวจะเกี่ยวข้องกับลำดับขั้นตอนต่างๆ เช่น การโหลดข้อมูล การตรวจสอบความผิดปกติ การนำเข้าข้อมูล การส่งรายงานสถานะ และอื่นๆ
ในกรณีเช่นนี้ ฉันมักจะพยายามแบ่งสคริปต์ออกเป็นสคริปต์เล็กๆ แยกกัน และรายงานสถานะและเวลาดำเนินการโดยใช้:
time source "${filepath}" "${args}">> "${LOG_DIR}/RUN_LOG" 2>&1
หลังจากนั้นฉันสามารถดูเวลาดำเนินการด้วย:
tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"
ซึ่งช่วยฉันระบุปัญหา/ส่วนที่ช้าในสคริปต์ที่ต้องการการปรับให้เหมาะสม
โชคดี!
มีอะไรให้อ่านอีก:
ที่มา: will.com
