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

case

توی این درس یاد می‌گیری وقتی یک مقدار را باید با چند حالت مقایسه کنی، به‌جای زنجیره‌ی بلند elif از case استفاده کنی: case "$1" in start) ... ;; stop) ... ;; *) ... ;; esac. می‌فهمی هر شاخه چطور با ;; تمام می‌شود، شاخه‌ی پیش‌فرض *) چیست، چطور با | چند الگو را یک‌جا می‌گیری، الگوها wildcard هستند نه regex، و چطور با case و حلقه یا دستور select منوی ساده می‌سازی. تمرین: اسکریپت مدیریت سرویس با گزینه‌های start، stop و status.

مسئله: زنجیره‌ی بلند elif

Section titled “مسئله: زنجیره‌ی بلند elif”

فرض کن اسکریپتت یک دستور می‌گیرد (start، stop، status، restart) و بر اساسش یکی از چهار کار را می‌کند. با if می‌شود:

با if و elif (کار می‌کند ولی تکراری است)
if [[ $1 == start ]]; then
...
elif [[ $1 == stop ]]; then
...
elif [[ $1 == status ]]; then
...
else
...
fi

هر شاخه [[ $1 == ... ]] را تکرار می‌کند و چشم باید در میان این تکرارها بگردد تا ببیند «کدام مقدار → کدام کار». case دقیقاً برای همین ساخته شده: یک مقدار، چند الگو، هر الگو یک کار.

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

ساختار case
case "$مقدار" in
الگوی۱)
دستورها
;;
الگوی۲ | الگوی۳)
دستورها
;;
*)
دستورهای پیش‌فرض
;;
esac
  • esac همان case برعکس است (مثل fi برای if).
  • هر شاخه با الگو) شروع می‌شود و با ;; تمام می‌شود. ;; یعنی «اینجا شاخه تمام؛ از case بیرون برو».
  • | بین چند الگو یعنی «یا».
  • *) با هر چیزی جور است؛ آخر بگذارش تا شاخه‌ی پیش‌فرض باشد.
  • الگوها از بالا به پایین بررسی می‌شوند و اولین الگوی منطبق اجرا می‌شود.
همان تصمیم‌گیری با دو شکل. در case مقدار فقط یک بار نوشته می‌شود و هر الگو یک خط است؛ خواندن و تغییر دادنش خیلی راحت‌تر است.
/home/ali/bashlab/hello-lang.sh
#!/usr/bin/env bash
# hello-lang.sh: greet in the language given as the first argument
case "$1" in
en)
echo "Hello!"
;;
fa)
echo "Salam!"
;;
de | at)
echo "Hallo!"
;;
*)
echo "Sorry, I do not speak '$1'" >&2
exit 1
;;
esac
Terminal window
cd ~/bashlab
chmod +x hello-lang.sh
for lang in en fa de at fr; do
./hello-lang.sh $lang
done
echo "--- بدون آرگومان:"
./hello-lang.sh; echo "کد خروج: $?"
خروجی
Hello!
Salam!
Hallo!
Hallo!
Sorry, I do not speak 'fr'
--- بدون آرگومان:
Sorry, I do not speak ''
کد خروج: 1

de | at) یعنی «اگر de یا at بود». برای fr هیچ شاخه‌ی خاصی نبود و *) گرفتش و پیام خطا داد. بدون آرگومان هم "$1" رشته‌ی خالی است که فقط با *) جور می‌شود.

مثال ۲: الگوها، wildcard نه regex

Section titled “مثال ۲: الگوها، wildcard نه regex”

الگوهای case همان wildcardهای نام فایل‌اند (که در درس لینوکس دیدی)، نه regex: * هر تعداد کاراکتر، ? دقیقاً یک کاراکتر، و [abc] یا [0-9] یکی از چند کاراکتر:

/home/ali/bashlab/classify.sh
#!/usr/bin/env bash
# classify.sh: guess what a file is from its name
name="$1"
case "$name" in
*.tar.gz | *.tgz) echo "$name: gzip-compressed archive" ;;
*.zip) echo "$name: zip archive" ;;
*.txt | *.md) echo "$name: text document" ;;
*.jpg | *.jpeg | *.png) echo "$name: image" ;;
[0-9]*) echo "$name: starts with a digit" ;;
?) echo "$name: a one-character name" ;;
.*) echo "$name: hidden file" ;;
*) echo "$name: unknown" ;;
esac
Terminal window
cd ~/bashlab
chmod +x classify.sh
for f in backup.tar.gz site.zip README.md photo.png photo.PNG 2026-report 2026.txt x .bashrc data.bin; do
./classify.sh "$f"
done
خروجی
backup.tar.gz: gzip-compressed archive
site.zip: zip archive
README.md: text document
photo.png: image
photo.PNG: unknown
2026-report: starts with a digit
2026.txt: text document
x: a one-character name
.bashrc: hidden file
data.bin: unknown

