اشکالزدایی یکی از حیاتیترین مهارتهای یک اسکریپتنویس حرفهای است. اسکریپتهای Bash علیرغم سادگی ظاهری، میتوانند رفتاری غیرمنتظره داشته باشند؛ متغیرها خالی میشوند، فضاهای اضافی باعث تغییر رفتار میشوند، و خطاها در زیرشلها ناپدید میگردند. خوشبختانه Bash مجموعهای از ابزارهای توکار برای اشکالزدایی ارائه میدهد که در کنار ابزارهای خارجی مثل ShellCheck، امکان نوشتن اسکریپتهای بدون باگ را فراهم میکنند. در این درس، تمام این ابزارها و تکنیکها را بهطور کامل بررسی میکنیم.
چرا اشکالزدایی در Bash دشوار است؟
اسکریپتنویسی Bash با چالشهای منحصربهفردی همراه است:
-
نوعدهی پویا: متغیرها میتوانند بدون اعلان استفاده شوند و مقدار
nullیا رشتهی خالی رفتارهای غیرقابلپیشبینی ایجاد کند. -
Word Splitting و Globbing: فضاها و کاراکترهای خاص بهطور خودکار باعث تقسیم یا گسترش میشوند.
-
زیرشلها: تغییرات متغیر درون لولهها و زیرشلها در شل اصلی دیده نمیشود.
-
سکوت در برابر خطا: بسیاری از خطاها بهطور پیشفرض فقط یک پیام چاپ میکنند و اسکریپت ادامه مییابد.
این درس به شما میآموزد چگونه با این چالشها روبرو شوید.
set -x — ردیابی (Tracing) دستورات
گزینهی -x (یا set -o xtrace) باعث میشود Bash قبل از اجرای هر دستور، آن را بههمراه مقادیر گسترشیافتهی متغیرها، روی stderr چاپ کند. این پرکاربردترین ابزار برای دنبالکردن جریان اجرای اسکریپت و مشاهدهی مقادیر واقعی متغیرهاست.
فعالسازی و غیرفعالسازی:
set -x # فعالسازی ردیابی # دستورات اینجا با جزئیات چاپ میشوند set +x # غیرفعالسازی ردیابی
مثال:
#!/bin/bash name="Ali" set -x echo "سلام $name" dir=$(pwd) set +x echo "اسکریپت تمام شد"
خروجی:
+ echo 'سلام Ali' سلام Ali ++ pwd + dir=/home/user + set +x اسکریپت تمام شد
علائم خروجی:
-
+(یک علامت مثبت) نشاندهندهی دستور اصلی است. -
++(دو علامت) نشاندهندهی دستور درون یک زیرشل (مثل جانشینی دستورات) است. -
پرانتزهای تکی (
') نشاندهندهی مرزهای آرگومانها هستند.
فعالسازی از ابتدای اسکریپت:
میتوانید با قراردادن set -x در بالای اسکریپت، کل اسکریپت را ردیابی کنید:
#!/bin/bash set -x # باقیی اسکریپت
نمایش شمارهی خطوط در خروجی set -x:
متغیر PS4 (پرامپت پیشفرض برای set -x) را میتوان تغییر داد تا اطلاعات بیشتری مانند شمارهی خط و نام تابع را نشان دهد:
export PS4='+ ${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]} - ' set -x
خروجی نمونه:
+ ./script.sh:10:main - echo 'سلام دنیا' سلام دنیا
bash -x — اجرای اسکریپت با ردیابی از بیرون
گاهی نیازی به تغییر خود اسکریپت ندارید و میخواهید آن را با ردیابی اجرا کنید. با استفاده از آپشن -x در اجرای bash، میتوانید این کار را بدون تغییر اسکریپت انجام دهید:
bash -x script.sh
این روش برای اشکالزدایی اسکریپتهایی که نمیخواهید یا نمیتوانید تغییر دهید، ایدهآل است. همچنین میتوانید با -v (verbose) نیز ترکیب کنید که خود دستورات را بدون گسترش متغیرها چاپ میکند:
bash -xv script.sh
مقایسهی -x و -v:
| گزینه | رفتار |
|---|---|
-v | خود دستور را (با همان شکلی که در اسکریپت نوشته شده) چاپ میکند |
-x | دستور را پس از گسترش متغیرها و جانشینیها چاپ میکند |
trap برای اشکالزدایی پیشرفته
در بخش قبل با trap برای مدیریت خطا آشنا شدید. در اینجا از trap برای اشکالزدایی و دیباگ استفاده میکنیم.
۱. trap '...' DEBUG — قدمبهقدم پیش از هر دستور
سیگنال DEBUG باعث میشود دستور دادهشده، قبل از اجرای هر دستور در اسکریپت، اجرا شود. این ویژگی برای لاگکردن دقیق لحظهبهلحظهی اسکریپت بسیار قدرتمند است.
#!/bin/bash trap 'echo "در حال اجرا: $BASH_COMMAND (خط $LINENO)"' DEBUG echo "اولین دستور" x=10 echo "x = $x"
خروجی:
در حال اجرا: echo "اولین دستور" (خط 5) اولین دستور در حال اجرا: x=10 (خط 6) در حال اجرا: echo "x = $x" (خط 7) x = 10
۲. trap '...' RETURN — پس از خروج از تابع
سیگنال RETURN به ازای هر تابع یا اسکریپت source شده، پس از بازگشت از آن، اجرا میشود.
#!/bin/bash trap 'echo "بازگشت از تابع: $FUNCNAME"' RETURN my_func() { echo "داخل تابع" } my_func echo "بعد از تابع"
خروجی:
داخل تابع بازگشت از تابع: my_func بعد از تابع
۳. ترکیب DEBUG با set -x برای دیباگ دقیقتر
اگر بخواهید هم ردیابی set -x را داشته باشید و هم پیامهای سفارشی، میتوانید هر دو را ترکیب کنید:
#!/bin/bash set -x trap 'echo "==> در خط $LINENO: $BASH_COMMAND"' DEBUG echo "شروع"
تکنیکهای عملی اشکالزدایی
در این بخش، چند تکنیک کاربردی را مرور میکنیم که هر اسکریپتنویسی باید بداند.
۱. چاپ متغیرها با declare -p
برای دیدن نوع و مقدار دقیق یک متغیر (بهویژه آرایهها و متغیرهای با نامهای خاص)، از declare -p استفاده کنید:
my_array=(apple banana cherry) declare -p my_array
خروجی:
declare -a my_array=([0]="apple" [1]="banana" [2]="cherry")
این روش بسیار دقیقتر از echo $my_array است.
۲. دیباگ با printf بهجای echo
printf رفتار قابلپیشبینیتری دارد و با کاراکترهای خاص مثل \n یا \t بهدرستی برخورد میکند:
var="hello\nworld" printf "var: %q\n" "$var" # %q مقدار را بهصورت escaped چاپ میکند
خروجی:
var: hello\\nworld
۳. دیباگ تابع با FUNCNAME و BASH_SOURCE
آرایههای FUNCNAME و BASH_SOURCE به شما اجازه میدهند ببینید کدام تابع و کدام فایل در حال اجراست:
#!/bin/bash func1() { func2 } func2() { echo "نام تابع فعلی: ${FUNCNAME[0]}" echo "نام تابع فراخواندهنده: ${FUNCNAME[1]}" echo "فایل منبع: ${BASH_SOURCE[0]}" } func1
خروجی:
نام تابع فعلی: func2 نام تابع فراخواندهنده: func1 فایل منبع: script.sh
۴. بررسی کد خروج با $? و لاگکردن آن
برای ردیابی دقیق خطاها، بعد از هر دستور حساس، کد خروج آن را لاگ کنید:
command_that_may_fail echo "کد خروج command: $?" >&2
۵. استفاده از bash -n برای بررسی سینتکس
آپشن -n (یا --noexec) فقط سینتکس اسکریپت را بررسی میکند و آن را اجرا نمیکند. این روش برای یافتن خطاهای گرامری و پرانتزهای باز/بستهنشده بسیار مفید است:
bash -n script.sh
اگر خطایی باشد، پیام آن چاپ میشود؛ در غیر این صورت هیچ خروجی نخواهد داشت.
ShellCheck — تحلیلگر ایستای اسکریپت
ShellCheck یک ابزار خارجی و بسیار قدرتمند برای تحلیل ایستای اسکریپتهای Bash و شل است. این ابزار صدها هشدار و نکته در مورد اشتباهات رایج، مشکلات قابلحمل، و ضدالگوهای (anti-patterns) اسکریپتنویسی ارائه میدهد.
نصب
# در اوبونتو/دبیان sudo apt install shellcheck # در RedHat/CentOS sudo yum install epel-release sudo yum install ShellCheck # با استفاده از snap snap install shellcheck # با استفاده از package manager زبانهای دیگر cabal install shellcheck
استفاده
shellcheck script.sh
خروجی شامل شمارهی خط، سطح خطا (error/warning/info)، و توضیح با لینک به مستندات است.
مثال خروجی:
In script.sh line 4: if [ $name = "Ali" ]; then ^---^ SC2086: Double quote to prevent globbing and word splitting.
ادغام با ویرایشگرها
-
VS Code: افزونهی
ShellCheckرا نصب کنید. -
Vim/Neovim: از پلاگینهای
aleیاsyntasticاستفاده کنید. -
Sublime Text: پکیج
SublimeLinter-shellcheckرا نصب کنید.
مثالهای رایج خطاهای شناساییشده توسط ShellCheck
| شماره قاعده | خطا | توضیح |
|---|---|---|
SC2086 | استفاده از متغیر بدون گیومه | میتواند باعث word splitting شود |
SC2046 | جانشینی دستورات بدون گیومه | خروجی دستور به چند کلمه تقسیم میشود |
SC2002 | استفاده از cat غیرضروری | cat file | grep بهجای grep < file |
SC2034 | متغیر تعریف شده اما استفاده نشده | احتمالاً خطای تایپی |
SC2155 | تعریف متغیر با export و declare در یک خط با جانشینی دستور | کد خروج دستور نادیده گرفته میشود |
دیباگگری با bashdb
bashdb یک دیباگر کامل برای Bash است (مشابه gdb برای C/C++). با آن میتوانید:
-
نقاط شکست (breakpoint) تعیین کنید.
-
قدمبهقدم جلو بروید (step into, step over).
-
متغیرها را در حین اجرا بررسی و تغییر دهید.
نصب و اجرا:
# نصب (در اوبونتو) sudo apt install bashdb # اجرا bashdb script.sh
دستورات پایه در bashdb:
| دستور | توضیح |
|---|---|
run | اجرای اسکریپت |
break N | قراردادن نقطهی شکست در خط N |
continue | ادامهی اجرا تا نقطهی شکست بعدی |
step | اجرای یک قدم (ورود به توابع) |
next | اجرای یک قدم (بدون ورود به توابع) |
print var | چاپ مقدار متغیر |
quit | خروج |
استفاده از دیباگر کامل برای اسکریپتهای بسیار بزرگ و پیچیده توصیه میشود، اما برای اکثر موارد، set -x و ShellCheck کافی هستند.
الگوی جامع اشکالزدایی
در اینجا یک الگوی پیشنهادی برای اسکریپتهایی که در حال توسعه یا دیباگ هستند آورده شده است:
#!/bin/bash # فعالسازی دیباگ در صورت وجود متغیر محیطی DEBUG if [[ -n "${DEBUG:-}" ]]; then set -Eeuo pipefail export PS4='+ [${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]}] ' set -x trap 'echo "⚠️ خطا در خط $LINENO: $BASH_COMMAND" >&2' ERR else set -euo pipefail fi # تابع دیباگ شرطی debug() { if [[ -n "${DEBUG:-}" ]]; then echo "[DEBUG] $*" >&2 fi } # ---------- بدنهی اسکریپت ---------- debug "شروع اسکریپت" name="Ali" debug "نام: $name" # ادامهی کد...
با این الگو، اگر اسکریپت را بهصورت عادی اجرا کنید (./script.sh)، دیباگ خاموش است. اما با تنظیم متغیر محیطی DEBUG=1 ./script.sh، تمام ردیابیها و خطاها با جزئیات کامل نمایش داده میشوند.
جمعبندی تکنیکها
| ابزار/تکنیک | کاربرد اصلی |
|---|---|
set -x / bash -x | نمایش اجرای لحظهبهلحظه با مقادیر متغیرها |
set -v / bash -v | نمایش کد اصلی اسکریپت بدون گسترش |
trap ... DEBUG | اجرای کد سفارشی پیش از هر دستور |
trap ... RETURN | اجرای کد پس از خروج از تابع |
trap ... ERR | گرفتن خطاهای غیرمنتظره با set -e |
bash -n | بررسی سینتکس بدون اجرا |
declare -p | نمایش دقیق نوع و مقدار متغیر |
printf %q | چاپ مقدار escaped برای دیباگ |
FUNCNAME و BASH_SOURCE | ردگیری تابع و فایل در حال اجرا |
| ShellCheck | تحلیل ایستا و شناسایی خطاهای رایج پیش از اجرا |
bashdb | دیباگ کامل با نقاط شکست و اجرای قدمبهقدم |
اشتباهات رایج در دیباگ
۱. دیباگ در محیط تولید (Production)
هرگزset -xرا در اسکریپتهایی که در حال اجرا در سرورهای تولید هستند فعال نگذارید، زیرا اطلاعات حساس (مانند پسوردها) ممکن است در خروجی لاگ چاپ شوند. همیشه دیباگ را با یک متغیر محیطی شرطی کنید یا پس از اتمام دیباگ، آن را حذف نمایید.
۲. فراموشی
set +xبعد از دیباگ
اگرset -xرا فعال کردهاید، حتماً باset +xآن را غیرفعال کنید تا خروجی تمیز بماند.
۳. نادیدهگرفتن خطاهای ShellCheck
ShellCheck هشدارهای زیادی میدهد. سعی کنید تمام آنها را برطرف کنید، مگر اینکه دلیل محکمی برای نادیدهگرفتن داشته باشید (که در این صورت با# shellcheck disable=SC...آن را غیرفعال کنید).
۴. دیباگ در کدهای interactive
set -xدر اسکریپتهایی که ورودی تعاملی میگیرند، خروجی را به هم میریزد. در چنین مواردی، ازset -xقبل ازreadوset +xبعد از آن استفاده کنید تا فقط بخشهای غیرتعاملی ردیابی شوند.
۵. انتظار
set -xبرای نشاندادن خطاهایstderrset -xفقط دستورات را نشان میدهد، نه خروجی آنها را. برای دیدن خطاها، باید بهطور معمولstderrرا به فایل یا ترمینال هدایت کنید.
نکتهی پایانی: هیچچیز جای دیباگ سیستماتیک و گامبهگام را نمیگیرد. اسکریپت خود را به بخشهای کوچک بشکنید، هر بخش را مجزا تست کنید، و پیش از اضافهکردن بخش بعدی، از صحت بخش فعلی مطمئن شوید. ترکیب ShellCheck، set -x و لاگگذاری هدفمند، شما را به یک اسکریپتنویس بینقص تبدیل خواهد کرد.