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

شرط‌ها

توی این درس یاد می‌گیری اسکریپتت را «باهوش» کنی: بر اساس شرایط تصمیم بگیرد. ساختار if و elif و else را می‌نویسی و می‌فهمی که if در Bash یک عبارت منطقی نمی‌سنجد، بلکه کد خروج یک دستور را می‌سنجد؛ فرق [[ ]] با [ ] را می‌دانی؛ عدد و رشته را درست مقایسه می‌کنی ([[ $a -gt $b ]]، ==، -z، =~)؛ فایل و پوشه را با -f، -d و -e می‌آزمایی؛ شرط‌ها را با &&، || و ! ترکیب می‌کنی؛ و $? را درست می‌خوانی. تمرین: اسکریپتی که اگر فضای دیسک بیشتر از ۸۰٪ پر بود هشدار بدهد.

مسئله: اسکریپتی که فقط خط به خط می‌رود

Section titled “مسئله: اسکریپتی که فقط خط به خط می‌رود”

تا اینجا اسکریپت‌ها از اول تا آخر اجرا می‌شدند، بی‌توجه به اینکه چه اتفاقی افتاد. کار واقعی پر از «اگر» است: اگر پوشه‌ی بکاپ وجود ندارد، بساز. اگر فایل نیست، با پیام خطا تمام کن. اگر دیسک پر شده، هشدار بده. اگر کاربر y زد، ادامه بده. بدون شرط، اسکریپت فقط یک خط‌کش است؛ با شرط، کم‌کم یک برنامه می‌شود.

تشبیه: دستور پخت شرط‌دار

Section titled “تشبیه: دستور پخت شرط‌دار”

دستور پخت‌های خوب پر از «اگر» هستند: «اگر خمیر آماده بود، فر را روشن کن؛ وگرنه ده دقیقه صبر کن». آشپز اول وضعیت را نگاه می‌کند (جواب بله/نه)، و بر اساس آن یکی از دو مسیر را می‌رود. نکته‌ی عجیب Bash این است که «نگاه‌کردن» همیشه اجرای یک دستور است و جواب «بله/نه» همان کد خروج آن است. این را در بخش بعد می‌بینی.

if در Bash: کد خروج را می‌سنجد

Section titled “if در Bash: کد خروج را می‌سنجد”

ساختار کلی:

ساختار if
if دستور; then
... اگر دستور موفق بود (کد خروج ۰)
elif دستور دیگر; then
... اگر اولی شکست خورد و این موفق بود
else
... اگر هیچ‌کدام موفق نبود
fi

fi یعنی پایان if (if را برعکس بخوان). then یا باید روی خط جدا بیاید یا بعد از ;. و عمق را با فاصله‌گذاری نشان بده تا خواندنش آسان باشد.

در Bash شرط یک «عبارت درست یا غلط» نیست؛ یک دستور است. اگر با کد خروج ۰ تمام شد (موفق)، شاخه‌ی then اجرا می‌شود؛ هر کد غیر صفر (شکست) یعنی elif یا else. این برعکسِ بیشتر زبان‌هاست که ۰ را «غلط» می‌گیرند.

پس [[ -f file ]] یا [ -f file ] که در اسکریپت‌ها می‌بینی، دستورهایی‌اند که شرط را بررسی می‌کنند و با کد ۰ (درست) یا ۱ (غلط) تمام می‌شوند. چطور؟ در «پشت پرده» می‌بینی.

مثال ۱: if یعنی «این دستور موفق شد؟»

Section titled “مثال ۱: if یعنی «این دستور موفق شد؟»”

قبل از هر چیز، if را با یک دستور معمولی امتحان کن، بدون هیچ [ ]:

Terminal window
if grep -q root /etc/passwd; then
echo "root exists in /etc/passwd"
else
echo "root not found"
fi
if grep -q nothere /etc/passwd; then
echo "nothere exists"
else
echo "nothere not found"
fi
echo "--- کد خروج‌ها با خودِ \$?:"
grep -q root /etc/passwd; echo "match: $?"
grep -q nothere /etc/passwd; echo "no match: $?"
خروجی
root exists in /etc/passwd
nothere not found
--- کد خروج‌ها با خودِ $?:
match: 0
no match: 1

grep -q (quiet) چیزی چاپ نمی‌کند و فقط با کد خروج جواب می‌دهد: ۰ یعنی پیدا شد، ۱ یعنی پیدا نشد. if همان کد را می‌سنجد. echo $? همان عدد را نشان می‌دهد؛ $? کد خروج آخرین دستور است (یادآوری: ۰ موفق، هر عدد دیگر شکست).

مثال ۲: مقایسه‌ی عددها، [[ ]] و (( ))

Section titled “مثال ۲: مقایسه‌ی عددها، [[ ]] و (( ))”

برای مقایسه‌ی عدد، دو دستور رایج داری. عملگرهای [[ ]] با حروف‌اند (-gt یعنی greater than، بزرگ‌تر)، و (( )) (درس قبل: محاسبه) شبیه ریاضی:

Terminal window
a=15
b=8
if [[ $a -gt $b ]]; then
echo "$a is greater than $b"
fi
if [[ $a -eq 15 ]]; then echo "a equals 15"; fi
[[ $a -lt $b ]]
echo "a < b ? status=$? (1 means false)"
echo "--- همان با (( )) که شبیه ریاضی است:"
if (( a > b )); then echo "(( a > b )) is true"; fi
(( a % 5 == 0 )) && echo "a is divisible by 5"
خروجی
15 is greater than 8
a equals 15
a < b ? status=1 (1 means false)
--- همان با (( )) که شبیه ریاضی است:
(( a > b )) is true
a is divisible by 5

حالا elif: چند شاخه پشت هم. شرط‌ها از بالا به پایین بررسی می‌شوند و اولین شاخه‌ای که درست بود اجرا می‌شود و بقیه نادیده گرفته می‌شوند (ترتیب مهم است):

/home/ali/bashlab/grade.sh
#!/usr/bin/env bash
# grade.sh: turn a score (0-100) into a letter
score="$1"
if [[ $score -ge 90 ]]; then
echo "$score -> A"
elif [[ $score -ge 80 ]]; then
echo "$score -> B"
elif [[ $score -ge 70 ]]; then
echo "$score -> C"
else
echo "$score -> F"
fi
Terminal window
cd ~/bashlab
chmod +x grade.sh
for s in 95 85 72 40 90; do ./grade.sh $s; done
خروجی
95 -> A
85 -> B
72 -> C
40 -> F
90 -> A

نمره‌ی ۹۵ هم از -ge 80 می‌گذرد، ولی چون شرط اول (-ge 90) زودتر درست بود، B نشد. (حلقه‌ی for را در درس حلقه‌ها می‌خوانی؛ اینجا یعنی «برای هر نمره».)

مثال ۳: مقایسه‌ی رشته‌ها

Section titled “مثال ۳: مقایسه‌ی رشته‌ها”

برای رشته‌ها عملگرهای نمادین داری: ==، !=، و دو آزمون ویژه: -z (خالی است؟) و -n (خالی نیست؟). سمت راست == در [[ ]] می‌تواند الگو (wildcard) باشد، و =~ یک regex می‌گیرد:

Terminal window
name="Ali"
[[ $name == "Ali" ]] && echo "name is Ali"
[[ $name != "Sara" ]] && echo "name is not Sara"
empty=""
[[ -z $empty ]] && echo "empty is empty (-z)"
[[ -n $name ]] && echo "name is not empty (-n)"
echo "--- الگو (wildcard) سمت راست ==:"
file="report.txt"
[[ $file == *.txt ]] && echo "$file is a .txt file"
[[ $file == "*.txt" ]] || echo "quoted: the * is literal, so no match"
echo "--- regex با =~:"
date="2026-10-04"
if [[ $date =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "$date looks like YYYY-MM-DD"
fi
echo "--- ترتیب الفبایی با > (نه عددی!):"
[[ 9 > 10 ]] && echo "9 > 10 is TRUE (compared as text)"
[[ 9 -gt 10 ]] || echo "9 -gt 10 is false (compared as numbers)"
خروجی
name is Ali
name is not Sara
empty is empty (-z)
name is not empty (-n)
--- الگو (wildcard) سمت راست ==:
report.txt is a .txt file
quoted: the * is literal, so no match
--- regex با =~:
2026-10-04 looks like YYYY-MM-DD
--- ترتیب الفبایی با > (نه عددی!):
9 > 10 is TRUE (compared as text)
9 -gt 10 is false (compared as numbers)
  • سمت راست == بدون کوتیشن، الگو است: *.txt با هر چیزی که به .txt ختم شود جور درمی‌آید. اگر کوتیشن بگذاری، همان متن مقایسه می‌شود ("*.txt" فقط با خود ستاره‌دار جور است). این برای وقتی که می‌خواهی دقیقاً مقایسه کنی مهم است: [[ $a == "$b" ]].
  • =~ regex‌های گسترده (ERE) را می‌پذیرد، همان‌هایی که در درس grep و regex دیدی. الگو را بدون کوتیشن بنویس. گروه‌های داخل پرانتز در آرایه‌ی ویژه‌ی BASH_REMATCH می‌مانند (در درس آرایه‌ها).
  • > و < مقایسه‌ی الفبایی‌اند (مثل ترتیب لغت‌نامه)، و برای عدد خطرناک: «۹» از «۱۰» بزرگ‌تر است چون 9 از 1 بعد می‌آید. برای عدد حتماً -gt و -lt یا (( )).

مثال ۴: آزمون فایل و پوشه

Section titled “مثال ۴: آزمون فایل و پوشه”

مهم‌ترین کاربرد شرط‌ها در اسکریپت‌های سیستمی: ببین فایل یا پوشه وجود دارد یا نه، و چه نوعی است:

Terminal window
mkdir -p ~/bashlab/ft && cd ~/bashlab/ft
mkdir folder
echo "data" > notes.txt
: > empty.txt
ln -s notes.txt shortcut
for p in notes.txt folder shortcut empty.txt missing.txt; do
printf '%-12s' "$p"
[[ -e $p ]] && printf ' exists' || printf ' (missing)'
[[ -f $p ]] && printf ' file'
[[ -d $p ]] && printf ' directory'
[[ -L $p ]] && printf ' symlink'
[[ -s $p ]] && printf ' non-empty'
echo
done
خروجی
notes.txt exists file non-empty
folder exists directory non-empty
shortcut exists file symlink non-empty
empty.txt exists file
missing.txt (missing)

هر آزمون یک حرف دارد: -e وجود دارد (هر نوعی)، -f فایل معمولی است، -d پوشه است، -L پیوند نمادین است، -s وجود دارد و خالی نیست (اندازه‌اش بیشتر از صفر). دقت کن shortcut هم -f است (آزمون‌ها پیوند را دنبال می‌کنند) و هم -L، و empty.txt فایل است ولی -s نمی‌خورد.

آزمون‌های مجوز و زمان:

Terminal window
cd ~/bashlab/ft
echo "echo hi" > run.sh
chmod 644 run.sh
[[ -x run.sh ]] || echo "run.sh is not executable"
chmod +x run.sh
[[ -x run.sh ]] && echo "run.sh is executable now"
[[ -r run.sh && -w run.sh ]] && echo "run.sh is readable and writable"
touch -d '2020-01-01' old.txt
[[ notes.txt -nt old.txt ]] && echo "notes.txt is newer than old.txt"
خروجی
run.sh is not executable
run.sh is executable now
run.sh is readable and writable
notes.txt is newer than old.txt

-r، -w و -x به ترتیب خواندن، نوشتن و اجرا را برای کاربری که اسکریپت را اجرا می‌کند بررسی می‌کنند (نه صرفاً حروف مجوز). -nt (newer than) و -ot (older than) دو فایل را از نظر زمان ویرایش مقایسه می‌کنند.

یک الگوی همیشگی: پوشه را فقط اگر نبود بساز. اسکریپت را دو بار اجرا کن:

Terminal window
backup_dir="$HOME/bashlab/backups"
for attempt in 1 2; do
echo "--- attempt $attempt"
if [[ ! -d $backup_dir ]]; then
echo "creating $backup_dir"
mkdir -p "$backup_dir"
else
echo "$backup_dir already exists"
fi
done
خروجی
--- attempt 1
creating /home/ali/bashlab/backups
--- attempt 2
/home/ali/bashlab/backups already exists

! شرط را برعکس می‌کند: «اگر این یک پوشه نیست». بار اول ساخته شد، بار دوم دست نخورد؛ چنین اسکریپتی ایمن برای اجرای دوباره است (به اصطلاح idempotent).

در اسکریپت‌های قدیمی (و راهنماهای اینترنت) بیشتر [ ... ] می‌بینی. این دو شبیه‌اند ولی یکی نیستند، و تفاوتشان در موارد زیر خودش را نشان می‌دهد:

Terminal window
empty=""
file="my report.txt"
touch ~/bashlab/"$file"
cd ~/bashlab
echo "--- [ ] با متغیر خالی:"
[ $empty = x ]
echo "status=$?"
echo "--- [[ ]] با همان:"
[[ $empty = x ]]
echo "status=$?"
echo "--- [ ] با نام فایل دارای فاصله:"
[ -f $file ]
echo "status=$?"
echo "--- [[ ]] با همان:"
[[ -f $file ]]
echo "status=$?"
echo "--- مقدار غیرعددی با -eq:"
[ abc -eq 1 ]
echo "[ ] status=$?"
[[ abc -eq 1 ]]
echo "[[ ]] status=$?"
echo "--- در sh (که روی Ubuntu همان dash است):"
sh -c '[ a == a ]'
echo "dash with ==: status=$?"
sh -c '[ a = a ]'
echo "dash with =: status=$?"
خروجی
--- [ ] با متغیر خالی:
bash: [: =: unary operator expected
status=2
--- [[ ]] با همان:
status=1
--- [ ] با نام فایل دارای فاصله:
bash: [: my: binary operator expected
status=2
--- [[ ]] با همان:
status=0
--- مقدار غیرعددی با -eq:
bash: [: abc: integer expression expected
[ ] status=2
[[ ]] status=1
--- در sh (که روی Ubuntu همان dash است):
sh: 1: [: a: unexpected operator
dash with ==: status=2
dash with =: status=0
  • [ یک دستور معمولی است: متغیرهای بدون کوتیشن قبل از رسیدن به آن در فاصله‌ها شکسته می‌شوند. [ $empty = x ] برای [ می‌شود [ = x ] و خطای «unary operator expected» می‌دهد؛ [ -f $file ] با نام فایل دارای فاصله [ -f my report.txt ] می‌شود و binary operator expected. در [ ] باید همه‌ی متغیرها در کوتیشن باشند: [ -f "$file" ].
  • [[ یک کلمه‌ی کلیدی خود Bash است: شل کل عبارت را خودش تجزیه می‌کند، پس شکستن در فاصله‌ها و باز شدن * در آن اتفاق نمی‌افتد. متغیر خالی یا با فاصله در آن بی‌خطر است. فقط سمت راست == و =~ معنی ویژه دارد: بدون کوتیشن «الگو» است و با کوتیشن «متن دقیق».
  • [[ abc -eq 1 ]] بی‌صدا غلط است (کد ۱)، ولی [ abc -eq 1 ] خطا می‌دهد (کد ۲)؛ در [[ متن غیرعددی مثل متغیر خالی صفر حساب می‌شود (درس قبل).
  • [ a == a ] در bash کار می‌کند ولی در sh (dash) نه: [ استاندارد فقط = را می‌شناسد. [[ اصلاً در sh نیست (درس اولین اسکریپت).

قاعده: در اسکریپت‌های bash همیشه [[ ]] بنویس. فقط اگر اسکریپت باید با sh هم اجرا شود از [ ] با کوتیشن و = استفاده کن.

مثال ۶: ترکیب شرط‌ها، &&، || و !

Section titled “مثال ۶: ترکیب شرط‌ها، &&، || و !”

دو جور ترکیب داری. داخل [[ ]] با && (و)، || (یا) و ! (نه). و بین دو دستور با همان && و ||: «اگر قبلی موفق شد، بعدی را اجرا کن» و «اگر شکست خورد، بعدی را»:

Terminal window
age=25
country="IR"
echo "--- داخل [[ ]]: && و ||"
if [[ $age -ge 18 && $country == "IR" ]]; then echo "adult from IR"; fi
if [[ $age -lt 18 || $country != "IR" ]]; then echo "minor or foreign"; else echo "adult from IR"; fi
echo "--- نفی با !"
if [[ ! -e /no/such/file ]]; then echo "/no/such/file does not exist"; fi
echo "--- بین دستورها: && (اگر موفق شد) و || (اگر شکست خورد)"
mkdir -p ~/bashlab/work && cd ~/bashlab/work && echo "now in $(pwd)"
cd /nonexistent 2>/dev/null || echo "cd failed, so this runs"
echo "--- دام: A && B || C دقیقاً if/else نیست"
[[ 1 -eq 1 ]] && false || echo "C ran even though the condition was true (B failed)"
خروجی
--- داخل [[ ]]: && و ||
adult from IR
adult from IR
--- نفی با !
/no/such/file does not exist
--- بین دستورها: && (اگر موفق شد) و || (اگر شکست خورد)
now in /home/ali/bashlab/work
cd failed, so this runs
--- دام: A && B || C دقیقاً if/else نیست
C ran even though the condition was true (B failed)

الگوهای پرکاربرد: mkdir -p dir && cd dir (بساز و برو)، و cd /path || exit 1 (اگر نشد، متوقف شو؛ در درس مدیریت خطا زیاد می‌بینی). ولی A && B || C را به‌جای if/else به کار نبر: اگر B شکست بخورد، C هم اجرا می‌شود (آخرین خط). برای دو شاخه‌ی واقعی if ... else ... fi بنویس.

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

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

حالا چند شرط کنار هم در یک اسکریپت کاربردی: بکاپ‌گیری که قبل از کار، همه‌چیز را بررسی می‌کند:

/home/ali/bashlab/safe-backup.sh
#!/usr/bin/env bash
# safe-backup.sh: archive a directory, with sanity checks
# Usage: ./safe-backup.sh <directory>
if [[ $# -ne 1 ]]; then
echo "Usage: $0 <directory>" >&2
exit 1
fi
source_dir="$1"
backup_dir="$HOME/bashlab/backups"
backup_file="$backup_dir/$(basename "$source_dir")-$(date +%F).tar.gz"
if [[ ! -d "$source_dir" ]]; then
echo "Error: '$source_dir' is not a directory" >&2
exit 2
fi
if [[ ! -d "$backup_dir" ]]; then
echo "Creating $backup_dir"
mkdir -p "$backup_dir"
fi
if [[ -e "$backup_file" ]]; then
echo "Backup already exists: $backup_file (skipping)"
exit 0
fi
if tar -czf "$backup_file" -C "$(dirname "$source_dir")" "$(basename "$source_dir")"; then
echo "Backup created: $backup_file"
else
echo "Backup FAILED" >&2
exit 3
fi
Terminal window
cd ~/bashlab
chmod +x safe-backup.sh
rm -rf backups
mkdir -p project/src && echo "print(1)" > project/src/app.py
echo "--- بدون آرگومان:"
./safe-backup.sh; echo "کد خروج: $?"
echo "--- پوشه‌ی ناموجود:"
./safe-backup.sh nowhere; echo "کد خروج: $?"
echo "--- بار اول:"
./safe-backup.sh project; echo "کد خروج: $?"
echo "--- بار دوم (همان روز):"
./safe-backup.sh project; echo "کد خروج: $?"
خروجی
--- بدون آرگومان:
Usage: ./safe-backup.sh <directory>
کد خروج: 1
--- پوشه‌ی ناموجود:
Error: 'nowhere' is not a directory
کد خروج: 2
--- بار اول:
Creating /home/ali/bashlab/backups
Backup created: /home/ali/bashlab/backups/project-2026-10-04.tar.gz
کد خروج: 0
--- بار دوم (همان روز):
Backup already exists: /home/ali/bashlab/backups/project-2026-10-04.tar.gz (skipping)
کد خروج: 0

چهار آزمون به ترتیب: تعداد آرگومان، وجود پوشه‌ی مبدأ، وجود پوشه‌ی مقصد (ساختن در صورت نبود)، و جلوگیری از رونویسی بکاپ امروز. و آخرین if یک دستور واقعی (tar) را مستقیم می‌سنجد، بدون [[ ]]: اگر tar موفق شد پیام موفقیت، وگرنه خطا با کد ۳. هر مسیر خروج کد خودش را دارد، تا هر کس (cron یا اسکریپت دیگر) بفهمد چه شد.

مثال ۸: $? را فوراً بخوان

Section titled “مثال ۸: $? را فوراً بخوان”

$? بعد از هر دستور عوض می‌شود، حتی echo. اگر لازمش داری، همان لحظه بخوان یا در متغیر بگذار:

Terminal window
ls /etc/hostname > /dev/null
echo "status of ls: $?"
echo "next command"
echo "status now (of the previous echo, not ls): $?"
echo "--- ذخیره در متغیر:"
grep -q root /etc/passwd
status=$?
echo "searched"
echo "grep status was $status"
خروجی
status of ls: 0
next command
status now (of the previous echo, not ls): 0
--- ذخیره در متغیر:
searched
grep status was 0

اگر فقط می‌خواهی «موفق شد یا نه»، به‌جای $? مستقیم از if دستور; then استفاده کن (مثال ۱)؛ کوتاه‌تر و بی‌خطاتر است. $? وقتی خوب است که عدد دقیق را می‌خواهی (مثل تفاوت کد ۱ و ۲ در grep) یا باید در لاگ بنویسی.

پشت پرده: if چطور کار می‌کند؟

Section titled “پشت پرده: if چطور کار می‌کند؟”

if هر دستوری را اجرا می‌کند و کد خروجش را می‌سنجد. حتی true و false که فقط کد خروج برمی‌گردانند:

Terminal window
if true; then echo "true -> then"; fi
if false; then echo "never"; else echo "false -> else"; fi
if ls /etc/passwd > /dev/null; then echo "ls succeeded -> then"; fi
if ! ls /nope 2> /dev/null; then echo "ls failed, and ! turned it into a success"; fi
if false; true; then echo "with several commands, the LAST one decides"; fi
خروجی
true -> then
false -> else
ls succeeded -> then
ls failed, and ! turned it into a success
with several commands, the LAST one decides

آخرین خط نشان می‌دهد وقتی چند دستور بین if و then باشد، کد خروج آخری حساب می‌شود. و ! کد خروج را برعکس می‌کند.

پس [ و [[ چه هستند؟ خود دستورند:

Terminal window
type [ [[ test
ls -l /usr/bin/[ /usr/bin/test
echo "--- خودشان کد خروج می‌دهند:"
[[ 1 -eq 1 ]]; echo "1 -eq 1 -> $?"
[[ 1 -eq 2 ]]; echo "1 -eq 2 -> $?"
خروجی
[ is a shell builtin
[[ is a shell keyword
test is a shell builtin
-rwxr-xr-x 1 root root 67760 Aug 25 15:09 /usr/bin/[
-rwxr-xr-x 1 root root 67760 Aug 25 15:09 /usr/bin/test
--- خودشان کد خروج می‌دهند:
1 -eq 1 -> 0
1 -eq 2 -> 1
  • [ یک دستور داخلی bash است (builtin). همین نام یک فایل برنامه هم در /usr/bin/[ هست (هم‌اندازه با test، چون همان برنامه است). آخرین آرگومانش را باید ] بگذاری، فقط برای زیبایی ظاهر. چون یک دستور معمولی است، تمام قواعد آرگومان و کوتیشن درس قبل درباره‌اش صدق می‌کند.
  • [[ یک کلمه‌ی کلیدی (keyword) است، نه دستور. شل آن را خودش تجزیه می‌کند؛ برای همین امکانات ویژه دارد (=~، &&، == الگو) و شکستن کلمه‌ها در آن نیست.
  • test عبارت همان [ عبارت ] است.

این توضیح می‌دهد چرا فاصله‌ها مهم‌اند (بخش اشتباهات): [[$a بدون فاصله، کلمه‌ی [[1 می‌شود و شل دنبال دستوری به این اسم می‌گردد ([[1: command not found).

مقایسه‌ی عددها:

معنی داخل [[ ]] داخل (( ))
برابر -eq ==
نابرابر -ne !=
کوچک‌تر -lt <
کوچک‌تر یا مساوی -le <=
بزرگ‌تر -gt >
بزرگ‌تر یا مساوی -ge >=

آزمون‌های رشته (داخل [[ ]]):

آزمون درست است وقتی…
[[ $a == "$b" ]] دو رشته برابرند (= هم همین)
[[ $a != "$b" ]] برابر نیستند
[[ $a == الگو* ]] با الگوی wildcard جور است (الگو بدون کوتیشن)
[[ $a =~ regex ]] با regex جور است
[[ -z $a ]] رشته خالی است
[[ -n $a ]] رشته خالی نیست
[[ $a < $b ]] [[ $a > $b ]] ترتیب الفبایی (نه عددی)

آزمون‌های فایل:

آزمون درست است وقتی…
-e مسیر وجود دارد (هر نوعی)
-f مسیر فایل معمولی است
-d مسیر پوشه است
-L مسیر پیوند نمادین است
-s مسیر وجود دارد و خالی نیست
-r -w -x برای کاربر فعلی خواندنی، نوشتنی، اجرایی است
a -nt b / a -ot b a تازه‌تر / قدیمی‌تر از b است

ترکیب و نفی: داخل [[ ]] با &&، ||، ! و پرانتز؛ بین دستورها با && (اگر موفق) و || (اگر شکست). ! قبل از دستور هم کد خروج را برعکس می‌کند.

[[ ]] در برابر [ ]:

ویژگی [[ ]] (bash) [ ] / test (POSIX)
نوع کلمه‌ی کلیدی؛ شل خودش تجزیه می‌کند دستور معمولی
متغیر بدون کوتیشن امن (شکسته نمی‌شود) خطرناک؛ حتماً کوتیشن
==، =~، &&، ||، < بله = فقط؛ -a/-o قدیمی
در sh (dash) نیست هست

۱) فاصله‌ی لازم را نگذاشتن

Section titled “۱) فاصله‌ی لازم را نگذاشتن”

[[ و ]] کلمه‌های جدا هستند و عملگرها هم باید با فاصله بیایند:

Terminal window
a=1
b=2
[[ $a==$b ]] && echo "equal?! (wrong: a=1, b=2)"
if [[$a -gt 1]]; then echo "yes"; fi
echo "کد خروج if بدون شاخه‌ی اجراشده: $?"
خروجی
equal?! (wrong: a=1, b=2)
bash: [[1: command not found
کد خروج if بدون شاخه‌ی اجراشده: 0

[[ $a==$b ]] فقط یک کلمه (1==2) می‌بیند، و رشته‌ی غیرخالی همیشه «درست» است؛ برای همین نتیجه‌ی خنده‌دار و غلط. [[$a بدون فاصله [[1 می‌شود، دستوری که وجود ندارد. (ملاحظه: چون شرط «دستور پیدا نشد» غلط حساب می‌شود، if ساکت و با کد ۰ تمام شد.) راه‌حل: [[ $a == $b ]]، فاصله دور هر عملگر و کنار [[ و ]].

۲) رشته‌ای مقایسه کردن عدد

Section titled “۲) رشته‌ای مقایسه کردن عدد”

مثال ۳: [[ 9 > 10 ]] درست است! راه‌حل: -gt و -lt، یا (( 9 > 10 )). و برعکسش، == برای عدد هم متنی مقایسه می‌کند:

Terminal window
[[ 5 == 05 ]]
echo "5 == 05 (as text): status=$?"
[[ 5 -eq 05 ]]
echo "5 -eq 05 (as number): status=$?"
خروجی
5 == 05 (as text): status=1
5 -eq 05 (as number): status=0

برای عدد همیشه -eq/(( ))؛ برای رشته ==.

Terminal window
cd ~/bashlab
printf 'if true; then\n echo hi\n' > no-fi.sh
printf 'if true\n echo hi\nfi\n' > no-then.sh
printf 'if [[ 1 -eq 1 ]]; then\n echo hi\nelif\nfi\n' > bad-elif.sh
for f in no-fi.sh no-then.sh bad-elif.sh; do
echo "--- $f:"
bash -n $f
done
خروجی
--- no-fi.sh:
no-fi.sh: line 3: syntax error: unexpected end of file
--- no-then.sh:
no-then.sh: line 3: syntax error near unexpected token `fi'
no-then.sh: line 3: `fi'
--- bad-elif.sh:
bad-elif.sh: line 4: syntax error near unexpected token `fi'
bad-elif.sh: line 4: `fi'

bash -n (درس اولین اسکریپت) فقط نحو را بررسی می‌کند. شماره‌ی خطی که می‌گیری جایی است که شل دیگر نتوانست ادامه بدهد، و همیشه خودِ ایراد نیست: برای fi ناموجود، انتهای فایل (unexpected end of file)؛ برای then ناموجود، خودِ fi (چون شل منتظر then بود و fi دید)؛ و elif بدون شرط هم همین‌طور. وقتی پیام را می‌بینی، به خط‌های قبلی هم نگاه کن.

۴) [ ] و متغیر بدون کوتیشن

Section titled “۴) [ ] و متغیر بدون کوتیشن”

مثال ۵. راه‌حل: [[ ]]، یا [ "$var" = x ].

مثال ۶: وقتی B شکست بخورد، C هم اجرا می‌شود. راه‌حل: if ...; then B; else C; fi.

۶) خواندن $? بعد از دستور دیگر

Section titled “۶) خواندن $? بعد از دستور دیگر”
Terminal window
grep -q root /etc/passwd
echo "searched"
if [ $? -eq 0 ]; then echo "found (but this checked the echo, not the grep!)"; fi
خروجی
searched
found (but this checked the echo, not the grep!)

بین grep و if یک echo آمده و $? مال echo است. اگر خودِ grep را بخواهی، یا if grep -q ...; then بنویس یا کد را بلافاصله در متغیر بگذار.

۷) خطا را «پیدا نشد» فرض کردن

Section titled “۷) خطا را «پیدا نشد» فرض کردن”

grep سه کد دارد: ۰ پیدا شد، ۱ پیدا نشد، ۲ خطا (مثلاً فایل نیست). if grep -q ... هر دو ۱ و ۲ را «غلط» می‌بیند:

Terminal window
if grep -q root /nonexistent-file; then
echo "found"
else
echo "NOT FOUND (really? the file does not exist!)"
fi
grep -q root /nonexistent-file; echo "grep status: $?"
خروجی
grep: /nonexistent-file: No such file or directory
NOT FOUND (really? the file does not exist!)
grep: /nonexistent-file: No such file or directory
grep status: 2

پیام خطای grep ریخته شد و اسکریپت با اطمینان گفت «پیدا نشد». راه‌حل: اگر فرق ۱ و ۲ برایت مهم است، اول وجود فایل را بسنج ([[ -f ... ]]) یا $? را بخوان.

✎ تمرینآسان

اسکریپت is-even.sh بنویس که یک عدد از آرگومان بگیرد و بگوید زوج است یا فرد (7 is odd). از (( )) و باقیمانده استفاده کن. اگر آرگومان نبود، راهنما بده.

دیدن جواب
mkdir -p ~/bashlab && cd ~/bashlab
cat > is-even.sh <<'EOF'
#!/usr/bin/env bash
# is-even.sh: say whether a number is even or odd
if [[ $# -ne 1 ]]; then
echo "Usage: $0 <number>" >&2
exit 1
fi
n="$1"
if (( n % 2 == 0 )); then
echo "$n is even"
else
echo "$n is odd"
fi
EOF
chmod +x is-even.sh
./is-even.sh 10
./is-even.sh 7
./is-even.sh; echo "کد خروج: $?"
خروجی
10 is even
7 is odd
Usage: ./is-even.sh <number>
کد خروج: 1
✎ تمرینمتوسط

تمرین اصلی درس: اسکریپت disk-warn.sh که اگر فضای دیسک بیشتر از ۸۰٪ پر بود هشدار بدهد. درصد پر بودن پارتیشن / را از df بگیر. آستانه به‌صورت آرگومان اختیاری باشد (پیش‌فرض ۸۰) تا بتوانی هشدار را هم امتحان کنی. اگر آستانه عدد نبود، خطا بده. وقتی هشدار داد با کد ۱ و وقتی همه‌چیز خوب بود با کد ۰ تمام شود (تا cron یا اسکریپت دیگر بتواند ببیند).

دیدن جواب
cd ~/bashlab
cat > disk-warn.sh <<'EOF'
#!/usr/bin/env bash
# disk-warn.sh: warn when the root partition is too full
# Usage: ./disk-warn.sh [threshold-percent] (default 80)
threshold="${1:-80}"
if [[ ! $threshold =~ ^[0-9]+$ ]]; then
echo "Error: threshold must be a number, got '$threshold'" >&2
exit 2
fi
usage=$(df --output=pcent / | tail -n 1 | tr -dc '0-9')
if [[ $usage -gt $threshold ]]; then
echo "WARNING: / is ${usage}% full (limit ${threshold}%)"
exit 1
else
echo "OK: / is ${usage}% full (limit ${threshold}%)"
fi
EOF
chmod +x disk-warn.sh
echo "--- پیش‌فرض (۸۰٪):"
./disk-warn.sh; echo "کد خروج: $?"
echo "--- آستانه‌ی ۵٪ (برای دیدن هشدار):"
./disk-warn.sh 5; echo "کد خروج: $?"
echo "--- آستانه‌ی نامعتبر:"
./disk-warn.sh abc; echo "کد خروج: $?"
خروجی
--- پیش‌فرض (۸۰٪):
OK: / is 6% full (limit 80%)
کد خروج: 0
--- آستانه‌ی ۵٪ (برای دیدن هشدار):
WARNING: / is 6% full (limit 5%)
کد خروج: 1
--- آستانه‌ی نامعتبر:
Error: threshold must be a number, got 'abc'
کد خروج: 2

df --output=pcent / ستون «Use%» را چاپ می‌کند (یک خط عنوان + یک خط مقدار)؛ tail -n 1 فقط خط مقدار را نگه می‌دارد و tr -dc '0-9' هر چیزی جز رقم (فاصله‌ها و %) را پاک می‌کند. در لینوکس خالص این کار می‌کند؛ --output مخصوص GNU df است (روی مک: df -P / | awk 'NR==2 {print $5}').

✎ تمرینمتوسط

اسکریپت what-is.sh بنویس که یک مسیر بگیرد و بگوید: وجود ندارد، فایل است، پوشه است یا پیوند نمادین است؛ و اگر وجود دارد، بگوید خواندنی، نوشتنی و اجرایی است یا نه. (از if/elif/else استفاده کن.)

دیدن جواب
mkdir -p ~/bashlab/wi && cd ~/bashlab/wi
echo "data" > file.txt; mkdir dir; ln -s file.txt link; printf '#!/bin/sh\n' > tool.sh; chmod +x tool.sh
cat > ../what-is.sh <<'EOF'
#!/usr/bin/env bash
# what-is.sh: describe a path
path="$1"
if [[ ! -e $path && ! -L $path ]]; then
echo "$path: does not exist"
exit 1
fi
if [[ -L $path ]]; then
kind="symbolic link"
elif [[ -d $path ]]; then
kind="directory"
elif [[ -f $path ]]; then
kind="regular file"
else
kind="something else"
fi
perms=""
[[ -r $path ]] && perms="$perms readable"
[[ -w $path ]] && perms="$perms writable"
[[ -x $path ]] && perms="$perms executable"
echo "$path: $kind,${perms:- no permissions}"
EOF
chmod +x ../what-is.sh
for p in file.txt dir link tool.sh nothing; do ../what-is.sh $p; done
خروجی
file.txt: regular file, readable writable
dir: directory, readable writable executable
link: symbolic link, readable writable
tool.sh: regular file, readable writable executable
nothing: does not exist

(${perms:- no permissions} مقدار پیش‌فرض است که در درس «کار با رشته‌ها» می‌آید.)

✎ تمرینسخت

اسکریپت check-user.sh که اسم کاربر بگیرد و بگوید: ۱) اسم معتبر است؟ (با regex: با حرف کوچک شروع شود، فقط حرف کوچک و عدد و _ و -، طول ۳ تا ۱۶)؛ ۲) آن کاربر روی سیستم وجود دارد؟ (با id، که اگر کاربر نباشد با کد غیرصفر تمام می‌شود، و خروجی‌اش را دور بریز). اگر معتبر نبود، با کد ۲، اگر وجود داشت با کد ۰، و اگر معتبر ولی نبود با کد ۱ تمام شود.

دیدن جواب
cd ~/bashlab
cat > check-user.sh <<'EOF'
#!/usr/bin/env bash
# check-user.sh: validate a user name and check whether the user exists
# Exit codes: 0 = exists, 1 = valid but does not exist, 2 = invalid name
user="$1"
if [[ ! $user =~ ^[a-z][a-z0-9_-]{2,15}$ ]]; then
echo "'$user' is not a valid user name" >&2
exit 2
fi
if id "$user" > /dev/null 2>&1; then
echo "'$user' exists"
else
echo "'$user' is valid but does not exist"
exit 1
fi
EOF
chmod +x check-user.sh
for u in ali root Bad_Name ab nobody-here; do ./check-user.sh "$u"; echo " -> exit $?"; done
خروجی
'ali' exists
-> exit 0
'root' exists
-> exit 0
'Bad_Name' is not a valid user name
-> exit 2
'ab' is not a valid user name
-> exit 2
'nobody-here' is valid but does not exist
-> exit 1

دقت کن if id ... مستقیم یک دستور را می‌سنجد؛ > /dev/null 2>&1 هر دو خروجی (عادی و خطا) را دور می‌ریزد تا فقط کد خروج بماند. (ali کاربر فعلی لاب است و root همیشه هست؛ Bad_Name حرف بزرگ دارد و ab کوتاه است.)

⚡ بررسی سریع

در Bash، if دقیقاً چه چیزی را می‌سنجد؟

؟ آزمونک
  1. نتیجه‌ی [[ 9 > 10 ]] چیست؟

  2. چرا [[ $a==$b ]] (بدون فاصله) همیشه درست است؟

  3. فرق اصلی [[ ]] با [ ] در متغیر بدون کوتیشن چیست؟

  4. در [[ 1 -eq 1 ]] && false || echo "C"، آیا C چاپ می‌شود؟

  5. کدام آزمون می‌گوید مسیر یک «فایل معمولی» است؟

  6. grep -q الگو فایل وقتی فایل اصلاً وجود ندارد چه کد خروجی می‌دهد، و if چه می‌کند؟

  • if دستور; then ... elif ...; then ... else ... fi: if یک دستور اجرا می‌کند و کد خروجش را می‌سنجد؛ ۰ یعنی «درست» (then)، هر عدد دیگر «غلط». با ! برعکس می‌شود.
  • [[ ]] (bash) و [ ] (POSIX) خودشان دستورند و کد ۰ یا ۱ می‌دهند. در اسکریپت bash همیشه [[ ]]؛ در [ ] متغیرها باید در کوتیشن باشند.
  • عدد: -eq -ne -lt -le -gt -ge یا (( )). رشته: == != -z -n و الگو و =~؛ < و > الفبایی‌اند و برای عدد غلط.
  • فایل: -e -f -d -L -s -r -w -x و -nt -ot.
  • ترکیب: && و || و ! داخل [[ ]] و بین دستورها؛ A && B || C معادل if/else نیست.
  • $? کد خروج آخرین دستور است و با هر دستور عوض می‌شود؛ فوراً بخوان یا مستقیم if دستور بنویس.
  • هر مسیر خروج اسکریپت را با کد خروج مناسب (exit 1، exit 2، …) تمام کن تا ابزارهای دیگر بفهمند چه شد.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
if [[ -f file ]]; then ... fiاگر فایل معمولی است
if ...; then ... elif ...; then ... else ... fiچند شاخه
[[ $a -gt $b ]]عدد: -eq -ne -lt -le -gt -ge
(( a > b ))مقایسه‌ی عدد با ریاضی
[[ $s == "Ali" ]] [[ -z $s ]]رشته: برابر / خالی
[[ $f == *.txt ]] [[ $d =~ ^[0-9]+$ ]]الگو / regex
[[ -e p ]] [[ -d p ]] [[ -s p ]]وجود / پوشه / غیرخالی
[[ -r p && -w p ]]ترکیب با && (و || و !)
mkdir -p d && cd d / cmd || exit 1اگر موفق شد / اگر شکست خورد
echo $?کد خروج آخرین دستور (۰ = موفق)
if ! grep -q x file; then ...نفی یک دستور
bash -n script.shبررسی نحو پیش از اجرا