چند نکته: 2026.txt به شاخه‌ی *.txt رفت، نه [0-9]*، چون اولین الگوی منطبق برنده است و *.txt بالاتر بود. photo.PNG با حروف بزرگ جور نشد (case به‌صورت پیش‌فرض به حروف بزرگ و کوچک حساس است). و شاخه‌های ?) و .*) هم با کاراکترهای ویژه کار می‌کنند. می‌توانی روی یک خط هم بنویسی (الگو) دستور ;;) وقتی شاخه کوتاه است.

مثال ۳: پاسخ بله/نه و اعتبارسنجی عدد

Section titled “مثال ۳: پاسخ بله/نه و اعتبارسنجی عدد”

دو کاربرد بسیار رایج: خواندن جواب y/n از کاربر با هر املایی، و بررسی اینکه ورودی عدد است:

Terminal window
for answer in y Y yes YES yEs n NO maybe ""; do
case "$answer" in
[yY] | [yY][eE][sS]) echo "'$answer' -> confirmed" ;;
[nN] | [nN][oO]) echo "'$answer' -> cancelled" ;;
*) echo "'$answer' -> please answer yes or no" ;;
esac
done
echo "--- روش کوتاه‌تر: nocasematch (حروف بزرگ و کوچک یکی)"
shopt -s nocasematch
case "YeS" in
y | yes) echo "matched with nocasematch" ;;
esac
shopt -u nocasematch
خروجی
'y' -> confirmed
'Y' -> confirmed
'yes' -> confirmed
'YES' -> confirmed
'yEs' -> confirmed
'n' -> cancelled
'NO' -> cancelled
'maybe' -> please answer yes or no
'' -> please answer yes or no
--- روش کوتاه‌تر: nocasematch (حروف بزرگ و کوچک یکی)
matched with nocasematch

الگوی [yY] یعنی «y یا Y». برای چندحرفی باید حرف‌به‌حرف بنویسی ([yY][eE][sS])، یا shopt -s nocasematch را روشن کنی تا case حروف را یکی بگیرد (بعدش -u خاموشش می‌کند). یک راه سوم، تبدیل به حروف کوچک با ${answer,,} است که در درس «کار با رشته‌ها» می‌آید.

الگوی «عدد است؟» با wildcard معمولی نمی‌شود، چون * و ? ندارند «یک یا چند رقم». Bash الگوهای گسترده (extglob) هم دارد که با shopt -s extglob روشن می‌شوند:

Terminal window
shopt -s extglob
for input in 42 007 4x2 "" -5 12.5; do
case "$input" in
+([0-9])) echo "'$input' is a non-negative integer" ;;
*) echo "'$input' is NOT a non-negative integer" ;;
esac
done
خروجی
'42' is a non-negative integer
'007' is a non-negative integer
'4x2' is NOT a non-negative integer
'' is NOT a non-negative integer
'-5' is NOT a non-negative integer
'12.5' is NOT a non-negative integer

+([0-9]) یعنی «یک یا چند بار یک رقم». خالی، عدد منفی و اعشاری رد شدند. (در [[ ]] با =~ و regex هم می‌شد: [[ $input =~ ^[0-9]+$ ]]، مثال درس قبل. انتخاب سلیقه‌ای است؛ case با الگوی ساده خواناتر است.)

مثال ۴: ;& و ;;&، ادامه یا آزمون دوباره

Section titled “مثال ۴: ;& و ;;&، ادامه یا آزمون دوباره”

با ;; بعد از اولین شاخه‌ی منطبق بیرون می‌روی. در Bash نسخه‌ی ۴ به بعد دو پایان دیگر هم هست:

Terminal window
echo "--- ;;& : بعد از این شاخه، الگوهای بعدی را هم امتحان کن"
for w in abc xyz; do
echo "$w:"
case "$w" in
a*) echo " starts with a" ;;&
*b*) echo " contains b" ;;&
*) echo " reached the last branch" ;;
esac
done
echo "--- ;& : بی‌قید و شرط به شاخه‌ی بعدی برو"
for n in 1 2 3; do
echo "$n:"
case "$n" in
1) echo " one" ;&
2) echo " two (also after one)" ;;
3) echo " three" ;;
esac
done
خروجی
--- ;;& : بعد از این شاخه، الگوهای بعدی را هم امتحان کن
abc:
starts with a
contains b
reached the last branch
xyz:
reached the last branch
--- ;& : بی‌قید و شرط به شاخه‌ی بعدی برو
1:
one
two (also after one)
2:
two (also after one)
3:
three

