ترجمه فصل ۲۹ – کنترل جریان: حلقه‌زدن با while / until

در فصل قبل، یک برنامه منویی ساختیم که انواع مختلفی از اطلاعات سیستم را تولید می‌کرد. برنامه کار می‌کرد، اما یک مشکل جدی در استفاده‌پذیری داشت. فقط یک بار یک گزینه را اجرا می‌کرد و بعد تمام می‌شد. بدتر اینکه اگر یک انتخاب نامعتبر انجام می‌شد، برنامه با خطا تمام می‌شد، بدون اینکه به کاربر فرصت دوباره‌ امتحان بدهد. بهتر بود می‌توانستیم برنامه را طوری بسازیم که منو را پشت‌سر‌هم تکرار کند تا زمانی که کاربر گزینه خروج را انتخاب کند.

در این فصل، به مفهوم برنامه‌نویسی‌ای به نام حلقه‌زدن (looping) نگاه می‌کنیم که برای تکرار بخش‌هایی از برنامه استفاده می‌شود. شل سه دستور ترکیبی برای حلقه‌زدن ارائه می‌دهد. در این فصل دو تای آن‌ها را بررسی می‌کنیم و سومی را در فصل بعد.


حلقه‌زدن

زندگی روزمره پر از فعالیت‌های تکراری است. هر روز رفتن سر کار، راه بردن سگ، یا خرد کردن هویج همگی شامل تکرار مجموعه‌ای از مراحل هستند. مثلا بیایید خرد کردن هویج را بررسی کنیم. اگر این کار را به صورت شبه‌کد بنویسیم، چیزی شبیه این می‌شود:

  1. تخته آشپزی را بردار
  2. چاقو را بردار
  3. هویج را روی تخته بگذار
  4. چاقو را بالا ببر
  5. هویج را جلو بده
  6. هویج را ببر
  7. اگر تمام هویج برش داده شده، تمام کن؛ وگرنه برو به مرحله ۴

مراحل ۴ تا ۷ یک حلقه تشکیل می‌دهند. این کارها تا زمانی تکرار می‌شوند که شرط «تمام هویج برش خورده» برقرار شود.


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 چند قابلیت دیگر هم دارد، اما آن‌ها بیشتر حالت‌های تکمیلی همین مفاهیم پایه هستند.


برای مطالعه بیشتر