debug.txt การหาที่ผิด (Debugging) ใน Shell script ------------------------------------ ไม่ว่าใครก็มีโอกาสเขียน script ที่ทำงานผิดพลาดได้ แต่ก็ไม่ควรปล่อยให้เป็นเช่นนั้น เคล็ดลับง่ายๆ ต่อไปนี้จะช่วยทำให้แน่ใจได้ว่า script จะทำงานได้ตามที่ตั้งใจไว้ สิ่งแรกสุดที่ต้องทำในการหาที่ผิดใน script คือ "คิด" ให้รอบคอบตั้งแต่ตอนเขียนโปรแกรมว่าอะไรที่น่าจะทำให้เกิดความผิดพลาดได้บ้าง เช่น คิดว่ากำลังจะอ่านแฟ้มหนึ่ง จะเกิดอะไรขึ้นหากแฟ้มนั้นไม่มีอยู่จริง และหากผู้ที่เรียกใช้ script ไม่มีสิทธิในการอ่านแฟ้ม จะเป็นอย่างไร ขอเพียงผู้เขียนคิดให้ทั่วว่าอะไรที่น่าจะมีโอกาสเกิดความผิดพลาด ผู้เขียน script ก็สามารถเขียนคำสั่งเพื่อตรวจสอบไว้ล่วงหน้าได้ เมื่อเกิดความผิดพลาดก็เพียวแต่แจ้งให้ผู้ใช้ทราบด้วยถ้อยคำสั่นๆ แต่ตรงประเด็นว่าความผิดพลาดนั้นเกิดจากอะไร และที่สำคัญที่สุด อย่าทำอะไรให้ต้องตกอยู่ในสถานการณ์ที่เลวร้ายกว่าตอนก่อนที่จะให้ script ทำงาน เพราะหาก script ทำงานไปได้ส่วนหนึ่ง แล้วก็หยุดทำงาน ผู้เขียนโปรแกรมต้องแก้ไขปัญหาโดยการป้อนคำสั่งที่ command line ทีละคำสั่ง เพื่อไปยังจุดเริ่มต้นก่อนที่จะเริ่มใช้งาน script นอกจากจะตรวจสอบจำนวนของอาร์กิวเมนต์แล้ว ยังควรตรวจสอบต่อไปว่าอาร์กิวเมนต์แต่ละตัวตรงตามเงื่อนไขที่ต้องการหรือไม่ เช่น #! #!/bin/bash if [ $# -ne 1 ]; then echo "USAGE: $0 file" exit 1 else if [ ! -f $1 ] || [ ! -r $1 ]; then echo "ERROR: $1 cannot be read or is not a file" exit 1 fi ... นอกจากจะทดสอบการทำงานของ script ด้วยแฟ้มที่ต้องการแล้ว ยังต้องทดสอบด้วยอาร์กิวเมนต์และข้อมูลที่ไม่ถูกต้อง หากโปรแกรมต้องใช้อาร์กิวเมนต์สองตัว ลองไม่ผ่านอาร์กิวเมนต์เลย หรือผ่านอาร์กิวเมนต์สามตัวหรือมากกว่า ป้อนชื่อแฟ้มที่ไม่มีสิทธิอ่าน ป้อนชื่อไดเรกทอรีแทนชื่อแฟ้ม ลองสลับลำดับของอารกิวเมนต์ ให้แน่ใจว่าโปรแกรมทำงานถูกต้องไม่ว่าจะเกิดอะไรขึ้นก็ตาม ผู้เขียนอาจยอมให้ผู้ใช้ผ่านอาร์กิวเมนต์จำนวนมากกว่าที่โปรแกรมต้องการ โดยอาร์กิวเมนต์ที่เกินมาจะไม่ถูกนำไปใช้ โดยเปลี่ยนเงื่อนไขจากการกำหนดให้จำนวนอาร์กิวเมนต์ ($#) เท่ากับ มากกว่าหรือเท่ากับค่าคงที่ เช่นเปลี่ยนจาก [ $# -ne 2 ] เป็น [ $# -ge 2 ] เป็นต้น เมื่อโปรแกรมมีโครงสร้างที่ซับซ้อน ผู้เขียนบทความมักจะตรวจสอบ "ตรรกะ" หรือความสมเหตุสมผลของโครงสร้าง if, loop, case และฟังก์ชัน และเพื่อให้แน่ใจว่าโครงสร้างเหล่านี้ทำงานได้อย่างถูกต้อง ผู้เขียนนิยมแทรกคำสั่ง echo ลงในจุดที่ต้องการทดสอบ เมื่อเห็นว่าโครงสร้างทำงานถูกต้องแล้ว จึงแทนที่ด้วยคำสั่งที่ต้องการใช้จริง วิธีนี้ช่วยให้ผู้ที่เริ่มพัฒนาโปรแกรมเขียนและทำความเข้าใจการทำงานของ script ได้ดีขึ้นด้วย #!/bin/bash function chk_user { echo "running chk_user with $1" } for user in $(egrep "\b1[0-9]{3}\b" /etc/passwd | awk -F: '{print $1}') do echo "user name: $user" chk_user $user done ตัวอย่างนี้แสดงการดำเนินการกับ "ชือ" ผู้ใช้ที่มีหมายเลข (uid) ระหว่าง 1000-9999 ในแฟ้ม /etc/passwd โดยกำหนดวิธีดำเนืนการไว้ในฟังก์ชัน chk_user แต่ก่อนที่จะกำหนดคำสั่งที่ต้องการใช้จริงในฟังก์ชัน ทดสอบ flow ของโปรแกรมก่อนด้วย คำสั่ง echo เทคนิคการ debug อีกแบบหนึ่งคือ แทรกคำสั่ง echo ไว้ในจุดที่ต้องการแสดงการทำงาน หาก script เกิดทำงานผิดพลาด และผู้เขียนต้องการรู้ว่าจุดที่เกิดความผิดพลาดนั้น อยู่ตอนต้นแฟ้มหรือท้ายแฟ้ม ใช้การแทรกคำสั่ง echo กระจายไปในระหว่างจุดที่สงสัย เมื่อให้โปรแกรมทำงาน ข้อความที่ได้จากคำสั่ง echo จะช่วยบอกได้ว่าปัญหาอยู่ในบริเวณใด ถึงแม้ว่าการแทรกคำสั่ง echo จะเป็นวิธีการที่ดี แต่ทำให้เกิดปัญหาอีกอย่างหนึ่งคือ ผู้เขียนต้องตามลบบรรทัดคำสั่ง echo ที่แทรกไว้หลังจากโปรแกรมทำงานได้ถูกต้องสมบูรณ์แล้ว หากไม่อยากทำเช่นนี้ มีทางเลือกอื่นอีกสองทาง วิธีการแรกคือการติดตามการทำงานของคำสั่งแต่ละคำสั่ง โดยเปิดใช้งาน trace mode (หรือ debug mode) ซึ่งสามารถกำหนดตัวเลือกได้สองแบบคือ -x พิมพ์คำสั่งที่มีการขยาย (expansion) แล้วจึงทำงานตามคำสั่งนั้น หรือกล่าวอีกนัยหนึ่งคือแสดงคำสั่งที่มีการแทนค่า ตัวแปรหรือแทนคำสั่งด้วยผลลัพธ์ (command substitution) โดยนำหน้าบรรทัดคำสั่งด้วยอักขระ หรือสายอักขระที่ กำหนดไว้ในตัวแปร PS4 ซึ่งมีค่าโดยปริยายเป็น "+" -v แสดงบรรทัดคำสั่งที่อ่านมาจากแฟ้ม script โดยตรง (ไม่มีการขยาย) ก่อนจะทำงานตามบรรทัดคำสั่งนั้น การเปิดใช้ งาน trace mode ทำได้หลายวิธี หากกำหนดให้โปรแกรมมีชื่อเป็น myscript การกำหนดตัวเลือก -x และ/หรือ -v ทำได้หลายวิธีคือ - กำหนดไว้ในบรรทัดแรกของ script #!/bin/bash -x # หรือ #!/bin/bash -v ไม่สะดวกนัก เพราะหลังจากแก้ไขโปรแกรมให้ทำงานได้แล้ว ต้องแก้ไขบบรรทัดนี้ด้วย - ให้ myscript ทำงานในเชลล์ใหม่ $ bash -x myscript # หรือ bash -v myscript เป็นวิธีการที่สะดวก สำหรับโปรแกรมที่ไม่ยาว และไม่ซับซ้อนมากนัก - กำหนดเช่วงที่ต้องการตรวจสอบในโปรแกรม โดยกำหนดจุดเริ่มต้นด้วย set -x และจุดสุดท้ายด้วย set +x วิธีการที่สองคือการแทรกคำสั่ง echo ไว้ในโปรแกรม ซึ่งจะทำงานเฉพาะเมื่อกำหนดให้โปรแกรมทำงานใน debugging mode วิธีนี้ทำได้โดยเพิ่มอาร์กิวเมนต์สำหรับตรวจสอบ และกำหนดให้คำสั่ง echo ทำงานตามอาร์กิวเมนต์นั้น #!/bin/bash if [ $# == 2 ] && [ $2 == "debug" ]; then echo "debug mode is on" debug="on" else debug=off fi if [ $# -ge 1 ] && [ -d $1 ]; then # หากไดเรกทอรีที่กำหนด มีอยู่จริง for log in $(ls $1); do if [ $debug == "on" ]; then echo "working on $log" fi cat ./$1/$log # ดำเนินการกับในไดเรกทอรีนั้น -- ใช้ cat เป็นตัวอย่าง done else echo "USAGE: $0 logdir" exit 1 fi หากผู้ใช้ไม่กำหนดอาร์กิวเมนต์ลำดับที่สองเป็น debug คำสั้ง echo ที่วางไว้ในโครงสร้าง if จะไม่ทำง่น แนวคิดนี้ช่วยให้สามารถแก้ปัญหาของ script ได้ทั้งในปัจจุบันและอนาคต ทางเลือกอีกทางหนึ่งที่ผู้เขียนชอบคือ เขียน script ที่ตอบสนองต่อตัวแปรกำหนดการ debug และทำงานตามปกติหากไม่มีการกำหนด โดยตัวแปรดังกล่าวนี้ไม่ได้กำหนดใน script ที่แสดงในตัวอย่างที่แล้ว แต่กำหนดจากเชลล์ ดังตัวอย่าง script ต่อไปนี้ที่กำหนดให้ทำงานใน debug mode ด้วยการกำหนดและถ่ายทอดตัวแปร debug จากเชลล์ "$ export debug=1" if [ $# -ge 1 ] && [ -d $1 ]; then # หากไดเรกทอรีที่กำหนด มีอยู่จริง for log in $(ls $1); do if [ $debug ]; then echo "working on $log" fi cat ./$1/$log # ดำเนินการกับในไดเรกทอรีนั้น -- ใช้ cat เป็นตัวอย่าง done else echo "USAGE: $0 logdir" exit 1 fi วิธีการนี้เมื่อโปรแกรมทำงานแล้ว ไม่ค้องแก้ไข script เลย การที่ส่วนของโปรแกรมที่แสดงการ debug ยังคงค้างอยู่ในโปรแกรม ไม่มีผลต่อการทำงานของ script เลย และในกรณีที่เป็นโปรแกรมที่ค่อนข้างซับซ้อน วิธีการนี้จะช่วยได้มาก เมื่อต้องย้อนกลับมาบำรุงรักษา หรือแก้ไขเพิ่มเติมโปรแกรมหลังจากใช้งานมาเป็นปี และเป็นสิ่งที่ดีหากมีผู้เขียนโปรแกรมคนอื่นๆ มาดูแล และบำรุงรักษาโปรแกรมแทนผู้ที่เขียนไว้ คำแนะนำสุดท้ายก่อนจะจบบทความนี้ คือหากโปรแกรมต้องทำงานกับข้อมูลจำนวนมาก ไม่จำเป็นต้องนำข้อมูลจริงทั้งหมดมาทดลอง หากโปรแกรมต้องทำงานกับข้อมูล 20 ล้านเรคอร์ด อาจเลือกข้อมูลมาใช้ทดลอบประมาณ 100 - 1,000 เรคอร์ด แต่ต้องเป็นข้อมูลที่ครอบคลุมการทำงานของโปรแกรม เพื่อทดสอบให้แน่ใจว่าโปรแกรมไม่มีข้อผิดพลาดเหลืออยู่อีกแล้ว เรียบเรียงโดยอาศัยแนวคิดหลักจาก: Unix: Debugging your scripts, Sandra Henry-Stocker, Unix Dweeb, Network World, November 15, 2013 https://www.networkworld.com/article/2702908/operating-systems/unix--debugging-your-scripts.html