;;& یعنی «این شاخه را اجرا کردی؟ باشه؛ ولی الگوهای بعدی را هم بررسی کن». برای abc هر سه شاخه اجرا شد و برای xyz فقط آخری. ;& یعنی «بدون بررسی الگو، دستورهای شاخه‌ی بعد را هم اجرا کن» (1 هم one را چاپ کرد هم two را). این دو را کم می‌بینی و در macOS (که bash ۳.۲ دارد) کار نمی‌کنند؛ معمولاً ;; کافی است.

مثال ۵: ابزار با زیردستور، case و shift

Section titled “مثال ۵: ابزار با زیردستور، case و shift”

بسیاری از ابزارهای واقعی (git، docker) زیردستور دارند (git commit، docker run). همان را با case و shift (درس ورودی و آرگومان‌ها) می‌سازی. این هم یک ابزار کوچک یادداشت:

/home/ali/bashlab/notes.sh
#!/usr/bin/env bash
# notes.sh: a tiny notes manager with sub-commands
# Usage: ./notes.sh add <text...> | list | count | clear
notes_file="$HOME/bashlab/notes.txt"
touch "$notes_file"
command="$1"
shift
case "$command" in
add)
echo "$*" >> "$notes_file"
echo "Added."
;;
list)
cat -n "$notes_file"
;;
count)
wc -l < "$notes_file"
;;
clear)
: > "$notes_file"
echo "Cleared."
;;
*)
echo "Usage: $0 add <text...> | list | count | clear" >&2
exit 1
;;
esac
Terminal window
cd ~/bashlab
chmod +x notes.sh
./notes.sh add Buy milk
./notes.sh add Call Sara tomorrow
./notes.sh list
echo "count: $(./notes.sh count)"
./notes.sh clear
echo "count: $(./notes.sh count)"
echo "--- زیردستور ناشناخته:"
./notes.sh remove 1; echo "کد خروج: $?"
echo "--- بدون آرگومان:"
./notes.sh; echo "کد خروج: $?"
خروجی
Added.
Added.
1 Buy milk
2 Call Sara tomorrow
count: 2
Cleared.
count: 0
--- زیردستور ناشناخته:
Usage: ./notes.sh add <text...> | list | count | clear
کد خروج: 1
--- بدون آرگومان:
Usage: ./notes.sh add <text...> | list | count | clear
کد خروج: 1

command="$1"; shift اولین آرگومان (زیردستور) را کنار می‌گذارد و بقیه ($*) متن یادداشت می‌شود. case هم زیردستور را به کار درست می‌رساند. *) شاخه‌ی راهنماست و کد خروج خطا می‌دهد. با همین الگو اسکریپتی بنویس که مثل ابزارهای حرفه‌ای رفتار کند.

منوی ساده: منو را چاپ می‌کنی، جواب را با read می‌گیری، با case کار را انتخاب می‌کنی و تا وقتی کاربر خروج نزده دوباره از اول:

/home/ali/bashlab/menu.sh
#!/usr/bin/env bash
# menu.sh: a simple text menu
while true; do
echo
echo "=== Server menu ==="
echo "1) Show date"
echo "2) Show disk usage"
echo "3) Who am I"
echo "q) Quit"
read -r -p "Choose: " choice
case "$choice" in
1) date ;;
2) df -h / ;;
3) whoami ;;
q | Q) echo "Bye"; break ;;
*) echo "Invalid choice: '$choice'" ;;
esac
done

while true; do ... done یعنی «برای همیشه تکرار کن»؛ حلقه‌ها را در درس بعد کامل می‌خوانی. فعلاً بدان break از حلقه بیرون می‌آید. برای دیدن واقعی کار، آن را داخل یک ترمینال مجازی (tmux، مثل درس‌های قبل) اجرا می‌کنم و جواب‌ها را تایپ می‌کنم:

Terminal window
start_terminal() {
tmux -u kill-server 2>/dev/null
tmux -u new-session -d -s demo -x 60 -y "${1:-12}" "env PS1='\$ ' bash --norc"
sleep 1
tmux send-keys -t demo "cd ~/bashlab" Enter "clear" Enter
}
screen() { tmux capture-pane -t demo -p | sed -e :a -e '/^[[:space:]]*$/{$d;N;ba' -e '}'; }
chmod +x ~/bashlab/menu.sh
start_terminal 40
tmux send-keys -t demo "./menu.sh" Enter
sleep 0.7
tmux send-keys -t demo "2" Enter
sleep 0.7
tmux send-keys -t demo "9" Enter
sleep 0.7
tmux send-keys -t demo "q" Enter
sleep 0.7
screen
خروجی
$ ./menu.sh
=== Server menu ===
1) Show date
2) Show disk usage
3) Who am I
q) Quit
Choose: 2
Filesystem Size Used Avail Use% Mounted on
overlay 453G 23G 408G 6% /
=== Server menu ===
1) Show date
2) Show disk usage
3) Who am I
q) Quit
Choose: 9
Invalid choice: '9'
=== Server menu ===
1) Show date
2) Show disk usage
3) Who am I
q) Quit
Choose: q
Bye
$

