فصل ‎32‎- اشکالزدایی

 

اشکالزدایی دو چندان سخت‌تر از نوشتن کد اولیه است. بنابراین اگر شما کد را به هوشمندانه‌ترین حالت مقدورتان بنویسید، طبق تعریف، شما برای اشکالزدایی آن به اندازه کافی با هوش نیستید.

--برایان کرنیهان

پوسته ‎Bash‎ شامل اشکال‌زدای درونی نیست، و فقط دارای ساختارها و فرمان‌های مختص اشکال‌یابی حداقلی است. خطاهای گرامری یا اشتباهات تایپی آشکار در اسکریپت‌ها پیغام خطاهای مرموزی تولید می‌کنند که اغلب کمکی به اشکالزدایی اسکریپت غیر کارآمد نمی‌کنند.

مثال ‎32-1‎. یک اسکریپت دارای باگ‌

#!/bin/bash
# ex74.sh

#        این یک اسکریپت باگ‌دار است.
# کجاست، اوه  خطا در کدام قسمت است؟

a=37

if [$a -gt 27 ]
then
  echo $a
fi  

exit $?   # 0! Why?

خروجی اسکریپت:

./ex74.sh: [37: command not found

چه چیزی در اسکریپت فوق اشتباه است؟ اشاره: بعد از if.

مثال ‎32-2‎. فقدان کلید واژه

#!/bin/bash
# missing-keyword.sh
# این اسکریپت چه پیغام خطایی تولید خواهد نمود؟ و چرا؟

for a in 1 2 3
do
  echo "$a"
# done    #کلمه‌کلیدی ضروری «done» در سطر ‎8‎ توضیح شده.

exit 0    #             اسکریپت در اینجا خارج نمی‌شود!

# ================================================== #

#                   پس از خاتمه اسکریپت، در خط فرمان:
  echo $?    #  2

خروجی اسکریپت:

missing-keyword.sh: line 10: syntax error: unexpected end of file

توجه نمایید که پیغام خطا، ضرورتاً به سطری که خطا در آن رخ داده‌ است اشاره نمی‌کند، بلکه به سطری که عاقبت مفسر Bash از خطا آگاه می‌شود اشاره می‌کند.

پیغام خطا هنگام گزارش کردن شماره سطر خطای گرامری ممکن است سطرهای توضیح در اسکریپت را نادیده بگیرد.

اگر اسکریپت اجرا بشود، اما آنطور که انتظار است کار نکند چطور؟ این خطای منطقی بسیار آشنایی است.

مثال ‎32-3‎.‏ test24: یک اسکریپت باگ‌دار دیگر

#!/bin/bash

# تصور می‌شود، این اسکریپت تمام فایل‌های دایرکتوری جاری را
#+ که نام آنها شامل فاصله‌ است، حدف کند. کار نمی‌کند. چرا؟


badname=`ls | grep ' '`

# این را امتحان کنید:
# echo "$badname"

rm "$badname"

exit 0

تلاش کنید آنچه را که در مثال ‎32-3‎ اشتباه است بوسیله از توضیح خارج کردن سطر ‎echo "$badname"‎ پیدا کنید. دستورهای echo برای دیدن آنکه آیا آنچه در عمل به دست می‌آورید، همان است که مورد انتظار شماست، سودمند هستند.

در این حالت خاص، ‎rm "$badname"‎ نتایج دلخواه را ارایه نخواهد نمود زیرا ‎$badname‎ نباید نقل‌قولی باشد. قرار دادن آن در نقل‌قول‌ها اطمینان می‌دهد که rm دارای تنها یک شناسه است (فقط با یک نام فایل مطابقت می‌کند). یک تعمیر نیمه‌کامل آن، حذف نقل‌قول‌ها از ‎$badname‎ و تنظیم مجدد ‎$IFS‎ به صورت ‎IFS=$'\n'‎ است تا فقط شامل کاراکتر سطرجدید باشد. اما، روش‌های ساده‌تری هم برای دور زدن آن وجود دارد.

# روش‌های صحیح حذف کردن فایل‌هایی با نام شامل کاراکترهای فاصله‌.
rm *\ *
rm *" "*
rm *' '*
# با تشکر از ‎S.C.‎

خلاصه نشانه‌های یک اسکریپت باگ‌دار:

  1. با یک پیغام خطای «‎syntax error‎» در اجرا ناکام می‌شود یا آنکه

  2. اجرا می‌شود، اما آنطور که انتظار است کار نمی‌کند ( خطای منطقی).

  3. اجرا می‌شود، آنطور که انتظار می‌رود کار می‌کند، اما با اثرات جانبی نامطبوع (شکست منطق).

ابزارهای اشکالزدایی اسکریپت‌هایی که کار نمی‌کنند عباتند از:

  1. درج دستورهای echo در نقاط حساس داخل اسکریپت برای پیگردی متغیرها، و یا ارایه تصویر لحظه‌ای از آنچه در اسکریپت روی می‌دهد.

  2. tip

    حتی یک echo که فقط موقع برقرار بودن debug نمایش بدهد، مناسب‌تر است.

    ### ‎debecho (debug-echo)‎، توسط ‎Stefano Falsetto‎ ###
    #  ## فقط اگر ‎DEBUG‎ به یک مقدار تنظیم شده باشد، ###
    #    ## پارامترهای داده شده را نمایش خواهد داد. ###
    debecho () {
      if [ ! -z "$DEBUG" ]; then
         echo "$1" >&2
         #         ‎^^^‎     ‎stderr‎ به
      fi
    }
    
    DEBUG=on
    Whatever=whatnot
    debecho $Whatever   # whatnot
    
    DEBUG=
    Whatever=notwhat
    debecho $Whatever   # (echo نخواهد کرد.)

  3. به کار بردن فیلتر tee برای کنترل پردازش‌ها یا جریان‌های داده در نقاط حساس.

  4. تنظیم گزینه نشانه‌های ‎-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» ارایه می‌دهد و اسکریپت را لغو می‌کند.

  5. set -u   # یا  ‎set -o nounset‎
    
    # تنظیم یک متغیر به مقدار تهی موجب خطا-انصراف نمی‌شود.
    # unset_var=
    
    echo $unset_var   # متغیر ‎Unset‎ (و تعریف نشده).
    
    echo "Should not echo!"
    
    
    # sh t2.sh
    # t2.sh: line 6: unset_var: unbound variable

  6. به کار بردن یک تابع «assert» در نقاط حساس داخل اسکریپت برای تست یک متغیر یا شرط. (این یک ایده وام گرفته شده از C است.)

  7. مثال ‎32-4‎. تست کردن یک شرط با یک ‎assert‎

    #!/bin/bash
    # assert.sh
    
    #######################################################################
    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 $?
  8. به کار بردن متغیر ‎$LINENO‎ و فرمان داخلی caller.

  9. به دام انداختن exit.

    فرمان exit داخل یک اسکریپت، سیگنال 0 را رها می‌کند و پردازش، یعنی خود اسکریپت را خاتمه می‌دهد. ‎[1]‎ این مورد بیشتر اوقات برای trap کردن exit مفید است، برای مثال ارایه یک «نتیجه نهایی» از متغیرها را تحمیل می‌کند. trap باید اولین فرمان در اسکریپت باشد.

به دام انداختن سیگنال‌ها

trap

عملی را مشخص می‌کند تا در اثر دریافت یک سیگنال انجام بشود، برای اشکالزدایی هم مفید است.

یک نمونه ساده:

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

# logon.sh: یک اسکریپت ناپرورده برای کنترل اینکه آیا شما هنوز آنلاین هستید.

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
# progress-bar2.sh
#  نوشته ‎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 باعث می‌شود عملی که تعیین گردیده است، پس از هر فرمان داخل اسکریپت اجرا شود. برای نمونه، این شناسه ردگیری متغیرها را میسر می‌سازد.

مثال ‎32-8‎. پیگردی یک متغیر

#!/bin/bash

trap 'echo "VARIABLE-TRACE> \$variable = \"$variable\""' DEBUG
# بعد از هر فرمان مقدار ‎$variable‎ را بازتاب می‌دهد.

variable=29; line=$LINENO

echo "  Just initialized \$variable to $variable in line number $line."

let "variable *= 3"; line=$LINENO
echo "  Just multiplied \$variable by 3 in line number $line."

exit 0

#  ساختار «‎trap 'command1 . . . command2 . . .' DEBUG‎» در متن یک
#+ اسکریپت پیچیده، جایی که درج چندین جمله «echo ‎$variable‎ » شاید
#+                     ناخوشایند و وقت‌گیر باشد، بیشتر مناسب است.

#     ‎Stephane Chazelas‎ برای اشاره نمودن به این نکته تشکر می‌کنم.


Output of script:

VARIABLE-TRACE> $variable = ""
VARIABLE-TRACE> $variable = "29"
  Just initialized $variable to 29.
VARIABLE-TRACE> $variable = "29"
VARIABLE-TRACE> $variable = "87"
  Just multiplied $variable by 3.
VARIABLE-TRACE> $variable = "87"

البته، فرمان trap قطع نظر از اشکالزدایی دارای استفاده‌های دیگری هم هست، از جمله غیر فعال نمودن ضربه‌کلیدهای معین، در داخل یک اسکریپت (مثال ‎A-43‎ را ببینید).

مثال ‎32-9‎. اجرای پردازش‌های چندگانه (روی یک ‎SMP box‎)

#!/bin/bash

# parent.sh
# اجرای پردازش‌های چندگانه روی یک ‎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

# child.sh
#         اجرای پردازش‌های چندگانه روی یک ‎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‎ فعال می‌شود.
	

یادداشت‌ها

‎[1]‎

مطابق قرارداد، ‎signal 0‎ به exit تخصیص داده می‌شود.