توی این درس یاد میگیری کد تکراری را در تابع (function) بگذاری و با یک اسم صدایش کنی: log() { echo "[$(date)] $*"; }. میفهمی آرگومانهای تابع ($1، $#، "$@") از آرگومانهای اسکریپت جدا هستند؛ تابع دو جور «نتیجه» دارد که نباید قاطیشان کنی: کد خروج (return) و خروجی (echo که با $(...) گرفته میشود)؛ و متغیرها در Bash بهصورت پیشفرض سراسری هستند و برای محدودکردنشان باید local بنویسی. تابعهای کمکی (die، confirm)، کتابخانهی تابعها که با source میآوری، و بازگشت را هم میبینی. تمرین: یک تابع log که هم چاپ کند هم در فایل بنویسد.
مسئله: همان ده خط، در پنج جای اسکریپت
Section titled “مسئله: همان ده خط، در پنج جای اسکریپت”اسکریپت بکاپات رشد میکند. هر بار که چیزی اتفاق میافتد، همان سه خط را مینویسی: زمان را بگیر، پیام را با زمان چاپ کن، در فایل لاگ هم بنویس. بیست جا کپیشده و حالا میخواهی قالب زمان را عوض کنی: بیست جا. تابع این را حل میکند: کد را یک بار با یک اسم تعریف میکنی و هر جا لازم شد فقط اسم را مینویسی. تغییر؟ یک جا. تابعها اسکریپت را خوانا هم میکنند: check_disk و send_alert خودشان توضیحاند.
تشبیه: دستور فرعی در دستور پخت
Section titled “تشبیه: دستور فرعی در دستور پخت”وقتی در چند دستور پخت مختلف مینویسی «خمیر را آماده کن»، خستهکننده است هر بار تمام مراحل خمیر را تکرار کنی. بهتر است یک دستور فرعی به اسم «خمیر پیتزا» جدا بنویسی و هر جا لازم بود بگویی «خمیر پیتزا را (با این مقدار آرد) بساز». دستور فرعی مواد خودش را از سفارشدهنده میگیرد (آرگومانها)، ممکن است یک چیز به او برگرداند (نتیجه)، و ظرفهای کوچک خودش را دارد که با ظرفهای آشپزخانه قاطی نشوند (متغیر local).
تابع در Bash
Section titled “تابع در Bash”name() { دستورها}
function name { دستورها}- شکل اول (
name() { ...; }) استاندارد POSIX است و درshهم کار میکند. شکل دوم (کلمهیfunction) مخصوص bash است. در این دوره شکل اول را مینویسیم. - بعد از
{باید فاصله باشد و دستور آخر باید با;یا خط جدید تمام شود (قبل از}). - تابع باید قبل از صدازدنش اجرا (تعریف) شده باشد. اسکریپت از بالا به پایین اجرا میشود؛ تابعی که پایینتر تعریف شده، بالاتر ناشناخته است. (برای همین معمولاً همهی تعریفها بالای فایل میآیند.)
- صدازدن: مثل هر دستور،
name آرگومانها، بدون پرانتز.
مثالهای عملی
Section titled “مثالهای عملی”در این درس هر دستوری که ممکن است وسط یک خط خطای «توقف» بدهد را روی خط جدا گذاشتهام (درس شرطها و متغیرها: خطای محاسبه بقیهی همان خط را اجرا نمیکند).
مثال ۱: تعریف و فراخوانی
Section titled “مثال ۱: تعریف و فراخوانی”greet() { echo "Hello, $1!"}
greet Aligreet "Sara Karimi"
function farewell { echo "Goodbye, $1!"}
farewell Aliecho "--- فهرست توابع تعریفشده (declare -F):"declare -FHello, Ali!Hello, Sara Karimi!Goodbye, Ali!--- فهرست توابع تعریفشده (declare -F):declare -f farewelldeclare -f greetgreet را مثل یک دستور جدید صدا زدیم. آرگومان اول (Ali) در $1 داخل تابع بود. declare -F فقط اسم تابعهای فعلی شل را میدهد.
یک شرط مهم: تابع باید قبل از استفاده تعریف شده باشد:
cd ~/bashlabprintf 'hello\nhello() { echo hi; }\nhello\n' > early.shbash early.shearly.sh: line 1: hello: command not foundhiبار اول hello را صدا زدیم و هنوز تعریف نشده بود: command not found. بعد از تعریف، کار میکند. قاعده: تعریفها بالای اسکریپت، منطق اصلی پایین.
مثال ۲: آرگومانهای تابع
Section titled “مثال ۲: آرگومانهای تابع”داخل تابع $1، $2، $#، "$@" و shift مال خود تابع هستند، نه اسکریپت. (فقط $0 همان اسم اسکریپت میماند.)
#!/usr/bin/env bash# argdemo.sh: script arguments and function arguments are separate
show() { echo " function got $# args: first=[$1] all=[$*]" echo " \$0 inside the function: $0" shift echo " after shift inside the function: first=[$1] count=$#"}
echo "script got $# args: first=[$1]"show x y zecho "script still has $# args: first=[$1]"cd ~/bashlabbash argdemo.sh one twoscript got 2 args: first=[one] function got 3 args: first=[x] all=[x y z] $0 inside the function: argdemo.sh after shift inside the function: first=[y] count=2script still has 2 args: first=[one]تابع سه آرگومان (x y z) گرفت، با اینکه اسکریپت دو تا داشت. shift داخل تابع فقط آرگومانهای تابع را جابهجا کرد و بعد از برگشت، آرگومانهای اسکریپت دستنخورده بودند. اگر لازم است آرگومانهای اسکریپت را به تابع بدهی، صریح بنویس: show "$@". مقدار پیشفرض هم مثل قبل: name="${1:-World}".
مثال ۳: تابع چطور «نتیجه» برمیگرداند؟
Section titled “مثال ۳: تابع چطور «نتیجه» برمیگرداند؟”در Bash تابع دو جور نتیجه دارد و تفاوتشان مهمترین نکتهی این درس است:
۱. کد خروج (عدد ۰ تا ۲۵۵): با return N، یا خودکار کد آخرین دستور. برای «موفق شد؟» و استفاده در if.
۲. خروجی (متن): با echo/printf چاپ میشود و با $(تابع) گرفته میشود. برای داده.
add() { echo $(( $1 + $2 ))}
sum=$(add 2 3)echo "sum=$sum"
is_even() { (( $1 % 2 == 0 ))}
if is_even 4; then echo "4 is even"; fiif is_even 7; then echo "7 is even"; else echo "7 is odd"; fi
is_dir() { [[ -d $1 ]]}
is_dir /etc && echo "/etc is a directory"is_dir /etc/hostname || echo "/etc/hostname is not a directory"
both() { echo "text output" return 3}
out=$(both)echo "out=[$out] status=$?"sum=54 is even7 is odd/etc is a directory/etc/hostname is not a directoryout=[text output] status=3addدادهاش را باechoبرمیگرداند و$(add 2 3)آن را در متغیر میگیرد.is_evenوis_dirهیچ چیزی چاپ نمیکنند؛ فقط کد خروج میدهند، چون کد خروج آخرین دستورشان ((( ))یا[[ ]]) همان کد خروج تابع است. نتیجه: میتوانی آنها را مستقیم درifیا با&&/||به کار ببری، درست مثلgrep -q. نامگذاری با «is_» یا «has_» قرارداد خوبی برای این جور تابعهاست.bothهر دو را دارد: متن را باechoو کد ۳ را باreturnداد.out=$(both)متن را گرفت و$?بلافاصله بعدش، کد ۳ را.
چند قاعده دربارهی return:
big() { return 300}
bigecho "return 300 -> status $?"
text() { return abc}
textecho "return abc -> status $?"return 300 -> status 44bash: return: abc: numeric argument requiredreturn abc -> status 2کد خروج یک عدد بین ۰ تا ۲۵۵ است: ۳۰۰ دور میزند و میشود ۴۴ (۳۰۰ mod ۲۵۶). و متن را نمیشود با return برگرداند (numeric argument required). برای متن، echo.
مثال ۴: local، متغیرها سراسریاند
Section titled “مثال ۴: local، متغیرها سراسریاند”در Bash هر متغیری که داخل تابع بسازی یا عوض کنی، سراسری است و بعد از تابع هم میماند. این بیشترین منبع باگ در اسکریپتهای بزرگ است:
x=globalsetx() { x=changed-by-function y=leaked}
setxecho "x=$x y=$y"x=changed-by-function y=leakedتابع x را عوض کرد و y را از خودش به بیرون «نشت» داد. برای محدود کردن به تابع، با local تعریف کن:
x=globalouter() { local x=local-value echo "inside outer: $x" inner}
inner() { echo "inner sees: $x"}
outerecho "outside: $x"inside outer: local-valueinner sees: local-valueoutside: globalداخل outer متغیر x محلی است و بیرون دستنخورده میماند (global). ولی نکتهی عجیب Bash: inner که از داخل outer صدا زده شد، همان x محلی outer را میبیند (local-value)! به این دامنهی دینامیک میگویند: متغیر محلی برای تابع و همهی تابعهایی که از آن صدا زده میشوند دیده میشود (در زبانهای دیگر معمولاً اینطور نیست). نتیجه: همیشه هر متغیر داخل تابع را local کن، تا از بیرون چیزی نشت نکند یا عوض نشود.
یک مثال واقعی از خرابکاری متغیر سراسری، با شمارندهی حلقه:
bad() { for i in 1 2; do :; done}
for i in a b c; do bad echo "outer loop: i=$i"doneouter loop: i=2outer loop: i=2outer loop: i=2تابع bad متغیر i خودش را در حلقه به کار برد و چون local نبود، i حلقهی بیرونی را خراب کرد (i=2 بهجای a، b، c). با local i در bad مشکل حل میشود.
و یک دام ظریف: local و جایگزینی دستور در یک خط، کد خروج دستور را پنهان میکند:
masked() { local out=$(false) echo "local out=\$(false) -> status $?"}
proper() { local out out=$(false) echo "local out; out=\$(false) -> status $?"}
maskedproperecho "--- local بیرون از تابع:"local z=1local out=$(false) -> status 0local out; out=$(false) -> status 1--- local بیرون از تابع:bash: local: can only be used in a functionlocal out=$(false) کد خروج خودِ local را برمیگرداند (که موفق است) نه false را. راهحل: اول local out بنویس و بعد در خط جدا out=$(دستور). (ShellCheck این را با هشدار SC2155 میگیرد.) و local بیرون از تابع خطاست.
مثال ۵: تابعهای کمکی برای اسکریپتهای واقعی
Section titled “مثال ۵: تابعهای کمکی برای اسکریپتهای واقعی”اکثر اسکریپتهای حرفهای چند تابع کوچک دارند: خطا چاپ کن و برو (die)، ببین ابزار نصب است (require_cmd)، بپرس «مطمئنی؟» (confirm):
#!/usr/bin/env bash# tool.sh: delete a file after checks and a confirmation# Usage: ./tool.sh <file> (REQUIRED="cmd1 cmd2" lists the tools it needs)
die() { echo "Error: $*" >&2 exit 1}
require_cmd() { command -v "$1" > /dev/null 2>&1 || die "'$1' is required but not installed"}
confirm() { local reply read -r -p "$1 [y/N] " reply [[ $reply == [yY] || $reply == [yY][eE][sS] ]]}
[[ $# -eq 1 ]] || die "usage: $0 <file>"
for tool in ${REQUIRED:-rm stat}; do require_cmd "$tool"done
[[ -f $1 ]] || die "'$1' is not a file"
if confirm "Delete $1 ($(stat -c %s "$1") bytes)?"; then rm -- "$1" echo "deleted"else echo "kept"ficd ~/bashlabchmod +x tool.shecho "data" > a.txtecho "--- بدون آرگومان:"./tool.sh; echo "کد خروج: $?"echo "--- فایل ناموجود:"./tool.sh nofile.txt; echo "کد خروج: $?"echo "--- ابزار لازم نصب نیست:"REQUIRED="rm no-such-tool" ./tool.sh a.txt; echo "کد خروج: $?"echo "--- جواب n:"echo n | ./tool.sh a.txt; echo "کد خروج: $?"ls a.txtecho "--- جواب y:"echo y | ./tool.sh a.txt; echo "کد خروج: $?"ls a.txt--- بدون آرگومان:Error: usage: ./tool.sh <file>کد خروج: 1--- فایل ناموجود:Error: 'nofile.txt' is not a fileکد خروج: 1--- ابزار لازم نصب نیست:Error: 'no-such-tool' is required but not installedکد خروج: 1--- جواب n:keptکد خروج: 0a.txt--- جواب y:deletedکد خروج: 0ls: cannot access 'a.txt': No such file or directoryسه تابع، هر کدام یک کار:
dieپیام را روی stderr مینویسد و باexit 1کل اسکریپت را میبندد. (چون تابع در همان پروسه است،exitداخل آن خود اسکریپت را میبندد؛ برای فقط برگشتن از تابعreturnمینویسی.)require_cmdباcommand -vمیپرسد دستور وجود دارد یا نه، و اگر نهdie.A || Bیعنی «اگر A شکست خورد، B».confirmیک پرسش میپرسد و کد خروج میدهد (درست اگر جوابyیاyesبود)، پس مستقیم درif confirm "..."; thenبه کار میرود. دقت کنreplyراlocalگرفتیم. (پرسشread -pدر ثبت بالا نیامد چون ورودی از pipe بود؛ در ترمینال میبینی.)${REQUIRED:-rm stat}یعنی «مقدارREQUIRED، و اگر خالی بودrm stat». عمداً بدون کوتیشن نوشته شد تا در فاصلهها بشکند وforروی هر ابزار بچرخد؛ یکی از معدود جاهایی که کوتیشن نمیخواهیم.
مثال ۶: کتابخانهی تابعها با source
Section titled “مثال ۶: کتابخانهی تابعها با source”تابعهایی را که در چند اسکریپت لازم داری، در یک فایل جدا بگذار و با source بیاور:
mkdir -p ~/bashlab/lib-demo/bincat > ~/bashlab/lib-demo/lib.sh <<'EOF'# lib.sh: shared helper functions (meant to be sourced, not executed)greet() { echo "Hello, $1 (from lib.sh)"}EOFcat > ~/bashlab/lib-demo/bin/main.sh <<'EOF'#!/usr/bin/env bashscript_dir="$(cd "$(dirname "$0")" && pwd)"source "$script_dir/../lib.sh"greet "$1"EOFcat > ~/bashlab/lib-demo/bin/bad-main.sh <<'EOF'#!/usr/bin/env bashsource ../lib.shgreet "$1"EOFchmod +x ~/bashlab/lib-demo/bin/*.shcd /tmpecho "--- از یک پوشهی دیگر، با مسیر نسبی به خود اسکریپت:"~/bashlab/lib-demo/bin/main.sh Aliecho "--- با مسیر نسبی ../lib.sh (وابسته به پوشهی فعلی):"~/bashlab/lib-demo/bin/bad-main.sh Ali--- از یک پوشهی دیگر، با مسیر نسبی به خود اسکریپت:Hello, Ali (from lib.sh)--- با مسیر نسبی ../lib.sh (وابسته به پوشهی فعلی):/home/ali/bashlab/lib-demo/bin/bad-main.sh: line 2: ../lib.sh: No such file or directory/home/ali/bashlab/lib-demo/bin/bad-main.sh: line 3: greet: command not foundنسخهی خوب اول پوشهی خود اسکریپت را حساب میکند (dirname "$0"، بعد cd و pwd برای مسیر مطلق) و کتابخانه را نسبت به آن میخواند؛ پس از هر جا اجرا شود کار میکند. نسخهی بد ../lib.sh را نسبت به پوشهی فعلی ترمینال میخواند و از /tmp پیدایش نمیکند. نکتهی دیگر: source فایل را در همان شل اجرا میکند (درس اولین اسکریپت)، پس تابعهایش در اسکریپت اصلی در دسترس میشوند.
مثال ۷: بازگشت (recursion)
Section titled “مثال ۷: بازگشت (recursion)”تابع میتواند خودش را صدا بزند. مثال کلاسیک، فاکتوریل (n! = n × (n-1)!):
fact() { local n=$1 if (( n <= 1 )); then echo 1 else echo $(( n * $(fact $((n - 1))) )) fi}
fact 5fact 101203628800fact 5 ← 5 × fact 4 ← … تا fact 1. چون echo خروجی را برمیگرداند، هر سطح با $(...) نتیجهی سطح پایینتر را میگیرد. هر فراخوانی متغیر n محلی خودش را دارد (برای همین local n مهم است). بازگشت بینهایت یا خیلی عمیق پشته را پر میکند؛ Bash بهصورت پیشفرض حدی ندارد، ولی با FUNCNEST میشود سقف گذاشت:
countdown() { echo -n "$1 " (( $1 > 0 )) && countdown $(( $1 - 1 ))}
countdown 5echoecho "--- با سقف FUNCNEST=4 (داخل پرانتز، تا روی بقیهی درس اثر نکند):"( FUNCNEST=4; countdown 10 )5 4 3 2 1 0--- با سقف FUNCNEST=4 (داخل پرانتز، تا روی بقیهی درس اثر نکند):10 9 8 7 bash: countdown: maximum function nesting level exceeded (4)countdown 5 تا صفر رفت. با FUNCNEST=4 بعد از چهار سطح Bash بازگشت را قطع کرد و خطا داد. در عمل در Bash کمتر بازگشت مینویسی؛ حلقه معمولاً بهتر و سریعتر است (هر سطحِ $(...) در فاکتوریل یک پروسهی فرزند هم میسازد).
پشت پرده: تابع کجاست و چطور پیدا میشود؟
Section titled “پشت پرده: تابع کجاست و چطور پیدا میشود؟”تابعها در حافظهی خود شل نگه داشته میشوند (اسم ← بدنه). میتوانی تعریفشان را ببینی:
log() { echo "[$(date +%T)] $*"}
type logecho "--- فقط اسمها (declare -F) و تعریف کامل (declare -f):"declare -F logdeclare -f loglog is a functionlog (){ echo "[$(date +%T)] $*"}--- فقط اسمها (declare -F) و تعریف کامل (declare -f):loglog (){ echo "[$(date +%T)] $*"}وقتی اسمی را مینویسی، شل به این ترتیب میگردد: alias ← تابع ← دستور داخلی (builtin) ← برنامهی داخل PATH. پس یک تابع میتواند یک دستور واقعی را «بپوشاند»:
date() { echo "fake date"}
dateecho "--- command: رد کردن تابع و اجرای نسخهی واقعی:"command date +%Yecho "--- type -a: همهی معنیهای یک اسم (اولی برنده است):"type -a dateunset -f dateecho "--- بعد از unset -f:"date +%Yfake date--- command: رد کردن تابع و اجرای نسخهی واقعی:2026--- type -a: همهی معنیهای یک اسم (اولی برنده است):date is a functiondate (){ echo "fake date"}date is /usr/bin/datedate is /bin/date--- بعد از unset -f:2026command date تابع (و alias) را رد میکند و برنامهی واقعی را اجرا میکند؛ unset -f تابع را از حافظه پاک میکند. پوشاندن دستورها گاهی مفید است (مثلاً ls() { command ls --color "$@"; }) ولی خطرناک هم هست:
( FUNCNEST=8; ls() { ls -l "$@"; }; ls /etc/hostname )echo "--- درست: command ls"( ls() { command ls -l "$@"; }; ls /etc/hostname )bash: ls: maximum function nesting level exceeded (8)--- درست: command ls-rw-r--r-- 1 root root 3 Oct 3 21:00 /etc/hostnamels() { ls -l; } خودش را صدا میزند (نه ls واقعی را) و بازگشت بیپایان میشود (اینجا با FUNCNEST قطعش کردیم). راهحل command ls.
تابع پروسهی جدید نمیسازد، ولی $(تابع) میسازد:
echo "shell: BASHPID=$BASHPID"pf() { echo "in function: BASHPID=$BASHPID"}pfecho "via \$(pf): $(pf)"echo "--- تغییر متغیر داخل تابع، مستقیم و با \$(...):"cnt=0inc() { ((cnt++)); }incincecho "direct: cnt=$cnt"cnt=0v=$(inc)echo "in \$(...): cnt=$cnt"shell: BASHPID=22756in function: BASHPID=22756via $(pf): in function: BASHPID=22821--- تغییر متغیر داخل تابع، مستقیم و با $(...):direct: cnt=2in $(...): cnt=0BASHPID شمارهی پروسهی فعلی است. صدازدن مستقیم همان پروسهی شل است (پس inc شمارنده را زیاد کرد)، ولی $(pf) یک پروسهی فرزند است و تغییرهایش (cnt) با پایانش از بین میروند. نتیجه: اگر میخواهی تابع چیزی را در متغیر بیرونی بگذارد، آن را مستقیم صدا بزن (و از متغیر سراسری استفاده کن)؛ اگر فقط داده میخواهی، $(...).
export -f: تابعها بهصورت پیشفرض به پروسههای فرزند (مثل bash script.sh یا bash -c) نمیرسند، مگر با export -f:
hi() { echo "hi from an exported function"}
bash -c 'hi' 2>&1export -f hibash -c 'hi'unset -f hibash -c 'hi' 2>&1bash: line 1: hi: command not foundhi from an exported functionbash: line 1: hi: command not foundبار اول فرزند تابع را نمیشناخت، بعد از export -f شناخت، و با unset -f دوباره از محیط رفت. (export -f مخصوص bash است؛ برای تنظیمات ثابت بیشتر source یک کتابخانه را به کار ببر.)
جدولهای مرجع
Section titled “جدولهای مرجع”سه راه برای گرفتن نتیجه از تابع:
| راه | شکل | مناسب |
|---|---|---|
| کد خروج | return N یا کد آخرین دستور؛ if f; then |
«موفق شد؟» (is_dir، confirm) |
| خروجی | echo در تابع و x=$(f) بیرون |
دادهٔ متنی (add، fact) |
| متغیر سراسری | تابع مستقیم صدا زده شود و متغیر را عوض کند | چند مقدار یا وقتی subshell نمیخواهی (با احتیاط) |
فرمانهای مرتبط با تابع:
| دستور | کار |
|---|---|
local var |
متغیر محلی (فقط داخل تابع و تابعهای صدا زدهشده از آن) |
return [N] |
برگشت از تابع با کد N (۰ تا ۲۵۵؛ پیشفرض: کد آخرین دستور) |
exit [N] |
بستن کل اسکریپت (حتی اگر داخل تابع باشد) |
declare -F / declare -f name |
فهرست اسمها / نمایش تعریف |
type name / type -a name |
تابع یا دستور؟ همهی معنیها |
unset -f name |
حذف تابع |
export -f name |
رساندن تابع به پروسههای bash فرزند |
command name |
رد کردن تابع و alias، اجرای دستور واقعی |
FUNCNEST=N |
سقف عمق صدازدن تو در تو |
تابعهای کمکی که تقریباً هر اسکریپت جدی دارد:
| تابع | کار |
|---|---|
log LEVEL پیام |
چاپ با زمان و سطح؛ نوشتن در فایل لاگ |
die پیام |
پیام روی stderr و exit 1 |
usage |
چاپ راهنما |
require_cmd ابزار |
ببین ابزار نصب است؛ وگرنه die |
confirm "پرسش" |
بپرس y/n؛ کد خروج بده |
is_number، is_dir، … |
آزمونهای کوچک با کد خروج |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) صدا زدن تابع قبل از تعریف
Section titled “۱) صدا زدن تابع قبل از تعریف”مثال ۱: hello: command not found. راهحل: تعریفها بالای فایل.
۲) } بدون ; یا خط جدید قبلش
Section titled “۲) } بدون ; یا خط جدید قبلش”cd ~/bashlabprintf 'f() { echo hi }\nf\n' > no-semicolon.shbash -n no-semicolon.shprintf 'f() { echo hi; }\nf\n' > with-semicolon.shbash with-semicolon.shno-semicolon.sh: line 3: syntax error: unexpected end of filehiاگر } فاصله و دستور قبلش را ببیند، آن را یک آرگومان echo میداند (echo hi }) و آکولاد بسته را پیدا نمیکند. راهحل: echo hi; } یا } روی خط جدا.
۳) return با متن یا عدد بزرگ
Section titled “۳) return با متن یا عدد بزرگ”مثال ۳: متن را با echo برگردان؛ return فقط کد ۰ تا ۲۵۵ است.
۴) فراموشکردن local
Section titled “۴) فراموشکردن local”مثال ۴: شمارندهی حلقهی بیرونی خراب میشود و متغیرها نشت میکنند. راهحل: هر متغیر داخل تابع local (حتی local i برای حلقهها).
۵) local x=$(cmd) و کد خروج گمشده
Section titled “۵) local x=$(cmd) و کد خروج گمشده”مثال ۴. راهحل: local x و بعد در خط جدا x=$(cmd).
۶) انتظار تغییر متغیر از $(تابع)
Section titled “۶) انتظار تغییر متغیر از $(تابع)”پشت پرده: v=$(inc) شمارنده را عوض نمیکند. راهحل: مستقیم صدا بزن (و از متغیر سراسری یا return)، یا خروجی را بگیر.
۷) exit بهجای return
Section titled “۷) exit بهجای return”bash -c 'f() { echo "in f"; exit 5; }; f; echo "after f (never printed)"'echo "status: $?"bash -c 'f() { echo "in f"; return 5; }; f; echo "after f, status=$?"'in fstatus: 5in fafter f, status=5exit داخل تابع، کل اسکریپت را میبندد (برای die همین را میخواهیم). return فقط از تابع برمیگردد و ادامهی اسکریپت اجرا میشود. هر کدام را دانسته به کار ببر.
۸) تابع همنام یک دستور واقعی
Section titled “۸) تابع همنام یک دستور واقعی”پشت پرده: ls() { ls -l; } بازگشت بیپایان است. راهحل: command ls.
تابع square بنویس که یک عدد بگیرد و مربعش را echo کند؛ بعد با یک حلقه مربع ۱ تا ۵ را به شکل 3 squared is 9 چاپ کن و خروجی تابع را با $(...) بگیر.
دیدن جواب
square() { echo $(( $1 * $1 ))}
for n in 1 2 3 4 5; do result=$(square "$n") echo "$n squared is $result"done1 squared is 12 squared is 43 squared is 94 squared is 165 squared is 25تمرین اصلی درس: تابع log که هر پیام را هم روی صفحه چاپ کند هم در فایل لاگ بنویسد. قالب هر خط: 2026-10-04 09:30:01 [INFO] پیام. سطح (INFO، WARN، ERROR) آرگومان اول باشد؛ پیامهای ERROR روی stderr هم بروند (تا جدا قابلدیدن باشند). اسکریپت نمونهای بنویس که از تابع استفاده کند و ثابت کن فایل لاگ هر سه پیام را دارد، ولی با 2>/dev/null فقط INFO و WARN روی صفحه میمانند.
دیدن جواب
cd ~/bashlabcat > app.sh <<'EOF'#!/usr/bin/env bash# app.sh: demo of a log() function that prints and appends to a file
LOG_FILE="$HOME/bashlab/app.log"
log() { local level="$1" shift local line line="$(date '+%F %T') [$level] $*" if [[ $level == ERROR ]]; then echo "$line" | tee -a "$LOG_FILE" >&2 else echo "$line" | tee -a "$LOG_FILE" fi}
log INFO "backup started"log WARN "disk is 78% full"log ERROR "could not reach the database"log INFO "backup finished"EOFchmod +x app.shrm -f app.logecho "--- همهی خروجی (stdout و stderr):"./app.shecho "--- فقط stdout (خطاها دور ریخته میشوند):"./app.sh 2>/dev/nullecho "--- محتوای فایل لاگ (دو بار اجرا شد، پس هشت خط):"wc -l < app.loghead -n 4 app.log--- همهی خروجی (stdout و stderr):2026-10-04 10:26:47 [INFO] backup started2026-10-04 10:26:47 [WARN] disk is 78% full2026-10-04 10:26:47 [ERROR] could not reach the database2026-10-04 10:26:47 [INFO] backup finished--- فقط stdout (خطاها دور ریخته میشوند):2026-10-04 10:26:47 [INFO] backup started2026-10-04 10:26:47 [WARN] disk is 78% full2026-10-04 10:26:47 [INFO] backup finished--- محتوای فایل لاگ (دو بار اجرا شد، پس هشت خط):82026-10-04 10:26:47 [INFO] backup started2026-10-04 10:26:47 [WARN] disk is 78% full2026-10-04 10:26:47 [ERROR] could not reach the database2026-10-04 10:26:47 [INFO] backup finishedنکتهها: ۱) local level="$1"; shift اولین آرگومان را سطح میگیرد و بقیه ($*) پیام است. ۲) tee -a همزمان روی صفحه و (با -a، افزودن) در فایل مینویسد؛ >&2 بعد از آن خروجی tee را به stderr میبرد. ۳) تمام متغیرها local هستند و LOG_FILE در بالای اسکریپت تعریف شده (در دنیای واقعی از ${LOG_FILE:-...} برای قابلتنظیمبودن استفاده کن).
سه تابع آزمون بنویس: is_number (آرگومان فقط رقم است؟)، is_dir (پوشهی موجود است؟) و is_empty (رشته خالی است؟) که هیچچیز چاپ نکنند و فقط کد خروج بدهند. سپس اسکریپتی بنویس که دو آرگومان بگیرد (یک عدد و یک پوشه) و با همین تابعها در if اعتبارسنجی کند.
دیدن جواب
cd ~/bashlabcat > validate.sh <<'EOF'#!/usr/bin/env bash# validate.sh: ./validate.sh <number> <directory>
is_number() { [[ $1 =~ ^[0-9]+$ ]]; }is_dir() { [[ -d $1 ]]; }is_empty() { [[ -z $1 ]]; }
if is_empty "$1" || is_empty "$2"; then echo "Usage: $0 <number> <directory>" >&2 exit 1fiif ! is_number "$1"; then echo "Error: '$1' is not a number" >&2 exit 2fiif ! is_dir "$2"; then echo "Error: '$2' is not a directory" >&2 exit 3fiecho "OK: $1 and $2 are valid"EOFchmod +x validate.sh./validate.sh; echo "کد خروج: $?"./validate.sh abc /tmp; echo "کد خروج: $?"./validate.sh 5 /no/such/dir; echo "کد خروج: $?"./validate.sh 5 /tmp; echo "کد خروج: $?"Usage: ./validate.sh <number> <directory>کد خروج: 1Error: 'abc' is not a numberکد خروج: 2Error: '/no/such/dir' is not a directoryکد خروج: 3OK: 5 and /tmp are validکد خروج: 0دقت کن هر تابع یکخطی است و ; قبل از } را دارد. خواندن اسکریپت اصلی مثل خواندن جمله است: «اگر خالی بود… اگر عدد نبود… اگر پوشه نبود…».
تابع retry بنویس: retry تعداد دستور آرگومانها... یک دستور را حداکثر چند بار امتحان کند (با کمی صبر بین تلاشها). اگر موفق شد، شمارهی تلاش را بگوید و کد ۰ برگرداند؛ اگر همهی تلاشها شکست خورد، پیام خطا و کد ۱. روی یک دستور «ناپایدار» (که اول دو بار شکست میخورد و بار سوم موفق میشود) امتحانش کن.
دیدن جواب
cd ~/bashlabrm -f flaky.countcat > flaky.sh <<'EOF'#!/usr/bin/env bash# flaky.sh: fails on the first two calls and succeeds from the third onecounter_file="$HOME/bashlab/flaky.count"count=$(cat "$counter_file" 2>/dev/null || echo 0)((count++))echo "$count" > "$counter_file"echo " flaky: call number $count" >&2(( count >= 3 ))EOFchmod +x flaky.shcat > retry-demo.sh <<'EOF'#!/usr/bin/env bash# retry-demo.sh: the retry() function
retry() { local attempts="$1" shift local n=1 until "$@"; do if (( n >= attempts )); then echo "failed after $n attempts: $*" >&2 return 1 fi echo "attempt $n failed; retrying..." >&2 ((n++)) sleep 0.2 done echo "succeeded on attempt $n"}
retry "$@"EOFchmod +x retry-demo.shecho "--- با ۵ تلاش (بار سوم موفق میشود):"./retry-demo.sh 5 ./flaky.sh; echo "کد خروج: $?"echo "--- شمارنده را صفر میکنیم و فقط ۲ تلاش میدهیم:"rm -f flaky.count./retry-demo.sh 2 ./flaky.sh; echo "کد خروج: $?"--- با ۵ تلاش (بار سوم موفق میشود): flaky: call number 1attempt 1 failed; retrying... flaky: call number 2attempt 2 failed; retrying... flaky: call number 3succeeded on attempt 3کد خروج: 0--- شمارنده را صفر میکنیم و فقط ۲ تلاش میدهیم: flaky: call number 1attempt 1 failed; retrying... flaky: call number 2failed after 2 attempts: ./flaky.shکد خروج: 1نکتهها: ۱) local attempts="$1"; shift اولین آرگومان را میگیرد؛ بعد "$@" خودِ دستور و آرگومانهایش است (الگوی درس ورودی و آرگومانها). ۲) until "$@"; do ...; done دستور را اجرا میکند و تا وقتی شکست میخورد حلقه ادامه دارد (until). ۳) return 1 بعد از پایان تلاشها از تابع برمیگردد (نه exit، تا صدازننده بتواند تصمیم بگیرد). ۴) چون شمارندهی flaky را در فایل نگه داشتیم، هر اجرا به تلاش بعدی میرسد.
آزمونک
Section titled “آزمونک”در x=$(greet Ali) وقتی greet یک تابع است، چه چیزی در x میرود؟
$(...) خروجی چاپشده را میگیرد. کد خروج در $? است و تغییر متغیرها در پروسهی فرزند از بین میرود.
در Bash، متغیری که داخل یک تابع (بدون local) مقدار میگیرد، بعد از تابع چه حالتی دارد؟
برای محدودکردن به تابع باید local بنویسی.
تفاوت return و echo برای نتیجهی یک تابع چیست؟
return 300 میشود ۴۴ و return abc خطای numeric argument required میدهد.
چرا local out=$(false) کد خروج دستور را از دست میدهد؟
ShellCheck این را با SC2155 هشدار میدهد.
اگر داخل تابعی exit 1 بنویسی چه میشود؟
برای برگشتن از تابع return بنویس؛ die() عمداً exit دارد.
v=$(inc) که inc شمارندهی cnt را زیاد میکند. بعد از آن cnt چند است؟
برای تغییر متغیر بیرونی، تابع را مستقیم (بدون $(...)) صدا بزن.
تابعی در اسکریپت تعریف کردهای و میخواهی در bash script2.sh (فرزند) هم کار کند. چه میکنی؟
بهصورت پیشفرض تابعها به پروسههای فرزند نمیرسند؛ export -f آنها را به محیط میفرستد.
جمعبندی
Section titled “جمعبندی”- تعریف:
name() { ...; }(آکولاد با فاصله،;قبل از})؛ قبل از صدازدن تعریف شود. صدازدن مثل هر دستور:name آرگومانها. - آرگومانهای تابع
$1،$#،"$@"،shiftمال خود تابعاند؛$0همان اسم اسکریپت است. آرگومانهای اسکریپت را صریح پاس بده:f "$@". - نتیجهی تابع دو جور است: کد خروج با
return N(۰ تا ۲۵۵) برای «موفق شد؟» (if f; then)، و خروجی باechoکه با$(f)گرفته میشود. - متغیرها بهصورت پیشفرض سراسریاند. هر متغیر داخل تابع را
localکن (حتی شمارندهی حلقه).local x=$(cmd)کد خروج را پنهان میکند:local xو بعدx=$(cmd). دامنهیlocalدینامیک است. exitداخل تابع کل اسکریپت را میبندد؛returnفقط از تابع برمیگردد.- تابع در همان شل اجرا میشود؛
$(f)در پروسهی فرزند (تغییرهایش گم میشود). ترتیب جستوجو: alias ← تابع ← builtin ←PATH؛command nameتابع را رد میکند. تابع همنام دستور، بازگشت بیپایان میسازد. - کتابخانهی تابعها: در یک فایل بگذار و با
source "$script_dir/lib.sh"بیاور (مسیر نسبت به خود اسکریپت).export -fبرای فرزندهای bash. - تابعهای کمکی استاندارد:
log،die،usage،require_cmd،confirm.
| دستور | کاری که میکند |
|---|---|
greet() { echo "Hello, $1"; } | تعریف تابع (قبل از استفاده) |
greet Ali | صدازدن؛ آرگومانها در $1 و $# |
x=$(add 2 3) | گرفتن خروجی (echo) تابع |
is_dir() { [[ -d $1 ]]; } if is_dir x; then | تابع شرط: کد خروج |
return 3 / exit 1 | برگشت از تابع / بستن کل اسکریپت |
local var local out; out=$(cmd) | متغیر محلی؛ بدون پنهانکردن کد خروج |
log() { echo "$(date +%T) $*" | tee -a "$LOG"; } | چاپ و ثبت در فایل |
die() { echo "Error: $*" >&2; exit 1; } | خطا و خروج |
source "$script_dir/lib.sh" | آوردن کتابخانهی توابع |
declare -f name type -a name | دیدن تعریف / همهی معنیهای یک اسم |
unset -f name command name | حذف تابع / رد کردن تابع |
export -f name | رساندن تابع به bash فرزند |