دو بار منو چاپ شد: بعد از 2 (دیسک)، بعد از 9 (نامعتبر؛ شاخه‌ی *)) و با q حلقه تمام شد. دقت کن q | Q) هر دو حالت را می‌پذیرد.

Bash برای همین کار یک دستور آماده دارد، select: لیست را شماره‌دار چاپ می‌کند، پرسش را از PS3 می‌گیرد و جواب را در متغیر می‌گذارد. معمولاً با case ترکیب می‌شود:

/home/ali/bashlab/select-demo.sh
#!/usr/bin/env bash
# select-demo.sh: the built-in menu command
PS3="Pick a fruit: "
select fruit in apple banana cherry quit; do
case "$fruit" in
quit) echo "Bye"; break ;;
"") echo "Invalid number: $REPLY" ;;
*) echo "You picked $fruit"; break ;;
esac
done
Terminal window
cd ~/bashlab
chmod +x select-demo.sh
printf '9\n2\n' | ./select-demo.sh
خروجی
1) apple
2) banana
3) cherry
4) quit
Pick a fruit: Invalid number: 9
Pick a fruit: You picked banana

select گزینه‌ها را شماره‌گذاری و چاپ کرد (روی stderr)، و با عدد 9 که شماره‌ی معتبری نبود، fruit خالی ماند (شاخه‌ی "")) و عددی که تایپ شده در REPLY مانده بود. با 2 گزینه‌ی دوم (banana) انتخاب شد. select تا break یا پایان ورودی تکرار می‌شود. وقتی کنترل بیشتری می‌خواهی (مثلاً q برای خروج)، منوی while+case مثال قبل انعطاف‌پذیرتر است.

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

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

وقتی شل case را اجرا می‌کند:

  1. کلمه‌ی بعد از case را باز می‌کند (متغیر، دستور، محاسبه)، ولی نه شکستن در فاصله‌ها و نه *.
  2. الگوها را یکی‌یکی از بالا با همان موتور wildcard نام فایل مقایسه می‌کند.
  3. اولین الگوی منطبق را اجرا می‌کند و تا ;; ادامه می‌دهد.
  4. کد خروج case همان کد خروج آخرین دستور اجراشده است (یا ۰ اگر هیچ شاخه‌ای نخورد).

برای همین نبودن کوتیشن دور مقدار، برخلاف بیشتر جاها، در case خراب نمی‌کند؛ ولی کوتیشن بگذار تا الگوی یکسانی با بقیه‌ی اسکریپت داشته باشی:

Terminal window
x="a b"
case $x in
"a b") echo "matched even though \$x was not quoted" ;;
esac
case x in
y) echo "never" ;;
esac
echo "case without a matching branch: status=$?"
echo "--- case استاندارد POSIX است و در sh (dash) هم کار می‌کند:"
sh -c 'case abc in a*) echo "dash matched a*" ;; esac'
خروجی
matched even though $x was not quoted
case without a matching branch: status=0
--- case استاندارد POSIX است و در sh (dash) هم کار می‌کند:
dash matched a*

این برخلاف [[ ]] است که فقط bash دارد. پس اگر اسکریپتت باید روی sh هم کار کند، case انتخاب امن‌تری از [[ ]] است.

حالا یک نمونه‌ی واقعی. اسکریپت راه‌انداز سرویس cron که با خود این سیستم می‌آید، دقیقاً همین ساختار را دارد (دستورهای log_daemon_msg و … توابع کمکی خود دبیان‌اند):

Terminal window
sed -n '66,92p' /etc/init.d/cron
خروجی
case "$1" in
start) log_daemon_msg "Starting periodic command scheduler" "cron"
start_daemon -p $PIDFILE $DAEMON -P $EXTRA_OPTS
log_end_msg $?
;;
stop) log_daemon_msg "Stopping periodic command scheduler" "cron"
killproc -p $PIDFILE $DAEMON
RETVAL=$?
[ $RETVAL -eq 0 ] && [ -e "$PIDFILE" ] && rm -f $PIDFILE
log_end_msg $RETVAL
;;
restart) log_daemon_msg "Restarting periodic command scheduler" "cron"
$0 stop
$0 start
;;
reload|force-reload) log_daemon_msg "Reloading configuration files for periodic command scheduler" "cron"
# cron reloads automatically
log_end_msg 0
;;
status)
status_of_proc -p $PIDFILE $DAEMON $NAME && exit 0 || exit $?
;;
*) log_action_msg "Usage: /etc/init.d/cron {start|stop|status|restart|reload|force-reload}"
exit 2
;;
esac
exit 0

