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

حلقه‌ها

توی این درس یاد می‌گیری یک کار را روی چند چیز یا چند بار تکرار کنی. 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) یک بلوک دستور را چند بار اجرا می‌کند، هر بار با مقدار تازه‌ای در یک متغیر حلقه.

حلقه مثل خط مونتاژ است: یک ایستگاه (بلوک دستور) ثابت است و قطعه‌ها یکی‌یکی از جلوی آن رد می‌شوند. for وقتی خوب است که قطعه‌ها از قبل معلوم‌اند (فهرست فایل‌ها)، while وقتی که «تا وقتی شرط برقرار است ادامه بده» (مثلاً تا وقتی خط‌هایی برای خواندن مانده)، و until وقتی که «تا وقتی چیزی اتفاق بیفتد صبر کن».

حلقه تا کی تکرار می‌شود
for x in فهرست; do ...; done به‌تعداد اعضای فهرست (هر بار x یک عضو)
for ((i=0; i<5; i++)); do ...; done تا وقتی شرط حسابی برقرار است (سبک C)
while دستور; do ...; done تا وقتی دستور موفق است (کد خروج ۰)
until دستور; do ...; done تا وقتی دستور موفق نشده است
while و until دقیقاً شرط یکدیگرند: while تا وقتی دستور موفق است تکرار می‌کند، until تا وقتی موفق شود. شرط هم مثل if یک دستور است و کد خروجش سنجیده می‌شود.

مثال ۱: for روی فهرست و بازه

Section titled “مثال ۱: for روی فهرست و بازه”

ساختار: for متغیر in فهرست; do ... done. فهرست هر چیزی می‌تواند باشد که به چند کلمه باز شود:

Terminal window
echo "--- فهرست کلمه‌ها:"
for fruit in apple banana cherry; do
echo "I like $fruit"
done
echo "--- اگر عضوی فاصله دارد، کوتیشن:"
for name in "Ali Ahmadi" "Sara Karimi"; do
echo "[$name]"
done
echo "--- بازه‌ها با {..} (بدون خط جدید با echo -n):"
for i in {1..5}; do echo -n "$i "; done; echo
for i in {5..1}; do echo -n "$i "; done; echo
for i in {0..10..2}; do echo -n "$i "; done; echo
for i in {01..05}; do echo -n "$i "; done; echo
for c in {a..e}; do echo -n "$c "; done; echo
echo "--- با متغیر، آکولاد کار نمی‌کند:"
n=3
for i in {1..$n}; do echo "i=$i"; done
echo "--- راه‌حل: seq یا حلقه‌ی C-style:"
for i in $(seq 1 $n); do echo -n "$i "; done; echo
for ((i = 1; i <= n; i++)); do echo -n "$i "; done; echo
خروجی
--- فهرست کلمه‌ها:
I like apple
I like banana
I like cherry
--- اگر عضوی فاصله دارد، کوتیشن:
[Ali Ahmadi]
[Sara Karimi]
--- بازه‌ها با {..} (بدون خط جدید با echo -n):
1 2 3 4 5
5 4 3 2 1
0 2 4 6 8 10
01 02 03 04 05
a b c d e
--- با متغیر، آکولاد کار نمی‌کند:
i={1..3}
--- راه‌حل: seq یا حلقه‌ی C-style:
1 2 3
1 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: انجام یک کار برای هر فایل. الگوی *.jpg (wildcard) را خود شل به فهرست فایل‌های مطابق باز می‌کند:

Terminal window
mkdir -p ~/bashlab/photos && cd ~/bashlab/photos
touch a.jpg b.jpg "my photo.jpg" notes.txt
echo "--- همه‌ی jpgها:"
for f in *.jpg; do
echo "found: [$f]"
done
echo "--- اشتباه: for f in \$(ls *.jpg) نام دارای فاصله را می‌شکند:"
for f in $(ls *.jpg); do
echo "found: [$f]"
done
echo "--- وقتی هیچ فایلی نمی‌خورد:"
for f in *.png; do
echo "found: [$f]"
done
echo "--- با nullglob، الگوی بی‌تطابق «هیچی» می‌شود:"
shopt -s nullglob
for f in *.png; do
echo "found: [$f]"
done
echo "(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”

