ترجمه فصل ۲۹ – کنترل جریان: حلقهزدن با while / until
در فصل قبل، یک برنامه منویی ساختیم که انواع مختلفی از اطلاعات سیستم را تولید میکرد. برنامه کار میکرد، اما یک مشکل جدی در استفادهپذیری داشت. فقط یک بار یک گزینه را اجرا میکرد و بعد تمام میشد. بدتر اینکه اگر یک انتخاب نامعتبر انجام میشد، برنامه با خطا تمام میشد، بدون اینکه به کاربر فرصت دوباره امتحان بدهد. بهتر بود میتوانستیم برنامه را طوری بسازیم که منو را پشتسرهم تکرار کند تا زمانی که کاربر گزینه خروج را انتخاب کند.
در این فصل، به مفهوم برنامهنویسیای به نام حلقهزدن (looping) نگاه میکنیم که برای تکرار بخشهایی از برنامه استفاده میشود. شل سه دستور ترکیبی برای حلقهزدن ارائه میدهد. در این فصل دو تای آنها را بررسی میکنیم و سومی را در فصل بعد.
حلقهزدن
زندگی روزمره پر از فعالیتهای تکراری است. هر روز رفتن سر کار، راه بردن سگ، یا خرد کردن هویج همگی شامل تکرار مجموعهای از مراحل هستند. مثلا بیایید خرد کردن هویج را بررسی کنیم. اگر این کار را به صورت شبهکد بنویسیم، چیزی شبیه این میشود:
- تخته آشپزی را بردار
- چاقو را بردار
- هویج را روی تخته بگذار
- چاقو را بالا ببر
- هویج را جلو بده
- هویج را ببر
- اگر تمام هویج برش داده شده، تمام کن؛ وگرنه برو به مرحله ۴
مراحل ۴ تا ۷ یک حلقه تشکیل میدهند. این کارها تا زمانی تکرار میشوند که شرط «تمام هویج برش خورده» برقرار شود.
while
bash میتواند همین ایده را پیاده کند. فرض کنید میخواهیم پنج عدد را پشت سر هم از یک تا پنج نمایش بدهیم. یک اسکریپت bash میتواند اینطور باشد:
#!/bin/bash
# while-count: display a series of numbers
count=1
while [[ $count -le 5 ]]; do
echo $count
count=$((count + 1))
done
echo "Finished."
وقتی اجرا شود، این خروجی را میدهد:
1
2
3
4
5
Finished.
ساختار دستور while این است:
while commands; do commands; done
مثل دستور if، while وضعیت خروج مجموعهای از دستورات را بررسی میکند. تا زمانی که وضعیت خروج صفر باشد، دستورات داخل حلقه را اجرا میکند.
در اسکریپت بالا، متغیر count ایجاد شده و مقدار اولیه ۱ میگیرد. دستور while وضعیت خروج دستور test را بررسی میکند. تا وقتی این دستور وضعیت صفر برگرداند، دستورات داخل حلقه اجرا میشود. در پایان هر چرخه، دستور test دوباره اجرا میشود. بعد از شش بار اجرای حلقه، مقدار count به ۶ میرسد، test دیگر صفر نمیدهد و حلقه متوقف میشود. برنامه ادامه میدهد و به دستور بعد از حلقه میرسد.
بهبود برنامه read-menu با یک حلقه while
#!/bin/bash
# while-menu: a menu driven system information program
DELAY=3 # تعداد ثانیه نمایش نتایج
while [[ $REPLY != 0 ]]; do
clear
cat <<- _EOF_
Please Select:
1. Display System Information
2. Display Disk Space
3. Display Home Space Utilization
0. Quit
_EOF_
read -p "Enter selection [0-3] > "
if [[ $REPLY =~ ^[0-3]$ ]]; then
if [[ $REPLY == 1 ]]; then
echo "Hostname: $HOSTNAME"
uptime
sleep $DELAY
fi
if [[ $REPLY == 2 ]]; then
df -h
sleep $DELAY
fi
if [[ $REPLY == 3 ]]; then
if [[ $(id -u) -eq 0 ]]; then
echo "Home Space Utilization (All Users)"
du -sh /home/*
else
echo "Home Space Utilization ($USER)"
du -sh $HOME
fi
sleep $DELAY
fi
else
echo "Invalid entry."
sleep $DELAY
fi
done
echo "Program terminated."
با قرار دادن منو داخل یک حلقهٔ while، برنامه میتواند بعد از هر انتخاب، دوباره منو را نمایش دهد. حلقه تا زمانی ادامه پیدا میکند که مقدار REPLY برابر “0” نباشد؛ بنابراین منو دوباره نشان داده میشود و کاربر فرصت دارد انتخاب دیگری انجام دهد. در پایان هر عمل، دستور sleep اجرا میشود تا برنامه چند ثانیه مکث کند و نتایج قابل دیدن باشد، قبل از اینکه صفحه پاک شود و منو دوباره نمایش داده شود. وقتی مقدار REPLY برابر “0” شد، که یعنی گزینهٔ “خروج” انتخاب شده است، حلقه پایان مییابد و اجرای برنامه با خطی که بعد از done قرار دارد ادامه پیدا میکند.
خروج از یک حلقه
bash دو دستور داخلی فراهم میکند که میتوانند جریان اجرای برنامه را داخل حلقه کنترل کنند.
دستور break بلافاصله یک حلقه را متوقف میکند و کنترل برنامه به اولین دستور بعد از حلقه منتقل میشود.
دستور continue باعث میشود بخش باقیماندهٔ حلقه نادیده گرفته شود و اجرای برنامه با تکرار بعدی حلقه ادامه پیدا کند.
در اینجا نسخهای از برنامه while-menu را میبینیم که هر دو دستور break و continue در آن بهکار رفتهاند:
#!/bin/bash
# while-menu2: a menu driven system information program
DELAY=3 # Number of seconds to display results
while true; do
clear
cat <<- _EOF_
Please Select:
1. Display System Information
2. Display Disk Space
3. Display Home Space Utilization
0. Quit
_EOF_
read -p "Enter selection [0-3] > "
if [[ $REPLY =~ ^[0-3]$ ]]; then
if [[ $REPLY == 1 ]]; then
echo "Hostname: $HOSTNAME"
uptime
sleep $DELAY
continue
fi
if [[ $REPLY == 2 ]]; then
df -h
sleep $DELAY
continue
fi
if [[ $REPLY == 3 ]]; then
if [[ $(id -u) -eq 0 ]]; then
echo "Home Space Utilization (All Users)"
du -sh /home/*
else
echo "Home Space Utilization ($USER)"
du -sh $HOME
fi
sleep $DELAY
continue
fi
if [[ $REPLY == 0 ]]; then
break
fi
else
echo "Invalid entry."
sleep $DELAY
fi
done
echo "Program terminated."
در این نسخه از اسکریپت، یک حلقهٔ بیپایان ایجاد کردهایم (حلقهای که خودش هرگز تمام نمیشود) با استفاده از دستور true که برای while یک وضعیت خروج ارائه میدهد. چون true همیشه وضعیت خروج صفر برمیگرداند، حلقه هیچوقت بهصورت خودکار تمام نخواهد شد. این یک تکنیک بسیار رایج در اسکریپتنویسی است.
از آنجا که حلقه خودش هیچوقت تمام نمیشود، برنامهنویس باید راهی فراهم کند تا در زمان مناسب از حلقه خارج شود. در این اسکریپت، دستور break برای خروج از حلقه استفاده میشود وقتی گزینهٔ “0” انتخاب شود.
دستور continue در پایان سایر گزینهها برای اجرای کارآمدتر قرار داده شده است. با استفاده از continue، اسکریپت از اجرای کدهایی که لازم نیستند عبور میکند. مثلاً اگر گزینهٔ “1” انتخاب و شناخته شد، دیگر دلیلی برای بررسی گزینههای دیگر وجود ندارد.
until
دستور until خیلی شبیه while است، با این تفاوت که بهجای پایان دادن به حلقه هنگام دریافت یک وضعیت خروج غیرصفر، برعکس عمل میکند. یک حلقهٔ until تا زمانی ادامه پیدا میکند که وضعیت خروج صفر دریافت کند.
در اسکریپت while-count، حلقه را تا زمانی ادامه دادیم که مقدار متغیر count کمتر یا مساوی ۵ بود. میتوانیم همان نتیجه را با استفاده از until بگیریم:
#!/bin/bash
# until-count: display a series of numbers
count=1
until [[ $count -gt 5 ]]; do
echo $count
count=$((count + 1))
done
echo "Finished."
با تغییر عبارت شرطی به $count -gt 5، دستور until حلقه را در زمان درست متوقف میکند. انتخاب بین while و until معمولاً به این بستگی دارد که کدامیک امکان نوشتن شرطی شفافتر را فراهم میکند.
خواندن فایلها با حلقهها
دستورات while و until میتوانند ورودی استاندارد را پردازش کنند. این ویژگی باعث میشود بتوان فایلها را نیز با این حلقهها پردازش کرد. در مثال زیر، محتوای فایل distros.txt که در فصلهای قبلی استفاده شد چاپ میکنیم:
#!/bin/bash
# while-read: read lines from a file
while read distro version release; do
printf "Distro: %s\tVersion: %s\tReleased: %s\n" \
$distro \
$version \
$release
done < distros.txt
برای اینکه یک فایل را به حلقه منتقل کنیم، عملگر بازپرستی (redirection) را بعد از دستور done قرار میدهیم. حلقه از دستور read برای خواندن فیلدهای ورودی استفاده میکند. دستور read بعد از هر خط با وضعیت خروج صفر پایان مییابد، تا زمانی که به انتهای فایل برسد؛ آنوقت با وضعیت غیرصفر خارج میشود و حلقه خاتمه پیدا میکند.
امکان انتقال ورودی استاندارد از طریق pipe نیز وجود دارد:
#!/bin/bash
# while-read2: read lines from a file
sort -k 1,1 -k 2n distros.txt | while read distro version release; do
printf "Distro: %s\tVersion: %s\tReleased: %s\n" \
$distro \
$version \
$release
done
در اینجا خروجی دستور sort را گرفته و به حلقه ارسال کردهایم. توجه به یک نکته مهم ضروری است: چون استفاده از pipe باعث میشود حلقه در یک زیرشل (subshell) اجرا شود، هر متغیری که داخل حلقه ساخته یا مقداردهی شود، بعد از پایان حلقه از دست میرود.
جمعبندی
با معرفی حلقهها و مباحث قبلی مثل انشعابها، زیربرنامهها و توالیها، حالا مهمترین انواع کنترل جریان برنامه را پوشش دادهایم. bash چند قابلیت دیگر هم دارد، اما آنها بیشتر حالتهای تکمیلی همین مفاهیم پایه هستند.
برای مطالعه بیشتر
-
Bash Guide for Beginners از Linux Documentation Project، مثالهای بیشتری از حلقههای while دارد:
http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_02.html -
مقالهٔ ویکیپدیا دربارهٔ حلقهها که بخشی از مقالهٔ کنترل جریان است:
http://en.wikipedia.org/wiki/Control_flow#Loops