start)، stop)، restart)، reload|force-reload) (دو الگو با |)، status) و *) برای راهنما: عیناً همان چیزی که یاد گرفتی. (و exit 2 برای استفاده‌ی غلط.) هر سرویس /etc/init.d همین الگو را دارد، و service cron restart یا /etc/init.d/cron restart همین case را اجرا می‌کند.

الگوهای case (wildcard):

الگو با چه جور است
start دقیقاً همین متن
* هر چیزی (حتی خالی)؛ شاخه‌ی پیش‌فرض
*.txt هر چیزی که به .txt ختم شود
[0-9]* با یک رقم شروع شود
? دقیقاً یک کاراکتر
[yY] یکی از y یا Y
a | b a یا b
+([0-9]) یک یا چند رقم (نیاز به shopt -s extglob)
"*" یا \* خود کاراکتر ستاره (کوتیشن، معنی ویژه را می‌گیرد)

پایان شاخه‌ها:

پایان معنی نسخه
;; از case بیرون برو (معمول) همه
;& بی‌قید و شرط به دستورهای شاخه‌ی بعد برو bash ۴+
;;& الگوهای بعدی را هم امتحان کن bash ۴+

case در برابر if/elif:

case if/elif
یک مقدار در برابر چند الگو شرط‌های مختلف و دلخواه
الگوی wildcard، بدون regex [[ ]] با =~ و -gt و آزمون فایل
خوانا برای دستورها و منوها خوانا برای شرط‌های ترکیبی
در sh هم هست [[ ]] فقط در bash
Terminal window
cd ~/bashlab
cat > no-dsemi.sh <<'EOF'
case "$1" in
start) echo "starting"
stop) echo "stopping" ;;
esac
EOF
cat > no-esac.sh <<'EOF'
case "$1" in
start) echo "starting" ;;
EOF
for f in no-dsemi.sh no-esac.sh; do
echo "--- $f:"
bash -n $f
done
خروجی
--- no-dsemi.sh:
no-dsemi.sh: line 3: syntax error near unexpected token `)'
no-dsemi.sh: line 3: ` stop) echo "stopping" ;;'
--- no-esac.sh:
no-esac.sh: line 3: syntax error: unexpected end of file

اگر ;; شاخه‌ی اول را فراموش کنی، شل stop) را ادامه‌ی دستور echo می‌بیند و روی ) خطا می‌دهد. نبودن esac هم «unexpected end of file» است. bash -n را قبل از اجرا بزن.

Terminal window
for n in 42 4+; do
case "$n" in
[0-9]+) echo "'$n' matched [0-9]+" ;;
*) echo "'$n' did NOT match [0-9]+" ;;
esac
done
خروجی
'42' did NOT match [0-9]+
'4+' matched [0-9]+

در wildcard + یک کاراکتر معمولی است؛ پس [0-9]+ یعنی «یک رقم و بعدش دقیقاً یک +». 42 جور نشد و 4+ شد. برای «یک یا چند رقم» از +([0-9]) با extglob (مثال ۳) یا [[ =~ ]] استفاده کن.

Terminal window
file="report.txt"
case "$file" in
"*.txt") echo "quoted pattern matched" ;;
*.txt) echo "unquoted pattern matched" ;;
esac
خروجی
unquoted pattern matched

کوتیشن معنی ویژه‌ی * را می‌گیرد؛ "*.txt" فقط با متن دقیق «ستاره‌نقطه‌txt» جور است. الگو را بدون کوتیشن بنویس (مگر وقتی واقعاً کاراکتر خاص را می‌خواهی).

Terminal window
for cmd in start stop; do
case "$cmd" in
*) echo "$cmd -> caught by the default" ;;
start) echo "$cmd -> starting" ;;
stop) echo "$cmd -> stopping" ;;
esac
done
خروجی
start -> caught by the default
stop -> caught by the default

اولین الگوی منطبق برنده است و *) با همه‌چیز جور می‌شود؛ پس شاخه‌های بعدی هرگز اجرا نمی‌شوند. *) همیشه آخر.

۵) حروف بزرگ و کوچک، و ستاره‌ی بی‌کوتیشن در آرگومان

