การดีบัก Bash
(wizardzines.com)- เมื่อสคริปต์ Bash ทำงานต่างจากที่คาดไว้ แค่ใช้วิธี ดูคำสั่งที่ถูกเรียกใช้ตามจริง ก็ช่วยให้ไล่หาสาเหตุได้ง่ายขึ้น
set -xจะแสดงแต่ละบรรทัดหลังจากขยายตัวแปรแล้ว ทำให้ตรวจสอบได้ว่าสคริปต์กำลังรันคำสั่งใดจริง ๆ- หากรันจากบรรทัดคำสั่งด้วย
bash -x script.shจะให้ผลเหมือนกับการใส่set -xไว้ที่ด้านบนของscript.sh - เมื่อใช้ trap
DEBUGร่วมกับreadจะหยุดก่อนรันแต่ละบรรทัด เพื่อให้ตรวจสอบชื่อไฟล์ หมายเลขบรรทัด และคำสั่งถัดไปได้ - ฟังก์ชัน
die() { echo $1 >&2; exit 1; }ช่วยทำให้โฟลว์หลังคำสั่งล้มเหลวง่ายขึ้น โดย พิมพ์ข้อความไปยัง standard error แล้วจบการทำงาน
ตรวจสอบโฟลว์การทำงานด้วยสายตา
set -xจะแสดงบรรทัดที่สคริปต์รัน และแสดงตัวแปรเป็นค่าที่ถูกขยายแล้ว- สามารถใช้งานได้โดยใส่
set -xไว้ที่ด้านบนของสคริปต์ - สามารถเรียกพฤติกรรมเดียวกันนี้จากบรรทัดคำสั่งได้เช่นกัน
$ bash -x script.sh- ซึ่งเหมือนกับการใส่
set -xไว้บนสุดของscript.sh
หยุดตรวจสอบทีละบรรทัด
- trap
DEBUGจะทำงานก่อนที่โค้ดแต่ละบรรทัดจะถูกเรียกใช้ - หากใส่โค้ดด้านล่างไว้ช่วงต้นของสคริปต์ จะรอให้กด Enter ก่อนรันคำสั่งถัดไป
trap '(read -p "\[$BASH_SOURCE: $LINENO] $BASH_COMMAND")' DEBUGread -pจะแสดงข้อความและรอให้กด Enter$BASH_SOURCEคือชื่อไฟล์สคริปต์$LINENOคือหมายเลขบรรทัด$BASH_COMMANDคือคำสั่งที่จะถูกรันถัดไป
เมื่อเกิดความล้มเหลว ให้ทิ้งข้อความไว้แล้วจบการทำงาน
- ฟังก์ชัน
dieสามารถใช้เพื่อแสดงข้อความและจบโปรแกรมเมื่อคำสั่งล้มเหลวได้die() { echo $1 >&2; exit 1; }- เพียงต่อท้ายคำสั่งที่อาจล้มเหลว เช่น
some_command || die "oh no!"
- ฟังก์ชันนี้จะส่งข้อความไปยัง standard error และจบการทำงานด้วย
exit 1
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
มีฟังก์ชันล็อก
zdebugแทรกอยู่ทั่วโค้ด: https://github.com/zbm-dev/zfsbootmenu/blob/master/zfsbootme...ถ้าเปิด debug logging แล้วกด Ctrl-T จากเมนูหลัก จะเห็นหน้าจอแบบนี้: https://i.imgur.com/Ge75zkP.png
นอกจากนี้ยังมี การทำ profiling แบบ flamegraph ที่เปิดได้ด้วย https://github.com/zbm-dev/zfsbootmenu/blob/master/zfsbootme... และถ้านำข้อมูลที่ dump ผ่านพอร์ตอนุกรมมาประกอบกลับ ก็สร้างกราฟแบบ https://raw.githubusercontent.com/zbm-dev/zfsbootmenu/master... ได้
Bash ยืดหยุ่นกว่าที่คิด
set -xการตั้งPS4='+ ${BASH_SOURCE:-}:${FUNCNAME[0]:-}:L${LINENO:-}: 'มีประโยชน์มากแบบนี้จะแสดง ชื่อไฟล์ ชื่อฟังก์ชัน และเลขบรรทัด ช่วยได้ไม่น้อยเวลาต้องดีบักสคริปต์ Bash ขนาดใหญ่
shellcheckด้วย แม้มันจะจับปัญหาให้โดยตรงไม่ได้ ก็ยังช่วยบอก ปัญหาที่อาจเกิดขึ้น ได้อีกอย่าง แนะนำให้เขียนสคริปต์ใหม่ด้วยภาษาอื่นด้วย ที่บริษัทกำลังเปลี่ยนสคริปต์ Bash ไปเป็น Rust อยู่ แม้ต้นทุนในการเริ่มต้นจะสูง แต่โค้ดที่ได้ดูแลง่ายกว่าและน่าเชื่อถือกว่ามาก
Bash ยังเหมาะกับสคริปต์เร็ว ๆ อยู่ แต่พอเกินราว 100 บรรทัด ก็คุ้มที่จะใช้ภาษาที่ให้การรับประกันที่แข็งแรงกว่า
die()เป็นฟังก์ชันช่วยที่พิมพ์ข้อความผิดพลาดไปยัง standard error แล้วออกด้วยรหัสข้อผิดพลาดที่กำหนดexit code และฟังก์ชันช่วยสำหรับ shell script เพิ่มเติมอยู่ที่นี่: https://github.com/SixArm/unix-shell-script-kit/blob/main/un...
แต่ปรัชญาของ
dieตัวนั้นยังน่าเสียดายนิดหน่อย ฟังก์ชันdieโดยพื้นฐานควรส่งต่อ exit code ของคำสั่งที่ล้มเหลว และไม่ควรซ่อน error output ของคำสั่งนั้นถ้าในสคริปต์ใหญ่ ๆ ผมอยากกำหนดความหมายให้ความล้มเหลวของคำสั่งเอง ก็คงใช้
dieที่เฉพาะทางกว่าแยกต่างหากdieของผมประมาณว่าเป็น__errex "$?" "${LINENO}" "$0"เพื่อพิมพ์ข้อผิดพลาดร้ายแรง เลขบรรทัด ชื่อสคริปต์ และข้อความ แล้วจบด้วย exit code นั้นตัวอย่าง implementation หนึ่งอยู่ที่นี่: https://github.com/TritonDataCenter/sdc-headnode/blob/master...
ใช้แบบ
some-command || fail "message"แล้วเมื่อsome-commandคืนค่าสถานะการจบที่ไม่ใช่ 0 ก็จะสร้าง stack trace และออกจาก shellถ้าอยากสร้าง stack trace ในฟังก์ชันแล้ว return กลับไป ใช้แบบ
some-command || softfail "message" || return $?ได้มันทำสิ่งที่ต้องทำได้ก็จริง แต่เทอะทะและไวยากรณ์ก็แย่มาก พอสคริปต์โตหรือซับซ้อนถึงระดับหนึ่ง มันก็เหมือนบังคับให้ย้ายไปใช้ภาษาจริงจัง บางทีนี่อาจเป็นการออกแบบโดยตั้งใจก็ได้
ถ้าเป็น distro สมัยใหม่ ก็มักมี Bash เวอร์ชันค่อนข้างใหม่ และถ้าไม่ได้จะใช้ของอย่าง array ก็แทบไม่ต้องใส่ใจเวอร์ชันมากนัก
เสน่ห์ของ Bash อยู่ที่ตำแหน่งของมันระหว่างภาษาและเครื่องมืออื่น ๆ มันเหมาะมากสำหรับผูกเครื่องมือต่าง ๆ เข้าด้วยกัน และอยู่ใกล้ระบบปฏิบัติการพอให้ใช้งานสะดวก อีกทั้งไม่ต้องติดตั้งไลบรารีเหมือน Python
มักได้ยินว่าถ้าสคริปต์ซับซ้อนกว่านี้ให้ย้ายไปภาษาอย่าง Python แต่ก็เป็นการเพิ่มชั้นความซับซ้อนที่ในระยะยาวอาจไม่ได้ช่วย สคริปต์ Bash ที่เขียนไว้เมื่อ 20 ปีก่อนยังรันได้ดีอยู่ แต่โปรแกรม Python เมื่อ 20 ปีก่อนมีโอกาสสูงที่จะเจอปัญหาเรื่องเวอร์ชัน
rcของ Plan 9 สะอาดกว่านิดหน่อย แต่แค่ “คล้ายกันแต่สะอาดกว่า” ไม่ทำให้ใครย้ายตาม ตอนนี้ก็สามารถติดตั้งตัวที่คล้ายกันแต่ดีกว่าได้ทันทีจาก https://pkgsrc.se/shells แต่เราก็ยังไม่ใช้กัน และวิธีรันให้คนอื่นก็จะไม่เหมือนเดิมด้วยถ้าจะทดแทนเทคโนโลยีที่ตั้งหลักได้แล้ว ต้องดีกว่าหลายเท่าในด้านสำคัญ ๆ Plan 9 เองก็ดีกว่าสาย UNIX แต่ก็ยังไม่ดีพอจะทดแทน
การสร้างอะไรสักอย่างที่ดีพอจะมาแทนช่องเฉพาะของ Bourne shell scripting นั้นยาก ก่อนจะดีถึงขั้นนั้น มันก็จะข้ามไปอยู่ใน niche ทางนิเวศหรือ problem space ของภาษาสคริปต์จริง ๆ อย่าง Perl, Python, Ruby ไปแล้ว
ใน problem space แคบ ๆ local optimum จะดูดอากาศไปหมด ทำให้คู่แข่งที่ใกล้เคียงกับ global optimum ในเชิงทฤษฎีเกิดขึ้นได้ยาก
แม้ว่าฟีเจอร์ที่เพิ่มเข้ามาใหม่ ๆ จะวางทับบน
sh/Bash ได้ค่อนข้างดี แต่ท้ายที่สุด shell scripting ก็เป็นเพียงเครื่องมือเพื่อบรรลุเป้าหมาย และควรวิวัฒนาการช้ากว่าภาษาโปรแกรมทั่วไปมากคุณลักษณะสำคัญของ Bash/
shคือมันมีลักษณะต้านเอนโทรปี แทบไม่มีการพัฒนาหรือวิวัฒนาการ จึงมีโอกาสน้อยที่จะปวดหัวเพราะ dependency หรือฟีเจอร์ใหม่ ๆ และสิ่งที่ใช้ได้เมื่อ 20 ปีก่อนก็ยังคงอยู่เป็นเครื่องมือพื้นฐานต่อไปด้วยการออกแบบเช่นนั้น มันจึงเป็นระบบที่ต่อต้านการเปลี่ยนแปลง และเมื่อชนขีดจำกัด ก็สร้างแรงจูงใจให้ผู้คนออกไปใช้อย่างอื่นนอกเหนือจากมัน
สคริปต์ส่วนใหญ่ของ FreeBSD เขียนมาสำหรับ
shและshเป็นส่วนหนึ่งของมาตรฐาน POSIX จึงรู้สึกว่ารองรับได้กว้างกว่ามาก ส่วน Bash มองว่าแค่ค่อนข้างได้รับความนิยมเท่านั้นแต่ Bash แย่มากจนผมทำยูทิลิตีที่ย่อ namespace ไว้จำนวนมากเพื่อจะใช้สคริปต์ Groovy แทน สามารถพัฒนาด้วย IDE ได้ ระบบไลบรารีก็ปลอดภัย และ Groovy ก็ขัดเกลาความไม่สะดวกของ Java ไปเกือบหมด จึงดีกว่ามาก
แม้จะมีข้อจำกัดบางอย่าง แต่โดยทั่วไปอาจมีประโยชน์: https://github.com/ketancmaheshwari/pd
die()นั้นดี แต่ Bash มีคุณสมบัติน่ารำคาญอยู่อย่างหนึ่ง คือถ้าพยายามexitภายใน subshell จะจบแค่ subshell เท่านั้น ส่วนที่เหลือของสคริปต์ยังคงรันต่อตัวอย่างเช่น ถ้าเรียก
dieภายใน pipeline อย่างcat myfile | while read line; do ... die "Found match" ... doneบรรทัดecho "I don't want this line"ที่ตามมาจะยังคงถูกพิมพ์ออกมาหลายกรณีหลีกเลี่ยง subshell ได้ และในตัวอย่างนี้
shellcheckชี้เรื่อง UUOC ได้ถูกต้อง และเมื่อแก้ตรงนั้น ปัญหาdieใน subshell ก็จะหายไปด้วยแต่บางครั้งก็หลีกเลี่ยง subshell ไม่ได้ หรือถ้าจะหลีกเลี่ยง สคริปต์จะซับซ้อนเกินไป ในกรณีนั้นสามารถเก็บ PID ไว้ตอนเริ่มสคริปต์ด้วย
MYPID=$$แล้วฆ่ามันด้วยแบบdie() { echo "$1" >&2; kill -9 $MYPID; exit 1; }ได้แน่นอนว่าวิธีนี้ก็มีข้อแลกเปลี่ยน วิธีฆ่าแบบนี้ค่อนข้างหยาบ และไม่รู้ทำไม แต่ก็ไม่ได้น่าเชื่อถือเต็มที่ด้วย
set -eเข้าไป เมื่อ subshell จบด้วยโค้ดข้อผิดพลาดที่ไม่ใช่ 0 สคริปต์ก็จะจบตามไปด้วยนึกเหตุผลไม่ค่อยออกว่าทำไมสคริปต์ shell ใด ๆ ถึงควรละ
set -eไว้set -euxo pipefailไว้เสมอแม้จะทำให้การทดสอบเงื่อนไขยากขึ้นบ้าง แต่โดยเฉพาะแค่
pipefailตัวเดียวก็คุ้มค่าหลายครั้งแล้วset +xได้ถ้าเปิดไว้ตลอดจะน่าเบื่อพอสมควร
แต่
-xผมจะเก็บไว้จนกว่าจะต้องดูเอาต์พุตดีบักรก ๆ ทั้งหมดจริง ๆ