توی این درس یاد میگیری یک کار را روی چند چیز یا چند بار تکرار کنی. for را روی فهرست کلمهها، فایلها (for f in *.log; do ... done) و بازهها (for i in {1..5}) مینویسی؛ while و until را تا وقتی یک شرط برقرار یا برقرار نشده؛ با break و continue حلقه را کنترل میکنی؛ و یاد میگیری یک فایل را خطبهخط بخوانی (while read -r line; do ... done < file) و دامهای مهم هر کدام را میشناسی. تمرین: همهی فایلهای jpg یک پوشه را با پیشوند تاریخ تغییر نام بده.
مسئله: چهارصد فایل، یک دستور
Section titled “مسئله: چهارصد فایل، یک دستور”مدیر میگوید «اسم ۴۰۰ عکس را عوض کن». یک بار mv زدن خوب است؛ چهارصد بار نه. یا باید چند سرور را یکییکی چک کنی، یا هر خط یک فایل را پردازش کنی. تکرار کارِ ماشین است. حلقه (loop) یک بلوک دستور را چند بار اجرا میکند، هر بار با مقدار تازهای در یک متغیر حلقه.
تشبیه: خط مونتاژ
Section titled “تشبیه: خط مونتاژ”حلقه مثل خط مونتاژ است: یک ایستگاه (بلوک دستور) ثابت است و قطعهها یکییکی از جلوی آن رد میشوند. for وقتی خوب است که قطعهها از قبل معلوماند (فهرست فایلها)، while وقتی که «تا وقتی شرط برقرار است ادامه بده» (مثلاً تا وقتی خطهایی برای خواندن مانده)، و until وقتی که «تا وقتی چیزی اتفاق بیفتد صبر کن».
انواع حلقه
Section titled “انواع حلقه”| حلقه | تا کی تکرار میشود |
|---|---|
for x in فهرست; do ...; done |
بهتعداد اعضای فهرست (هر بار x یک عضو) |
for ((i=0; i<5; i++)); do ...; done |
تا وقتی شرط حسابی برقرار است (سبک C) |
while دستور; do ...; done |
تا وقتی دستور موفق است (کد خروج ۰) |
until دستور; do ...; done |
تا وقتی دستور موفق نشده است |
مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: for روی فهرست و بازه
Section titled “مثال ۱: for روی فهرست و بازه”ساختار: for متغیر in فهرست; do ... done. فهرست هر چیزی میتواند باشد که به چند کلمه باز شود:
echo "--- فهرست کلمهها:"for fruit in apple banana cherry; do echo "I like $fruit"doneecho "--- اگر عضوی فاصله دارد، کوتیشن:"for name in "Ali Ahmadi" "Sara Karimi"; do echo "[$name]"doneecho "--- بازهها با {..} (بدون خط جدید با echo -n):"for i in {1..5}; do echo -n "$i "; done; echofor i in {5..1}; do echo -n "$i "; done; echofor i in {0..10..2}; do echo -n "$i "; done; echofor i in {01..05}; do echo -n "$i "; done; echofor c in {a..e}; do echo -n "$c "; done; echoecho "--- با متغیر، آکولاد کار نمیکند:"n=3for i in {1..$n}; do echo "i=$i"; doneecho "--- راهحل: seq یا حلقهی C-style:"for i in $(seq 1 $n); do echo -n "$i "; done; echofor ((i = 1; i <= n; i++)); do echo -n "$i "; done; echo--- فهرست کلمهها:I like appleI like bananaI like cherry--- اگر عضوی فاصله دارد، کوتیشن:[Ali Ahmadi][Sara Karimi]--- بازهها با {..} (بدون خط جدید با echo -n):1 2 3 4 55 4 3 2 10 2 4 6 8 1001 02 03 04 05a b c d e--- با متغیر، آکولاد کار نمیکند:i={1..3}--- راهحل: seq یا حلقهی C-style:1 2 31 2 3{1..5}را خود شل قبل از شروع حلقه به1 2 3 4 5باز میکند (brace expansion، اولین مرحلهی درس متغیرها).{5..1}نزولی است،{0..10..2}با قدم ۲، و{01..05}با صفر اول.- آکولاد قبل از باز شدن متغیرها اجرا میشود و در آن لحظه
$nهنوز عدد نیست؛ پس{1..$n}باز نمیشود، بعدش$nجایگزین میشود و متن{1..3}میماند (خروجی بالا:i={1..3}). با متغیر ازseq 1 $nیا حلقهی C-style (for ((i = 1; i <= n; i++))) استفاده کن. - متغیر حلقه (
i،fruit) بعد از حلقه هم میماند و آخرین مقدارش را دارد.
مثال ۲: for روی فایلها
Section titled “مثال ۲: for روی فایلها”مهمترین کاربرد for: انجام یک کار برای هر فایل. الگوی *.jpg (wildcard) را خود شل به فهرست فایلهای مطابق باز میکند:
mkdir -p ~/bashlab/photos && cd ~/bashlab/photostouch a.jpg b.jpg "my photo.jpg" notes.txtecho "--- همهی jpgها:"for f in *.jpg; do echo "found: [$f]"doneecho "--- اشتباه: for f in \$(ls *.jpg) نام دارای فاصله را میشکند:"for f in $(ls *.jpg); do echo "found: [$f]"doneecho "--- وقتی هیچ فایلی نمیخورد:"for f in *.png; do echo "found: [$f]"doneecho "--- با nullglob، الگوی بیتطابق «هیچی» میشود:"shopt -s nullglobfor f in *.png; do echo "found: [$f]"doneecho "(zero iterations)"shopt -u nullglob--- همهی jpgها:found: [a.jpg]found: [b.jpg]found: [my photo.jpg]--- اشتباه: for f in $(ls *.jpg) نام دارای فاصله را میشکند:found: [a.jpg]found: [b.jpg]found: [my]found: [photo.jpg]--- وقتی هیچ فایلی نمیخورد:found: [*.png]--- با nullglob، الگوی بیتطابق «هیچی» میشود:(zero iterations)سه نکته که باید بدانی:
"$f"را با کوتیشن بنویس (cp "$f" ...)؛ نامmy photo.jpgدرfor f in *.jpgسالم میماند، چون شل نامها را خودش از روی فایلها میسازد.- هرگز
for f in $(ls)ننویس. خروجیlsیک متن است که در فاصلهها میشکند (my photo.jpgشد دو تکه). همیشه از wildcard مستقیم استفاده کن. - اگر هیچ فایلی نخورد، شل الگو را همانطور خام نگه میدارد (
*.png) و حلقه یک بار با آن اجرا میشود! باshopt -s nullglobالگوی بیتطابق خالی میشود و حلقه اصلاً اجرا نمیشود. (راه دیگر: داخل حلقه[[ -e $f ]] || continue.)
مثال ۳: حلقهی C-style و while
Section titled “مثال ۳: حلقهی C-style و while”وقتی شمارنده میخواهی، دو شکل داری:
echo "--- C-style for:"for ((i = 1; i <= 5; i++)); do echo -n "$i "doneechoecho "--- while با شمارنده:"count=1while (( count <= 5 )); do echo -n "$count " ((count++))doneechoecho "--- مجموع ۱ تا ۱۰۰:"sum=0for ((i = 1; i <= 100; i++)); do ((sum += i))doneecho "sum=$sum"--- C-style for:1 2 3 4 5--- while با شمارنده:1 2 3 4 5--- مجموع ۱ تا ۱۰۰:sum=5050while شرطش را مثل if میسنجد: یک دستور است (اینجا (( count <= 5 ))، که اگر درست باشد کد ۰ میدهد) و تا وقتی موفق است بدنه تکرار میشود. نکتهی حیاتی: داخل بدنه باید چیزی عوض شود (اینجا ((count++))) وگرنه حلقه هیچوقت تمام نمیشود.
مثال ۴: until و while true
Section titled “مثال ۴: until و while true”until برعکس while است: تا وقتی دستور موفق نشده، تکرار کن. برای «صبر کن تا چیزی آماده شود» عالی است:
cd ~/bashlabrm -f ready.flag(sleep 2; touch ready.flag) &echo "waiting for ready.flag ..."checks=0until [[ -e ready.flag ]]; do ((checks++)) sleep 0.5doneecho "ready.flag appeared (checked $checks times)"waiting for ready.flag ...ready.flag appeared (checked 4 times)یک پروسهی پسزمینه بعد از دو ثانیه فایل را ساخت و حلقهی until که هر نیم ثانیه نگاه میکرد، آزاد شد. همین الگو برای «صبر کن تا سرویس بالا بیاید» یا «تا فایل قفل پاک شود» رایج است. و یک حلقهی بیپایان که خودت با break از آن بیرون میآیی (while true):
n=0while true; do ((n++)) if (( n % 2 == 0 )); then continue; fi if (( n > 7 )); then break; fi echo -n "$n "doneechoecho "loop ended at n=$n"1 3 5 7loop ended at n=9continue یعنی «بقیهی بدنه را رها کن و برو سراغ دور بعد» (اعداد زوج چاپ نشدند)، و break یعنی «از کل حلقه بیرون بیا» (وقتی n به ۹ رسید، اولین عدد فردِ بزرگتر از ۷).
مثال ۵: break و continue دقیقتر
Section titled “مثال ۵: break و continue دقیقتر”echo "--- continue: مضربهای ۳ را رد کن"for i in {1..10}; do (( i % 3 == 0 )) && continue echo -n "$i "doneechoecho "--- break: بعد از ۵ تمام کن"for i in {1..10}; do (( i > 5 )) && break echo -n "$i "doneechoecho "--- break 2: خروج از دو حلقهی تو در تو"for a in 1 2 3; do for b in 1 2 3; do if (( a * b == 4 )); then echo "found: $a x $b = 4" break 2 fi donedone--- continue: مضربهای ۳ را رد کن1 2 4 5 7 8 10--- break: بعد از ۵ تمام کن1 2 3 4 5--- break 2: خروج از دو حلقهی تو در توfound: 2 x 2 = 4break N (و continue N) روی N حلقهی تو در تو اثر میکند؛ break 2 هر دو حلقه را ترک کرد. (بدون عدد، فقط داخلیترین.)
مثال ۶: خواندن فایل خطبهخط
Section titled “مثال ۶: خواندن فایل خطبهخط”الگوی استاندارد و مهمترین چیز این درس:
cd ~/bashlabcat > servers.txt <<'EOF'web1 10.0.0.1 nginxweb2 10.0.0.2 nginxdb1 10.0.0.10 postgresEOFecho "--- هر خط کامل:"while IFS= read -r line; do echo "line: [$line]"done < servers.txtecho "--- شکستن هر خط به فیلد:"while read -r name ip service; do echo "$name -> $ip ($service)"done < servers.txtecho "--- شمارهی خط:"n=0while IFS= read -r line; do ((n++)) printf '%2d: %s\n' "$n" "$line"done < servers.txtecho "--- جداکنندهی دیگر (IFS=:) روی /etc/passwd، فقط کاربران عادی:"while IFS=: read -r user _ uid _ _ home shell; do (( uid >= 1000 && uid < 60000 )) && echo "$user uid=$uid home=$home shell=$shell"done < /etc/passwd--- هر خط کامل:line: [web1 10.0.0.1 nginx]line: [web2 10.0.0.2 nginx]line: [db1 10.0.0.10 postgres]--- شکستن هر خط به فیلد:web1 -> 10.0.0.1 (nginx)web2 -> 10.0.0.2 (nginx)db1 -> 10.0.0.10 (postgres)--- شمارهی خط: 1: web1 10.0.0.1 nginx 2: web2 10.0.0.2 nginx 3: db1 10.0.0.10 postgres--- جداکنندهی دیگر (IFS=:) روی /etc/passwd، فقط کاربران عادی:ubuntu uid=1000 home=/home/ubuntu shell=/bin/bashali uid=1001 home=/home/ali shell=/bin/bashاین الگو را تکهتکه بخوان: while IFS= read -r line; do ... done < فایل.
< servers.txtبعد ازdoneکل حلقه را به فایل وصل میکند: هرreadخط بعدی را میخواند، تا وقتی فایل تمام شود وreadشکست بخورد (و حلقه تمام شود).read -r: بکاسلش را نخورد (درس ورودی و آرگومان).IFS=(خالی، فقط برای همینread): فاصلههای ابتدا و انتهای خط را حذف نکند. وقتی میخواهی فیلدها را جدا کنی،read -r name ip serviceمیگذاری و فاصلهها جدا میکنند.IFS=:یعنی جداکننده:(مثل/etc/passwd)._یک اسم قراردادی برای «این فیلد را نمیخواهم» است.
مثال ۷: دام pipe، حلقه در یک پروسهی دیگر
Section titled “مثال ۷: دام pipe، حلقه در یک پروسهی دیگر”شاید به ذهنت برسد: cat فایل | while read .... کار میکند، ولی یک مشکل پنهان دارد:
cd ~/bashlabcount=0cat servers.txt | while read -r line; do ((count++)); doneecho "pipe version: count=$count"count=0while read -r line; do ((count++)); done < servers.txtecho "redirect version: count=$count"count=0while read -r line; do ((count++)); done < <(grep nginx servers.txt)echo "process substitution: count=$count (only the nginx lines)"pipe version: count=0redirect version: count=3process substitution: count=2 (only the nginx lines)نسخهی pipe شمارنده را صفر برگرداند! هر طرف | در یک پروسهی جداگانه (subshell) اجرا میشود؛ حلقه count را در همان پروسه زیاد کرد و با تمامشدنش از بین رفت. دو راهحل: تغییر جهت از فایل (< servers.txt، نسخهی دوم) یا process substitution با < <(دستور) که خروجی یک دستور را مثل یک فایل به حلقه میدهد، بدون اینکه حلقه از شل اصلی جدا شود (نسخهی سوم). نوشتن < <(...) دو < جداست: اولی تغییر جهت و دومی بخشی از <(...).
مثال ۸: اسکریپت واقعی، خلاصهی لاگ
Section titled “مثال ۸: اسکریپت واقعی، خلاصهی لاگ”همهی چیزهایی که تا اینجا یاد گرفتی، در یک ابزار کاربردی: شمردن سطح لاگها با while read و case، و چاپ خطاهای مهم:
cd ~/bashlabcat > app.log <<'EOF'2026-10-04 09:00:01 INFO Server started2026-10-04 09:00:05 WARN Disk usage at 78%2026-10-04 09:01:12 ERROR Database connection failed2026-10-04 09:01:13 INFO Retrying connection2026-10-04 09:01:15 ERROR Database connection failed2026-10-04 09:02:00 INFO ConnectedEOFcat > log-summary.sh <<'EOF'#!/usr/bin/env bash# log-summary.sh: count log levels and print the error lines# Usage: ./log-summary.sh <logfile>file="$1"info=0; warn=0; errors=0
while read -r day time level message; do case "$level" in INFO) ((info++)) ;; WARN) ((warn++)) ;; ERROR) ((errors++)) echo "ERROR at $time: $message" ;; esacdone < "$file"
echo "INFO=$info WARN=$warn ERROR=$errors"EOFchmod +x log-summary.sh./log-summary.sh app.logERROR at 09:01:12: Database connection failedERROR at 09:01:15: Database connection failedINFO=3 WARN=1 ERROR=2read -r day time level message هر خط را به چهار فیلد میشکند و آخری هر چه مانده را میگیرد (کل متن پیام). case روی سطح، شمارندهی درست را زیاد میکند. (همین کار را با grep -c هم میشد؛ ولی وقتی پردازش پیچیدهتر میشود، حلقه انعطافپذیرتر است.)
پشت پرده: حلقه چطور اجرا میشود؟
Section titled “پشت پرده: حلقه چطور اجرا میشود؟”۱) فهرست for اول کامل باز میشود، بعد حلقه شروع میشود. با set -x میتوانی ببینی شل چه میسازد:
cd ~/bashlab/photosset -xfor f in *.jpg; do echo "$f"doneset +x+ for f in *.jpg+ echo a.jpga.jpg+ for f in *.jpg+ echo b.jpgb.jpg+ for f in *.jpg+ echo 'my photo.jpg'my photo.jpg+ set +xسرخط + for f in *.jpg را شل هر دور با همان عبارتی که نوشتی چاپ میکند، ولی دستورهای داخل بدنه با مقدار واقعی f دیده میشوند (+ echo a.jpg، + echo 'my photo.jpg'): هر دور یک اسم. نتیجهی مهم: فهرست فقط یک بار و قبل از شروع ساخته میشود، پس فایلهایی که حین حلقه بسازی در همان حلقه نمیآیند:
cd ~/bashlab/photosfor f in *.jpg; do echo "processing: $f" touch "new-$f"doneecho "--- فایلهای ساختهشده حین حلقه (در فهرست حلقه نبودند):"ls new-*rm -f new-*processing: a.jpgprocessing: b.jpgprocessing: my photo.jpg--- فایلهای ساختهشده حین حلقه (در فهرست حلقه نبودند):new-a.jpgnew-b.jpgnew-my photo.jpg۲) done < فایل یک بار فایل را باز میکند. تمام readهای داخل حلقه از همان توصیفگر باز فایل میخوانند و هر کدام خط بعدی را برمیدارند. برای همین هر دستور دیگری که داخل حلقه از ورودی استاندارد بخواند (مثل head بدون فایل، cat، ssh، ffmpeg)، خطهای حلقه را میبلعد:
cd ~/bashlabprintf 'one\ntwo\nthree\nfour\n' > items.txtecho "--- بدون محافظ: head هم از همان فایل میخواند"while read -r item; do echo "item: $item" head -n 1 > /dev/nulldone < items.txtecho "--- با < /dev/null برای دستور داخل حلقه:"while read -r item; do echo "item: $item" head -n 1 > /dev/null < /dev/nulldone < items.txt--- بدون محافظ: head هم از همان فایل میخواندitem: oneitem: three--- با < /dev/null برای دستور داخل حلقه:item: oneitem: twoitem: threeitem: fourدر حالت اول two و four گم شدند، چون head آنها را از ورودی حلقه خواند و دور ریخت. این باگ در عمل با ssh host command داخل حلقهی while read میآید؛ ssh باقی فایل را میبلعد و حلقه بعد از اولین میزبان تمام میشود. راهحل: < /dev/null برای دستورهای داخل حلقه (یا ssh -n).
۳) کد خروج حلقه کد خروج آخرین دستور اجراشدهی بدنه است، یا ۰ اگر بدنه هرگز اجرا نشد:
for i in 1 2 3; do false; doneecho "loop whose last command failed: $?"while false; do :; doneecho "loop that never ran: $?"loop whose last command failed: 1loop that never ran: 0جدولهای مرجع
Section titled “جدولهای مرجع”شکلهای for و بازه:
| شکل | نتیجه |
|---|---|
for x in a b c |
سه دور با a، b، c |
for f in *.log |
هر فایل .log پوشهی فعلی |
for x in "$@" |
هر آرگومان اسکریپت (و for x بدون in هم همین) |
for i in {1..5} |
1 2 3 4 5 (فقط عدد ثابت) |
for i in {0..10..2} |
0 2 4 6 8 10 |
for i in $(seq 1 $n) |
بازه با متغیر |
for ((i=0; i<n; i++)) |
C-style، با متغیر |
کنترل حلقه:
| دستور | کار |
|---|---|
break |
خروج از داخلیترین حلقه |
break N |
خروج از N حلقهی تو در تو |
continue |
رفتن به دور بعد |
continue N |
دور بعد برای Nامین حلقهی بیرونی |
خواندن فایل خطبهخط:
| الگو | کار |
|---|---|
while IFS= read -r line; do ...; done < file |
هر خط کامل و بیدستکاری |
while read -r a b c; do ...; done < file |
شکستن هر خط به فیلدها (فاصله) |
while IFS=: read -r a b; do ...; done < file |
جداکنندهی دلخواه |
... done < <(دستور) |
خواندن خروجی یک دستور، بدون subshell |
while read -r l || [[ -n $l ]]; do ... |
خط آخر بدون newline را هم بخوان |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) for f in $(ls) و فاصله در نام
Section titled “۱) for f in $(ls) و فاصله در نام”مثال ۲. راهحل: for f in *.jpg و "$f".
۲) حلقه روی الگوی بیتطابق
Section titled “۲) حلقه روی الگوی بیتطابق”mkdir -p ~/bashlab/empty && cd ~/bashlab/emptyfor f in *.jpg; do mv "$f" "backup-$f"; donemv: cannot stat '*.jpg': No such file or directoryوقتی هیچ jpg نیست، f برابر خود متن *.jpg میشود و mv خطا میدهد. راهحل: shopt -s nullglob، یا [[ -e $f ]] || continue اول بدنه.
۳) خط آخرِ بدون newline نادیده گرفته میشود
Section titled “۳) خط آخرِ بدون newline نادیده گرفته میشود”cd ~/bashlabprintf 'first\nsecond' > no-newline.txtecho "--- بدون محافظ:"while read -r line; do echo "[$line]"; done < no-newline.txtecho "--- با || [[ -n \$line ]]:"while read -r line || [[ -n $line ]]; do echo "[$line]"; done < no-newline.txt--- بدون محافظ:[first]--- با || [[ -n $line ]]:[first][second]read وقتی به پایان فایل بدون \n میرسد، متن را در متغیر میگذارد ولی کد خروج غیرصفر میدهد و حلقه تمام میشود، پس آخرین خط هرگز چاپ نمیشود. فایلهایی که ویرایشگرهای مختلف ذخیره کردهاند، گاهی اینطورند. || [[ -n $line ]] میگوید «اگر read شکست خورد ولی چیزی خوانده بود، باز هم ادامه بده».
۴) فاصلهی ابتدا و انتها بدون IFS=
Section titled “۴) فاصلهی ابتدا و انتها بدون IFS=”cd ~/bashlabprintf ' indented line\n trailing \n' > ws.txtecho "--- بدون IFS=:"while read -r line; do echo "[$line]"; done < ws.txtecho "--- با IFS=:"while IFS= read -r line; do echo "[$line]"; done < ws.txt--- بدون IFS=:[indented line][trailing]--- با IFS=:[ indented line][ trailing ]۵) حلقهی بیپایان
Section titled “۵) حلقهی بیپایان”اگر داخل while چیزی را که شرط به آن بستگی دارد عوض نکنی، حلقه هیچوقت تمام نمیشود. برای دیدن آن بدون گیرافتادن، با timeout (که بعد از مدت دادهشده پروسه را میکشد) اجرا میکنیم:
timeout 1 bash -c 'i=0; while (( i < 5 )); do :; done'echo "exit status of timeout: $?"exit status of timeout: 124کد ۱۲۴ یعنی timeout مجبور شد دستور را بکشد، چون i هرگز زیاد نشد (: دستوری است که کاری نمیکند). راهحل: مطمئن شو داخل حلقه شمارنده یا شرط عوض میشود. برای حلقههای while true حتماً راه خروج (break) بگذار.
۶) cat file | while read و متغیرِ گمشده
Section titled “۶) cat file | while read و متغیرِ گمشده”مثال ۷. راهحل: done < file، یا done < <(دستور).
شمارش معکوس ۵ تا ۱ بنویس: هر عدد را چاپ کند، بین هر دو عدد کمی صبر کند (sleep 0.2) و در آخر Liftoff! بنویسد.
دیدن جواب
for i in {5..1}; do echo "$i" sleep 0.2doneecho "Liftoff!"54321Liftoff!تمرین اصلی درس: همهی فایلهای jpg یک پوشه را با پیشوند تاریخ تغییر نام بده (IMG_1.jpg ← 2026-10-04-IMG_1.jpg). اسکریپت date-prefix.sh پوشه را از آرگومان بگیرد؛ فایلهایی که قبلاً پیشوند تاریخ دارند را دست نزند (تا با اجرای دوباره پیشوند تکراری نشود)، روی هیچ فایل موجودی بازنویسی نکند، و اسمهای دارای فاصله را درست مدیریت کند. قبل و بعد را نشان بده.
دیدن جواب
mkdir -p ~/bashlab/gallery && cd ~/bashlabtouch gallery/IMG_0001.jpg gallery/IMG_0002.jpg "gallery/holiday photo.jpg" gallery/readme.txtcat > date-prefix.sh <<'EOF'#!/usr/bin/env bash# date-prefix.sh: prefix every .jpg in a folder with today's date# Usage: ./date-prefix.sh <folder>if [[ $# -ne 1 || ! -d $1 ]]; then echo "Usage: $0 <folder>" >&2 exit 1fi
cd "$1" || exit 1today=$(date +%F)shopt -s nullglob # no .jpg files -> the loop simply does not run
for f in *.jpg; do case "$f" in [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-*) echo "skip (already has a date): $f" continue ;; esac mv -n -- "$f" "$today-$f" echo "renamed: $f -> $today-$f"doneEOFchmod +x date-prefix.shecho "--- قبل:"; ls galleryecho "--- اجرا:"; ./date-prefix.sh galleryecho "--- بعد:"; ls galleryecho "--- اجرای دوباره (نباید پیشوند تکراری بیاید):"; ./date-prefix.sh galleryecho "--- پوشهی بدون jpg:"; mkdir -p nojpg && ./date-prefix.sh nojpg; echo "کد خروج: $?"--- قبل:IMG_0001.jpgIMG_0002.jpgholiday photo.jpgreadme.txt--- اجرا:renamed: IMG_0001.jpg -> 2026-10-04-IMG_0001.jpgrenamed: IMG_0002.jpg -> 2026-10-04-IMG_0002.jpgrenamed: holiday photo.jpg -> 2026-10-04-holiday photo.jpg--- بعد:2026-10-04-IMG_0001.jpg2026-10-04-IMG_0002.jpg2026-10-04-holiday photo.jpgreadme.txt--- اجرای دوباره (نباید پیشوند تکراری بیاید):skip (already has a date): 2026-10-04-IMG_0001.jpgskip (already has a date): 2026-10-04-IMG_0002.jpgskip (already has a date): 2026-10-04-holiday photo.jpg--- پوشهی بدون jpg:کد خروج: 0نکتههای مهم این پاسخ: ۱) "$today-$f" و "$f" همه در کوتیشناند (فایل holiday photo.jpg). ۲) mv -n اگر مقصد وجود داشته باشد رونویسی نمیکند و -- از اشتباهشدن اسم فایلی که با - شروع شود با گزینه جلوگیری میکند. ۳) case با الگوی تاریخ، فایلهای قبلاً تغییرنامیافته را رد میکند (continue). ۴) nullglob حلقهی پوشهی بدون jpg را بیخطا رد میکند.
اسکریپت sum-file.sh که فایلی با یک عدد در هر خط بگیرد و تعداد، مجموع و میانگین (صحیح) را چاپ کند. خطهای خالی را رد کن و خطهای غیرعددی را با هشدار (روی stderr) نادیده بگیر.
دیدن جواب
cd ~/bashlabprintf '10\n20\n\nabc\n30\n40\n' > numbers.txtcat > sum-file.sh <<'EOF'#!/usr/bin/env bash# sum-file.sh: count, sum and average the numbers in a file (one per line)# Usage: ./sum-file.sh <file>if [[ $# -ne 1 || ! -f $1 ]]; then echo "Usage: $0 <file>" >&2 exit 1fi
count=0sum=0lineno=0while IFS= read -r line; do ((lineno++)) [[ -z $line ]] && continue # skip blank lines if [[ ! $line =~ ^[0-9]+$ ]]; then echo "warning: line $lineno is not a number: '$line'" >&2 continue fi ((count++)) ((sum += line))done < "$1"
if (( count == 0 )); then echo "no numbers found" >&2 exit 2fiecho "count=$count sum=$sum average=$((sum / count))"EOFchmod +x sum-file.sh./sum-file.sh numbers.txtwarning: line 4 is not a number: 'abc'count=4 sum=100 average=25(در درس شرطها دیدی [[ $line =~ ^[0-9]+$ ]] عدد بودن را میسنجد؛ در درس قبل: abc بیصدا صفر میشد، این بررسی همان را جلو میگیرد.)
اسکریپت wait-for.sh <فایل> [ثانیه] که تا ظاهر شدن یک فایل صبر کند (مثلاً تا سرویسی فایل آمادهباش را بسازد)، هر نیم ثانیه نگاه کند و بعد از زمانمهلت (پیشفرض ۱۰ ثانیه) منصرف شود. اگر فایل ظاهر شد با کد ۰ و اگر مهلت تمام شد با کد ۱ تمام شود. امتحانش کن: یکبار فایل بعد از ۲ ثانیه ساخته شود و مهلت ۵ ثانیه باشد، و یکبار مهلت ۱ ثانیه.
دیدن جواب
cd ~/bashlabcat > wait-for.sh <<'EOF'#!/usr/bin/env bash# wait-for.sh: wait until a file appears (with a timeout)# Usage: ./wait-for.sh <file> [timeout-seconds] (default 10)if [[ $# -lt 1 ]]; then echo "Usage: $0 <file> [timeout-seconds]" >&2 exit 2fi
file="$1"timeout_s="${2:-10}"waited=0 # in half seconds
until [[ -e $file ]]; do if (( waited >= timeout_s * 2 )); then echo "timeout: $file did not appear within ${timeout_s}s" >&2 exit 1 fi sleep 0.5 ((waited++))doneecho "$file appeared"EOFchmod +x wait-for.shrm -f flag-a flag-becho "--- ظاهر میشود (۲ ثانیه) با مهلت ۵ ثانیه:"(sleep 2; touch flag-a) &./wait-for.sh flag-a 5; echo "کد خروج: $?"echo "--- ظاهر میشود (۲ ثانیه) با مهلت ۱ ثانیه:"(sleep 2; touch flag-b) &./wait-for.sh flag-b 1; echo "کد خروج: $?"--- ظاهر میشود (۲ ثانیه) با مهلت ۵ ثانیه:flag-a appearedکد خروج: 0--- ظاهر میشود (۲ ثانیه) با مهلت ۱ ثانیه:timeout: flag-b did not appear within 1sکد خروج: 1شمارنده به نیمثانیهها است (waited) و مهلت timeout_s * 2 است، چون هر دور نیم ثانیه صبر میکند. حلقهی until شرط «فایل هست؟» دارد و داخلش با exit 1 از اسکریپت بیرون میآید (بهجای break، چون میخواهیم کد خروج خطا بدهیم).
آزمونک
Section titled “آزمونک”until [[ -e ready ]]; do sleep 1; done تا کی تکرار میشود؟
until تا وقتی دستور شرط موفق نشده تکرار میکند و بهمحض موفقیت (فایل ساخته شد) متوقف میشود؛ برعکس while.
for f in *.jpg وقتی هیچ فایل jpg ای در پوشه نیست چه میشود؟
راهحل: shopt -s nullglob یا بررسی [[ -e $f ]] || continue.
چرا for i in {1..$n} کار نمیکند؟
for i in $(seq 1 $n) یا for ((i=1; i<=n; i++)).
تفاوت break و continue؟
break N و continue N روی N حلقهی تو در تو اثر میکنند.
در count=0; cat f | while read -r l; do ((count++)); done; echo $count چه چاپ میشود و چرا؟
راهحل: done < f یا done < <(cat f).
IFS= در while IFS= read -r line چه کاری میکند؟
بدون IFS= فاصلههای اول و آخر خط پاک میشوند.
چرا داخل while read ... done < file نباید ssh یا head بدون < /dev/null اجرا کرد؟
راهحل: < /dev/null برای آن دستور (یا ssh -n).
جمعبندی
Section titled “جمعبندی”for x in فهرست; do ...; doneبرای فهرستهای مشخص: کلمهها،*.jpg(فایلها)،{1..5}(بازهی ثابت)،"$@". با متغیر:$(seq 1 $n)یاfor ((i=1; i<=n; i++)).while دستور; do ...; doneتا وقتی دستور موفق است؛untilتا وقتی موفق شود. شرط یک دستور است (مثلif)؛ داخل بدنه باید چیزی عوض شود.breakاز حلقه بیرون میآید،continueدور بعد را شروع میکند؛ با عدد (break 2) برای حلقههای تو در تو.- فایلها:
for f in *.jpgبا"$f"؛ هرگزfor f in $(ls)؛ برای الگوی بیتطابقshopt -s nullglob. - خواندن خطبهخط:
while IFS= read -r line; do ...; done < file. فیلدها:read -r a b c. خط آخر بدون newline:|| [[ -n $line ]]. - دامها:
cat f | while readمتغیرها را گم میکند (از< fیا< <(cmd)استفاده کن)؛ دستور داخل حلقه که stdin میخواند (ssh، head) ورودی را میبلعد (< /dev/null)؛ حلقهی بیپایان. - کد خروج حلقه = کد خروج آخرین دستور بدنه (۰ اگر هرگز اجرا نشد).
| دستور | کاری که میکند |
|---|---|
for f in *.log; do ...; done | برای هر فایل |
for i in {1..5} | بازهی ثابت (نزولی {5..1}، قدم {0..10..2}) |
for ((i=1; i<=n; i++)); do ...; done | حلقهی C-style با متغیر |
while (( i < 5 )); do ...; done | تا وقتی شرط برقرار است |
until [[ -e file ]]; do sleep 1; done | تا وقتی شرط برقرار شود |
break / continue / break 2 | خروج / دور بعد / خروج از دو حلقه |
while IFS= read -r line; do ...; done < file | فایل خطبهخط |
while read -r a b c; do ...; done < file | شکستن هر خط به فیلد |
... done < <(grep x file) | حلقه روی خروجی دستور بدون subshell |
shopt -s nullglob | الگوی بیتطابق ← حلقه اجرا نشود |
mv -n -- "$f" "$new" | تغییر نام بدون رونویسی |
timeout 5 command | کشتن دستور بعد از ۵ ثانیه |