Section titled “۵) حروف بزرگ و کوچک، و ستاره‌ی بی‌کوتیشن در آرگومان”

./myapp.sh Start با start) جور نمی‌شود (مثال ۲: photo.PNG). راه‌حل: shopt -s nocasematch یا الگوی [sS]tart. و یک دام آشنا از درس قبل: ستاره‌ی بدون کوتیشن در خط فرمان قبل از رسیدن به اسکریپت به نام فایل‌ها باز می‌شود:

cd ~/bashlab
mkdir -p star && cd star && touch a.txt b.txt
cat > op.sh <<'EOF'
#!/usr/bin/env bash
echo "got $# arguments: $*"
EOF
chmod +x op.sh
./op.sh 6 * 7
./op.sh 6 '*' 7
خروجی
got 5 arguments: 6 a.txt b.txt op.sh 7
got 3 arguments: 6 * 7

بار اول * به a.txt b.txt باز شد و اسکریپت پنج آرگومان گرفت (6، سه نام فایل و 7). کاربر باید '*' بنویسد؛ یا اسکریپت x را هم به‌عنوان علامت ضرب بپذیرد (تمرین اول).

✎ تمرینآسان

اسکریپت calc.sh بنویس که ./calc.sh 6 + 3 را بگیرد و نتیجه را چاپ کند؛ عملگرها: +، -، x (ضرب) و /. اگر عملگر ناشناخته بود راهنما بده. (برای ضرب '*' را هم بپذیر.) تقسیم بر صفر را بگیر.

دیدن جواب
cd ~/bashlab
cat > calc.sh <<'EOF'
#!/usr/bin/env bash
# calc.sh: ./calc.sh <number> <operator> <number>
if [[ $# -ne 3 ]]; then
echo "Usage: $0 <number> <+|-|x|/> <number>" >&2
exit 1
fi
a="$1"
op="$2"
b="$3"
case "$op" in
+) echo $((a + b)) ;;
-) echo $((a - b)) ;;
x | '*') echo $((a * b)) ;;
/)
if [[ $b -eq 0 ]]; then
echo "Error: division by zero" >&2
exit 2
fi
echo $((a / b))
;;
*)
echo "Unknown operator: $op" >&2
exit 1
;;
esac
EOF
chmod +x calc.sh
./calc.sh 6 + 3
./calc.sh 6 - 3
./calc.sh 6 x 3
./calc.sh 6 '*' 3
./calc.sh 6 / 3
./calc.sh 6 / 0; echo "کد خروج: $?"
./calc.sh 6 % 3; echo "کد خروج: $?"
خروجی
9
3
18
18
2
Error: division by zero
کد خروج: 2
Unknown operator: %
کد خروج: 1

دقت کن x | '*'): ستاره در کوتیشن، یعنی خودِ ستاره.

✎ تمرینمتوسط

تمرین اصلی درس: اسکریپت مدیریت سرویس با گزینه‌های start، stop و status (و restart). «سرویس» ما یک sleep 300 است که در پس‌زمینه اجرا می‌شود و شماره‌ی پروسه‌اش (PID) را در یک فایل (demo.pid) نگه می‌داریم، مثل سرویس‌های واقعی. start اگر قبلاً در حال اجراست پیام بدهد و دوباره شروع نکند. status بگوید در حال اجراست یا نه. گزینه‌ی غلط ← راهنما و کد خروج ۱.

دیدن جواب
cd ~/bashlab
cat > myservice.sh <<'EOF'
#!/usr/bin/env bash
# myservice.sh: start/stop/status for a demo service (a background sleep)
# Usage: ./myservice.sh {start|stop|status|restart}
pidfile="$HOME/bashlab/demo.pid"
# is the service running? (PID file exists and that process is alive)
running() {
[[ -f $pidfile ]] && kill -0 "$(cat "$pidfile")" 2>/dev/null
}
case "$1" in
start)
if running; then
echo "already running (PID $(cat "$pidfile"))"
else
sleep 300 > /dev/null 2>&1 &
echo $! > "$pidfile"
echo "started (PID $!)"
fi
;;
stop)
if running; then
kill "$(cat "$pidfile")"
rm -f "$pidfile"
echo "stopped"
else
rm -f "$pidfile"
echo "not running"
fi
;;
status)
if running; then
echo "running (PID $(cat "$pidfile"))"
else
echo "stopped"
exit 3
fi
;;
restart)
"$0" stop
"$0" start
;;
*)
echo "Usage: $0 {start|stop|status|restart}" >&2
exit 1
;;
esac
EOF
chmod +x myservice.sh
./myservice.sh status; echo "کد خروج: $?"
./myservice.sh start
./myservice.sh start
./myservice.sh status; echo "کد خروج: $?"
./myservice.sh restart
./myservice.sh stop
./myservice.sh stop
./myservice.sh status; echo "کد خروج: $?"
./myservice.sh reload; echo "کد خروج: $?"
خروجی
stopped
کد خروج: 3
started (PID 22438)
already running (PID 22438)
running (PID 22438)
کد خروج: 0
stopped
started (PID 22451)
stopped
not running
stopped
کد خروج: 3
Usage: ./myservice.sh {start|stop|status|restart}
کد خروج: 1

