اشکالزدایی دو چندان سختتر از نوشتن کد اولیه است. بنابراین اگر شما کد را به هوشمندانهترین حالت مقدورتان بنویسید، طبق تعریف، شما برای اشکالزدایی آن به اندازه کافی با هوش نیستید. --برایان کرنیهان |
پوسته Bash شامل اشکالزدای درونی نیست، و فقط دارای ساختارها و فرمانهای مختص اشکالیابی حداقلی است. خطاهای گرامری یا اشتباهات تایپی آشکار در اسکریپتها پیغام خطاهای مرموزی تولید میکنند که اغلب کمکی به اشکالزدایی اسکریپت غیر کارآمد نمیکنند.
مثال 32-1. یک اسکریپت دارای باگ
#!/bin/bash # #این یک اسکریپت باگدار است. #کجاست، اوه خطا در کدام قسمت است؟ a=37 if [$a -gt 27 ] then echo $a fi exit $? #
خروجی اسکریپت:
./ex74.sh: [37: command not found
چه چیزی در اسکریپت فوق اشتباه است؟ اشاره: بعد از if.
مثال 32-2. فقدان کلید واژه
#!/bin/bash # #این اسکریپت چه پیغام خطایی تولید خواهد نمود؟ و چرا؟ for a in 1 2 3 do echo "$a" # #کلمهکلیدی ضروری «done» در سطر 8 توضیح شده. exit 0 #اسکریپت در اینجا خارج نمیشود! # #پس از خاتمه اسکریپت، در خط فرمان: echo $? #
خروجی اسکریپت:
missing-keyword.sh: line 10: syntax error: unexpected end of file
توجه نمایید که پیغام خطا، ضرورتاً به سطری که خطا در آن رخ داده است اشاره نمیکند، بلکه به سطری که عاقبت مفسر Bash از خطا آگاه میشود اشاره میکند.
پیغام خطا هنگام گزارش کردن شماره سطر خطای گرامری ممکن است سطرهای توضیح در اسکریپت را نادیده بگیرد.
اگر اسکریپت اجرا بشود، اما آنطور که انتظار است کار نکند چطور؟ این خطای منطقی بسیار آشنایی است.
مثال 32-3. test24: یک اسکریپت باگدار دیگر
#!/bin/bash #تصور میشود، این اسکریپت تمام فایلهای دایرکتوری جاری را #+که نام آنها شامل فاصله است، حدف کند. کار نمیکند. چرا؟ badname=`ls | grep ' '` #این را امتحان کنید: # rm "$badname" exit 0
تلاش کنید آنچه را که در مثال 32-3 اشتباه است بوسیله از توضیح خارج کردن سطر echo "$badname" پیدا کنید. دستورهای echo برای دیدن آنکه آیا آنچه در عمل به دست میآورید، همان است که مورد انتظار شماست، سودمند هستند.
در این حالت خاص، rm "$badname" نتایج دلخواه را ارایه نخواهد نمود زیرا
#
خلاصه نشانههای یک اسکریپت باگدار:
با یک پیغام خطای «syntax error» در اجرا ناکام میشود یا آنکه
اجرا میشود، اما آنطور که انتظار است کار نمیکند ( خطای منطقی).
اجرا میشود، آنطور که انتظار میرود کار میکند، اما با اثرات جانبی نامطبوع (شکست منطق).
ابزارهای اشکالزدایی اسکریپتهایی که کار نمیکنند عباتند از:
درج دستورهای echo در نقاط حساس داخل اسکریپت برای پیگردی متغیرها، و یا ارایه تصویر لحظهای از آنچه در اسکریپت روی میدهد.
حتی یک echo که فقط موقع برقرار بودن debug نمایش بدهد، مناسبتر است. # |
به کار بردن فیلتر tee برای کنترل پردازشها یا جریانهای داده در نقاط حساس.
تنظیم گزینه نشانههای -n -v -x
sh -n scriptname خطاهای گرامری را بدون اجرای واقعی اسکریپت بازرسی میکند. این معادل درج کردن set -n یا set -o noexec در داخل اسکریپت است. توجه نمایید که برخی انواع خطاهای گرامری میتوانند از چشم این بازرسی پنهان بمانند.
sh -v scriptname قبل از اجرای هر فرمان آن را نمایش میدهد. این معادل درج کردن set -v یا set -o verbose در اسکریپت است.
نشانههای -n و -v با یکدیگر خوب عمل میکنند. sh -nv scriptname یک بازرسی گرامر طویلتر ارایه میکند.
sh -x scriptname نتیجه هر فرمان را در یک حالت مختصر شده، بازتاب میدهد. این معادل درج کردن set -x یا set -o xtrace در اسکریپت است.
درج کردن set -u یا set -o nounset در اسکریپت، آن را اجرا میکند اما پیغام خطای «unbound variable» ارایه میدهد و اسکریپت را لغو میکند.
set -u #یا set -o nounset #تنظیم یک متغیر به مقدار تهی موجب خطا-انصراف نمیشود. # echo $unset_var #متغیر Unset (و تعریف نشده). echo "Should not echo!" # #
به کار بردن یک تابع «assert» در نقاط حساس داخل اسکریپت برای تست یک متغیر یا شرط. (این یک ایده وام گرفته شده از C است.)
مثال 32-4. تست کردن یک شرط با یک assert
#!/bin/bash # ####################################################################### assert () #در صورت غلط بودن شرط، خروج از { #+اسکریپت با پیغام خطای مناسب. E_PARAM_ERR=98 E_ASSERT_FAILED=99 if [ -z "$2" ] #پارامترهای کافی به تابع then #+assert() عبور داده نشده. return $E_PARAM_ERR #No damage done. fi lineno=$2 if [ ! $1 ] then echo "Assertion failed: \"$1\"" echo "File \"$0\", line $lineno" #ارایه نام فایل و شماره سطر. exit $E_ASSERT_FAILED #else #return #و ادامه اجرای اسکریپت. fi } #یک تابع assert() مشابه در اسکریپتی که باید اشکالزدایی نمایید، قرار بدهید. ####################################################################### a=5 b=4 condition="$a -lt $b" #پیغام خطا و خروج از اسکریپت. #تنظیم شرط به موردی دیگر را امتحان #+کنید و ببینید چه روی میدهد. assert "$condition" $LINENO #باقیمانده اسکریپت فقط در صورتیکه «assert» ناموفق نباشد، اجرا میشود. #چند فرمان. #چند فرمان دیگر . . . echo "This statement echoes only if the \"assert\" does not fail." #. . . #فرمانهای دیگری . . . exit $?
فرمان exit داخل یک اسکریپت، سیگنال 0 را رها میکند و پردازش، یعنی خود اسکریپت را خاتمه میدهد. [1] این مورد بیشتر اوقات برای trap کردن exit مفید است، برای مثال ارایه یک «نتیجه نهایی» از متغیرها را تحمیل میکند. trap باید اولین فرمان در اسکریپت باشد.
عملی را مشخص میکند تا در اثر دریافت یک سیگنال انجام بشود، برای اشکالزدایی هم مفید است.
یک سیگنال، پیغام فرستاده شده به یک پردازش، یا توسط کرنل یا به وسیله یک پردازش دیگر است تا به آن پردازش بگوید برخی اعمال مشخص شده (معمولاً خاتمه یافتن) را انجام بدهد. به عنوان مثال، زدن یک Control-C یک وقفه کاربر، یک سیگنال INT، به یک برنامه در حال اجرا میفرستد. |
یک نمونه ساده:
trap '' 2 #صرفنظر کردن از وقفه شماره 2 (Control-C)،بدون انجام هیچ عمل مشخصی. trap 'echo "Control-C disabled."' 2 #پیغام برای موقعی که Control-C فشرده شود.
مثال 32-5. به دام انداختن exit
#!/bin/bash #شکار کردن متغیرها با یک trap trap 'echo Variable Listing --- a = $a b = $b' EXIT #EXIT نام سیگنال تولید شده به مجرد خروج از اسکریپت است. # #فرمان تعیین شده به وسیله trap تا وقتی که یک سیگنال مقتضی #+فرستاده نشده، اجرا نخواهد گردید. echo "This prints before the \"trap\" --" echo "even though the script sees the \"trap\" first." echo a=39 b=36 exit 0 #توجه کنید که توضیح کردن فرمان exit تغییری ایجاد نمیکند، چون #+در هر صورت اسکریپت بعد از به پایان رسیدن فرمانها خارج میشود.
مثال 32-6. انجام پاکسازی بعد از Control-C
#!/bin/bash #یک اسکریپت ناپرورده برای کنترل اینکه آیا شما هنوز آنلاین هستید. umask 177 #تضمینی برای آنکه فایلهای موقتی قابل خواندن همگانی نباشند. TRUE=1 LOGFILE=/var/log/messages #توجه نمایید که $LOGFILE باید قابل خواندن باشد #+(به عنوان root از chmod 644 /var/log/messages استفاده کنید.) TEMPFILE=temp.$$ #با استفاده از id پردازش اسکریپت، یک نام فایل موقت منحصربهفرد #تولید کنید. استفاده از mktemp یک پیشنهاد است. #برای مثال: TEMPFILE=`mktemp temp.XXXXXX` KEYWORD=address #موقع ورود به سیستم سطر «remote IP address xxx.xxx.xxx.xxx» #در فایل /var/log/messages درج گردیده است. ONLINE=22 USER_INTERRUPT=13 CHECK_LINES=100 #تعداد سطرهایی از فایل ثبت رخداد که میخواهیم بازبینی بشوند. trap 'rm -f $TEMPFILE; exit $USER_INTERRUPT' TERM INT #اگر اسکریپت توسط control-c متوقف شود، فایل موقت را پاک میکند. echo while [ $TRUE ] #حلقه بیپایان. do tail -n $CHECK_LINES $LOGFILE> $TEMPFILE #100 سطر انتهای فایل log سیستم را در فایل موقت ذخیره میکند. #لازم است، چون کرنلهای جدیدتر موقع ورود به سیستم پیغامهای #log بسیار زیادی تولید میکنند. search=`grep $KEYWORD $TEMPFILE` #وجود عبارت «IP address» را که بیانگر ورود موفق است چک میکند. if [ ! -z "$search" ] #به علت فاصلههای محتمل، نقلقولها لازمند. then echo "On-line" rm -f $TEMPFILE #پاک کردن فایل موقت. exit $ONLINE else echo -n "." #گزینه -n با echo سطر جدید را منع میکند #+پس سطرهای متوالی نقطه به دست میآورید. fi sleep 1 done #نکته: اگر متغیر KEYWORD را به «Exit» تغییر بدهید، #+این اسکریپت میتواند در زمان آنلاین برای کنترل یک #+logoff غیر منتظره استفاده بشود. #تمرین: اسکریپت را طبق نکته فوق تغییر داده و آن را #تا اندازهای آراسته کنید. exit 0 #Nick Drage یک شیوه جایگزین پیشنهاد میکند: while true do ifconfig ppp0 | grep UP 1> /dev/null && echo "connected" && exit 0 echo -n "." #تا موقعی که متصل است نقطهها (.....) را چاپ میکند. sleep 2 done #مشکل: شاید Control-C برای خاتمه دادن این اسکریپت #+کافی نباشد. (ممکن است نمایش نقطهها ادامه یابد.) #تمرین: این مشکل را حل کنید. #Stephane Chazelas هنوز یک پیشنهاد دیگر دارد: CHECK_INTERVAL=1 while ! tail -n 1 "$LOGFILE" | grep -q "$KEYWORD" do echo -n . sleep $CHECK_INTERVAL done echo "On-line" #تمرین: ضعف و قوتهای نسبی هر یک از این راهکارها را مطرح کنید.
مثال 32-7. پیادهسازی سادهای از یک نوار پیشرفت
#! /bin/bash # #نوشته Graham Ewart (با قالببندی مجدد توسط نگارنده). #با مجوز در راهنمای ABS استفاده گردیده (تشکر!). #این اسکریپت را با bash احضار کنید. با sh کار نمیکند. interval=1 long_interval=10 { trap "exit" SIGUSR1 sleep $interval; sleep $interval while true do echo -n '.'استفاده از نقطهها. sleep $interval done; } & #شروع یک نوار پیشرفت به صورت یک پردازش پسزمینه. pid=$! trap "echo !; kill -USR1 $pid; wait $pid" EXIT #برای مدیریت ^C echo -n 'Long-running process ' sleep $long_interval echo ' Finished!' kill -USR1 $pid wait $pid #توقف نوار پیشرفت. trap EXIT exit $?
شناسه DEBUG با trap باعث میشود عملی که تعیین گردیده است، پس از هر فرمان داخل اسکریپت اجرا شود. برای نمونه، این شناسه ردگیری متغیرها را میسر میسازد.
#!/bin/bash trap 'echo "VARIABLE-TRACE> \$variable = \"$variable\""' DEBUG # |
البته، فرمان trap قطع نظر از اشکالزدایی دارای استفادههای دیگری هم هست، از جمله غیر فعال نمودن ضربهکلیدهای معین، در داخل یک اسکریپت (مثال A-43 را ببینید).
مثال 32-9. اجرای پردازشهای چندگانه (روی یک SMP box)
#!/bin/bash # #اجرای پردازشهای چندگانه روی یک SMP box. #نویسنده: Tedman Eng #این یکی از دو اسکریپتی است که هردو باید #+در دایرکتوری کاری جاری حاضر باشند. #================ اسکریپت اول ================ LIMIT=$1 #تعداد کل پردازشها برای شروع NUMPROC=4 #تعداد نخهای جاری (انشعابها؟) PROCID=1 #ID پردازش شروع echo "My PID is $$" function start_thread() { if [ $PROCID -le $LIMIT ] ; then ./child.sh $PROCID& let "PROCID++" else echo "Limit reached." wait exit fi } while [ "$NUMPROC" -gt 0 ]; do start_thread; let "NUMPROC--" done while true do trap "start_thread" SIGRTMIN done exit 0 #================ اسکریپت دوم ================= #!/bin/bash # #اجرای پردازشهای چندگانه روی یک SMP box #این اسکریپت به وسیله parent.sh فراخوانی میشود. #نویسنده: Tedman Eng temp=$RANDOM index=$1 shift let "temp %= 5" let "temp += 4" echo "Starting $index Time:$temp" "$@" sleep ${temp} echo "Ending $index" kill -s SIGRTMIN $PPID exit 0 #======================= ملاحظات نویسنده اسکریپت ====================== # #این اسکریپت کاملاً خالی از باگ نیست. #من اسکریپت را با limit = 500 اجرا کردم و بعد از صد و اندی تکرار نخست #+یکی از نخهای جاری ناپدید گردید! #اگر با سیگنالهای trap یا مورد دیگری تلاقی کند، مطمئن نیست. #یکبار که trap دریافت میشود، در حین اجرای گرداننده trap اما قبل از اینکه #+trap بعدی تنظیم شود، زمان کوتاهی وجود دارد. طی این مدت ممکن است یک #+سیگنال trap، و بنابراین تولید مثل یک پردازش فرزند از دست برود. #بدون شک شخصی میتواند باگ را کشف نماید و آن را خواهد نوشت. . . در آینده. # # # #در ادامه، اسکریپت اولیه نوشته شده توسط Vernia Damiano آمده است. #متاسفانه، به طور صحیح کار نمیکند. ################################################################# #!/bin/bash #اسکریپت باید با حداقل یک پارامتر عدد صحیح فراخوانی شود #+(تعداد پردازشهای جاری). #سایر پارامترها از طریق پردازشهای شروع شده عبور داده میشوند. INDICE=8 #تعداد کل پردازشها برای شروع TEMPO=5 #حداکثر زمان عدم فعالیت هر پردازش E_BADARGS=65 #شناسه(ها) به اسکریپت داده نشده است. if [ $# -eq 0 ] #بازرسی وجود حداقل یک شناسه برای اسکریپت. then echo "Usage: `basename $0` number_of_processes [passed params]" exit $E_BADARGS fi NUMPROC=$1 #تعداد پردازشهای جاری shift PARAMETRI=( "$@" ) #پارامترهای هر پردازش function avvia() { local temp local index temp=$RANDOM index=$1 shift let "temp %= $TEMPO" let "temp += 1" echo "Starting $index Time:$temp" "$@" sleep ${temp} echo "Ending $index" kill -s SIGRTMIN $$ } function parti() { if [ $INDICE -gt 0 ] ; then avvia $INDICE "${PARAMETRI[@]}" & let "INDICE--" else trap : SIGRTMIN fi } trap parti SIGRTMIN while [ "$NUMPROC" -gt 0 ]; do parti; let "NUMPROC--" done wait trap - SIGRTMIN exit $? : <<SCRIPT_AUTHOR_COMMENTS I had the need to run a program, with specified options, on a number of different files, using a SMP machine. So I thought [I'd] keep running a specified number of processes and start a new one each time . . . one of these terminates. The "wait" instruction does not help, since it waits for a given process or *all* process started in background. So I wrote [this] bash script that can do the job, using the "trap" instruction. --Vernia Damiano SCRIPT_AUTHOR_COMMENTS
trap '' SIGNAL (دو نقلقول منفردِ همجوار) برای باقیمانده اسکریپت SIGNAL را غیر فعال میکند. trap SIGNAL یکبار دیگر عمل کردن SIGNAL را بازیابی میکند. این کار برای محافظت از بخشهای حساس اسکریپت در برابر یک وقفه ناخواسته مفید است. |
trap '' 2 #سیگنال 2 یعنی Control-C اکنون غیر فعال شده است. command command command trap 2 #دوباره Control-C فعال میشود.
Bash در نگارش 3، متغیرهای داخلی زیر را برای استفاده در اشکالزدایی اضافه نموده.
|
[1] | مطابق قرارداد، signal 0 به exit تخصیص داده میشود. |