توی این درس یاد میگیری وقتی یک مقدار را باید با چند حالت مقایسه کنی، بهجای زنجیرهی بلند 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 [[ $1 == start ]]; then ...elif [[ $1 == stop ]]; then ...elif [[ $1 == status ]]; then ...else ...fiهر شاخه [[ $1 == ... ]] را تکرار میکند و چشم باید در میان این تکرارها بگردد تا ببیند «کدام مقدار → کدام کار». case دقیقاً برای همین ساخته شده: یک مقدار، چند الگو، هر الگو یک کار.
تشبیه: تلفن گویا
Section titled “تشبیه: تلفن گویا”وقتی با یک شرکت تماس میگیری، یک منوی صوتی میشنوی: «برای فروش یک را بزنید، برای پشتیبانی دو را بزنید، برای هر چیز دیگر منتظر بمانید». یک ورودی (عددی که زدی)، چند شاخه، و یک مسیر پیشفرض برای «غیر از اینها». case همین است: مقدار را با الگوها یکییکی از بالا مقایسه میکند، اولین الگوی جور را اجرا میکند و تمام.
ساختار case
Section titled “ساختار case”case "$مقدار" in الگوی۱) دستورها ;; الگوی۲ | الگوی۳) دستورها ;; *) دستورهای پیشفرض ;;esacesacهمانcaseبرعکس است (مثلfiبرایif).- هر شاخه با
الگو)شروع میشود و با;;تمام میشود.;;یعنی «اینجا شاخه تمام؛ ازcaseبیرون برو». |بین چند الگو یعنی «یا».*)با هر چیزی جور است؛ آخر بگذارش تا شاخهی پیشفرض باشد.- الگوها از بالا به پایین بررسی میشوند و اولین الگوی منطبق اجرا میشود.
مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: اولین case
Section titled “مثال ۱: اولین case”#!/usr/bin/env bash# hello-lang.sh: greet in the language given as the first argumentcase "$1" in en) echo "Hello!" ;; fa) echo "Salam!" ;; de | at) echo "Hallo!" ;; *) echo "Sorry, I do not speak '$1'" >&2 exit 1 ;;esaccd ~/bashlabchmod +x hello-lang.shfor lang in en fa de at fr; do ./hello-lang.sh $langdoneecho "--- بدون آرگومان:"./hello-lang.sh; echo "کد خروج: $?"Hello!Salam!Hallo!Hallo!Sorry, I do not speak 'fr'--- بدون آرگومان:Sorry, I do not speak ''کد خروج: 1de | at) یعنی «اگر de یا at بود». برای fr هیچ شاخهی خاصی نبود و *) گرفتش و پیام خطا داد. بدون آرگومان هم "$1" رشتهی خالی است که فقط با *) جور میشود.
مثال ۲: الگوها، wildcard نه regex
Section titled “مثال ۲: الگوها، wildcard نه regex”الگوهای case همان wildcardهای نام فایلاند (که در درس لینوکس دیدی)، نه regex: * هر تعداد کاراکتر، ? دقیقاً یک کاراکتر، و [abc] یا [0-9] یکی از چند کاراکتر:
#!/usr/bin/env bash# classify.sh: guess what a file is from its namename="$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" ;;esaccd ~/bashlabchmod +x classify.shfor 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"donebackup.tar.gz: gzip-compressed archivesite.zip: zip archiveREADME.md: text documentphoto.png: imagephoto.PNG: unknown2026-report: starts with a digit2026.txt: text documentx: a one-character name.bashrc: hidden filedata.bin: unknownچند نکته: 2026.txt به شاخهی *.txt رفت، نه [0-9]*، چون اولین الگوی منطبق برنده است و *.txt بالاتر بود. photo.PNG با حروف بزرگ جور نشد (case بهصورت پیشفرض به حروف بزرگ و کوچک حساس است). و شاخههای ?) و .*) هم با کاراکترهای ویژه کار میکنند. میتوانی روی یک خط هم بنویسی (الگو) دستور ;;) وقتی شاخه کوتاه است.
مثال ۳: پاسخ بله/نه و اعتبارسنجی عدد
Section titled “مثال ۳: پاسخ بله/نه و اعتبارسنجی عدد”دو کاربرد بسیار رایج: خواندن جواب y/n از کاربر با هر املایی، و بررسی اینکه ورودی عدد است:
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" ;; esacdoneecho "--- روش کوتاهتر: nocasematch (حروف بزرگ و کوچک یکی)"shopt -s nocasematchcase "YeS" in y | yes) echo "matched with nocasematch" ;;esacshopt -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 روشن میشوند:
shopt -s extglobfor 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" ;; esacdone'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 نسخهی ۴ به بعد دو پایان دیگر هم هست:
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" ;; esacdoneecho "--- ;& : بیقید و شرط به شاخهی بعدی برو"for n in 1 2 3; do echo "$n:" case "$n" in 1) echo " one" ;& 2) echo " two (also after one)" ;; 3) echo " three" ;; esacdone--- ;;& : بعد از این شاخه، الگوهای بعدی را هم امتحان کنabc: starts with a contains b reached the last branchxyz: 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 (درس ورودی و آرگومانها) میسازی. این هم یک ابزار کوچک یادداشت:
#!/usr/bin/env bash# notes.sh: a tiny notes manager with sub-commands# Usage: ./notes.sh add <text...> | list | count | clearnotes_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 ;;esaccd ~/bashlabchmod +x notes.sh./notes.sh add Buy milk./notes.sh add Call Sara tomorrow./notes.sh listecho "count: $(./notes.sh count)"./notes.sh clearecho "count: $(./notes.sh count)"echo "--- زیردستور ناشناخته:"./notes.sh remove 1; echo "کد خروج: $?"echo "--- بدون آرگومان:"./notes.sh; echo "کد خروج: $?"Added.Added. 1 Buy milk 2 Call Sara tomorrowcount: 2Cleared.count: 0--- زیردستور ناشناخته:Usage: ./notes.sh add <text...> | list | count | clearکد خروج: 1--- بدون آرگومان:Usage: ./notes.sh add <text...> | list | count | clearکد خروج: 1command="$1"; shift اولین آرگومان (زیردستور) را کنار میگذارد و بقیه ($*) متن یادداشت میشود. case هم زیردستور را به کار درست میرساند. *) شاخهی راهنماست و کد خروج خطا میدهد. با همین الگو اسکریپتی بنویس که مثل ابزارهای حرفهای رفتار کند.
مثال ۶: منو با case و حلقه
Section titled “مثال ۶: منو با case و حلقه”منوی ساده: منو را چاپ میکنی، جواب را با read میگیری، با case کار را انتخاب میکنی و تا وقتی کاربر خروج نزده دوباره از اول:
#!/usr/bin/env bash# menu.sh: a simple text menuwhile 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'" ;; esacdonewhile true; do ... done یعنی «برای همیشه تکرار کن»؛ حلقهها را در درس بعد کامل میخوانی. فعلاً بدان break از حلقه بیرون میآید. برای دیدن واقعی کار، آن را داخل یک ترمینال مجازی (tmux، مثل درسهای قبل) اجرا میکنم و جوابها را تایپ میکنم:
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.shstart_terminal 40tmux send-keys -t demo "./menu.sh" Entersleep 0.7tmux send-keys -t demo "2" Entersleep 0.7tmux send-keys -t demo "9" Entersleep 0.7tmux send-keys -t demo "q" Entersleep 0.7screen$ ./menu.sh
=== Server menu ===1) Show date2) Show disk usage3) Who am Iq) QuitChoose: 2Filesystem Size Used Avail Use% Mounted onoverlay 453G 23G 408G 6% /
=== Server menu ===1) Show date2) Show disk usage3) Who am Iq) QuitChoose: 9Invalid choice: '9'
=== Server menu ===1) Show date2) Show disk usage3) Who am Iq) QuitChoose: qBye$دو بار منو چاپ شد: بعد از 2 (دیسک)، بعد از 9 (نامعتبر؛ شاخهی *)) و با q حلقه تمام شد. دقت کن q | Q) هر دو حالت را میپذیرد.
مثال ۷: select، منوی آماده
Section titled “مثال ۷: select، منوی آماده”Bash برای همین کار یک دستور آماده دارد، select: لیست را شمارهدار چاپ میکند، پرسش را از PS3 میگیرد و جواب را در متغیر میگذارد. معمولاً با case ترکیب میشود:
#!/usr/bin/env bash# select-demo.sh: the built-in menu commandPS3="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 ;; esacdonecd ~/bashlabchmod +x select-demo.shprintf '9\n2\n' | ./select-demo.sh1) apple2) banana3) cherry4) quitPick a fruit: Invalid number: 9Pick a fruit: You picked bananaselect گزینهها را شمارهگذاری و چاپ کرد (روی stderr)، و با عدد 9 که شمارهی معتبری نبود، fruit خالی ماند (شاخهی "")) و عددی که تایپ شده در REPLY مانده بود. با 2 گزینهی دوم (banana) انتخاب شد. select تا break یا پایان ورودی تکرار میشود. وقتی کنترل بیشتری میخواهی (مثلاً q برای خروج)، منوی while+case مثال قبل انعطافپذیرتر است.
پشت پرده: case چطور کار میکند؟
Section titled “پشت پرده: case چطور کار میکند؟”وقتی شل case را اجرا میکند:
- کلمهی بعد از
caseرا باز میکند (متغیر، دستور، محاسبه)، ولی نه شکستن در فاصلهها و نه*. - الگوها را یکییکی از بالا با همان موتور wildcard نام فایل مقایسه میکند.
- اولین الگوی منطبق را اجرا میکند و تا
;;ادامه میدهد. - کد خروج
caseهمان کد خروج آخرین دستور اجراشده است (یا ۰ اگر هیچ شاخهای نخورد).
برای همین نبودن کوتیشن دور مقدار، برخلاف بیشتر جاها، در case خراب نمیکند؛ ولی کوتیشن بگذار تا الگوی یکسانی با بقیهی اسکریپت داشته باشی:
x="a b"case $x in "a b") echo "matched even though \$x was not quoted" ;;esaccase x in y) echo "never" ;;esacecho "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 quotedcase without a matching branch: status=0--- case استاندارد POSIX است و در sh (dash) هم کار میکند:dash matched a*این برخلاف [[ ]] است که فقط bash دارد. پس اگر اسکریپتت باید روی sh هم کار کند، case انتخاب امنتری از [[ ]] است.
حالا یک نمونهی واقعی. اسکریپت راهانداز سرویس cron که با خود این سیستم میآید، دقیقاً همین ساختار را دارد (دستورهای log_daemon_msg و … توابع کمکی خود دبیاناند):
sed -n '66,92p' /etc/init.d/croncase "$1" instart) 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 ;;esacexit 0start)، stop)، restart)، reload|force-reload) (دو الگو با |)، status) و *) برای راهنما: عیناً همان چیزی که یاد گرفتی. (و exit 2 برای استفادهی غلط.) هر سرویس /etc/init.d همین الگو را دارد، و service cron restart یا /etc/init.d/cron restart همین case را اجرا میکند.
جدولهای مرجع
Section titled “جدولهای مرجع”الگوهای 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 |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) جاانداختن ;; یا esac
Section titled “۱) جاانداختن ;; یا esac”cd ~/bashlabcat > no-dsemi.sh <<'EOF'case "$1" in start) echo "starting" stop) echo "stopping" ;;esacEOFcat > no-esac.sh <<'EOF'case "$1" in start) echo "starting" ;;EOFfor f in no-dsemi.sh no-esac.sh; do echo "--- $f:" bash -n $fdone--- 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 را قبل از اجرا بزن.
۲) regex بهجای wildcard
Section titled “۲) regex بهجای wildcard”for n in 42 4+; do case "$n" in [0-9]+) echo "'$n' matched [0-9]+" ;; *) echo "'$n' did NOT match [0-9]+" ;; esacdone'42' did NOT match [0-9]+'4+' matched [0-9]+در wildcard + یک کاراکتر معمولی است؛ پس [0-9]+ یعنی «یک رقم و بعدش دقیقاً یک +». 42 جور نشد و 4+ شد. برای «یک یا چند رقم» از +([0-9]) با extglob (مثال ۳) یا [[ =~ ]] استفاده کن.
۳) کوتیشن دور الگو
Section titled “۳) کوتیشن دور الگو”file="report.txt"case "$file" in "*.txt") echo "quoted pattern matched" ;; *.txt) echo "unquoted pattern matched" ;;esacunquoted pattern matchedکوتیشن معنی ویژهی * را میگیرد؛ "*.txt" فقط با متن دقیق «ستارهنقطهtxt» جور است. الگو را بدون کوتیشن بنویس (مگر وقتی واقعاً کاراکتر خاص را میخواهی).
۴) *) را بالا گذاشتن
Section titled “۴) *) را بالا گذاشتن”for cmd in start stop; do case "$cmd" in *) echo "$cmd -> caught by the default" ;; start) echo "$cmd -> starting" ;; stop) echo "$cmd -> stopping" ;; esacdonestart -> caught by the defaultstop -> caught by the defaultاولین الگوی منطبق برنده است و *) با همهچیز جور میشود؛ پس شاخههای بعدی هرگز اجرا نمیشوند. *) همیشه آخر.
۵) حروف بزرگ و کوچک، و ستارهی بیکوتیشن در آرگومان
Section titled “۵) حروف بزرگ و کوچک، و ستارهی بیکوتیشن در آرگومان”./myapp.sh Start با start) جور نمیشود (مثال ۲: photo.PNG). راهحل: shopt -s nocasematch یا الگوی [sS]tart. و یک دام آشنا از درس قبل: ستارهی بدون کوتیشن در خط فرمان قبل از رسیدن به اسکریپت به نام فایلها باز میشود:
cd ~/bashlabmkdir -p star && cd star && touch a.txt b.txtcat > op.sh <<'EOF'#!/usr/bin/env bashecho "got $# arguments: $*"EOFchmod +x op.sh./op.sh 6 * 7./op.sh 6 '*' 7got 5 arguments: 6 a.txt b.txt op.sh 7got 3 arguments: 6 * 7بار اول * به a.txt b.txt باز شد و اسکریپت پنج آرگومان گرفت (6، سه نام فایل و 7). کاربر باید '*' بنویسد؛ یا اسکریپت x را هم بهعنوان علامت ضرب بپذیرد (تمرین اول).
اسکریپت calc.sh بنویس که ./calc.sh 6 + 3 را بگیرد و نتیجه را چاپ کند؛ عملگرها: +، -، x (ضرب) و /. اگر عملگر ناشناخته بود راهنما بده. (برای ضرب '*' را هم بپذیر.) تقسیم بر صفر را بگیر.
دیدن جواب
cd ~/bashlabcat > 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 1fi
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 ;;esacEOFchmod +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 "کد خروج: $?"9318182Error: division by zeroکد خروج: 2Unknown operator: %کد خروج: 1دقت کن x | '*'): ستاره در کوتیشن، یعنی خودِ ستاره.
تمرین اصلی درس: اسکریپت مدیریت سرویس با گزینههای start، stop و status (و restart). «سرویس» ما یک sleep 300 است که در پسزمینه اجرا میشود و شمارهی پروسهاش (PID) را در یک فایل (demo.pid) نگه میداریم، مثل سرویسهای واقعی. start اگر قبلاً در حال اجراست پیام بدهد و دوباره شروع نکند. status بگوید در حال اجراست یا نه. گزینهی غلط ← راهنما و کد خروج ۱.
دیدن جواب
cd ~/bashlabcat > 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 ;;esacEOFchmod +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کد خروج: 3started (PID 22438)already running (PID 22438)running (PID 22438)کد خروج: 0stoppedstarted (PID 22451)stoppednot runningstoppedکد خروج: 3Usage: ./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 و چیز دیگر ← «نمیدانم چطور باز کنم». (با درس آرشیو و فشردهسازی دستورها را میشناسی.)
دیدن جواب
mkdir -p ~/bashlab/ex && cd ~/bashlab/exmkdir -p data && echo "hello" > data/a.txttar -czf data.tar.gz data && tar -cf data.tar data && gzip -k data/a.txt && cp data/a.txt.gz note.gz && rm -rf datacat > ../extract.sh <<'EOF'#!/usr/bin/env bash# extract.sh: unpack an archive according to its extensionfile="$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 ;;esacEOFchmod +x ../extract.shfor f in data.tar.gz data.tar note.gz photo.rar; do ../extract.sh $f; donelsdata.tar.gz: unpacked with tar -xzfdata.tar: unpacked with tar -xfnote.gz: unpacked with gunzipphoto.rar: don't know how to extract thisdatadata.tardata.tar.gznotenote.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 امتحان کن. یک آرگومان اختیاری برای مسیر فایل بده تا بتوانی توزیعهای دیگر را هم با فایل ساختگی امتحان کنی.
دیدن جواب
cd ~/bashlabecho "--- خطهای مهم /etc/os-release:"grep -E '^(ID|ID_LIKE)=' /etc/os-releasecat > 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 1fi
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 ;;esacEOFchmod +x install-cmd.shecho "--- این سیستم:"./install-cmd.sh nginxecho "--- فایلهای ساختگی برای توزیعهای دیگر:"printf 'ID=alpine\n' > fake-alpineprintf 'ID=linuxmint\nID_LIKE="ubuntu debian"\n' > fake-mintprintf 'ID=oraclelinux\nID_LIKE="fedora"\n' > fake-oracleprintf 'ID=templeos\n' > fake-unknownfor f in fake-alpine fake-mint fake-oracle fake-unknown; do printf '%-13s -> ' "$f" ./install-cmd.sh nginx "$f"done--- خطهای مهم /etc/os-release:ID=ubuntuID_LIKE=debian--- این سیستم:apt-get install -y nginx--- فایلهای ساختگی برای توزیعهای دیگر:fake-alpine -> apk add nginxfake-mint -> apt-get install -y nginxfake-oracle -> dnf install -y nginxfake-unknown -> Unknown distribution: templeosدو case تو در تو: بیرونی روی ID و درونی روی ID_LIKE (توزیعهای مشتق مثل Linux Mint ID=linuxmint ولی ID_LIKE="ubuntu debian" دارند). الگوهای *debian* هر مقدار حاوی «debian» را میگیرند، و برای توزیع کاملاً ناشناخته پیام خطا و کد خروج ۲ میگیری.
آزمونک
Section titled “آزمونک”در یک case با شاخههای «start | run)»، «stop)» و «*)»، اگر اسکریپت را با آرگومان run اجرا کنی کدام شاخه اجرا میشود؟
start | run یعنی «start یا run». بعد از اولین شاخهی منطبق و رسیدن به ;; از case بیرون میرویم، پس *) اجرا نمیشود.
«;;» در انتهای یک شاخهی case چه کاری میکند؟
برای ادامه به شاخهی بعد باید ;& یا ;;& بنویسی (bash 4+).
الگوهای case از چه نوعاند؟
برای regex از [[ $x =~ regex ]] استفاده کن.
چرا شاخهی پیشفرض *) باید آخر باشد؟
الگوها از بالا به پایین بررسی میشوند و اولین منطبق برنده است.
برای اینکه y، Y، yes و YES همه «تأیید» حساب شوند چه مینویسی؟
یا shopt -s nocasematch را روشن کن تا y|yes همهی حالتها را بگیرد.
در ./op.sh 6 * 7، اسکریپت چند آرگومان میگیرد؟
باید «*» را در کوتیشن بنویسی.
کدام یک در sh (dash) هم کار میکند؟
case استاندارد POSIX است؛ [[ ]] مخصوص bash است.
جمعبندی
Section titled “جمعبندی”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 | منوی آماده |