خواندن ورودی صفحه کلید در اسکریپتنویسی
اسکریپتی که تا اینجا نوشتیم فاقد یک ویژگی رایج در برنامهها میباشد. این ویژگی تعامل (Interactivity) نام دارد. یعنی توانایی اینکه برنامه با کاربر تعامل داشته باشد. در حالیکه بسیاری از برنامهها نیاز ندارند تا تعاملی باشند ولی در مقابل برنامههای زیادی از این قابلیت سود میبرند و از این طریق به صورت مستقیم داده را از کاربر قبول میکنند.
برای مثال این اسکریپت را از فصل قبل به یاد بیاورید:
#!/bin/bash
# test-integer2: evaluate the value of an integer.
INT=-5
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
if [ $INT -eq 0 ]; then
echo "INT is zero."
else
if [ $INT -lt 0 ]; then
echo "INT is negative."
else
echo "INT is positive."
fi
if [ $((INT % 2)) -eq 0 ]; then
echo "INT is even."
else
echo "INT is odd."
fi
fi
else
echo "INT is not an integer." >&2
exit 1
fi
هر زمان که میخواهیم مقدار INT را تغییر دهیم بایستی اسکریپت را ویرایش کنیم. در حالیکه اگر اسکریپت هر بار برای مقدار مورد نظر از کاربر سوال کند بسیار بهتر خواهد بود. در این درس ما یاد میگیریم که چگونه قابلیت تعاملی را به برنامههای خود اضافه کنیم.
فرمان read – خواندن مقادیر از ورودی استاندارد
فرمان read یک فرمان درون ساخت شل (Shell Balls-in Command) که به منظور خواندن یک خط از ورودی استاندارد به کار میرود. این فرمان را میتوان بهمنظور خواندن ورودی صفحه کلید یا زمانی که هدایت و تغییر مسیر یک خط از داده را از یک قابل به کار گرفته میگیرد. استفاده کرد.
read [-options] [variable...]
که در این فرمان options یکی از گزینههای موجود لیست شده در جدول زیر هست و variable نام یک یا چند متغیر استفاده شده به منظور نگهداری مقدار ورودی میباشد. اگر هیچ نام متغیری ارایه نشده باشد. متغیر شل REPLY ارایه میشود.
| گزینه | توضیحات |
|---|---|
| -a array | اختصاص ورودی به آرایه که با ایندکس صفر آغاز میشود. |
| -d delimiter | اولین کاراکتر در رشته delimiter (جداکننده) استفاده شده تا پایان ورودی را نشان دهد. |
| -e | استفاده از readline برای بکارگیری ورودی. این گزینه موجب میشود که ویرایش ورودی به همان شیوه خط فرمان امکانپذیر شود. |
| -n num | خواندن کاراکترهای num از ورودی به جای کل خط |
| -p prompt | نمایش یک پیامواره برای ورودی با استفاده از رشته prompt |
| -r | Raw mode. کاراکترهای بکاسلش را بهعنوان کاراکتر Escape در نظر نگیر. |
| -s | Silent mode. کاراکترها را در صفحهنمایش چاپ نکن/ این کار زمان وارد کردن پسوردها مفید خواهد بود. |
| -t seconds | Timeout. پایان دادن ورودی پس از چند seconds. فرمان read یک وضعیت خروج غیر از صفر را بازمیگرداند. |
| -u fd | استفاده از ورودی توصیفکننده فایل fd به جای ورودی استاندارد. |
اساسا فرمان read فیلدها را از ورودی استاندارد به متغیرهای تعیین شده اختصاص می دهد. اگر که اسکریپت ارزیابی عدد صحیح خود را به منظور استفاده از فرمان read تغییر دهیم. به صورت زیر میشود:
#!/bin/bash
# read-integer: evaluate the value of an integer.
echo -n "Please enter an integer -> "
read int
if [[ "$int" =~ ^-?[0-9]+$ ]]; then
if [ $int -eq 0 ]; then
echo "$int is zero."
else
if [ $int -lt 0 ]; then
echo "$int is negative."
else
echo "$int is positive."
fi
if [ $((int % 2)) -eq 0 ]; then
echo "$int is even."
else
echo "$int is odd."
fi
fi
else
echo "Input value is not an integer." >&2
exit 1
fi
ما از فرمان echo به همراه گزینه -n که از ایجاد خط جدید در ادامه جلوگیری میکند برای نمایش یک پیام واره (Prompt) و سپس از read استفاده کرده تا یک مقدار برای متغیر 100 وارد کنیم. فرمان را به صورت زیر اجرا میکنیم:
[me@linuxbox ~]$ read-integer Please enter an integer -> 5 5 is positive. 5 is odd.
Read میتواند ورودی را به چندین متغیر اختصاص دهد درست مثل زیر:
#!/bin/bash # read-multiple: read multiple values from keyboard echo -n "Enter one or more values > " read var1 var2 var3 var4 var5 echo "var1 = '$var1'" echo "var2 = '$var2'" echo "var3 = '$var3'" echo "var4 = '$var4'" echo "var5 = '$var5'"
در این اسکریپت میتوانیم چهار مقدار را نمایش و اختصاص دهیم. توجه کنید چگونه read تعداد متفاوتی از مقادیر را دریافت میکند:
[me@linuxbox ~]$ read-multiple Enter one or more values > a b c d e var1 = 'a' var2 = 'b' var3 = 'c' var4 = 'd' var5 = 'e' [me@linuxbox ~]$ read-multiple Enter one or more values > a var1 = 'a' var2 = '' var3 = '' var4 = '' var5 = '' [me@linuxbox ~]$ read-multiple Enter one or more values > a b c d e f g var1 = 'a' var2 = 'b' var3 = 'c' var4 = 'd' var5 = 'e f g'
اگر فرمان read تعداد کمتر از میزانی که توقع داریم را دریافت کند. دیگر متغیرها خالی هستند. در حالیکه مقدار زیادی از نتایج ورودی در متغیر نهایی حاوی دیگر ورودی های اضافی هستند. اگر هیچ متغیری پس از فرمان read لیست نشده باشد. یک متغير شل (متغیر REPLY) همه ورودیها را دریافت خواهد کرد:
#!/bin/bash # read-single: read multiple values into default variable echo -n "Enter one or more values > " read echo "REPLY = '$REPLY'"
اجرای نتایج اسکریپت در این:
[me@linuxbox ~]$ read-single Enter one or more values > a b c d REPLY = 'a b c d'
گزینهها
فرمان read از گزینه هایی که در جدول قبلی در درس قبل پشتیبانی میکنند. با استفاده از گزینههای مختلف میتوانیم کارهای جالبی را با read انجام دهیم. برای مثال با گزینه -p میتوانیم یک رشته پیامواره (Prompt) را فراهم کنیم:
#!/bin/bash # read-single: read multiple values into default variable read -p "Enter one or more values > " echo "REPLY = '$REPLY'"
با استفاده از گزینه -t و گزینه -s میتوانیم اسکریپتی که ورودی “secrets” را میخواند بنویسیم و در صورتی که ورودی در زمان تعیین شده کامل نشود یک زمان اضافی مجدد تعیین کند:
#!/bin/bash
# read-secret: input a secret passphrase
if read -t 10 -sp "Enter secret passphrase > " secret_pass; then
echo -e "\nSecret passphrase = '$secret_pass'"
else
echo -e "\nInput timed out" >&2
exit 1
fi
اسکریپت کاربر را متوقف کرده تا عبارت secret را وارد کند و برای ورودی ۱۰ ثانیه صبر میکند. اگر که ورودی در این زمان تعیین شده کامل نشود. اسکریپت خارج میشود و یک پیام خطا نمایش میدهد. از آنجایی که گزینه شامل شده است. کاراکترهای عبارت همانطور که وارد میشوند در صفحهنمایش نشان داده نمیشوند.
جدا کردن فیلدهای ورودی با IFS
به طور طبیعی، Shell جداسازی کلمات را ورودی که به read داده میشود انجام میدهد. همانطور که دیدیم. به این معنی است که چندین کلمه با یک یا چند فاصله تبدیل به آیتمهایی جدا در خط ورودی میشوند و توسط read به متغیرهای جداگانهای تبدیل میشوند. این رفتار بوسیله یک متغیر شل (Shell Variable) که IFS نامیده میشود انجام میشود. مقدار پیشفرض IFS حاوی یک فاصله یک tab و یک کاراکتر خط جدید است که هر کدام آیتم ها را از یکدیگر جدا میکنند.
ما میتوانیم مقدار متغیر IFS را تغییر دهیم تا جداسازی فیلدهای ورودی به دستور read را کنترل کنیم. برای مال مسیر etc/passwd/ حاوی خطوطی از داده است که از کاراکتر دو نقطه به منظور جداسازی فیلدها استفاده میکند. با تغییر مقدار متغیر IFS به یک نقطه میتوانیم از دستور read به منظور وارد کردن محتویات etc/passwd و جداسازی موفقیتآمیز فیلدها به متغیرهای متفاوت استفاده کنیم. در اینجا اسکریپتی داریم که این کار را انجام میدهد:
#!/bin/bash
# read-ifs: read fields from a file
FILE=/etc/passwd
read -p "Enter a username > " user_name
file_info=$(grep "^$user_name:" $FILE)
if [ -n "$file_info" ]; then
IFS=":" read user pw uid gid name home shell <<< "$file_info"
echo "User = '$user'"
echo "UID = '$uid'"
echo "GID = '$gid'"
echo "Full Name = '$name'"
echo "Home Dir. = '$home'"
echo "Shell = '$shell'"
else
echo "No such user '$user_name'" >&2
exit 1
fi
این اسکربیت کاربر را به منظور وارد کردن نام کاربر حساب خود در سیستم متوقف کرده و سپس فیلدهای متفاوتی که در رکورد کاربر پیدا شده در فایل etc/passwd/ را نمایش میدهد. اسکریپت دارای دو خط جالب توجه است.
file_info=$(grep “^$user_name:” $FILE)
اولی که نتایج دستور grep را به متغیر file_info اختصاص میدهد. عبارت منظمی که توسط grep استفاده شده اطمینان حاصل میکنند که نام کاربری فقط با یک خط در فایل etc/passwd/ مطابقت دارد.
دومین خط جالب توجه “$file_info” ]; then
IFS=”:” read user pw uid gid name home shell <<< “$file_info” شامل از سه بخش است: یک اختصاص متغیر، یک فرمان read به همراه لیستی از اسامی متغیرها به عنوان آرگومان و یک عملگر هدایت جدید عجیب ابتدا نگاهی به اختصاص متغیر میاندازیم.
شل (Shell) اجازه اختصاص یک یا چند متغیر بلافاصله قبل از فرمان را میدهد. این اختصاصها محیط را برای فرمان پس از آن تغییر میدهد. تاثیر اختصاص موقتی است و محیط را فقط برای دوره زمانی فرمان تغییر میدهد. در مثال ما مقدار IFS به یک کاراکتر نقطه تغییر کرده به روش دیگر میتوانیم کد زیر را داشته باشیم:
OLD_IFS="$IFS" IFS=":" read user pw uid gid name home shell <<< "$file_info" IFS="$OLD_IFS"
که در آن مقدار متغیر IFS را تعیین میکنیم فرمان read را اجرا میکنیم و سپس مقدار IFS را مجدد به مقدار اولیه بازیابی میکنیم. واضح است که قرار دادن اختصاص متغیر در جلو فرمان شیوهای کوتاهتر برای انجام این کار است. عملگر >>> نشان دهنده یک here string است. here string درست شبیه یک here document است فقط کوتاهتر است و فقط شامل یک رشته میباشد. در مثال ما خط داده از فایل etc/passwd/ به ورودی استاندارد فرمان read تغذیه میشود . شاید تعجب کنید که چرا این شیوه عجیب غریب را برای وارد کردن داده استفاده کردهایم؟ چرا به جای آن از شیوه زیر استفاده نکردیم؟
echo "$file_info" | IFS=":" read user pw uid gid name home shell
دلیل آن این است که دستور read را نمیتوان پایپ کرد.
اعتبارسنجی ورودی
با اضافه کردن قابلیت جدید حالت تعاملی در برنامهها و دریافت ورودی از کاربر از طریق صفحه کلید با یک چالش جدید روبرو میشویم و آن اعتبارسنجی دادههای ورودی است. در بسیاری از مواقع تفاوت بین یک کدنویسی خوب با یک کد بد به توانایی برنامهنویس در مواجهه با موقعیتهای غیرمنتظره برمیگردد.
اغلب اوقات این موقعیتهای ناخوشایند و غیرمنتظره زمان وارد کردن دادههای نابهنجار خود را نشان میدهند. در درس قبلی زمانی که مقادیر عدد صحیح را بررسی میکردیم و حتی مقادیر خالی و کاراکترهای غیرعددی را بررسی میکردیم. در حقیقت داشیتم برنامه را ارزیابی میکردیم.
این نوع کارها در زمان نوشتن برنامه بسیار ضروری هستند چرا که برنامههایی که دادههای ورودی را دریافت میکنند نیاز به یک محافظ و گارد ورودی دارند تا دادههای نامعتبر برنامه را از بین نبرد. این کار بویژه برای برنامههایی که بین چندین کاربر به اشتراک گذاشته میشود ضروری است. حتی زمانی که خود برنامه میخواهد وظایفی مثل حذف داده را انجام دهد. قرار دادن این محافظها قبل از اجرای دستور حذف و انجام اعتبارسنجی کاری حیاتی به شمار میرود. در اینجا برنامهای نمونه را آوردهایم که انواع مختلف اعتبارسنجی بر روی دادههای ورودی را انجام میدهد:
#!/bin/bash
# read-validate: validate input
invalid_input () {
echo "Invalid input '$REPLY'" >&2
exit 1
}
read -p "Enter a single item > "
# input is empty (invalid)
[[ -z $REPLY ]] && invalid_input
# input is multiple items (invalid)
(( $(echo $REPLY | wc -w) > 1 )) && invalid_input
# is input a valid filename?
if [[ $REPLY =~ ^[-[:alnum:]\._]+$ ]]; then
echo "'$REPLY' is a valid filename."
if [[ -e $REPLY ]]; then
echo "And file '$REPLY' exists."
else
echo "However, file '$REPLY' does not exist."
fi
# is input a floating point number?
if [[ $REPLY =~ ^-?[[:digit:]]*\.[[:digit:]]+$ ]]; then
echo "'$REPLY' is a floating point number."
else
echo "'$REPLY' is not a floating point number."
fi
# is input an integer?
if [[ $REPLY =~ ^-?[[:digit:]]+$ ]]; then
echo "'$REPLY' is an integer."
else
echo "'$REPLY' is not an integer."
fi
else
echo "The string '$REPLY' is not a valid filename."
fi
این اسکریپت از کاربر میخواهد که یک آیتم را وارد کنند. آیتم متعاقبا آنالیز شده تا محتویات آن شناسایی شود. همانطور که می بینیم این اسکربیت مفاهیم زیادی را شامل میشود که تا اینجا کار آنها را توضیح دادهایم.
برنامههای دارای منو (Menu Driven Programs)
یک نوع از ویژگیهای رایج تعاملی menu driven نامیده میشود. یعنی برنامههای دارای منو در برنامههای دارای منو، کاربر با لیستی از انتخابها که همان منوهاست مواجه میشود و از وی درخواست میشود که یک منو را انتخاب کند. برای مثال ما میتوانیم برنامهای را فرض کنیم که موارد زیر را به ما ارایه میکند:
Please Select: 1. Display System Information 2. Display Disk Space 3. Display Home Space Utilization 0. Quit Enter selection [0-3] >
با توجه به آنچه در نوشتن برنامه sys_info_page یاد گرفتیم. اکنون میتوانیم یک برنامه منو دار برای انجام وظایف بالا ایجاد کنیم. با استفاده از کد زیر:
#!/bin/bash
# read-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] > "
if [[ $REPLY =~ ^[0-3]$ ]]; then
if [[ $REPLY == 0 ]]; then
echo "Program terminated."
exit
fi
if [[ $REPLY == 1 ]]; then
echo "Hostname: $HOSTNAME"
uptime
exit
fi
if [[ $REPLY == 2 ]]; then
df -h
exit
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
exit
fi
else
echo "Invalid entry." >&2
exit 1
fi
این اسکریپت به صورت منطقی به دو بخش تقسیم شده است. بخش اول منو را نمایش میدهد و پاسخ مورد نظر را از کاربر دریافت میکنند و بخش دوم پاسخ را شناسایی میکند و گزینه انتخاب شده را به کار میگیرد. به استفاده از دستور exit در این اسکریپت توجه داشته باشید. در اینجا به کار رفته تا از اجرای غیرضروری کند جلوگیری به عمل آورد. حضور چندین است در برنامه در کل ایده خوبی نیست و باعث می شود مفهوم برنامه پیچیدهتر شود ولی در این اسکربیت وظیفه خود را به خوبی انجام میدهد.
درباره فرشید نوتاش حقیقت
همیشه نیازمند یک منبع آموزشی فارسی در حوزه نرمافزارهای آزاد/ متنباز و سیستمعامل گنو/لینوکس بودم. از این رو این رسالت رو برای خودم تعریف کردم تا رسانه «محتوای باز» رو بوجود بیارم.
نوشتههای بیشتر از فرشید نوتاش حقیقتاین سایت از اکیسمت برای کاهش جفنگ استفاده میکند. درباره چگونگی پردازش دادههای دیدگاه خود بیشتر بدانید.
دیدگاهتان را بنویسید