وقتی شمارنده می‌خواهی، دو شکل داری:

Terminal window
echo "--- C-style for:"
for ((i = 1; i <= 5; i++)); do
echo -n "$i "
done
echo
echo "--- while با شمارنده:"
count=1
while (( count <= 5 )); do
echo -n "$count "
((count++))
done
echo
echo "--- مجموع ۱ تا ۱۰۰:"
sum=0
for ((i = 1; i <= 100; i++)); do
((sum += i))
done
echo "sum=$sum"
خروجی
--- C-style for:
1 2 3 4 5
--- while با شمارنده:
1 2 3 4 5
--- مجموع ۱ تا ۱۰۰:
sum=5050

while شرطش را مثل if می‌سنجد: یک دستور است (اینجا (( count <= 5 ))، که اگر درست باشد کد ۰ می‌دهد) و تا وقتی موفق است بدنه تکرار می‌شود. نکته‌ی حیاتی: داخل بدنه باید چیزی عوض شود (اینجا ((count++))) وگرنه حلقه هیچ‌وقت تمام نمی‌شود.

until برعکس while است: تا وقتی دستور موفق نشده، تکرار کن. برای «صبر کن تا چیزی آماده شود» عالی است:

Terminal window
cd ~/bashlab
rm -f ready.flag
(sleep 2; touch ready.flag) &
echo "waiting for ready.flag ..."
checks=0
until [[ -e ready.flag ]]; do
((checks++))
sleep 0.5
done
echo "ready.flag appeared (checked $checks times)"
خروجی
waiting for ready.flag ...
ready.flag appeared (checked 4 times)

یک پروسه‌ی پس‌زمینه بعد از دو ثانیه فایل را ساخت و حلقه‌ی until که هر نیم ثانیه نگاه می‌کرد، آزاد شد. همین الگو برای «صبر کن تا سرویس بالا بیاید» یا «تا فایل قفل پاک شود» رایج است. و یک حلقه‌ی بی‌پایان که خودت با break از آن بیرون می‌آیی (while true):

Terminal window
n=0
while true; do
((n++))
if (( n % 2 == 0 )); then continue; fi
if (( n > 7 )); then break; fi
echo -n "$n "
done
echo
echo "loop ended at n=$n"
خروجی
1 3 5 7
loop ended at n=9

continue یعنی «بقیه‌ی بدنه را رها کن و برو سراغ دور بعد» (اعداد زوج چاپ نشدند)، و break یعنی «از کل حلقه بیرون بیا» (وقتی n به ۹ رسید، اولین عدد فردِ بزرگ‌تر از ۷).

مثال ۵: break و continue دقیق‌تر

Section titled “مثال ۵: break و continue دقیق‌تر”
Terminal window
echo "--- continue: مضرب‌های ۳ را رد کن"
for i in {1..10}; do
(( i % 3 == 0 )) && continue
echo -n "$i "
done
echo
echo "--- break: بعد از ۵ تمام کن"
for i in {1..10}; do
(( i > 5 )) && break
echo -n "$i "
done
echo
echo "--- 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
done
done
خروجی
--- continue: مضرب‌های ۳ را رد کن
1 2 4 5 7 8 10
--- break: بعد از ۵ تمام کن
1 2 3 4 5
--- break 2: خروج از دو حلقه‌ی تو در تو
found: 2 x 2 = 4

break N (و continue N) روی N حلقه‌ی تو در تو اثر می‌کند؛ break 2 هر دو حلقه را ترک کرد. (بدون عدد، فقط داخلی‌ترین.)

مثال ۶: خواندن فایل خط‌به‌خط

Section titled “مثال ۶: خواندن فایل خط‌به‌خط”

الگوی استاندارد و مهم‌ترین چیز این درس:

Terminal window
cd ~/bashlab
cat > servers.txt <<'EOF'
web1 10.0.0.1 nginx
web2 10.0.0.2 nginx
db1 10.0.0.10 postgres
EOF
echo "--- هر خط کامل:"
while IFS= read -r line; do
echo "line: [$line]"
done < servers.txt
echo "--- شکستن هر خط به فیلد:"
while read -r name ip service; do
echo "$name -> $ip ($service)"
done < servers.txt
echo "--- شماره‌ی خط:"
n=0
while IFS= read -r line; do
((n++))
printf '%2d: %s\n' "$n" "$line"
done < servers.txt
echo "--- جداکننده‌ی دیگر (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/bash
ali 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 .... کار می‌کند، ولی یک مشکل پنهان دارد:

Terminal window
cd ~/bashlab
count=0
cat servers.txt | while read -r line; do ((count++)); done
echo "pipe version: count=$count"
count=0
while read -r line; do ((count++)); done < servers.txt
echo "redirect version: count=$count"
count=0
while read -r line; do ((count++)); done < <(grep nginx servers.txt)
echo "process substitution: count=$count (only the nginx lines)"
خروجی
pipe version: count=0
redirect version: count=3
process substitution: count=2 (only the nginx lines)

نسخه‌ی pipe شمارنده را صفر برگرداند! هر طرف | در یک پروسه‌ی جداگانه (subshell) اجرا می‌شود؛ حلقه count را در همان پروسه زیاد کرد و با تمام‌شدنش از بین رفت. دو راه‌حل: تغییر جهت از فایل (< servers.txt، نسخه‌ی دوم) یا process substitution با < <(دستور) که خروجی یک دستور را مثل یک فایل به حلقه می‌دهد، بدون اینکه حلقه از شل اصلی جدا شود (نسخه‌ی سوم). نوشتن < <(...) دو < جداست: اولی تغییر جهت و دومی بخشی از <(...).

مثال ۸: اسکریپت واقعی، خلاصه‌ی لاگ

Section titled “مثال ۸: اسکریپت واقعی، خلاصه‌ی لاگ”

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

Terminal window
cd ~/bashlab
cat > app.log <<'EOF'
2026-10-04 09:00:01 INFO Server started
2026-10-04 09:00:05 WARN Disk usage at 78%
2026-10-04 09:01:12 ERROR Database connection failed
2026-10-04 09:01:13 INFO Retrying connection
2026-10-04 09:01:15 ERROR Database connection failed
2026-10-04 09:02:00 INFO Connected
EOF
cat > 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" ;;
esac
done < "$file"
echo "INFO=$info WARN=$warn ERROR=$errors"
EOF
chmod +x log-summary.sh
./log-summary.sh app.log
خروجی
ERROR at 09:01:12: Database connection failed
ERROR at 09:01:15: Database connection failed
INFO=3 WARN=1 ERROR=2

read -r day time level message هر خط را به چهار فیلد می‌شکند و آخری هر چه مانده را می‌گیرد (کل متن پیام). case روی سطح، شمارنده‌ی درست را زیاد می‌کند. (همین کار را با grep -c هم می‌شد؛ ولی وقتی پردازش پیچیده‌تر می‌شود، حلقه انعطاف‌پذیرتر است.)

پشت پرده: حلقه چطور اجرا می‌شود؟

Section titled “پشت پرده: حلقه چطور اجرا می‌شود؟”

۱) فهرست for اول کامل باز می‌شود، بعد حلقه شروع می‌شود. با set -x می‌توانی ببینی شل چه می‌سازد:

Terminal window
cd ~/bashlab/photos
set -x
for f in *.jpg; do
echo "$f"
done
set +x
خروجی
+ for f in *.jpg
+ echo a.jpg
a.jpg
+ for f in *.jpg
+ echo b.jpg
b.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'): هر دور یک اسم. نتیجه‌ی مهم: فهرست فقط یک بار و قبل از شروع ساخته می‌شود، پس فایل‌هایی که حین حلقه بسازی در همان حلقه نمی‌آیند:

Terminal window
cd ~/bashlab/photos
for f in *.jpg; do
echo "processing: $f"
touch "new-$f"
done
echo "--- فایل‌های ساخته‌شده حین حلقه (در فهرست حلقه نبودند):"
ls new-*
rm -f new-*
خروجی
processing: a.jpg
processing: b.jpg
processing: my photo.jpg
--- فایل‌های ساخته‌شده حین حلقه (در فهرست حلقه نبودند):
new-a.jpg
new-b.jpg
new-my photo.jpg

