توی این درس یاد میگیری اسکریپتت را «باهوش» کنی: بر اساس شرایط تصمیم بگیرد. ساختار 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 دستور; then ... اگر دستور موفق بود (کد خروج ۰)elif دستور دیگر; then ... اگر اولی شکست خورد و این موفق بودelse ... اگر هیچکدام موفق نبودfifi یعنی پایان if (if را برعکس بخوان). then یا باید روی خط جدا بیاید یا بعد از ;. و عمق را با فاصلهگذاری نشان بده تا خواندنش آسان باشد.
پس [[ -f file ]] یا [ -f file ] که در اسکریپتها میبینی، دستورهاییاند که شرط را بررسی میکنند و با کد ۰ (درست) یا ۱ (غلط) تمام میشوند. چطور؟ در «پشت پرده» میبینی.
مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: if یعنی «این دستور موفق شد؟»
Section titled “مثال ۱: if یعنی «این دستور موفق شد؟»”قبل از هر چیز، if را با یک دستور معمولی امتحان کن، بدون هیچ [ ]:
if grep -q root /etc/passwd; then echo "root exists in /etc/passwd"else echo "root not found"fiif grep -q nothere /etc/passwd; then echo "nothere exists"else echo "nothere not found"fiecho "--- کد خروجها با خودِ \$?:"grep -q root /etc/passwd; echo "match: $?"grep -q nothere /etc/passwd; echo "no match: $?"root exists in /etc/passwdnothere not found--- کد خروجها با خودِ $?:match: 0no match: 1grep -q (quiet) چیزی چاپ نمیکند و فقط با کد خروج جواب میدهد: ۰ یعنی پیدا شد، ۱ یعنی پیدا نشد. if همان کد را میسنجد. echo $? همان عدد را نشان میدهد؛ $? کد خروج آخرین دستور است (یادآوری: ۰ موفق، هر عدد دیگر شکست).
مثال ۲: مقایسهی عددها، [[ ]] و (( ))
Section titled “مثال ۲: مقایسهی عددها، [[ ]] و (( ))”برای مقایسهی عدد، دو دستور رایج داری. عملگرهای [[ ]] با حروفاند (-gt یعنی greater than، بزرگتر)، و (( )) (درس قبل: محاسبه) شبیه ریاضی:
a=15b=8if [[ $a -gt $b ]]; then echo "$a is greater than $b"fiif [[ $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 8a equals 15a < b ? status=1 (1 means false)--- همان با (( )) که شبیه ریاضی است:(( a > b )) is truea is divisible by 5حالا elif: چند شاخه پشت هم. شرطها از بالا به پایین بررسی میشوند و اولین شاخهای که درست بود اجرا میشود و بقیه نادیده گرفته میشوند (ترتیب مهم است):
#!/usr/bin/env bash# grade.sh: turn a score (0-100) into a letterscore="$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"ficd ~/bashlabchmod +x grade.shfor s in 95 85 72 40 90; do ./grade.sh $s; done95 -> A85 -> B72 -> C40 -> F90 -> Aنمرهی ۹۵ هم از -ge 80 میگذرد، ولی چون شرط اول (-ge 90) زودتر درست بود، B نشد. (حلقهی for را در درس حلقهها میخوانی؛ اینجا یعنی «برای هر نمره».)
مثال ۳: مقایسهی رشتهها
Section titled “مثال ۳: مقایسهی رشتهها”برای رشتهها عملگرهای نمادین داری: ==، !=، و دو آزمون ویژه: -z (خالی است؟) و -n (خالی نیست؟). سمت راست == در [[ ]] میتواند الگو (wildcard) باشد، و =~ یک regex میگیرد:
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"fiecho "--- ترتیب الفبایی با > (نه عددی!):"[[ 9 > 10 ]] && echo "9 > 10 is TRUE (compared as text)"[[ 9 -gt 10 ]] || echo "9 -gt 10 is false (compared as numbers)"name is Aliname is not Saraempty is empty (-z)name is not empty (-n)--- الگو (wildcard) سمت راست ==:report.txt is a .txt filequoted: 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 “مثال ۴: آزمون فایل و پوشه”مهمترین کاربرد شرطها در اسکریپتهای سیستمی: ببین فایل یا پوشه وجود دارد یا نه، و چه نوعی است:
mkdir -p ~/bashlab/ft && cd ~/bashlab/ftmkdir folderecho "data" > notes.txt: > empty.txtln -s notes.txt shortcutfor 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' echodonenotes.txt exists file non-emptyfolder exists directory non-emptyshortcut exists file symlink non-emptyempty.txt exists filemissing.txt (missing)هر آزمون یک حرف دارد: -e وجود دارد (هر نوعی)، -f فایل معمولی است، -d پوشه است، -L پیوند نمادین است، -s وجود دارد و خالی نیست (اندازهاش بیشتر از صفر). دقت کن shortcut هم -f است (آزمونها پیوند را دنبال میکنند) و هم -L، و empty.txt فایل است ولی -s نمیخورد.
آزمونهای مجوز و زمان:
cd ~/bashlab/ftecho "echo hi" > run.shchmod 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 executablerun.sh is executable nowrun.sh is readable and writablenotes.txt is newer than old.txt-r، -w و -x به ترتیب خواندن، نوشتن و اجرا را برای کاربری که اسکریپت را اجرا میکند بررسی میکنند (نه صرفاً حروف مجوز). -nt (newer than) و -ot (older than) دو فایل را از نظر زمان ویرایش مقایسه میکنند.
یک الگوی همیشگی: پوشه را فقط اگر نبود بساز. اسکریپت را دو بار اجرا کن:
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" fidone--- attempt 1creating /home/ali/bashlab/backups--- attempt 2/home/ali/bashlab/backups already exists! شرط را برعکس میکند: «اگر این یک پوشه نیست». بار اول ساخته شد، بار دوم دست نخورد؛ چنین اسکریپتی ایمن برای اجرای دوباره است (به اصطلاح idempotent).
مثال ۵: [[ ]] در برابر [ ]
Section titled “مثال ۵: [[ ]] در برابر [ ]”در اسکریپتهای قدیمی (و راهنماهای اینترنت) بیشتر [ ... ] میبینی. این دو شبیهاند ولی یکی نیستند، و تفاوتشان در موارد زیر خودش را نشان میدهد:
empty=""file="my report.txt"touch ~/bashlab/"$file"cd ~/bashlabecho "--- [ ] با متغیر خالی:"[ $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 expectedstatus=2--- [[ ]] با همان:status=1--- [ ] با نام فایل دارای فاصله:bash: [: my: binary operator expectedstatus=2--- [[ ]] با همان:status=0--- مقدار غیرعددی با -eq:bash: [: abc: integer expression expected[ ] status=2[[ ]] status=1--- در sh (که روی Ubuntu همان dash است):sh: 1: [: a: unexpected operatordash with ==: status=2dash 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 “مثال ۶: ترکیب شرطها، &&، || و !”دو جور ترکیب داری. داخل [[ ]] با && (و)، || (یا) و ! (نه). و بین دو دستور با همان && و ||: «اگر قبلی موفق شد، بعدی را اجرا کن» و «اگر شکست خورد، بعدی را»:
age=25country="IR"echo "--- داخل [[ ]]: && و ||"if [[ $age -ge 18 && $country == "IR" ]]; then echo "adult from IR"; fiif [[ $age -lt 18 || $country != "IR" ]]; then echo "minor or foreign"; else echo "adult from IR"; fiecho "--- نفی با !"if [[ ! -e /no/such/file ]]; then echo "/no/such/file does not exist"; fiecho "--- بین دستورها: && (اگر موفق شد) و || (اگر شکست خورد)"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 IRadult from IR--- نفی با !/no/such/file does not exist--- بین دستورها: && (اگر موفق شد) و || (اگر شکست خورد)now in /home/ali/bashlab/workcd 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 “مثال ۷: اسکریپت واقعی، بکاپ با بررسیها”حالا چند شرط کنار هم در یک اسکریپت کاربردی: بکاپگیری که قبل از کار، همهچیز را بررسی میکند:
#!/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 1fi
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 2fi
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 0fi
if tar -czf "$backup_file" -C "$(dirname "$source_dir")" "$(basename "$source_dir")"; then echo "Backup created: $backup_file"else echo "Backup FAILED" >&2 exit 3ficd ~/bashlabchmod +x safe-backup.shrm -rf backupsmkdir -p project/src && echo "print(1)" > project/src/app.pyecho "--- بدون آرگومان:"./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/backupsBackup 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. اگر لازمش داری، همان لحظه بخوان یا در متغیر بگذار:
ls /etc/hostname > /dev/nullecho "status of ls: $?"echo "next command"echo "status now (of the previous echo, not ls): $?"echo "--- ذخیره در متغیر:"grep -q root /etc/passwdstatus=$?echo "searched"echo "grep status was $status"status of ls: 0next commandstatus now (of the previous echo, not ls): 0--- ذخیره در متغیر:searchedgrep status was 0اگر فقط میخواهی «موفق شد یا نه»، بهجای $? مستقیم از if دستور; then استفاده کن (مثال ۱)؛ کوتاهتر و بیخطاتر است. $? وقتی خوب است که عدد دقیق را میخواهی (مثل تفاوت کد ۱ و ۲ در grep) یا باید در لاگ بنویسی.
پشت پرده: if چطور کار میکند؟
Section titled “پشت پرده: if چطور کار میکند؟”if هر دستوری را اجرا میکند و کد خروجش را میسنجد. حتی true و false که فقط کد خروج برمیگردانند:
if true; then echo "true -> then"; fiif false; then echo "never"; else echo "false -> else"; fiif ls /etc/passwd > /dev/null; then echo "ls succeeded -> then"; fiif ! ls /nope 2> /dev/null; then echo "ls failed, and ! turned it into a success"; fiif false; true; then echo "with several commands, the LAST one decides"; fitrue -> thenfalse -> elsels succeeded -> thenls failed, and ! turned it into a successwith several commands, the LAST one decidesآخرین خط نشان میدهد وقتی چند دستور بین if و then باشد، کد خروج آخری حساب میشود. و ! کد خروج را برعکس میکند.
پس [ و [[ چه هستند؟ خود دستورند:
type [ [[ testls -l /usr/bin/[ /usr/bin/testecho "--- خودشان کد خروج میدهند:"[[ 1 -eq 1 ]]; echo "1 -eq 1 -> $?"[[ 1 -eq 2 ]]; echo "1 -eq 2 -> $?"[ is a shell builtin[[ is a shell keywordtest 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 -> 01 -eq 2 -> 1[یک دستور داخلی bash است (builtin). همین نام یک فایل برنامه هم در/usr/bin/[هست (هماندازه باtest، چون همان برنامه است). آخرین آرگومانش را باید]بگذاری، فقط برای زیبایی ظاهر. چون یک دستور معمولی است، تمام قواعد آرگومان و کوتیشن درس قبل دربارهاش صدق میکند.[[یک کلمهی کلیدی (keyword) است، نه دستور. شل آن را خودش تجزیه میکند؛ برای همین امکانات ویژه دارد (=~،&&،==الگو) و شکستن کلمهها در آن نیست.test عبارتهمان[ عبارت ]است.
این توضیح میدهد چرا فاصلهها مهماند (بخش اشتباهات): [[$a بدون فاصله، کلمهی [[1 میشود و شل دنبال دستوری به این اسم میگردد ([[1: command not found).
جدولهای مرجع
Section titled “جدولهای مرجع”مقایسهی عددها:
| معنی | داخل [[ ]] |
داخل (( )) |
|---|---|---|
| برابر | -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 “اشتباهات رایج”۱) فاصلهی لازم را نگذاشتن
Section titled “۱) فاصلهی لازم را نگذاشتن”[[ و ]] کلمههای جدا هستند و عملگرها هم باید با فاصله بیایند:
a=1b=2[[ $a==$b ]] && echo "equal?! (wrong: a=1, b=2)"if [[$a -gt 1]]; then echo "yes"; fiecho "کد خروج 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 )). و برعکسش، == برای عدد هم متنی مقایسه میکند:
[[ 5 == 05 ]]echo "5 == 05 (as text): status=$?"[[ 5 -eq 05 ]]echo "5 -eq 05 (as number): status=$?"5 == 05 (as text): status=15 -eq 05 (as number): status=0برای عدد همیشه -eq/(( ))؛ برای رشته ==.
۳) then یا fi را جا انداختن
Section titled “۳) then یا fi را جا انداختن”cd ~/bashlabprintf 'if true; then\n echo hi\n' > no-fi.shprintf 'if true\n echo hi\nfi\n' > no-then.shprintf 'if [[ 1 -eq 1 ]]; then\n echo hi\nelif\nfi\n' > bad-elif.shfor f in no-fi.sh no-then.sh bad-elif.sh; do echo "--- $f:" bash -n $fdone--- 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 ].
۵) && و || بهجای if/else
Section titled “۵) && و || بهجای if/else”مثال ۶: وقتی B شکست بخورد، C هم اجرا میشود. راهحل: if ...; then B; else C; fi.
۶) خواندن $? بعد از دستور دیگر
Section titled “۶) خواندن $? بعد از دستور دیگر”grep -q root /etc/passwdecho "searched"if [ $? -eq 0 ]; then echo "found (but this checked the echo, not the grep!)"; fisearchedfound (but this checked the echo, not the grep!)بین grep و if یک echo آمده و $? مال echo است. اگر خودِ grep را بخواهی، یا if grep -q ...; then بنویس یا کد را بلافاصله در متغیر بگذار.
۷) خطا را «پیدا نشد» فرض کردن
Section titled “۷) خطا را «پیدا نشد» فرض کردن”grep سه کد دارد: ۰ پیدا شد، ۱ پیدا نشد، ۲ خطا (مثلاً فایل نیست). if grep -q ... هر دو ۱ و ۲ را «غلط» میبیند:
if grep -q root /nonexistent-file; then echo "found"else echo "NOT FOUND (really? the file does not exist!)"figrep -q root /nonexistent-file; echo "grep status: $?"grep: /nonexistent-file: No such file or directoryNOT FOUND (really? the file does not exist!)grep: /nonexistent-file: No such file or directorygrep status: 2پیام خطای grep ریخته شد و اسکریپت با اطمینان گفت «پیدا نشد». راهحل: اگر فرق ۱ و ۲ برایت مهم است، اول وجود فایل را بسنج ([[ -f ... ]]) یا $? را بخوان.
اسکریپت is-even.sh بنویس که یک عدد از آرگومان بگیرد و بگوید زوج است یا فرد (7 is odd). از (( )) و باقیمانده استفاده کن. اگر آرگومان نبود، راهنما بده.
دیدن جواب
mkdir -p ~/bashlab && cd ~/bashlabcat > is-even.sh <<'EOF'#!/usr/bin/env bash# is-even.sh: say whether a number is even or oddif [[ $# -ne 1 ]]; then echo "Usage: $0 <number>" >&2 exit 1fi
n="$1"if (( n % 2 == 0 )); then echo "$n is even"else echo "$n is odd"fiEOFchmod +x is-even.sh./is-even.sh 10./is-even.sh 7./is-even.sh; echo "کد خروج: $?"10 is even7 is oddUsage: ./is-even.sh <number>کد خروج: 1تمرین اصلی درس: اسکریپت disk-warn.sh که اگر فضای دیسک بیشتر از ۸۰٪ پر بود هشدار بدهد. درصد پر بودن پارتیشن / را از df بگیر. آستانه بهصورت آرگومان اختیاری باشد (پیشفرض ۸۰) تا بتوانی هشدار را هم امتحان کنی. اگر آستانه عدد نبود، خطا بده. وقتی هشدار داد با کد ۱ و وقتی همهچیز خوب بود با کد ۰ تمام شود (تا cron یا اسکریپت دیگر بتواند ببیند).
دیدن جواب
cd ~/bashlabcat > 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 2fi
usage=$(df --output=pcent / | tail -n 1 | tr -dc '0-9')
if [[ $usage -gt $threshold ]]; then echo "WARNING: / is ${usage}% full (limit ${threshold}%)" exit 1else echo "OK: / is ${usage}% full (limit ${threshold}%)"fiEOFchmod +x disk-warn.shecho "--- پیشفرض (۸۰٪):"./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'کد خروج: 2df --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/wiecho "data" > file.txt; mkdir dir; ln -s file.txt link; printf '#!/bin/sh\n' > tool.sh; chmod +x tool.shcat > ../what-is.sh <<'EOF'#!/usr/bin/env bash# what-is.sh: describe a pathpath="$1"
if [[ ! -e $path && ! -L $path ]]; then echo "$path: does not exist" exit 1fi
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}"EOFchmod +x ../what-is.shfor p in file.txt dir link tool.sh nothing; do ../what-is.sh $p; donefile.txt: regular file, readable writabledir: directory, readable writable executablelink: symbolic link, readable writabletool.sh: regular file, readable writable executablenothing: does not exist(${perms:- no permissions} مقدار پیشفرض است که در درس «کار با رشتهها» میآید.)
اسکریپت check-user.sh که اسم کاربر بگیرد و بگوید: ۱) اسم معتبر است؟ (با regex: با حرف کوچک شروع شود، فقط حرف کوچک و عدد و _ و -، طول ۳ تا ۱۶)؛ ۲) آن کاربر روی سیستم وجود دارد؟ (با id، که اگر کاربر نباشد با کد غیرصفر تمام میشود، و خروجیاش را دور بریز). اگر معتبر نبود، با کد ۲، اگر وجود داشت با کد ۰، و اگر معتبر ولی نبود با کد ۱ تمام شود.
دیدن جواب
cd ~/bashlabcat > 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 nameuser="$1"
if [[ ! $user =~ ^[a-z][a-z0-9_-]{2,15}$ ]]; then echo "'$user' is not a valid user name" >&2 exit 2fi
if id "$user" > /dev/null 2>&1; then echo "'$user' exists"else echo "'$user' is valid but does not exist" exit 1fiEOFchmod +x check-user.shfor 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 کوتاه است.)
آزمونک
Section titled “آزمونک”در Bash، if دقیقاً چه چیزی را میسنجد؟
[[ ]] و [ ] هم دستورند و کد خروج ۰ یا ۱ میدهند. برای همین if grep -q ...; then هم کار میکند.
نتیجهی [[ 9 > 10 ]] چیست؟
برای عدد از -gt یا (( 9 > 10 )) استفاده کن.
چرا [[ $a==$b ]] (بدون فاصله) همیشه درست است؟
درست: [[ $a == $b ]] با فاصله دور هر عملگر.
فرق اصلی [[ ]] با [ ] در متغیر بدون کوتیشن چیست؟
[ یک دستور معمولی است و [[ کلمهی کلیدی bash.
در [[ 1 -eq 1 ]] && false || echo "C"، آیا C چاپ میشود؟
A && B || C همان if/else نیست؛ برای دو شاخهی واقعی if بنویس.
کدام آزمون میگوید مسیر یک «فایل معمولی» است؟
-e وجود هر نوع، -d پوشه، -s وجود و خالی نبودن.
grep -q الگو فایل وقتی فایل اصلاً وجود ندارد چه کد خروجی میدهد، و if چه میکند؟
اگر فرق «پیدا نشد» و «خطا» برایت مهم است، اول وجود فایل را بسنج.
جمعبندی
Section titled “جمعبندی”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 | بررسی نحو پیش از اجرا |