عیبیابی در اسکریپتنویسی
اسکریپت ما پیچیدهتر شده است و زمان آن رسیده که نگاهی به آن بیندازیم و ببنیم اتفاقی درون آن رخ داده است. در این درس نگاهی به برخی از رایجترین انواع خطاهایی که درون اسکریپتها رخ میدهد خواهیم انداخت و برخی از تکنیکها را به منظور عیبیابی و برطرف کردن خطاها توصیف میکنیم.
خطاهای نحوی (Syntactic Errors)
یک کلاس اصلی از خطاها خطاهای نحوی است. خطاهای نحوی در اثر اشتباهات تایپی برخی عناصر درون سینتکس شل (Shell) بوجود میآید. در بیشتر موارد این نوع خطاها موجب میشود که شل (Shell) از اجرای اسکریپت ممانعت کند.
در ادامه ما از این اسکریپت برای توضیح انواع خطاها استفاده خواهیم کرد:
#!/bin/bash
# trouble: script to demonstrate common errors
number=1
if [ $number = 1 ]; then
echo "Number is equal to 1."
else
echo "Number is not equal to 1."
fi
همانگونه که نوشته شده این اسکریپت با موفقیت اجرا میشود:
[me@linuxbox ~]$ trouble Number is equal to 1.
کوتیشنهای جامانده (Missing Quotes)
خوب بیایید اسکریپت خود را ویرایش کنیم و یک دابل کونیشن را حذف کنیم:
#!/bin/bash
# trouble: script to demonstrate common errors
number=1
if [ $number = 1 ]; then
echo "Number is equal to 1.
else
echo "Number is not equal to 1."
fi
اکنون آن را اجرا می کنیم تا ببینیم چه اتفاقی رخ خواهد داد:
[me@linuxbox ~]$ trouble /home/me/bin/trouble: line 10: unexpected EOF while looking for matching `"' /home/me/bin/trouble: line 13: syntax error: unexpected end of file
چه اتفاقی رخ داد؟
دو خطا ایجاد شد. خیلی جالب خطاها را نشان میدهد. شماره خطوطی که گزارش شده جایی نیست که کوتیشنها را حذف کردیم! بلکه کمی جلوتر در برنامه است. میتوانیم ببنیم که چرا جلوتر این خطا نشان داده شده است.
به این دلیل که بش (Bash) میخواهد به دنبال یک دایل کوتیشن بگردد تا اینکه یکی را پیدا میکنند که این دقیقا پس از فرمان
echo دوم است . بعد از این نقطه دیگر بش (Bash) کامل گیج شده و سینتکس دستور if شکسته میشود چونکه در این حالت عبارت fi اکنون درون یک رشته کوتیشندار باز است. در اسکریپتهای بلند. این نوع خطا را به سختی میتوان یافت و استفاده از یک ویرایشگر متنی که سینتکس شما را به خوبی هایلایت میکنند در خطا یابی کمک شایانی می کند.
توکنهای جامانده یا غیرمنتظره
یک خطای رایج دیگر این است که فراموش کنیم یک فرمان ترکیبی مثل که if یا while را کامل کنیم نگاه کنید که اگر ویرگول نقطه را از بعد از تست درون فرمان if حذف کنیم چه اتفاقی میافتد:
#!/bin/bash
# trouble: script to demonstrate common errors
number=1
if [ $number = 1 ] then
echo "Number is equal to 1."
else
echo "Number is not equal to 1."
fi
نتیجه اجرای این دستور به صورت زیر است:
[me@linuxbox ~]$ trouble /home/me/bin/trouble: line 9: syntax error near unexpected token `else' /home/me/bin/trouble: line 9: `else'
بار دیگر ناحیه ای بعد از رخ دادن خطا را نشان میدهد. اتفاقی که رخ میدهد واقعا جالب است. همانطور که گفتیم if یک لیست از فرمانها را قبول کرده و که خروج آخرین فرمان در لیست را ارزیابی میکند. در برنامه ما قصد داریم که این لیست شامل یک فرمان ] باشد. دستور ] آنچه به دنبالش بیاید را به عنوان آرگومانها میگیرد. در این مورد چهار آرگومان $number = و ۱ و ] میباشند. وقتیکه ویرگول نقطه (:) حذف گردد. در نتیجه کلمه then به لیست آرگومانها اضافه میشود که از نظر
نحوی ایرادی ندارد. همچنین دستور echo بعد از آن نیز از نظر نحوی ایرادی ندارد. این فرمان به عنوان فرمان دیگری در لیست فرمانها تفسیر میشود که که خروج آن را ارزیابی میکنند. سپس با else مواجه میشود ولی این درجای خود نیست چرا که شل (Shell) آن را به عنوان یک کلمه رزرو شده کلمهای که معنی خاصی در شل دارد در نظر میگیرد و به عنوان نام یک فرمان به کار نمیرود و در نتیجه از اینجا به بعد پیام خطا بوجود میآید.
بسطهای پیشبینی نشده (Unanticipated Expansions)
ممکن است که خطاهایی داشته باشیم که فقط به صورت متناوب درون یک اسکریپت رخ دهند. گاهی اوقات اسکریپت به خوبی اجرا میشود و زمانهای دیگر به دلیل نتایج یک بسط یا شکست مواجه میشود. به کد خود برمیگردیم ویرگول نقطه جا انداخته را تصحیح کرده و سپس این بار مقدار number را به یک متغیر خالی تغییر دهید:
#!/bin/bash
# trouble: script to demonstrate common errors
number=
if [ $number = 1 ]; then
echo "Number is equal to 1."
else
echo "Number is not equal to 1."
fi
اکنون باردیگر فرمان را با تغییرات ایجاد شده اجرا کنید:
[me@linuxbox ~]$ trouble /home/me/bin/trouble: line 7: [: =: unary operator expected Number is not equal to 1.
یک خطای مرموز را به همراه خروجی دومین فرمان echo دریافت می کنیم. مشکل بسط متغیر number درون فرمان test میباشد. زمانی که فرمان [ $number = 1 ] دستخوش بسط با یک مقدار number خالی میشود نتیجه بدست آمده اینگونه [ = 1 ] است که این مقدار نامعتبر است و در نتیجه خطا ایجاد میشود.
عملگر = یک عملگر باینری هست نیاز به مقدار در دو طرف خود دارد ولی در اینجا مقدار اولی وجود ندارد بنابراین فرمان تست به جای آن منتظر یک عملگر یگانی (مثل -z) می باشد که وجود ندارد. در ادامه از آنجایی که تست به دلیل خطا با شکست
مواجه شد. دستور if کدخروج غیر صفر دریافت کرده و بر این اساس عمل کرده و فرمان دوم echo اجرا میشود. این مشکل را میتوان با اضافه کردن دابل کوتیشن دور آرگومان اول درون فرمان test بر طرف ساخت یعنی [ “$number” = 1 ] سپس زمانیکه بسط صورت پذیرد. نتیجه به این صورت خواهد بود: [ “” = 1 ]
خطاهای منطقی (Logical Errors)
بر خلاف خطاهای نحوی، خطاهای منطقی یک اسکریپت را از اجرا باز نمیدارند. اسکریپت اجرا خواهد شد ولی نتیجه دلخواه را برای ما به ارمغان نمی آورد. چرا؟ به این دلیل که منطق برنامه مشکل دارد. تعداد بیشماری از خطاهای منطقی ممکن وجود دارد ولی در اینجا برخی از رایجترین انواع آنها را اشاره میکنیم:
۱) عبارات شرطی نادرست: خیلی اتفاق میافتد که یک عبارت if/then/else را به صورت نادرست تایپ کنیم و در نتیجه منطق برنامه نیز نادرست خواهد بود. برخی اوقات حتی منطق برنامه معکوس میشود.
2) خطاهای Off by one: زمانیکه حلقهها را کدنویسی میکنیم. ممکن است تا متوجه نشویم که شماره گذاری به جای ۱ با صفر آغاز میشود تا در نتیجه آن مقدار count در زمان صحیح به حلقه پایان دهد. این نوع خطاها باعث میشوند که یا حلقه دیرتر پایان پذیرد یا اینکه آخرین تکرار حلقه جا مانده و حلقه زودتر پایان پذیرد.
3) موقعیتهای پیشبینی نشده: بیشتر خطاهای منطقی که از مواجهه یک برنامه با داده یا موقعیتهایی که توسط برنامهنویس دیده نمیشوند حاصل میشود. علاوه بر این، این خطاها میتواند شامل بسطهای بیپاسخ باشد.
برنامه نویسی دفاعی (Defensive Programming)
همیشه در حین برنامهویسی فرضیات را در نظر بگیریم. یعنی اینکه یک ارزیابی حساب شده سنجیده و دقیق در وضعیتهای خروج برنامه و فرمانهایی که در اسکریپت استفاده میشوند انجام شود. در اینجا یک مثال را بر اساس یک داستان واقعی آوردهایم. یک مدیر بداقبال سیستم اسکریپتی را نوشت تا وظیفه نگهداری بر روی یک سرور مهم را انجام دهد اسکربیت دارای کدهای زیر میباشد:
cd $dir_name rm *
هیچ چیز غلطی در این دو خط فرمان وجود ندارد. تا کی؟ تا زمانیکه پوشهای که درون متغیر dir_name نامگذاری شده وجود داشته باشد. ولی اگر وجود نداشته باشد چه اتفاقی رخ خواهد داد؟ در این مورد فرمان با شکست مواجه میشود و اسکریپت ادامه پیدا می کند به خط بعدی کد و در نتیجه آن! وای همه قابلهای پوشه فعلی را پاک خواهد کرد. اصلا خروجی دلخواه به دست نیامد. در نتیجه آن مدیر بیچاره سیستم کل دادههای مهم سرور را به خاطر یک تصمیم نادرست در طراحی که از بین برد. خوب حالا نگاهی بیندازیم که چگونه میتوان راهی پیدا کرد تا طراحی این کد بهینه شود. ابتدا هوشمندانه است که دستور rm را فقط در صورت موفقیتآمیز بودن فرمان cd اجرا کنیم به این صورت:
cd $dir_name && rm *
با این شیوه اگر فرمان cd با شکست مواجه شود. فرمان rm نیز اجرا نخواهد شد. این شیوه بهتر است ولی هنوز این امکان وجود دارد که متغیر dir_name تعیین نشده و خالی باشد که موجب آن میشود که فایلهای پوشه خانگی کاربر پاک شوند. از این موضوع نیز میتوان ممانعت کرد. چگونه؟ با بررسی کردن اینکه dir_name واقعا حاوی اسم پوشه موجود باشد:
[[ -d $dir_name ]] && cd $dir_name && rm *
برخی اوقات خوب است در چنین شرایط اسکریپت را با یک خطا متوقف کرد:
if [[ -d $dir_name ]]; then
if cd $dir_name; then
rm *
else
echo "cannot cd to '$dir_name'" >&2
exit 1
fi
else
echo "no such directory: '$dir_name'" >&2
exit 1
fi
در اینجا هر دو اسم را بررسی میکنیم تا ببینیم که آیا این پوشه موجود است یا نه و آیا فرمان cd با موفقیت اجرا شده یا نه. اگر هر کدام از این تستها با شکست مواجه شود یک خطای توصیفی به ما نشان داده میشود و خطا در صفحه نمایش مشاهده میشود و اسکربیت از بین میرود و با مقدار وضعیت خروج ۱ که نشانه شکست دستور است پایان میپذیرد.
تایید صحت ورودیها (Verifying Input)
یک قانون اصلی در برنامهنویسی این است که اگر یک برنامه ورودی را قبول میکنند. بایستی قادر باشد تا با هر مقدار دریافتی کار کند. یعنی اینکه ورودی بایستی با دقت بررسی شود تا اطمینان پیدا کنیم که معتبر است و برای پردازشهای بعدی پذیرفته شده است. در دروس قبلی وقتی درباره دستور read میخواندیم یک نمونه را دیدیم یک اسکربیت که حاوی تست زیر برای تایید انتخاب منو است:
[[ $REPLY =~ ^[0-3]$ ]]
این تست بسیار ویژه است. این فرمان فقط در صورتیکه رشته بازگشتی توسط کاربر یک عدد بین ۰ تا ۳ باشد یک وضعیت خروجی صفر را بازمیگرداند و هیچ مقدار دیگری پذیرفته نخواهد شد. برخی اوقات نوشتن این نوع تستها میتواند بسیار چالشانگیز باشد ولی با تلاش زیاد میتوان یک اسکریپت را با کیفیت بالا ایجاد نمود.
آزمون کردن (Testing)
آزمون کردن یک گام مهم در هر نوع توسعه نرمافزاری میباشد و شامل اسکریپت ها هم میشود یک اصطلاحی در دنیای متنباز وجود دارد که میگوید: release early, release often
این اصطلاح به معنی عرضه کردن زود و عرضه کردن مکرر نرمافزار میباشد. به چه معنی؟ یعنی اینکه با انتشار زود یک محصول برنامه این فرصت را پیدا میکند تا هر چه بیشتر اشکالاتش شناسایی گردد و برای استفاده هر چه بهتر آماده شود. تجربه نشان داده است که پیدا کردن ایرادهای نرمافزاری بسیار سادهتر و کم هزینهتر خواهد بود اگر که بسیار زود و در طی پروسه چرخه توسعه نرمافزار برطرف گردند.
Stubs
در گفتگوهای قبلی دیدیم که چگونه stubs را میتوان به منظور تایید جریان برنامه استفاده کرد. استفاده از stubs از مراحل اولیه توسعه اسکریپت به منظور بررسی پیشرفت کار تکنیکی ارزشمند است.
خوب نگاهی به مشکل قبلی حذف فایل بیندازیم و ببینیم که چگونه میتوان آن را برای یک تست آسان، کدنویسی کرد. تست کردن بخش اصلی کد، از آنجایی که هدفش حذف فایلهاست میتواند خطرناک باشد ولی ما میتوانیم کد را تغییر دهیم تا
است را ایمن کنیم:
if [[ -d $dir_name ]]; then
if cd $dir_name; then
echo rm * # TESTING
else
echo "cannot cd to '$dir_name'" >&2
exit 1
fi
else
echo "no such directory: '$dir_name'" >&2
exit 1
fi
exit # TESTING
از آنجایی که موقعیتهای خطا هم اکنون پیامهای مفیدی را نشان دادهاند ما نبایستی هیچ چیزی را اضافه کنیم. مهمترین بخش تغییر این است که یک فرمان echo درست قبل از فرمان قرار دهیم. چرا؟ تا به دستور و لیست آرگومانهایش اجازه دهیم. به جای اجرا نمایش داده شوند. این تغییر موجب اجرای ایمن فرمان میشود. در بخش آخر کد، یک دستور exit قرار میدهیم تا به تست پایان دهیم و از اجرای هر بخش دیگر اسکریپت ممانعت کنیم.
همچنین برخی کامنت ها را قرار میدهیم. اینها میتوانند به منظور کمک به پیدا کردن و حذف تغییرات در زمان کامل شدن تست استفاده شوند.
موارد آزمون (Test Cases)
برای اجرای تست مفید مهم است که موارد تست (tat case) خوبی را توسعه و اعمال کنیم. این کار را میتواند با انتخاب با دقت داده ورودی یا شرطهای عملیاتی خوب انجام داد. ما در کد خود که بسیار هم ساده است میخواهیم بدانیم که چگونه
که در سه شرط ویژه زیر انجام میشود:
- اینکه dr_name حاوی نام یک پوشه موجود باشد.
- اینکه dr_name حاوی نام یک پوشه ناموجود باشد.
- اینکه dr_name خالی باشد.
با انجام تست بر روی هر یک از این شرایط یک آزمون خوب را بدست می آوریم.
درست مثل طراحی تست نیز یک تابع زمان است و هر ویژگی اسکریپت نیاز به تست گسترده ندارد.
اشکالزدایی (Debugging)
اگر تست کردن مشکلی را در یک اسکریپت آشکار کرد. گام بعدی اشکالزدایی است. یک مشکل معمولا به این معنی است که اسکریپت به نحوی بر طبق انتظارات برنامهنویس عمل نمیکند. در این مورد ما نیاز داریم که با دقت تشخیص دهیم دقیقا اسکریپت چه کار میکند و چرا اینگونه عمل میکند. پیدا کردن اشکالات و باگها برخی مواقع می تواند کارهای شناسایی زیادی را طلب کند.
اسکریپتی که به خوبی طراحی شده است به اشکالزدایی کمک شایانی خواهد کرد. برنامه بایستی به صورت دفاعی برنامهنویسی شده باشد تا شرایط غیر عادی را را تشخیص دهد و فیدبکهای مفیدی را در اختیار کاربر قرار دهد. هرچند برخی مواقع، مشکلات عجیب و غیرمنتظره هستند و نیازمند تکنیکهای بیشتری برای اشکالزدایی هستند.
در برخی اسکریپتها بهویژه اسکربیتهای طولانی و بلند گاها مفید است منطقهای از اسکریپت را که خطا در آن رخ داده و با مشکل مرتبط است را ایزوله و جدا کنیم.
این همیشه خطای واقعی نخواهد بود ولی ایزوله کردن اغلب نشانهای از خطای حقیقی را به ما خواهد داد. یک تکنیک که میتوان به منظور ایزوله کردن کد استفاده کرد کامنت کردن بخش دارای خطای اسکریپت است وقتی که شما خطی از کد را کامنت میکنید یعنی اینکه این خط دیگر جزوی از کد نخواهد بود و اجرا نخواهد شد. برای مثال بخش حذف قابل را میتوان تغییر داد تا تشخیص دهیم که آیا بخش حذف شده مرتبط با خطا بوده است یا خیر:
if [[ -d $dir_name ]]; then
if cd $dir_name; then
rm *
else
echo "cannot cd to '$dir_name'" >&2
exit 1
fi
# else
# echo "no such directory: '$dir_name'" >&2
# exit 1
fi
با قرار دادن علامت کامنت (#) در ابتدای هر خط در حقیقت از اجرای آن خط از کد جلوگیری میکنیم. پس از این کار میتوان بار دیگر تست را انجام داد تا مشاهده کرد تاثیر حذف این خط از کد چه بود است.
ترسیم (Tracing)
باگها اغلب موارد غیرمنتظره جریان منطقی در یک اسکریپت هستند. بخشهایی از اسکریپت هستند که یا هرگز اجرا نمیشوند یا به شیوهای غلط یا در زمان غلط اجرا میشوند. به منظور نمایش جریان واقعی یک برنامه از تکنیکی با نام tracing یا همان ترسیم استفاده میکنیم. ترسیم مستلزم قراردادن پیامهای آگاه کننده در اسکریپت است که موقعیت اجرای فرمان را نمایش میدهد ما میتوانیم پیامهایی را به بخش که خود اضافه کنیم:
echo "preparing to delete files" >&2
if [[ -d $dir_name ]]; then
if cd $dir_name; then
echo "deleting files" >&2
rm *
else
echo "cannot cd to '$dir_name'" >&2
exit 1
fi
else
echo "no such directory: '$dir_name'" >&2
exit 1
fi
echo "file deletion complete" >&2
ما پیامهایی را به خطای استاندارد میفرستیم تا آنها را از خروجی عادی جدا کنیم. همچنین خطوطی که حاوی پیامهایی هستند را بدون تورفتگی مینویسیم تا پیدا کردن آنها در که آسانتر باشد.
علاوه بر این روش بش (Bash) متدی دیگر را برای ترسیم فراهم کرده است که بوسیله گزینه -x و فرمان set پیادهسازی شده میشود. با استفاده از اسکریپت trouble که قبلا ساخته بودیم میتوانیم ترسیم را برای کل اسکرییت با اضافه کردن گزینه -x به خط اول فعال کنیم:
#!/bin/bash -x
# trouble: script to demonstrate common errors
number=1
if [ $number = 1 ]; then
echo "Number is equal to 1."
else
echo "Number is not equal to 1."
fi
اکنون نتیجهای مشابه زیر را دریافت میکنیم:
[me@linuxbox ~]$ trouble + number=1 + '[' 1 = 1 ']' + echo 'Number is equal to 1.' Number is equal to 1.
با فعال کردن ترسیم میبینیم که دستورات به همراه بسطهای اعمال شده انجام میشوند. علامت های بعلاوه (+) که در ابتدای خطوط آمده است نمایش ترسیم را برای تفاوت قائل شدن با خطوط عادی نشان میدهد. علامت بعلاوه کاراکتر پیشفرض برای ترسیم خروجی است. این کاراکتر درون متغير Prompt String 4) PS4) قرار دارد. محتویات این متغیر را میتوان تغییر داد تا یک پیامواره (prompt) مفیدتر ایجاد کنیم. در اینجا آن را ویرایش میکنیم تا شماره خطوط فعلی را درون اسکریپت قرار دهیم.
توجه داشته باشید که به منظور ممانعت از بسط بایستی آن را درون تک کوتیشن قرار دهید:
[me@linuxbox ~]$ export PS4='$LINENO + ' [me@linuxbox ~]$ trouble 5 + number=1 7 + '[' 1 = 1 ']' 8 + echo 'Number is equal to 1.' Number is equal to 1.
واقعا زیباست. به منظور انجام یک ترسیم بر روی بخشی از یک اسکریپت به جای کل اسکریپت میتوانیم فرمان set را به همراه گزینه -x استفاده کنیم:
#!/bin/bash
# trouble: script to demonstrate common errors
number=1
set -x # Turn on tracing
if [ $number = 1 ]; then
echo "Number is equal to 1."
else
echo "Number is not equal to 1."
fi
set +x # Turn off tracing
ابتدا بایستی به بخش آغازین مورد نظر اسکریپت خود رفته و فرمان set را به همراه گزینه -x به کار ببرید. سپس به بخش پایانی یعنی جایی که میخواهید ترسیم پایان یابد بروید و فرمان set را به همراه گزینه -x به کار ببرید. این تکنیک را میتوان به منظور آزمون چندین بخش از یک کد نیز به کار برد.
آزمون مقادیر در حین اجرا
ما میتوانیم در طی پروسه ترسیم محتوای متغیرها را نیز نمایش دهیم تا کار درونی یک اسکریپت را در حین اجرا بینیم. با اضافه کردن عبارات اضافی echo این ترفند را اعمال میکنیم:
#!/bin/bash
# trouble: script to demonstrate common errors
number=1
echo "number=$number" # DEBUG
set -x # Turn on tracing
if [ $number = 1 ]; then
echo "Number is equal to 1."
else
echo "Number is not equal to 1."
fi
set +x # Turn off tracing
در این مثال ساده میبینیم که به سادگی مقدار متغیر number را نمایش میدهیم و خط اضافه شده را با یک کامنت همراه میکنیم تا در صورت نیاز بعدا به سادگی بتوانیم آن را حذف کنیم. این تکنیک بهویژه زمانی مفید است که میخواهیم رفتار حلقهها و محاسبات را درون یک اسکریپت در حین اجرا مشاهده کنیم.
درباره فرشید نوتاش حقیقت
همیشه نیازمند یک منبع آموزشی فارسی در حوزه نرمافزارهای آزاد/ متنباز و سیستمعامل گنو/لینوکس بودم. از این رو این رسالت رو برای خودم تعریف کردم تا رسانه «محتوای باز» رو بوجود بیارم.
نوشتههای بیشتر از فرشید نوتاش حقیقتاین سایت از اکیسمت برای کاهش جفنگ استفاده میکند. درباره چگونگی پردازش دادههای دیدگاه خود بیشتر بدانید.
دیدگاهتان را بنویسید