۲) done < فایل یک بار فایل را باز می‌کند. تمام readهای داخل حلقه از همان توصیف‌گر باز فایل می‌خوانند و هر کدام خط بعدی را برمی‌دارند. برای همین هر دستور دیگری که داخل حلقه از ورودی استاندارد بخواند (مثل head بدون فایل، cat، ssh، ffmpeg)، خط‌های حلقه را می‌بلعد:

Terminal window
cd ~/bashlab
printf 'one\ntwo\nthree\nfour\n' > items.txt
echo "--- بدون محافظ: head هم از همان فایل می‌خواند"
while read -r item; do
echo "item: $item"
head -n 1 > /dev/null
done < items.txt
echo "--- با < /dev/null برای دستور داخل حلقه:"
while read -r item; do
echo "item: $item"
head -n 1 > /dev/null < /dev/null
done < items.txt
خروجی
--- بدون محافظ: head هم از همان فایل می‌خواند
item: one
item: three
--- با < /dev/null برای دستور داخل حلقه:
item: one
item: two
item: three
item: four

در حالت اول two و four گم شدند، چون head آن‌ها را از ورودی حلقه خواند و دور ریخت. این باگ در عمل با ssh host command داخل حلقه‌ی while read می‌آید؛ ssh باقی فایل را می‌بلعد و حلقه بعد از اولین میزبان تمام می‌شود. راه‌حل: < /dev/null برای دستورهای داخل حلقه (یا ssh -n).

۳) کد خروج حلقه کد خروج آخرین دستور اجراشده‌ی بدنه است، یا ۰ اگر بدنه هرگز اجرا نشد:

Terminal window
for i in 1 2 3; do false; done
echo "loop whose last command failed: $?"
while false; do :; done
echo "loop that never ran: $?"
خروجی
loop whose last command failed: 1
loop that never ran: 0

شکل‌های 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 را هم بخوان

۱) for f in $(ls) و فاصله در نام

Section titled “۱) for f in $(ls) و فاصله در نام”

مثال ۲. راه‌حل: for f in *.jpg و "$f".

۲) حلقه روی الگوی بی‌تطابق

Section titled “۲) حلقه روی الگوی بی‌تطابق”
Terminal window
mkdir -p ~/bashlab/empty && cd ~/bashlab/empty
for f in *.jpg; do mv "$f" "backup-$f"; done
خروجی
mv: cannot stat '*.jpg': No such file or directory

وقتی هیچ jpg نیست، f برابر خود متن *.jpg می‌شود و mv خطا می‌دهد. راه‌حل: shopt -s nullglob، یا [[ -e $f ]] || continue اول بدنه.

۳) خط آخرِ بدون newline نادیده گرفته می‌شود

Section titled “۳) خط آخرِ بدون newline نادیده گرفته می‌شود”
Terminal window
cd ~/bashlab
printf 'first\nsecond' > no-newline.txt
echo "--- بدون محافظ:"
while read -r line; do echo "[$line]"; done < no-newline.txt
echo "--- با || [[ -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=”
Terminal window
cd ~/bashlab
printf ' indented line\n trailing \n' > ws.txt
echo "--- بدون IFS=:"
while read -r line; do echo "[$line]"; done < ws.txt
echo "--- با IFS=:"
while IFS= read -r line; do echo "[$line]"; done < ws.txt
خروجی
--- بدون IFS=:
[indented line]
[trailing]
--- با IFS=:
[ indented line]
[ trailing ]

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

Terminal window
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! بنویسد.

دیدن جواب
Terminal window
for i in {5..1}; do
echo "$i"
sleep 0.2
done
echo "Liftoff!"
خروجی
5
4
3
2
1
Liftoff!
✎ تمرینمتوسط

تمرین اصلی درس: همه‌ی فایل‌های jpg یک پوشه را با پیشوند تاریخ تغییر نام بده (IMG_1.jpg ← 2026-10-04-IMG_1.jpg). اسکریپت date-prefix.sh پوشه را از آرگومان بگیرد؛ فایل‌هایی که قبلاً پیشوند تاریخ دارند را دست نزند (تا با اجرای دوباره پیشوند تکراری نشود)، روی هیچ فایل موجودی بازنویسی نکند، و اسم‌های دارای فاصله را درست مدیریت کند. قبل و بعد را نشان بده.

