۳۱ – کنترل جریان: انشعاب با case

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

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


case

دستور چندگزینه‌ای bash با نام case شناخته می‌شود. سینتکس آن این است:

case word in
    pattern ) commands ;; 
    pattern ) commands ;;
    ...
esac

اگر به برنامهٔ read-menu از فصل ۲۸ نگاه کنیم، منطق اجرایی آن بر اساس if بود:

if [[ $REPLY =~ ^[0-3]$ ]]; then
    if [[ $REPLY == 0 ]]; then
        ...
    fi
    if [[ $REPLY == 1 ]]; then
        ...
    fi
    if [[ $REPLY == 2 ]]; then
        ...
    fi
    if [[ $REPLY == 3 ]]; then
        ...
    fi
else
    invalid entry
fi

با استفاده از case می‌توانیم همین منطق را بسیار ساده‌تر کنیم:


نسخهٔ ساده‌شده با case

#!/bin/bash
# case-menu: a menu driven system information program

clear
echo "
Please Select:
1. Display System Information
2. Display Disk Space
3. Display Home Space Utilization
0. Quit
"

read -p "Enter selection [0-3] > "

case $REPLY in
0)
    echo "Program terminated."
    exit
    ;;

1)
    echo "Hostname: $HOSTNAME"
    uptime
    ;;

2)
    df -h
    ;;

3)
    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
    ;;

*)
    echo "Invalid entry" >&2
    exit 1
    ;;
esac

دستور case مقدار word (در اینجا $REPLY) را بررسی می‌کند و تلاش می‌کند آن را با یکی از الگوها (pattern) هماهنگ کند. وقتی یک مورد مطابق پیدا شد، دستورات همان بخش اجرا می‌شوند و هیچ الگوی دیگری بررسی نمی‌شود.


Patterns — الگوها در case

الگوهای مورد استفاده در case همان الگوهایی هستند که در pathname expansion به‌کار می‌روند.
الگو با ) پایان می‌یابد.

چند نمونه از الگوهای معتبر:

الگو توضیح
a) اگر word دقیقاً “a” باشد.
[[:alpha:]]) اگر یک حرف الفبایی تک‌کاراکتری باشد.
???) اگر word دقیقاً سه کاراکتر باشد.
*.txt) اگر به .txt. ختم شود.
*) هر مقداری را می‌پذیرد؛ معمولاً آخرین الگو برای گرفتن حالت‌های نامعتبر.

مثال:

#!/bin/bash
read -p "enter word > "

case $REPLY in
    [[:alpha:]]) echo "is a single alphabetic character." ;;
    [ABC][0-9]) echo "is A, B, or C followed by a digit." ;;
    ???) echo "is three characters long." ;;
    *.txt) echo "is a word ending in '.txt'" ;;
    *) echo "is something else." ;;
esac

ترکیب چند الگو با «|»

می‌توان چند الگو را با | ترکیب کرد؛ یعنی «این یا آن».
این کار برای دریافت حرف‌های کوچک و بزرگ بسیار مفید است.

مثال زیر نسخهٔ دیگر منوی case با حروف به‌جای اعداد است:

#!/bin/bash

clear
echo "
Please Select:
A. Display System Information
B. Display Disk Space
C. Display Home Space Utilization
Q. Quit
"

read -p "Enter selection [A, B, C or Q] > "

case $REPLY in
    q|Q)
        echo "Program terminated."
        exit
        ;;

    a|A)
        echo "Hostname: $HOSTNAME"
        uptime
        ;;

    b|B)
        df -h
        ;;

    c|C)
        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
        ;;

    *)
        echo "Invalid entry" >&2
        exit 1
        ;;
esac

در این نسخه، الگوها ورودی حروف بزرگ و کوچک را هر دو می‌پذیرند.


انجام چند عمل در یک case

در نسخه‌های قبل از Bash 4.0، دستور case فقط اجازه می‌داد یک عمل برای هر تطبیق انجام شود. یعنی وقتی یک الگو match می‌شد، case بلافاصله خاتمه پیدا می‌کرد و نمی‌شد چندین الگو را بررسی کرد.

در مثال زیر اسکریپتی داریم که یک کاراکتر را تست می‌کند:

#!/bin/bash
# case4-1: test a character

read -n 1 -p "Type a character > "
echo

case $REPLY in
    [[:upper:]])    echo "'$REPLY' is upper case." ;;
    [[:lower:]])    echo "'$REPLY' is lower case." ;;
    [[:alpha:]])    echo "'$REPLY' is alphabetic." ;;
    [[:digit:]])    echo "'$REPLY' is a digit." ;;
    [[:graph:]])    echo "'$REPLY' is a visible character." ;;
    [[:punct:]])    echo "'$REPLY' is a punctuation symbol." ;;
    [[:space:]])    echo "'$REPLY' is a whitespace character." ;;
    [[:xdigit:]])   echo "'$REPLY' is a hexadecimal digit." ;;
esac

خروجی:

Type a character > a
'a' is lower case.

اسکریپت کار می‌کند، اما مشکل اینجاست که برخی کاراکترها متعلق به چندین کلاس POSIX هستند.
مثلاً:

در نسخه‌های قدیمی bash راهی نبود که case بتواند بیش از یک match انجام دهد؛ یعنی بعد از اولین match متوقف می‌شد.


راه‌حل در Bashهای جدید: استفاده از ;;&

نسخه‌های جدیدتر bash امکان استفاده از ;;& را اضافه کرده‌اند تا case به‌جای توقف، تست بعدی را نیز ادامه دهد.

نسخهٔ جدید اسکریپت:

#!/bin/bash
# case4-2: test a character

read -n 1 -p "Type a character > "
echo

case $REPLY in
    [[:upper:]])    echo "'$REPLY' is upper case." ;;&
    [[:lower:]])    echo "'$REPLY' is lower case." ;;&
    [[:alpha:]])    echo "'$REPLY' is alphabetic." ;;&
    [[:digit:]])    echo "'$REPLY' is a digit." ;;&
    [[:graph:]])    echo "'$REPLY' is a visible character." ;;&
    [[:punct:]])    echo "'$REPLY' is a punctuation symbol." ;;&
    [[:space:]])    echo "'$REPLY' is a whitespace character." ;;&
    [[:xdigit:]])   echo "'$REPLY' is a hexadecimal digit." ;;&
esac

خروجی:

Type a character > a
'a' is lower case.
'a' is alphabetic.
'a' is a visible character.
'a' is a hexadecimal digit.

اضافه شدن ;;& باعث می‌شود case تست بعدی را هم اجرا کند، نه اینکه با اولین match متوقف شود.


جمع‌بندی

دستور case ابزاری بسیار مفید در کنترل جریان برنامه است.
در فصل بعد خواهیم دید که این دستور برای نوع خاصی از مسائل بهترین گزینه است.


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

● فصل Conditional Constructs از Bash Reference Manual:
http://tiswww.case.edu/php/chet/bash/bashref.html#SEC21

● مثال‌های بیشتر از case در Advanced Bash-Scripting Guide:
http://tldp.org/LDP/abs/html/testbranch.html