رفتن به محتوا
LoopX

تابع‌ها

توی این درس یاد می‌گیری کد تکراری را در تابع (function) بگذاری و با یک اسم صدایش کنی: log() { echo "[$(date)] $*"; }. می‌فهمی آرگومان‌های تابع ($1، $#، "$@") از آرگومان‌های اسکریپت جدا هستند؛ تابع دو جور «نتیجه» دارد که نباید قاطی‌شان کنی: کد خروج (return) و خروجی (echo که با $(...) گرفته می‌شود)؛ و متغیرها در Bash به‌صورت پیش‌فرض سراسری هستند و برای محدودکردنشان باید local بنویسی. تابع‌های کمکی (die، confirm)، کتابخانه‌ی تابع‌ها که با source می‌آوری، و بازگشت را هم می‌بینی. تمرین: یک تابع log که هم چاپ کند هم در فایل بنویسد.

مسئله: همان ده خط، در پنج جای اسکریپت

Section titled “مسئله: همان ده خط، در پنج جای اسکریپت”

اسکریپت بکاپ‌ات رشد می‌کند. هر بار که چیزی اتفاق می‌افتد، همان سه خط را می‌نویسی: زمان را بگیر، پیام را با زمان چاپ کن، در فایل لاگ هم بنویس. بیست جا کپی‌شده و حالا می‌خواهی قالب زمان را عوض کنی: بیست جا. تابع این را حل می‌کند: کد را یک بار با یک اسم تعریف می‌کنی و هر جا لازم شد فقط اسم را می‌نویسی. تغییر؟ یک جا. تابع‌ها اسکریپت را خوانا هم می‌کنند: check_disk و send_alert خودشان توضیح‌اند.

تشبیه: دستور فرعی در دستور پخت

Section titled “تشبیه: دستور فرعی در دستور پخت”

وقتی در چند دستور پخت مختلف می‌نویسی «خمیر را آماده کن»، خسته‌کننده است هر بار تمام مراحل خمیر را تکرار کنی. بهتر است یک دستور فرعی به اسم «خمیر پیتزا» جدا بنویسی و هر جا لازم بود بگویی «خمیر پیتزا را (با این مقدار آرد) بساز». دستور فرعی مواد خودش را از سفارش‌دهنده می‌گیرد (آرگومان‌ها)، ممکن است یک چیز به او برگرداند (نتیجه)، و ظرف‌های کوچک خودش را دارد که با ظرف‌های آشپزخانه قاطی نشوند (متغیر local).

دو شکل تعریف
name() {
دستورها
}
function name {
دستورها
}
  • شکل اول (name() { ...; }) استاندارد POSIX است و در sh هم کار می‌کند. شکل دوم (کلمه‌ی function) مخصوص bash است. در این دوره شکل اول را می‌نویسیم.
  • بعد از { باید فاصله باشد و دستور آخر باید با ; یا خط جدید تمام شود (قبل از }).
  • تابع باید قبل از صدازدنش اجرا (تعریف) شده باشد. اسکریپت از بالا به پایین اجرا می‌شود؛ تابعی که پایین‌تر تعریف شده، بالاتر ناشناخته است. (برای همین معمولاً همه‌ی تعریف‌ها بالای فایل می‌آیند.)
  • صدازدن: مثل هر دستور، name آرگومان‌ها، بدون پرانتز.
تابع‌ها مثل دستورهای تازه‌اند، اما در همان پروسه‌ی شل اجرا می‌شوند. برای همین به متغیرها و پوشه‌ی جاری اسکریپت دسترسی دارند و می‌توانند آن‌ها را عوض کنند؛ ولی $(تابع) در یک پروسه‌ی فرزند اجرا می‌شود و تغییرهایش بیرون نمی‌آید.

در این درس هر دستوری که ممکن است وسط یک خط خطای «توقف» بدهد را روی خط جدا گذاشته‌ام (درس شرط‌ها و متغیرها: خطای محاسبه بقیه‌ی همان خط را اجرا نمی‌کند).

مثال ۱: تعریف و فراخوانی

Section titled “مثال ۱: تعریف و فراخوانی”
Terminal window
greet() {
echo "Hello, $1!"
}
greet Ali
greet "Sara Karimi"
function farewell {
echo "Goodbye, $1!"
}
farewell Ali
echo "--- فهرست توابع تعریف‌شده (declare -F):"
declare -F
خروجی
Hello, Ali!
Hello, Sara Karimi!
Goodbye, Ali!
--- فهرست توابع تعریف‌شده (declare -F):
declare -f farewell
declare -f greet

greet را مثل یک دستور جدید صدا زدیم. آرگومان اول (Ali) در $1 داخل تابع بود. declare -F فقط اسم تابع‌های فعلی شل را می‌دهد.

یک شرط مهم: تابع باید قبل از استفاده تعریف شده باشد:

Terminal window
cd ~/bashlab
printf 'hello\nhello() { echo hi; }\nhello\n' > early.sh
bash early.sh
خروجی
early.sh: line 1: hello: command not found
hi

بار اول hello را صدا زدیم و هنوز تعریف نشده بود: command not found. بعد از تعریف، کار می‌کند. قاعده: تعریف‌ها بالای اسکریپت، منطق اصلی پایین.

مثال ۲: آرگومان‌های تابع

Section titled “مثال ۲: آرگومان‌های تابع”

داخل تابع $1، $2، $#، "$@" و shift مال خود تابع هستند، نه اسکریپت. (فقط $0 همان اسم اسکریپت می‌ماند.)

/home/ali/bashlab/argdemo.sh
#!/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 z
echo "script still has $# args: first=[$1]"
Terminal window
cd ~/bashlab
bash argdemo.sh one two
خروجی
script 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=2
script still has 2 args: first=[one]

تابع سه آرگومان (x y z) گرفت، با اینکه اسکریپت دو تا داشت. shift داخل تابع فقط آرگومان‌های تابع را جابه‌جا کرد و بعد از برگشت، آرگومان‌های اسکریپت دست‌نخورده بودند. اگر لازم است آرگومان‌های اسکریپت را به تابع بدهی، صریح بنویس: show "$@". مقدار پیش‌فرض هم مثل قبل: name="${1:-World}".

مثال ۳: تابع چطور «نتیجه» برمی‌گرداند؟

Section titled “مثال ۳: تابع چطور «نتیجه» برمی‌گرداند؟”

در Bash تابع دو جور نتیجه دارد و تفاوتشان مهم‌ترین نکته‌ی این درس است:

۱. کد خروج (عدد ۰ تا ۲۵۵): با return N، یا خودکار کد آخرین دستور. برای «موفق شد؟» و استفاده در if. ۲. خروجی (متن): با echo/printf چاپ می‌شود و با $(تابع) گرفته می‌شود. برای داده.

Terminal window
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"; fi
if 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=5
4 is even
7 is odd
/etc is a directory
/etc/hostname is not a directory
out=[text output] status=3
  • add دادهاش را با echo برمی‌گرداند و $(add 2 3) آن را در متغیر می‌گیرد.
  • is_even و is_dir هیچ چیزی چاپ نمی‌کنند؛ فقط کد خروج می‌دهند، چون کد خروج آخرین دستورشان ((( )) یا [[ ]]) همان کد خروج تابع است. نتیجه: می‌توانی آن‌ها را مستقیم در if یا با &&/|| به کار ببری، درست مثل grep -q. نام‌گذاری با «is_» یا «has_» قرارداد خوبی برای این جور تابع‌هاست.
  • both هر دو را دارد: متن را با echo و کد ۳ را با return داد. out=$(both) متن را گرفت و $? بلافاصله بعدش، کد ۳ را.

چند قاعده درباره‌ی return:

Terminal window
big() {
return 300
}
big
echo "return 300 -> status $?"
text() {
return abc
}
text
echo "return abc -> status $?"
خروجی
return 300 -> status 44
bash: return: abc: numeric argument required
return abc -> status 2

کد خروج یک عدد بین ۰ تا ۲۵۵ است: ۳۰۰ دور می‌زند و می‌شود ۴۴ (۳۰۰ mod ۲۵۶). و متن را نمی‌شود با return برگرداند (numeric argument required). برای متن، echo.

مثال ۴: local، متغیرها سراسری‌اند

Section titled “مثال ۴: local، متغیرها سراسری‌اند”

در Bash هر متغیری که داخل تابع بسازی یا عوض کنی، سراسری است و بعد از تابع هم می‌ماند. این بیشترین منبع باگ در اسکریپت‌های بزرگ است:

Terminal window
x=global
setx() {
x=changed-by-function
y=leaked
}
setx
echo "x=$x y=$y"
خروجی
x=changed-by-function y=leaked

تابع x را عوض کرد و y را از خودش به بیرون «نشت» داد. برای محدود کردن به تابع، با local تعریف کن:

Terminal window
x=global
outer() {
local x=local-value
echo "inside outer: $x"
inner
}
inner() {
echo "inner sees: $x"
}
outer
echo "outside: $x"
خروجی
inside outer: local-value
inner sees: local-value
outside: global

داخل outer متغیر x محلی است و بیرون دست‌نخورده می‌ماند (global). ولی نکته‌ی عجیب Bash: inner که از داخل outer صدا زده شد، همان x محلی outer را می‌بیند (local-value)! به این دامنه‌ی دینامیک می‌گویند: متغیر محلی برای تابع و همه‌ی تابع‌هایی که از آن صدا زده می‌شوند دیده می‌شود (در زبان‌های دیگر معمولاً این‌طور نیست). نتیجه: همیشه هر متغیر داخل تابع را local کن، تا از بیرون چیزی نشت نکند یا عوض نشود.

یک مثال واقعی از خرابکاری متغیر سراسری، با شمارنده‌ی حلقه:

Terminal window
bad() {
for i in 1 2; do :; done
}
for i in a b c; do
bad
echo "outer loop: i=$i"
done
خروجی
outer loop: i=2
outer loop: i=2
outer loop: i=2

تابع bad متغیر i خودش را در حلقه به کار برد و چون local نبود، i حلقه‌ی بیرونی را خراب کرد (i=2 به‌جای a، b، c). با local i در bad مشکل حل می‌شود.

و یک دام ظریف: local و جایگزینی دستور در یک خط، کد خروج دستور را پنهان می‌کند:

Terminal window
masked() {
local out=$(false)
echo "local out=\$(false) -> status $?"
}
proper() {
local out
out=$(false)
echo "local out; out=\$(false) -> status $?"
}
masked
proper
echo "--- local بیرون از تابع:"
local z=1
خروجی
local out=$(false) -> status 0
local out; out=$(false) -> status 1
--- local بیرون از تابع:
bash: local: can only be used in a function

local out=$(false) کد خروج خودِ local را برمی‌گرداند (که موفق است) نه false را. راه‌حل: اول local out بنویس و بعد در خط جدا out=$(دستور). (ShellCheck این را با هشدار SC2155 می‌گیرد.) و local بیرون از تابع خطاست.

مثال ۵: تابع‌های کمکی برای اسکریپت‌های واقعی

Section titled “مثال ۵: تابع‌های کمکی برای اسکریپت‌های واقعی”

اکثر اسکریپت‌های حرفه‌ای چند تابع کوچک دارند: خطا چاپ کن و برو (die)، ببین ابزار نصب است (require_cmd)، بپرس «مطمئنی؟» (confirm):

/home/ali/bashlab/tool.sh
#!/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"
fi
Terminal window
cd ~/bashlab
chmod +x tool.sh
echo "data" > a.txt
echo "--- بدون آرگومان:"
./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.txt
echo "--- جواب 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
کد خروج: 0
a.txt
--- جواب y:
deleted
کد خروج: 0
ls: 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 بیاور:

Terminal window
mkdir -p ~/bashlab/lib-demo/bin
cat > ~/bashlab/lib-demo/lib.sh <<'EOF'
# lib.sh: shared helper functions (meant to be sourced, not executed)
greet() {
echo "Hello, $1 (from lib.sh)"
}
EOF
cat > ~/bashlab/lib-demo/bin/main.sh <<'EOF'
#!/usr/bin/env bash
script_dir="$(cd "$(dirname "$0")" && pwd)"
source "$script_dir/../lib.sh"
greet "$1"
EOF
cat > ~/bashlab/lib-demo/bin/bad-main.sh <<'EOF'
#!/usr/bin/env bash
source ../lib.sh
greet "$1"
EOF
chmod +x ~/bashlab/lib-demo/bin/*.sh
cd /tmp
echo "--- از یک پوشه‌ی دیگر، با مسیر نسبی به خود اسکریپت:"
~/bashlab/lib-demo/bin/main.sh Ali
echo "--- با مسیر نسبی ../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 فایل را در همان شل اجرا می‌کند (درس اولین اسکریپت)، پس تابع‌هایش در اسکریپت اصلی در دسترس می‌شوند.

تابع می‌تواند خودش را صدا بزند. مثال کلاسیک، فاکتوریل (n! = n × (n-1)!):

Terminal window
fact() {
local n=$1
if (( n <= 1 )); then
echo 1
else
echo $(( n * $(fact $((n - 1))) ))
fi
}
fact 5
fact 10
خروجی
120
3628800

fact 5 ← 5 × fact 4 ← … تا fact 1. چون echo خروجی را برمی‌گرداند، هر سطح با $(...) نتیجه‌ی سطح پایین‌تر را می‌گیرد. هر فراخوانی متغیر n محلی خودش را دارد (برای همین local n مهم است). بازگشت بی‌نهایت یا خیلی عمیق پشته را پر می‌کند؛ Bash به‌صورت پیش‌فرض حدی ندارد، ولی با FUNCNEST می‌شود سقف گذاشت:

Terminal window
countdown() {
echo -n "$1 "
(( $1 > 0 )) && countdown $(( $1 - 1 ))
}
countdown 5
echo
echo "--- با سقف 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 “پشت پرده: تابع کجاست و چطور پیدا می‌شود؟”

تابع‌ها در حافظه‌ی خود شل نگه داشته می‌شوند (اسم ← بدنه). می‌توانی تعریفشان را ببینی:

Terminal window
log() {
echo "[$(date +%T)] $*"
}
type log
echo "--- فقط اسم‌ها (declare -F) و تعریف کامل (declare -f):"
declare -F log
declare -f log
خروجی
log is a function
log ()
{
echo "[$(date +%T)] $*"
}
--- فقط اسم‌ها (declare -F) و تعریف کامل (declare -f):
log
log ()
{
echo "[$(date +%T)] $*"
}

وقتی اسمی را می‌نویسی، شل به این ترتیب می‌گردد: alias ← تابع ← دستور داخلی (builtin) ← برنامه‌ی داخل PATH. پس یک تابع می‌تواند یک دستور واقعی را «بپوشاند»:

Terminal window
date() {
echo "fake date"
}
date
echo "--- command: رد کردن تابع و اجرای نسخه‌ی واقعی:"
command date +%Y
echo "--- type -a: همه‌ی معنی‌های یک اسم (اولی برنده است):"
type -a date
unset -f date
echo "--- بعد از unset -f:"
date +%Y
خروجی
fake date
--- command: رد کردن تابع و اجرای نسخه‌ی واقعی:
2026
--- type -a: همه‌ی معنی‌های یک اسم (اولی برنده است):
date is a function
date ()
{
echo "fake date"
}
date is /usr/bin/date
date is /bin/date
--- بعد از unset -f:
2026

command date تابع (و alias) را رد می‌کند و برنامه‌ی واقعی را اجرا می‌کند؛ unset -f تابع را از حافظه پاک می‌کند. پوشاندن دستورها گاهی مفید است (مثلاً ls() { command ls --color "$@"; }) ولی خطرناک هم هست:

Terminal window
( 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/hostname

ls() { ls -l; } خودش را صدا می‌زند (نه ls واقعی را) و بازگشت بی‌پایان می‌شود (اینجا با FUNCNEST قطعش کردیم). راه‌حل command ls.

تابع پروسه‌ی جدید نمی‌سازد، ولی $(تابع) می‌سازد:

Terminal window
echo "shell: BASHPID=$BASHPID"
pf() {
echo "in function: BASHPID=$BASHPID"
}
pf
echo "via \$(pf): $(pf)"
echo "--- تغییر متغیر داخل تابع، مستقیم و با \$(...):"
cnt=0
inc() { ((cnt++)); }
inc
inc
echo "direct: cnt=$cnt"
cnt=0
v=$(inc)
echo "in \$(...): cnt=$cnt"
خروجی
shell: BASHPID=22756
in function: BASHPID=22756
via $(pf): in function: BASHPID=22821
--- تغییر متغیر داخل تابع، مستقیم و با $(...):
direct: cnt=2
in $(...): cnt=0

BASHPID شماره‌ی پروسه‌ی فعلی است. صدازدن مستقیم همان پروسه‌ی شل است (پس inc شمارنده را زیاد کرد)، ولی $(pf) یک پروسه‌ی فرزند است و تغییرهایش (cnt) با پایانش از بین می‌روند. نتیجه: اگر می‌خواهی تابع چیزی را در متغیر بیرونی بگذارد، آن را مستقیم صدا بزن (و از متغیر سراسری استفاده کن)؛ اگر فقط داده می‌خواهی، $(...).

export -f: تابع‌ها به‌صورت پیش‌فرض به پروسه‌های فرزند (مثل bash script.sh یا bash -c) نمی‌رسند، مگر با export -f:

Terminal window
hi() {
echo "hi from an exported function"
}
bash -c 'hi' 2>&1
export -f hi
bash -c 'hi'
unset -f hi
bash -c 'hi' 2>&1
خروجی
bash: line 1: hi: command not found
hi from an exported function
bash: line 1: hi: command not found

بار اول فرزند تابع را نمی‌شناخت، بعد از export -f شناخت، و با unset -f دوباره از محیط رفت. (export -f مخصوص bash است؛ برای تنظیمات ثابت بیشتر source یک کتابخانه را به کار ببر.)

سه راه برای گرفتن نتیجه از تابع:

راه شکل مناسب
کد خروج 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 “۱) صدا زدن تابع قبل از تعریف”

مثال ۱: hello: command not found. راه‌حل: تعریف‌ها بالای فایل.

۲) } بدون ; یا خط جدید قبلش

Section titled “۲) } بدون ; یا خط جدید قبلش”
Terminal window
cd ~/bashlab
printf 'f() { echo hi }\nf\n' > no-semicolon.sh
bash -n no-semicolon.sh
printf 'f() { echo hi; }\nf\n' > with-semicolon.sh
bash with-semicolon.sh
خروجی
no-semicolon.sh: line 3: syntax error: unexpected end of file
hi

اگر } فاصله و دستور قبلش را ببیند، آن را یک آرگومان echo می‌داند (echo hi }) و آکولاد بسته را پیدا نمی‌کند. راه‌حل: echo hi; } یا } روی خط جدا.

۳) return با متن یا عدد بزرگ

Section titled “۳) return با متن یا عدد بزرگ”

مثال ۳: متن را با echo برگردان؛ return فقط کد ۰ تا ۲۵۵ است.

مثال ۴: شمارنده‌ی حلقه‌ی بیرونی خراب می‌شود و متغیرها نشت می‌کنند. راه‌حل: هر متغیر داخل تابع local (حتی local i برای حلقه‌ها).

۵) local x=$(cmd) و کد خروج گمشده

Section titled “۵) local x=$(cmd) و کد خروج گمشده”

مثال ۴. راه‌حل: local x و بعد در خط جدا x=$(cmd).

۶) انتظار تغییر متغیر از $(تابع)

Section titled “۶) انتظار تغییر متغیر از $(تابع)”

پشت پرده: v=$(inc) شمارنده را عوض نمی‌کند. راه‌حل: مستقیم صدا بزن (و از متغیر سراسری یا return)، یا خروجی را بگیر.

Terminal window
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 f
status: 5
in f
after f, status=5

exit داخل تابع، کل اسکریپت را می‌بندد (برای die همین را می‌خواهیم). return فقط از تابع برمی‌گردد و ادامه‌ی اسکریپت اجرا می‌شود. هر کدام را دانسته به کار ببر.

۸) تابع هم‌نام یک دستور واقعی

Section titled “۸) تابع هم‌نام یک دستور واقعی”

پشت پرده: ls() { ls -l; } بازگشت بی‌پایان است. راه‌حل: command ls.

✎ تمرینآسان

تابع square بنویس که یک عدد بگیرد و مربعش را echo کند؛ بعد با یک حلقه مربع ۱ تا ۵ را به شکل 3 squared is 9 چاپ کن و خروجی تابع را با $(...) بگیر.

دیدن جواب
Terminal window
square() {
echo $(( $1 * $1 ))
}
for n in 1 2 3 4 5; do
result=$(square "$n")
echo "$n squared is $result"
done
خروجی
1 squared is 1
2 squared is 4
3 squared is 9
4 squared is 16
5 squared is 25
✎ تمرینمتوسط

تمرین اصلی درس: تابع log که هر پیام را هم روی صفحه چاپ کند هم در فایل لاگ بنویسد. قالب هر خط: 2026-10-04 09:30:01 [INFO] پیام. سطح (INFO، WARN، ERROR) آرگومان اول باشد؛ پیام‌های ERROR روی stderr هم بروند (تا جدا قابل‌دیدن باشند). اسکریپت نمونه‌ای بنویس که از تابع استفاده کند و ثابت کن فایل لاگ هر سه پیام را دارد، ولی با 2>/dev/null فقط INFO و WARN روی صفحه می‌مانند.

دیدن جواب
cd ~/bashlab
cat > 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"
EOF
chmod +x app.sh
rm -f app.log
echo "--- همه‌ی خروجی (stdout و stderr):"
./app.sh
echo "--- فقط stdout (خطاها دور ریخته می‌شوند):"
./app.sh 2>/dev/null
echo "--- محتوای فایل لاگ (دو بار اجرا شد، پس هشت خط):"
wc -l < app.log
head -n 4 app.log
خروجی
--- همه‌ی خروجی (stdout و stderr):
2026-10-04 10:26:47 [INFO] backup started
2026-10-04 10:26:47 [WARN] disk is 78% full
2026-10-04 10:26:47 [ERROR] could not reach the database
2026-10-04 10:26:47 [INFO] backup finished
--- فقط stdout (خطاها دور ریخته می‌شوند):
2026-10-04 10:26:47 [INFO] backup started
2026-10-04 10:26:47 [WARN] disk is 78% full
2026-10-04 10:26:47 [INFO] backup finished
--- محتوای فایل لاگ (دو بار اجرا شد، پس هشت خط):
8
2026-10-04 10:26:47 [INFO] backup started
2026-10-04 10:26:47 [WARN] disk is 78% full
2026-10-04 10:26:47 [ERROR] could not reach the database
2026-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 ~/bashlab
cat > 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 1
fi
if ! is_number "$1"; then
echo "Error: '$1' is not a number" >&2
exit 2
fi
if ! is_dir "$2"; then
echo "Error: '$2' is not a directory" >&2
exit 3
fi
echo "OK: $1 and $2 are valid"
EOF
chmod +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>
کد خروج: 1
Error: 'abc' is not a number
کد خروج: 2
Error: '/no/such/dir' is not a directory
کد خروج: 3
OK: 5 and /tmp are valid
کد خروج: 0

دقت کن هر تابع یک‌خطی است و ; قبل از } را دارد. خواندن اسکریپت اصلی مثل خواندن جمله است: «اگر خالی بود… اگر عدد نبود… اگر پوشه نبود…».

✎ تمرینسخت

تابع retry بنویس: retry تعداد دستور آرگومان‌ها... یک دستور را حداکثر چند بار امتحان کند (با کمی صبر بین تلاش‌ها). اگر موفق شد، شماره‌ی تلاش را بگوید و کد ۰ برگرداند؛ اگر همه‌ی تلاش‌ها شکست خورد، پیام خطا و کد ۱. روی یک دستور «ناپایدار» (که اول دو بار شکست می‌خورد و بار سوم موفق می‌شود) امتحانش کن.

دیدن جواب
cd ~/bashlab
rm -f flaky.count
cat > flaky.sh <<'EOF'
#!/usr/bin/env bash
# flaky.sh: fails on the first two calls and succeeds from the third one
counter_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 ))
EOF
chmod +x flaky.sh
cat > 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 "$@"
EOF
chmod +x retry-demo.sh
echo "--- با ۵ تلاش (بار سوم موفق می‌شود):"
./retry-demo.sh 5 ./flaky.sh; echo "کد خروج: $?"
echo "--- شمارنده را صفر می‌کنیم و فقط ۲ تلاش می‌دهیم:"
rm -f flaky.count
./retry-demo.sh 2 ./flaky.sh; echo "کد خروج: $?"
خروجی
--- با ۵ تلاش (بار سوم موفق می‌شود):
flaky: call number 1
attempt 1 failed; retrying...
flaky: call number 2
attempt 2 failed; retrying...
flaky: call number 3
succeeded on attempt 3
کد خروج: 0
--- شمارنده را صفر می‌کنیم و فقط ۲ تلاش می‌دهیم:
flaky: call number 1
attempt 1 failed; retrying...
flaky: call number 2
failed after 2 attempts: ./flaky.sh
کد خروج: 1

نکته‌ها: ۱) local attempts="$1"; shift اولین آرگومان را می‌گیرد؛ بعد "$@" خودِ دستور و آرگومان‌هایش است (الگوی درس ورودی و آرگومان‌ها). ۲) until "$@"; do ...; done دستور را اجرا می‌کند و تا وقتی شکست می‌خورد حلقه ادامه دارد (until). ۳) return 1 بعد از پایان تلاش‌ها از تابع برمی‌گردد (نه exit، تا صدازننده بتواند تصمیم بگیرد). ۴) چون شمارنده‌ی flaky را در فایل نگه داشتیم، هر اجرا به تلاش بعدی می‌رسد.

⚡ بررسی سریع

در x=$(greet Ali) وقتی greet یک تابع است، چه چیزی در x می‌رود؟

؟ آزمونک
  1. در Bash، متغیری که داخل یک تابع (بدون local) مقدار می‌گیرد، بعد از تابع چه حالتی دارد؟

  2. تفاوت return و echo برای نتیجه‌ی یک تابع چیست؟

  3. چرا local out=$(false) کد خروج دستور را از دست می‌دهد؟

  4. اگر داخل تابعی exit 1 بنویسی چه می‌شود؟

  5. v=$(inc) که inc شمارنده‌ی cnt را زیاد می‌کند. بعد از آن cnt چند است؟

  6. تابعی در اسکریپت تعریف کرده‌ای و می‌خواهی در bash script2.sh (فرزند) هم کار کند. چه می‌کنی؟

  • تعریف: 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 فرزند