دیدن جواب
mkdir -p ~/bashlab/gallery && cd ~/bashlab
touch gallery/IMG_0001.jpg gallery/IMG_0002.jpg "gallery/holiday photo.jpg" gallery/readme.txt
cat > 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 1
fi
cd "$1" || exit 1
today=$(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"
done
EOF
chmod +x date-prefix.sh
echo "--- قبل:"; ls gallery
echo "--- اجرا:"; ./date-prefix.sh gallery
echo "--- بعد:"; ls gallery
echo "--- اجرای دوباره (نباید پیشوند تکراری بیاید):"; ./date-prefix.sh gallery
echo "--- پوشه‌ی بدون jpg:"; mkdir -p nojpg && ./date-prefix.sh nojpg; echo "کد خروج: $?"
خروجی
--- قبل:
IMG_0001.jpg
IMG_0002.jpg
holiday photo.jpg
readme.txt
--- اجرا:
renamed: IMG_0001.jpg -> 2026-10-04-IMG_0001.jpg
renamed: IMG_0002.jpg -> 2026-10-04-IMG_0002.jpg
renamed: holiday photo.jpg -> 2026-10-04-holiday photo.jpg
--- بعد:
2026-10-04-IMG_0001.jpg
2026-10-04-IMG_0002.jpg
2026-10-04-holiday photo.jpg
readme.txt
--- اجرای دوباره (نباید پیشوند تکراری بیاید):
skip (already has a date): 2026-10-04-IMG_0001.jpg
skip (already has a date): 2026-10-04-IMG_0002.jpg
skip (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 ~/bashlab
printf '10\n20\n\nabc\n30\n40\n' > numbers.txt
cat > 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 1
fi
count=0
sum=0
lineno=0
while 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 2
fi
echo "count=$count sum=$sum average=$((sum / count))"
EOF
chmod +x sum-file.sh
./sum-file.sh numbers.txt
خروجی
warning: line 4 is not a number: 'abc'
count=4 sum=100 average=25

(در درس شرط‌ها دیدی [[ $line =~ ^[0-9]+$ ]] عدد بودن را می‌سنجد؛ در درس قبل: abc بی‌صدا صفر می‌شد، این بررسی همان را جلو می‌گیرد.)

✎ تمرینسخت

اسکریپت wait-for.sh <فایل> [ثانیه] که تا ظاهر شدن یک فایل صبر کند (مثلاً تا سرویسی فایل آماده‌باش را بسازد)، هر نیم ثانیه نگاه کند و بعد از زمان‌مهلت (پیش‌فرض ۱۰ ثانیه) منصرف شود. اگر فایل ظاهر شد با کد ۰ و اگر مهلت تمام شد با کد ۱ تمام شود. امتحانش کن: یک‌بار فایل بعد از ۲ ثانیه ساخته شود و مهلت ۵ ثانیه باشد، و یک‌بار مهلت ۱ ثانیه.

دیدن جواب
cd ~/bashlab
cat > 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 2
fi
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++))
done
echo "$file appeared"
EOF
chmod +x wait-for.sh
rm -f flag-a flag-b
echo "--- ظاهر می‌شود (۲ ثانیه) با مهلت ۵ ثانیه:"
(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، چون می‌خواهیم کد خروج خطا بدهیم).

⚡ بررسی سریع

until [[ -e ready ]]; do sleep 1; done تا کی تکرار می‌شود؟

؟ آزمونک
  1. for f in *.jpg وقتی هیچ فایل jpg ای در پوشه نیست چه می‌شود؟

  2. چرا for i in {1..$n} کار نمی‌کند؟

  3. تفاوت break و continue؟

  4. در count=0; cat f | while read -r l; do ((count++)); done; echo $count چه چاپ می‌شود و چرا؟

  5. IFS= در while IFS= read -r line چه کاری می‌کند؟

  6. چرا داخل while read ... done < file نباید ssh یا head بدون < /dev/null اجرا کرد؟

  • 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کشتن دستور بعد از ۵ ثانیه