تابع running (توابع را در درس بعد می‌خوانی) فقط یک شرط است: «فایل PID هست و پروسه‌اش زنده است». kill -0 PID سیگنالی نمی‌فرستد؛ فقط بررسی می‌کند پروسه وجود دارد (کد خروج ۰ اگر زنده، ۱ اگر نه) و برای همین برای آزمون «زنده است؟» ایده‌آل است. status وقتی متوقف است کد ۳ می‌دهد (قرارداد سرویس‌های لینوکس: ۳ = «در حال اجرا نیست»)، تا اسکریپت‌های دیگر بتوانند آن را بخوانند. sleep 300 > /dev/null 2>&1 & خروجی را دور می‌ریزد و در پس‌زمینه (&) می‌گذارد.

✎ تمرینمتوسط

اسکریپت extract.sh که اسم یک آرشیو بگیرد و با توجه به پسوند با ابزار درست بازش کند: .tar.gz و .tgz ← tar -xzf، .tar ← tar -xf، .gz ← gunzip -k و چیز دیگر ← «نمی‌دانم چطور باز کنم». (با درس آرشیو و فشرده‌سازی دستورها را می‌شناسی.)

دیدن جواب
Terminal window
mkdir -p ~/bashlab/ex && cd ~/bashlab/ex
mkdir -p data && echo "hello" > data/a.txt
tar -czf data.tar.gz data && tar -cf data.tar data && gzip -k data/a.txt && cp data/a.txt.gz note.gz && rm -rf data
cat > ../extract.sh <<'EOF'
#!/usr/bin/env bash
# extract.sh: unpack an archive according to its extension
file="$1"
case "$file" in
*.tar.gz | *.tgz) tar -xzf "$file" && echo "$file: unpacked with tar -xzf" ;;
*.tar) tar -xf "$file" && echo "$file: unpacked with tar -xf" ;;
*.gz) gunzip -kf "$file" && echo "$file: unpacked with gunzip" ;;
*) echo "$file: don't know how to extract this" >&2; exit 1 ;;
esac
EOF
chmod +x ../extract.sh
for f in data.tar.gz data.tar note.gz photo.rar; do ../extract.sh $f; done
ls
خروجی
data.tar.gz: unpacked with tar -xzf
data.tar: unpacked with tar -xf
note.gz: unpacked with gunzip
photo.rar: don't know how to extract this
data
data.tar
data.tar.gz
note
note.gz

ترتیب مهم است: *.tar.gz باید بالاتر از *.gz باشد، وگرنه data.tar.gz به شاخه‌ی .gz می‌رفت و فقط فشرده‌سازی را باز می‌کرد، نه tar را.

✎ تمرینسخت

اسکریپت install-cmd.sh که اسم یک بسته بگیرد و دستور نصب مناسب توزیع را چاپ کند (نه اجرا): apt-get install -y برای Ubuntu/Debian، dnf install -y برای Fedora/RHEL/Rocky/AlmaLinux، apk add برای Alpine، pacman -S برای Arch. اسم توزیع را از ID در فایل /etc/os-release بخوان (آن فایل خودش یک اسکریپت ساده‌ی متغیرهاست؛ با source می‌شود خواندش). اگر ID ناشناخته بود، با ID_LIKE امتحان کن. یک آرگومان اختیاری برای مسیر فایل بده تا بتوانی توزیع‌های دیگر را هم با فایل ساختگی امتحان کنی.

دیدن جواب
Terminal window
cd ~/bashlab
echo "--- خط‌های مهم /etc/os-release:"
grep -E '^(ID|ID_LIKE)=' /etc/os-release
cat > install-cmd.sh <<'EOF'
#!/usr/bin/env bash
# install-cmd.sh: print the install command for this distribution
# Usage: ./install-cmd.sh <package> [os-release-file]
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <package> [os-release-file]" >&2
exit 1
fi
package="$1"
os_release="${2:-/etc/os-release}"
source "$os_release" # defines ID (and often ID_LIKE)
case "$ID" in
ubuntu | debian) echo "apt-get install -y $package" ;;
fedora | rhel | rocky | almalinux) echo "dnf install -y $package" ;;
alpine) echo "apk add $package" ;;
arch) echo "pacman -S $package" ;;
*)
# unknown ID: try the family (ID_LIKE)
case "$ID_LIKE" in
*debian*) echo "apt-get install -y $package" ;;
*rhel* | *fedora*) echo "dnf install -y $package" ;;
*) echo "Unknown distribution: $ID" >&2; exit 2 ;;
esac
;;
esac
EOF
chmod +x install-cmd.sh
echo "--- این سیستم:"
./install-cmd.sh nginx
echo "--- فایل‌های ساختگی برای توزیع‌های دیگر:"
printf 'ID=alpine\n' > fake-alpine
printf 'ID=linuxmint\nID_LIKE="ubuntu debian"\n' > fake-mint
printf 'ID=oraclelinux\nID_LIKE="fedora"\n' > fake-oracle
printf 'ID=templeos\n' > fake-unknown
for f in fake-alpine fake-mint fake-oracle fake-unknown; do
printf '%-13s -> ' "$f"
./install-cmd.sh nginx "$f"
done
خروجی
--- خط‌های مهم /etc/os-release:
ID=ubuntu
ID_LIKE=debian
--- این سیستم:
apt-get install -y nginx
--- فایل‌های ساختگی برای توزیع‌های دیگر:
fake-alpine -> apk add nginx
fake-mint -> apt-get install -y nginx
fake-oracle -> dnf install -y nginx
fake-unknown -> Unknown distribution: templeos

دو case تو در تو: بیرونی روی ID و درونی روی ID_LIKE (توزیع‌های مشتق مثل Linux Mint ‏ID=linuxmint ولی ID_LIKE="ubuntu debian" دارند). الگوهای *debian* هر مقدار حاوی «debian» را می‌گیرند، و برای توزیع کاملاً ناشناخته پیام خطا و کد خروج ۲ می‌گیری.

⚡ بررسی سریع

در یک case با شاخه‌های «start | run)»، «stop)» و «*)»، اگر اسکریپت را با آرگومان run اجرا کنی کدام شاخه اجرا می‌شود؟

؟ آزمونک
  1. «;;» در انتهای یک شاخه‌ی case چه کاری می‌کند؟

  2. الگوهای case از چه نوع‌اند؟

  3. چرا شاخه‌ی پیش‌فرض *) باید آخر باشد؟

  4. برای اینکه y، Y، yes و YES همه «تأیید» حساب شوند چه می‌نویسی؟

  5. در ./op.sh 6 * 7، اسکریپت چند آرگومان می‌گیرد؟

  6. کدام یک در sh (dash) هم کار می‌کند؟

  • case "$مقدار" in الگو) دستورها ;; ... esac: یک مقدار را با چند الگو مقایسه می‌کند، اولین الگوی منطبق را اجرا می‌کند و با ;; بیرون می‌رود.
  • الگوها wildcard‌اند (*، ?، [...])، نه regex. a | b یعنی «یا». *) پیش‌فرض است و باید آخر باشد. با shopt -s extglob الگوهایی مثل +([0-9]) هم داری.
  • حروف بزرگ و کوچک فرق دارند؛ برای نادیده‌گرفتنش shopt -s nocasematch.
  • ;& و ;;& (bash ۴+) ادامه یا آزمون دوباره‌اند، ولی ;; معمول است.
  • الگوی طلایی اسکریپت‌های کنترلی (start|stop|status) و ابزارهای با زیردستور: command="$1"; shift; case "$command" in ...، همان که در /etc/init.d هم می‌بینی؛ شاخه‌ی *) راهنما و کد خروج خطا.
  • منو: while true + read + case، یا دستور select با PS3.
  • case استاندارد POSIX است و در sh هم کار می‌کند؛ ستاره‌ی خط فرمان را در کوتیشن بنویس.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
case "$1" in start) ... ;; stop) ... ;; *) ... ;; esacساختار کامل
de | at)چند الگو (یا)
*.txt) [0-9]*) ?)الگوهای wildcard
*)پیش‌فرض؛ آخر بنویس
shopt -s nocasematchنادیده‌گرفتن حروف بزرگ و کوچک
shopt -s extglob +([0-9])الگوی «یک یا چند رقم»
;& ;;&ادامه‌ی بی‌قید / آزمون الگوهای بعدی (bash 4+)
command="$1"; shift; case "$command" in ...اسکریپت با زیردستور
kill -0 PIDبررسی زنده‌بودن پروسه (بدون سیگنال)
PS3="Pick: "; select x in a b c; do ...; doneمنوی آماده