برمجة Bash، الجزء 2: الحلقات والدوال
في الأسبوع الماضي كتبت أول سكربتات Bash: المتغيرات، والإدخال بالأمر read وبالوسيط $1، والعمليات الحسابية بالأقواس المزدوجة، والقرارات بجملة if. وكان كل سكربت منها يعمل من الأعلى إلى الأسفل مرة واحدة. ويضيف هذا الأسبوع الأداتين اللتين تجعلان السكربتات مفيدة حقا. الحلقات (loops) تكرر مجموعة من الأوامر، مرة لكل عدد، أو لكل كلمة، أو لكل ملف، أو لكل سطر. والدوال (functions) تعطي مجموعة من الأوامر اسما، فتكتبها مرة واحدة وتستدعيها كلما أردت. ونختم بسكربتات تستدعي سكربتات أخرى وتقرأ رموز خروجها (exit codes). أما نصف الساعة الأخير فلك: ستة تمارين، تحلها على الورق أولا ثم تشغلها على جهاز أوبونتو الخاص بك.
أنتج كل خرج (output) في هذه الصفحة بتشغيل السكربت فعلا بالإصدار 5.2 من bash على نظام أوبونتو 24.04، بصفة مستخدم عادي اسمه student، داخل مجلد اسمه lab12. وعندما يقرأ السكربت من لوحة المفاتيح، تظهر القيمة المكتوبة في سطر مستقل، تماما كما أظهرتها الطرفية.
الأهداف
بنهاية هذا الدرس ينبغي أن تكون قادرا على:
- تكرار الأوامر بحلقة
whileوحلقةuntil، واكتشاف خطأ الفرق بواحد (off by one error). - المرور على قائمة بحلقة
for: كلمات مكتوبة، ومتغير، ومدى (range) من الأعداد بخطوة (step)، وصيغة لغة Cfor ((...))، ومجموعة من الملفات، ووسائط سطر الأوامر، ومصفوفة (array). - الخروج من الحلقة مبكرا بالأمر
break، وتخطي تكرار (pass) واحد بالأمرcontinue، وقراءة ملف سطرا بسطر بالحلقةwhile read، وبناء قائمة خيارات (menu) بالأمرselect. - تعريف دالة واستدعاؤها، وتمرير الوسائط إليها، وإرسال حالة إرجاع (return status) بالأمر
return، وإرجاع قيمة باستبدال الأوامر (command substitution)$( ). - شرح نطاق المتغيرات (variable scope): ما الذي تغيره الكلمة
local، وما الذي تستطيع الدالة تغييره خارجها مع ذلك. - تشغيل سكربت من سكربت آخر، وقراءة رمز خروجه بالمتغير
$?، والتمييز بينsourceوbashوevalوexec.
قبل أن تبدأ: مجلد الدرس العملي
افتح طرفية (terminal) وأنشئ مجلدا واحدا للدرس العملي كله:
mkdir ~/lab12
cd ~/lab12لتجربة سكربت، احفظ نصه في ذلك المجلد باستخدام nano name.sh (أو بمحرر النصوص Text Editor)، ثم شغله بالأمر bash name.sh. وإذا فضلت ./name.sh فنفذ chmod +x name.sh مرة واحدة أولا، كما في الجزء 1.
تستخدم الأمثلة ملفين نصيين صغيرين. أنشئهما في المجلد نفسه:
hardware.txt
Monitor
Keyboard
Mouse
Scanner
Printermyfile.txt
Tomato
Lettuce
Capsicumالجزء 1: while وuntil
ما الحلقة؟
الحلقة تأخذ سلسلة من الأوامر وتظل تعيد تشغيلها حتى تتحقق حالة معينة. والحلقات تؤتمت المهام المتكررة: تكتب الأوامر مرة واحدة، وتشغلها الحلقة بعدد المرات المطلوب. وفي Bash ثلاث بنى أساسية للحلقات، ويقع جسم (body) كل حلقة بين الكلمتين المفتاحيتين do وdone.
| الحلقة | تشغل جسمها |
|---|---|
while | ما دام الشرط (test) صحيحا |
until | حتى يصبح الشرط صحيحا (أي ما دام خاطئا) |
for | مرة واحدة لكل عنصر في قائمة |
while
تقول حلقة while: ما دام التعبير صحيحا، استمر في تنفيذ هذه الأسطر من الشيفرة. ويكتب الشرط بين قوسين مربعين، تماما مثل شرط جملة if:
while [ some test ]
do
commands
doneيطبع هذا السكربت الأعداد من 1 إلى 10:
while_loop.sh
#!/bin/bash
# Basic while loop
counter=1
while [ $counter -le 10 ]
do
echo $counter
((counter++))
done
echo All done- السطر 3 يعطي المتغير
counterقيمته الابتدائية. - السطر 4: ما دام الشرط صحيحا (أي
counterأصغر من 10 أو يساويه)، نفذ الأوامر الواقعة بينdoوdone. - السطر 6 يطبع العداد؛ ويمكن وضع أي أوامر هنا.
- السطر 7 يستخدم الأقواس المزدوجة لإضافة 1 إلى
counter. - السطر 8 هو نهاية الحلقة، فيعود Bash إلى السطر 4 ويختبر الشرط مرة أخرى. فإذا كان صحيحا نفذ الجسم مرة أخرى؛ وإذا كان خاطئا أكمل بالأوامر التي تلي
done.
$ bash while_loop.sh
1
2
3
4
5
6
7
8
9
10
All doneمثال محلول 1: خطأ الفرق بواحد
غير شرط while_loop.sh وحده من -le (أصغر من أو يساوي) إلى -lt (أصغر من). توقع الخرج قبل أن تكمل القراءة.
while [ $counter -lt 10 ]$ bash while_lt.sh
1
2
3
4
5
6
7
8
9
All doneتتوقف الحلقة الآن عند 9. فعندما تكون قيمة counter هي 10 يكون الشرط 10 -lt 10 خاطئا، فلا ينفذ الجسم للقيمة 10 أبدا. ويسمى هذا الخطأ خطأ الفرق بواحد: سهل الوقوع فيه، وسهل الإصلاح بعد أن تراه. افحص التكرار الأول والتكرار الأخير لكل حلقة تكتبها.
until
تكتب حلقة until بطريقة شبه مطابقة لحلقة while (استبدل الكلمة فقط). والفرق أنها تنفذ الأوامر حتى يصبح الشرط صحيحا:
until [ some test ]
do
commands
doneuntil_loop.sh
#!/bin/bash
# Basic until loop
counter=1
until [ $counter -gt 10 ]
do
echo $counter
((counter++))
done
echo All done$ bash until_loop.sh
1
2
3
4
5
6
7
8
9
10
All doneالخرج مطابق لخرج while_loop.sh: فقد عكسنا الشرط فقط، من "ما دام counter لا يزيد على 10" إلى "حتى يصبح counter أكبر من 10". وكان يمكن لحلقة while وحدها أن تغطي كل الحالات؛ لكن until موجودة لأن بعض الشروط تقرأ بها بصورة أوضح. فعبارة "اترك المنشفة على الحبل حتى تجف" أوضح من "اترك المنشفة على الحبل ما دامت غير جافة". اختر الحلقة التي تجعل شيفرتك أسهل قراءة.
مثال محلول 2: الأعداد الزوجية القابلة للقسمة على 5
يطبع هذا السكربت من مستند الأمثلة الأعداد من 1 إلى 20 التي تكون زوجية وأيضا قابلة للقسمة على 5. توقع خرجه.
while.sh
#!/bin/bash
# Print the message
echo "Print the numbers which are even and divisible by 5"
# Initialize the counter
counter=1
# Iterate the loop while the $counter value is less than or equal to 20
while [ $counter -le 20 ]
do
# Check the $counter is divisible by 2 and 5
if [[ $counter%2 -eq 0 && $counter%5 -eq 0 ]]
then
echo "$counter"
fi
# Increment $counter by 1
((counter++))
done
echo "Done"$ bash while.sh
Print the numbers which are even and divisible by 5
10
20
Done- تعمل الحلقة 20 مرة، لقيم
counterمن 1 إلى 20. - داخل
[[ ]]يحسب طرفا-eqقبل مقارنتهما، فيكون$counter%2هو باقي القسمة على 2. والباقي 0 يعني "قابل للقسمة". &&تربط بين الشرطين: يجب أن يكون كلاهما صحيحا. ولا يجتاز الشرطين معا إلا 10 و20.
مثال محلول 3: العد التنازلي بحلقة until
يبدأ السكربت التالي من مستند الأمثلة من 20 ويطرح 1 في كل تكرار. ويقول تعليقه إنه يطبع الأعداد الفردية. توقع أول عدد يطبع وآخر عدد.
until.sh
#!/bin/bash
# Initialize the variable, n
n=20
# Repeat the loop until the value of $n is less than 0
until [ $n -lt 0 ]
do
# Check the value of n is odd
if [[ $n%2 -gt 0 ]]
then
echo $n
fi
# Decrement the value of n by 1
((n=$n-1))
done$ bash until.sh
19
17
15
13
11
9
7
5
3
1- يأخذ
nالقيم 20، 19، 18، ...، 1، 0. وبعد التكرار الذي يكون فيهn= 0 يصبحnمساويا -1، فيكون الشرط-1 -lt 0صحيحا، وتتوقفuntil. - الشرط
$n%2 -gt 0لا يكون صحيحا إلا عندما يكون الباقي 1، ولذلك لا تطبع إلا الأعداد الفردية: 19 أولا (لأن 20 زوجي)، و1 أخيرا (لأن 0 زوجي). ((n=$n-1))طريقة أخرى لكتابة الإنقاص؛ و((n--))تفعل الشيء نفسه.
أداة تفاعلية: مستعرض الحلقات
افتح مستعرض الحلقات بملء الشاشة. اختر while أو until أو قائمة أو مدى بالأقواس المعقوصة (brace range) أو صيغة لغة C لحلقة for، واضبط البداية والشرط والحد والخطوة، ثم اضغط خطوة أو تشغيل: يظلل السطر الحالي، ويبين المخطط الانسيابي هل يختبر Bash الشرط أم ينفذ الجسم أم يحدث العداد، ويظهر الخرج سطرا بعد سطر. جرب خطوة تبتعد عن الحد لترى حلقة لا نهائية (endless loop). وقد شغل كل سكربت تستطيع الأداة بناءه باستخدام Bash حقيقي، وتطابقت المخرجات كلها وعددها 965.
الجزء 2: حلقات for
حلقة for على قائمة
تختلف حلقة for قليلا عن الحلقتين الأخريين. فهي تقول: لكل عنصر من عناصر قائمة معطاة، نفذ مجموعة الأوامر المعطاة.
for var in list
do
commands
doneتأخذ الحلقة كل عنصر من القائمة بالترتيب، وتسنده إلى المتغير var، وتنفذ الأوامر الواقعة بين do وdone، ثم تعود إلى البداية فتأخذ العنصر التالي وتكرر. والقائمة سلسلة من النصوص مفصولة بمسافات. وتنتهي الحلقة عندما تنفد عناصر القائمة.
مثال محلول 4: أسبوع من الخطط
يمر مستند الأمثلة على أيام الأسبوع ويطبع خطة لكل يوم. توقع سطري Tuesday وSunday.
for.sh
#!/bin/bash
# Read a weekday name in each iteration of the loop
for day in Monday Tuesday Wednesday Thursday Friday Saturday Sunday
do
# Check the weekday name is Monday or Thursday
if [[ $day == 'Monday' || $day == 'Thursday' ]]
then
echo "Meeting on $day at 9:30 am"
# Check the weekday name is Tuesday or Wednesday or Friday
elif [[ $day == 'Tuesday' || $day == 'Wednesday' || $day == 'Friday' ]]
then
echo "Training on $day at 11:00 am"
else
# Print 'Holiday' for other days
echo "$day is Holiday"
fi
done$ bash for.sh
Meeting on Monday at 9:30 am
Training on Tuesday at 11:00 am
Training on Wednesday at 11:00 am
Meeting on Thursday at 9:30 am
Training on Friday at 11:00 am
Saturday is Holiday
Sunday is Holidayوبالفكرة نفسها تقرأ جملة كلمة كلمة: عندما تكتب الجملة بعد in تقسم عند مسافاتها، وتصبح كل كلمة تكرارا واحدا.
readstr.sh
#!/bin/bash
# Read each word of a text by using for loop
for value in Bash Programming for the Beginners
do
echo $value
done$ bash readstr.sh
Bash
Programming
for
the
Beginnersمثال محلول 5: قائمة مخزنة في متغير
يمكن أن تأتي القائمة من متغير. توقع خرج السكربتين؛ والفرق الوحيد بينهما زوج علامات التنصيص (quotes) المزدوجة في السطر 4.
for_loop.sh
#!/bin/bash
# Basic for loop
planets='Mercury Venus Earth'
for planet in $planets
do
echo $planet
done
echo All donefor_quoted.sh
#!/bin/bash
# The same list, but the variable is quoted
planets='Mercury Venus Earth'
for planet in "$planets"
do
echo $planet
done
echo All done$ bash for_loop.sh
Mercury
Venus
Earth
All done
$ bash for_quoted.sh
Mercury Venus Earth
All doneبدون علامات التنصيص، تقسم قيمة $planets عند مسافاتها إلى ثلاثة عناصر، فينفذ الجسم ثلاث مرات. ومع علامات التنصيص يكون النص كله عنصرا واحدا، فينفذ الجسم مرة واحدة ويطبع النص كله.
المديات العددية
يمكن لحلقة for أيضا أن تعالج سلسلة من الأعداد. فنقطتان بين عددين داخل أقواس معقوصة تصنعان مدى:
for_loop_series.sh
#!/bin/bash
# Basic range in for loop
for value in {1..5}
do
echo $value
done
echo All done$ bash for_loop_series.sh
1
2
3
4
5
All doneيجب ألا توجد مسافات داخل الأقواس المعقوصة. فمع المسافات لا يعد المكتوب مدى، بل قائمة من ثلاثة عناصر:
for value in { 1..5 }$ bash for_loop_spaces.sh
{
1..5
}
All doneيمكن أن تكون البداية والنهاية أي عددين. وإذا كان الأول أكبر من الثاني عد المدى تنازليا. وإذا أضفت نقطتين أخريين وعددا ضبطت الخطوة، أي القيمة التي يزاد بها أو ينقص في كل مرة:
for_loop_stepping.sh
#!/bin/bash
# Basic range with steps for loop
for value in {10..0..2}
do
echo $value
done
echo All done$ bash for_loop_stepping.sh
10
8
6
4
2
0
All doneمثال محلول 6: توقع المدى
دون تشغيل أي شيء، اكتب ما تطبعه كل حلقة (عددا في كل سطر)، ثم تحقق بمستعرض الحلقات، فهو يستخدم المديات نفسها.
| المدى | المطبوع |
|---|---|
1..10..3 داخل أقواس معقوصة | 1، 4، 7، 10 |
10..0..3 داخل أقواس معقوصة | 10، 7، 4، 1 |
0..10..2 داخل أقواس معقوصة | 0، 2، 4، 6، 8، 10 |
لا تطبع قيمة النهاية إلا إذا وقعت عليها خطوة تماما: 10 ناقص ثلاث خطوات مقدار كل منها 3 يعطي 1، والخطوة التالية، -2، تتجاوز 0.
حلقة for بأسلوب لغة C
تكتب صيغة ثانية البداية والشرط والتغيير في سطر واحد، بين أقواس مزدوجة ومفصولة بفواصل منقوطة:
for_loop_alternate_syntax.sh
#!/bin/bash
# Basic range with alternate syntax
for ((num = 1; num <= 5; num++))
do
echo $num
done
echo All done$ bash for_loop_alternate_syntax.sh
1
2
3
4
5
All donenum = 1ينفذ مرة واحدة، قبل أن تبدأ الحلقة.- الجزء الأوسط، "num أصغر من 5 أو يساويه"، هو الشرط، ويختبر قبل كل تكرار، مثل شرط حلقة
while. num++ينفذ في نهاية كل تكرار.
معالجة مجموعة من الملفات
من أكثر استخدامات حلقات for فائدة معالجة مجموعة من الملفات باستخدام محرف البدل (wildcard). ينشئ السكربت التالي نسخة .php من كل ملف .html في المجلد المعطى وسيطا أول:
copy_html.sh
#!/bin/bash
# Make a php copy of any html files
for value in $1/*.html
do
cp $value $1/$( basename -s .html $value ).php
done- يتوسع
$1/*.htmlإلى قائمة ملفات.htmlفي ذلك المجلد، فيأخذ كل تكرار اسم ملف واحد. - يطبع
basename -s .html site/about.htmlالنصabout: أي الاسم بلا المجلد وبلا اللاحقة. ويضع$( )ذلك الخرج في سطر الأمر، فتسمى النسخةsite/about.php.
$ ls site
about.html index.html
$ bash copy_html.sh site
$ ls site
about.html about.php index.html index.phpالمرور على وسائط سطر الأوامر
القائمة "$@" هي قائمة كل الوسائط المعطاة للسكربت، عنصر لكل وسيط. ويستخدمها مستند الأمثلة لقراءة وسائط مكتوبة على صورة أزواج name=value:
cl2.sh
#!/bin/bash
# Print the argument variables
echo "Argument values are: $1 $2"
# Read each argument separately using for loop
for arg in "$@"
do
# Separate argument name and value
key=$(echo $arg | cut -f1 -d=)
value=$(echo $arg | cut -f2 -d=)
# Print message based on the argument's name
case $key in
id) echo "Student ID = $value";;
mark) echo "Obtained mark = $value";;
*)
esac
done$ bash cl2.sh id=2026101 mark=90
Argument values are: id=2026101 mark=90
Student ID = 2026101
Obtained mark = 90- يأخذ كل تكرار وسيطا واحدا، مثل
id=2026101. - يبقي
cut -f1 -d=الجزء الذي قبل=(الاسم)، ويبقيcut -f2 -d=الجزء الذي بعده (القيمة). - تقارن
caseالاسم بكل نمط وتنفذ السطر المطابق؛ و*)يطابق أي شيء آخر ولا يفعل هنا شيئا.
تقسيم نص إلى مصفوفة
يقسم مستند الأمثلة جملة يكتبها المستخدم إلى عناصر مصفوفة، ثم يمر على المصفوفة بحلقة:
split.sh
#!/bin/bash
# Input a string value
echo "Enter a string value"
read text
# Set the delimiter
IFS=' '
# Split the value of $text into an array based on space delimiter
read -a arr <<< "$text"
# Print each value of the array
for value in "${arr[@]}"
do
printf "$value\n"
done$ bash split.sh
Enter a string value
Learn Bash programming
Learn
Bash
programming- المتغير
IFS(فاصل الحقول الداخلي، internal field separator) يضبط الفاصل (delimiter)، وهو هنا مسافة. - يقرأ
read -a arrكلمات النص إلى المصفوفةarr؛ وعلامات "أصغر من" الثلاث تمرر قيمة$textإلىreadبوصفها إدخاله. - قائمة حلقة for المكتوبة بالصيغة
arr[@]تعني "كل عنصر في المصفوفة"، عنصرا واحدا في كل تكرار.
الجزء 3: التحكم في الحلقات
تعمل الحلقة في أغلب الأحيان بسلاسة وانتظام. لكننا نحتاج أحيانا إلى التدخل، وتقوم بذلك جملتان: break وcontinue.
break
تطلب break من Bash مغادرة الحلقة فورا. فقد توجد حالة عادية تنهي الحلقة، وحالة استثنائية ينبغي أن تنهيها مبكرا. يبحث هذا السكربت في المدى من 22 إلى 30 ويتوقف عند أول عدد يقبل القسمة على 7:
break_demo.sh
#!/bin/bash
# Stop at the first number divisible by 7
for value in {22..30}
do
if (( value % 7 == 0 ))
then
echo "found $value"
break
fi
echo "checked $value"
done
echo All done$ bash break_demo.sh
checked 22
checked 23
checked 24
checked 25
checked 26
checked 27
found 28
All doneلا يفحص العددان 29 و30 أبدا: فبعد break يكمل Bash بأول أمر بعد done.
continue
تطلب continue من Bash إيقاف تنفيذ هذا التكرار من الحلقة وبدء التكرار التالي. يتخطى هذا السكربت مضاعفات العدد 3:
continue_demo.sh
#!/bin/bash
# Skip the multiples of 3
for n in {1..10}
do
if (( n % 3 == 0 ))
then
continue
fi
echo -n "$n "
done
echo
echo All done$ bash continue_demo.sh
1 2 4 5 7 8 10
All doneيطبع echo -n بلا سطر جديد، فتبقى الأعداد في سطر واحد؛ وecho المنفرد بعد الحلقة ينهي ذلك السطر.
استخدام حقيقي: نسخ ملفات مجلد نسخا احتياطيا، مع تخطي أي ملف لا يسمح لنا بقراءته بدلا من الفشل عنده. أنشئ أولا مجلدا باسم notes فيه ثلاثة ملفات ومجلدا فارغا باسم backup، ثم أزل كل أذونات الملف notes/secret.txt بالأمر chmod 000:
mkdir notes backup
echo "first note" > notes/a.txt
echo "second note" > notes/b.txt
echo "my password" > notes/secret.txt
chmod 000 notes/secret.txtcopy_check.sh
#!/bin/bash
# Back up every file of the folder $1, skipping unreadable ones
for value in $1/*
do
if [ ! -r $value ]
then
echo $value not readable 1>&2
continue
fi
cp $value backup/
echo "backed up $value"
done$ ls notes
a.txt b.txt secret.txt
$ bash copy_check.sh notes
backed up notes/a.txt
backed up notes/b.txt
notes/secret.txt not readable
$ ls backup
a.txt b.txt- الشرط
[ ! -r $value ]يكون صحيحا عندما يكون الملف غير قابل للقراءة. - يرسل
1>&2الرسالة إلى خرج الأخطاء بدلا من الخرج العادي؛ ويظهر كلاهما على الطرفية.
قراءة ملف سطرا بسطر
يقرأ cat الملف كله دفعة واحدة. أما حلقة while مع read فتقرؤه سطرا في كل تكرار: يعاد توجيه الملف إلى الحلقة بعد done، ويأخذ read line السطر التالي في كل تكرار. وعندما لا تبقى أسطر يفشل read وتنتهي الحلقة.
readfile.sh
#!/bin/bash
echo "Reading file using cat command"
# Read the content of the file using `cat` command
content=`cat hardware.txt`
echo $content
echo "Reading file line by line using loop"
# Assign the filename
filename='hardware.txt'
# Each line of the file will be read by each iteration of the loop
while read line
do
# print the line
echo $line
done < $filename$ cat hardware.txt
Monitor
Keyboard
Mouse
Scanner
Printer
$ bash readfile.sh
Reading file using cat command
Monitor Keyboard Mouse Scanner Printer
Reading file line by line using loop
Monitor
Keyboard
Mouse
Scanner
Printerتطبع الطريقة الأولى الكلمات الخمس في سطر واحد: فالمتغير $content يحمل الملف كله، وecho $content بلا علامات تنصيص يقسمه إلى كلمات ويصل بينها بمسافات مفردة. أما الحلقة فتبقي كل سطر منفصلا.
قوائم الخيارات بالأمر select
ينشئ select قائمة خيارات بسيطة. فهو يعرض كل عنصر من القائمة مع رقم، ثم يطبع محثا (prompt). وعندما يكتب المستخدم رقما يوضع العنصر المطابق في المتغير وتنفذ الأوامر الواقعة بين do وdone؛ ثم يظهر المحث مرة أخرى.
select_example.sh
#!/bin/bash
# A simple menu system
planets='Mars Venus Earth Quit'
PS3='Select planet: '
select planet in $planets
do
if [ "$planet" == 'Quit' ]
then
break
fi
echo Hello $planet
done
echo Bye$ bash select_example.sh
1) Mars
2) Venus
3) Earth
4) Quit
Select planet: 2
Hello Venus
Select planet: 1
Hello Mars
Select planet: 5
Hello
Select planet: 4
Bye- المتغير
PS3هو المحث الذي يطبعهselect. وبدونه يكون المحث#?. - لا يجري
selectأي تحقق من الأخطاء: فالرقم5لا يطابق أي عنصر، فيكونplanetفارغا ويطبع السكربتHelloولا شيء بعدها. - الضغط على Enter دون رقم يعرض القائمة مرة أخرى. وتنتهي الحلقة عند
breakأو عند انتهاء الإدخال. - وضع
"$planet"بين علامتي تنصيص عن قصد. فبدون علامات التنصيص يتركplanetالفارغ الشرط بلا شيء في طرفه الأيسر، ويسبب الإدخال نفسه5هذا الخطأ قبلHello:
select_unquoted.sh: line 7: [: ==: unary operator expectedويبني مستند الأمثلة قائمة خيارات للغات بالطريقة نفسها، بدون PS3، ويستخدم exit 0 لإنهاء السكربت كله:
menu.sh
#!/bin/bash
# Print message for the user
echo "Select your favorite language"
# Define the list of a menu item
select language in C# Java PHP Python Bash Exit
do
# Print the selected value
if [[ $language == "Exit" ]]
then
exit 0
else
echo "Selected language is $language"
fi
done$ bash menu.sh
Select your favorite language
1) C#
2) Java
3) PHP
4) Python
5) Bash
6) Exit
#? 3
Selected language is PHP
#? 6الجزء 4: الدوال
ما الدالة؟
الدالة سكربت صغير داخل سكربت: قطعة من الشيفرة لها اسم، ويمكنك استدعاؤها (call) بقدر ما تشاء من المرات. والدوال مفيدة عندما يلزم تنفيذ مهمة عدة مرات. فبدلا من كتابة الشيفرة نفسها مرة بعد مرة، تكتبها مرة واحدة في دالة وتستدعي الدالة في كل مرة.
يمكن كتابة الدالة بصيغتين. وكلتاهما تعمل بالطريقة نفسها؛ والاختيار بينهما تفضيل شخصي.
function_name () {
commands
}function function_name {
commands
}- في اللغات الأخرى تكتب الوسائط داخل الأقواس الهلالية. أما في Bash فالأقواس للزينة فقط: لا تضع بداخلها أي شيء أبدا.
- يجب أن يظهر تعريف الدالة في السكربت قبل أي استدعاء لها.
- اختر أسماء وصفية، حتى يتضح الغرض من كل دالة.
function_example.sh
#!/bin/bash
# Basic function
print_something () {
echo Hello I am a function
}
print_something
print_something$ bash function_example.sh
Hello I am a function
Hello I am a functionالأسطر من 3 إلى 5 تعرف الدالة فقط؛ ولا يطبع شيء أثناء قراءة Bash لها. وكل سطر من السطرين الأخيرين يستدعيها بكتابة اسمها، مثل أي أمر.
مثال محلول 7: الاستدعاء قبل التعريف
يستدعي هذا السكربت print_something مرة قبل التعريف ومرة بعده. توقع سطري الخرج.
call_before.sh
#!/bin/bash
# Calling a function before it is defined
print_something
print_something () {
echo Hello I am a function
}
print_something$ bash call_before.sh
call_before.sh: line 3: print_something: command not found
Hello I am a functionفي السطر 3 لم يكن Bash قد قرأ التعريف بعد، فكان print_something مجرد أمر غير معروف. ولا يتوقف السكربت: بل يقرأ التعريف وينجح الاستدعاء الثاني.
تمرير الوسائط
ترسل البيانات إلى الدالة بالطريقة نفسها التي ترسل بها وسائط سطر الأوامر إلى السكربت: تكتب مباشرة بعد اسم الدالة. وهي داخل الدالة $1 و$2 وهكذا.
arguments_example.sh
#!/bin/bash
# Passing arguments to a function
print_something () {
echo Hello $1
}
print_something Mars
print_something Jupiter$ bash arguments_example.sh
Hello Mars
Hello Jupiterكل استدعاء يحصل على $1 خاص به: Mars في الاستدعاء الأول وJupiter في الثاني.
حالة الإرجاع
في معظم اللغات تستطيع الدالة إرسال قيمة إلى المكان الذي استدعاها. أما دوال Bash فلا تستطيع ذلك. بل تستطيع ضبط حالة إرجاع بدلا من ذلك، تماما كما ينتهي برنامج أو أمر بحالة خروج (exit status) تبين هل نجح أم لا. والكلمة المفتاحية هي return، والمتغير $? يحمل حالة الأمر أو الدالة التي نفذت أخيرا.
return_status_example.sh
#!/bin/bash
# Setting a return status for a function
print_something () {
echo Hello $1
return 5
}
print_something Mars
print_something Jupiter
echo The previous function has a return value of $?$ bash return_status_example.sh
Hello Mars
Hello Jupiter
The previous function has a return value of 5- الحالة 0 تعني أن كل شيء تم بنجاح؛ والحالة غير الصفرية تعني حدوث خطأ.
- ليس من الضروري أن تكون الحالة عددا ثابتا؛ فيمكن أن تكون متغيرا.
- يتغير
$?بعد كل أمر. فاقرأه مباشرة بعد الاستدعاء، أو احفظه في متغير.
إرجاع قيمة باستبدال الأوامر
للحصول على قيمة حقيقية، اجعل الدالة تطبع النتيجة، والنتيجة وحدها، والتقط النص المطبوع باستبدال الأوامر $( ):
return_hack.sh
#!/bin/bash
# Setting a return value to a function
lines_in_file () {
cat $1 | wc -l
}
num_lines=$( lines_in_file $1 )
echo The file $1 has $num_lines lines in it.$ cat myfile.txt
Tomato
Lettuce
Capsicum
$ bash return_hack.sh myfile.txt
The file myfile.txt has 3 lines in it.- السطر 4 يطبع عدد أسطر الملف الذي يوجد اسمه في
$1. - السطر 6 يشغل الدالة داخل
$( )، فلا يظهر ما تطبعه على الشاشة: بل يصبح قيمةnum_lines. - داخل الدالة يكون
$1وسيط الدالة. أما في السطر 6 فيكون$1وسيط السكربت، وهوmyfile.txt، ويمرر إلى الدالة.
انتبه: إذا استدعيت الدالة بدون $( ) فإنها تطبع نتيجتها على الشاشة ويبقى num_lines فارغا. وتعرض أداة مكدس الاستدعاءات (call stack) أدناه النسختين؛ والثانية تطبع فعلا 3 ثم The file myfile.txt has lines in it.
نطاق المتغيرات: المحلي والعام
يحدد النطاق أي أجزاء السكربت ترى أي متغيرات. والمتغير افتراضيا عام (global): أي مرئي في كل مكان من السكربت، بما في ذلك داخل الدوال. أما الكلمة المفتاحية local، عندما تكتب قبل المتغير في أول مرة يضبط فيها داخل دالة، فتجعله محليا (local): أي مرئيا داخل تلك الدالة فقط.
local var_name=var_valueمثال محلول 8: المحلي مقابل العام
توقع الأسطر الثلاثة التي يطبعها هذا السكربت.
local_variables.sh
#!/bin/bash
# Experimenting with variable scope
var_change () {
local var1='local 1'
echo Inside function: var1 is $var1 : var2 is $var2
var1='changed again'
var2='2 changed again'
}
var1='global 1'
var2='global 2'
echo Before function call: var1 is $var1 : var2 is $var2
var_change
echo After function call: var1 is $var1 : var2 is $var2$ bash local_variables.sh
Before function call: var1 is global 1 : var2 is global 2
Inside function: var1 is local 1 : var2 is global 2
After function call: var1 is global 1 : var2 is 2 changed again| السطر | var1 | var2 |
|---|---|---|
| قبل الاستدعاء | العام، global 1 | العام، global 2 |
| داخل الدالة | المتغير المحلي var1، local 1 | ما زال العام، global 2 |
| بعد الاستدعاء | العام، دون أن يمس: global 1 | العام، وقد تغير داخل الدالة: 2 changed again |
- ينشئ
local var1متغيرا ثانيا محليا باسمvar1. وأثناء عمل الدالة يخفي المتغير العام؛ ويغيرvar1='changed again'النسخة المحلية. - ليس للمتغير
var2نسخة محلية، ولذلك يغيرvar2='2 changed again'داخل الدالة المتغير العامvar2. - عندما تنتهي الدالة يختفي
var1المحلي ويعود المتغير العام مرئيا، دون تغيير.
احذف الكلمة local فيصبح السطر الأخير After function call: var1 is changed again : var2 is 2 changed again: فالدالة الآن تكتب فوق المتغيرين العامين كليهما. ولهذا ينبغي أن تستخدم الدوال متغيرات محلية، حتى لا تغير مصادفة متغيرا يحمل الاسم نفسه في مكان آخر من السكربت. ولا تستخدم المتغيرات العامة إلا حلا أخيرا.
أداة تفاعلية: مكدس الاستدعاءات
افتح مكدس استدعاءات الدوال بملء الشاشة. فهو يشغل أربعة سكربتات من هذا الجزء سطرا بعد سطر: local_variables.sh (شغل local أو أوقفها)، وreturn_status_example.sh (اختر القيمة بعد return)، وreturn_hack.sh (داخل $( ) أو بوصفها استدعاء عاديا)، ودالة التغليف (wrapper) الخاصة بالأمر ls أدناه (مع command وبدونها). راقب إطارا (frame) جديدا يضاف إلى مكدس الاستدعاءات مع كل استدعاء، ومعه $1 الخاص به ومتغيراته المحلية، والمتغيرات العامة التي يخفيها أو يغيرها، وقيمة $?، والمكان الذي يذهب إليه النص المطبوع. وقد شغلت النسخ التسع كلها باستخدام Bash حقيقي، وتعرض الأداة بالضبط ما طبعته.
ثلاث دوال من مستند الأمثلة
يعرف func.sh ثلاث دوال: الأولى تطبع رسالة، والثانية تخزن نصا في متغير عام ليقرأه المستدعي، والثالثة تقرأ وسيطها إلى متغير محلي وتحسب مساحة دائرة باستخدام bc، وهي الآلة الحاسبة التي تتعامل مع الكسور العشرية.
func.sh
#!/bin/bash
# Declare a simple function
function print_message()
{
echo "Bash programming for beginner"
}
# Declare a function to return a string value
function ret_strdata()
{
# Initialize the variable with string value
return_str="Learn bash programming with functions"
}
# Declare a function to read argument value
function calculate_area()
{
# Read the passed argument value
local radius=$1
area=$(echo $radius*$radius*3.14 | bc)
# Print the area value
echo "Area of the circle is $area"
}
# Call the function to print a simple message
print_message
# Call the function that will assign a string value in a variable
ret_strdata
# Print the value of the variable
echo $return_str
# Read the radius value
echo "Enter the radius value"
read rad
# Call the function with radius value
calculate_area $rad$ bash func.sh
Bash programming for beginner
Learn bash programming with functions
Enter the radius value
4
Area of the circle is 50.24- تستخدم التعريفات الكلمة المفتاحية
functionمع الأقواس الهلالية: وBash يقبل ذلك أيضا. - الدالة
ret_strdata"ترجع" نصا بالطريقة العامة: فهي تضبطreturn_str، ويقرؤه المستدعي بعد الاستدعاء. - المساحة هي
4*4*3.14 = 50.24. والأقواس المزدوجة تعمل مع الأعداد الصحيحة فقط، ولذلك يجريbcهذه العملية الحسابية. وإذا لم يكنbcموجودا على جهازك فثبته بالأمرsudo apt install bc.
تجاوز أمر
يمكن أن تحمل الدالة اسم أمر موجود. وهذا يصنع دالة تغليف: فمثلا، نريد أن ينفذ كل ls في السكربت في الحقيقة ls -lh.
override.sh
#!/bin/bash
# Create a wrapper around the command ls
ls () {
command ls -lh
}
ls$ bash override.sh
total 4.0K
-rwxr-xr-x 1 student student 85 Sep 25 09:00 override.shللدالة عادة أسبقية على الأمر الذي يحمل الاسم نفسه. وداخل الدالة تقول الكلمة المفتاحية command: "نفذ الأمر ls، لا الدالة". ولولاها لكان ls -lh داخل الدالة استدعاء للدالة ls نفسها مرة أخرى، وهي بدورها تستدعي نفسها مرة أخرى، وهكذا. ويضيف كل استدعاء إطارا إلى مكدس الاستدعاءات ولا يعود أي منها أبدا، فيمتلئ المكدس. وعندما شغل السكربت بهذه الطريقة لم يطبع شيئا لبضع ثوان، ثم انهار برسالة Segmentation fault (core dumped) ورمز الخروج 139.
تصميم الدوال
- المهمة التي يجب تنفيذها عدة مرات مرشحة جيدة لتكون دالة.
- المهام المساعدة، مثل التحقق من أن ملفا موجود وقابل للقراءة، يمكن وضعها في دوال خاصة بها، منفصلة منطقيا عن الجزء الرئيسي من السكربت.
- تكون الدالة أكثر قابلية لإعادة الاستخدام عندما تؤدي مهمة واحدة فقط. ابحث عن التوازن: فالدوال التي تفعل الكثير تفقد فائدتها، والعدد الكبير من الدوال الصغيرة جدا يضخم الشيفرة بلا سبب.
الجزء 5: سكربتات تعمل معا
رموز الخروج بين السكربتات
ينتهي السكربت برمز خروج، تماما كما ترجع الدالة حالة. فالأمر exit 0 يعني النجاح، وأي عدد آخر يعني خطأ. ويستطيع سكربت ثان أن يشغل الأول ويقرأ رمزه بالمتغير $?.
first.sh
#!/bin/bash
echo "Enter a numeric value"
read n
# Check the input value is less than or equal to 100 or not
if [[ $n -le 100 ]]
then
exit 0
else
exit 1
fisecond.sh
#!/bin/bash
# Execute the file, first.sh
bash "first.sh"
# Check the return code is equal to 1 or not
if [ $? -eq 1 ]
then
echo "The input number is greater than 100"
else
echo "The input number is less than or equal to 100"
fi$ bash second.sh
Enter a numeric value
55
The input number is less than or equal to 100
$ bash second.sh
Enter a numeric value
110
The input number is greater than 100لا يطبع first.sh أي نتيجة على الإطلاق: إجابته الوحيدة هي رمز الخروج، 0 للعدد 55 و1 للعدد 110. ويختبر second.sh المتغير $? مباشرة بعد السطر bash "first.sh"، قبل أن يستطيع أي أمر آخر استبداله.
استدعاء سكربتات أخرى: source وbash وeval وexec
يستدعي مستند الأمثلة أربعة سكربتات صغيرة بأربع طرق مختلفة. احفظها بجوار callpro.sh:
add.sh
#!/bin/bash
a=60
b=40
((result=$a+$b))
echo "The addition of $a+$b=$result"subtract.sh
#!/bin/bash
a=$1
b=$2
((result=$a-$b))
echo "The subtraction of $a-$b=$result"multiply.sh
#!/bin/bash
((result=$1*$2))
echo "The multiplication of $1 and $2 is $result"divide.sh
#!/bin/bash
a=$1
b=2
((result=$a/$b))
echo "The division of $a by $b is $result"يشغل exec الملف divide.sh مباشرة بوصفه برنامجا، لا عن طريق bash، لذلك يجب أن يكون الملف قابلا للتنفيذ. نفذ هذا الأمر مرة واحدة، وإلا فشل الاستدعاء الأخير برسالة Permission denied:
chmod +x divide.shالسكربت callpro.sh (أضيف إليه سطرا echo ليظهرا ما يتركه كل استدعاء، وسطر أخير لا ينفذ أبدا):
#!/bin/bash
script1="add.sh"
script2="subtract.sh"
script3="multiply.sh"
script4="/home/student/lab12/divide.sh"
source "$script1"
echo "After source, result is $result"
bash $script2 50 20
echo "After bash, result is still $result"
echo "Enter the value of a"
read a
echo "Enter the value of b"
read b
eval bash $script3 $a $b
exec $script4 30
echo "This line never runs"$ bash callpro.sh
The addition of 60+40=100
After source, result is 100
The subtraction of 50-20=30
After bash, result is still 100
Enter the value of a
6
Enter the value of b
7
The multiplication of 6 and 7 is 42
The division of 30 by 2 is 15| الاستدعاء | ما يحدث |
|---|---|
source add.sh | يشغل add.sh داخل السكربت الحالي، فيبقى متغيره result (100) موجودا بعد ذلك |
bash subtract.sh 50 20 | يبدأ Bash جديدا للسكربت؛ ويختفي متغيره result (30) معه، فيبقى متغيرنا 100 |
eval bash multiply.sh $a $b | يبني سطر الأمر من وسائطه (هنا بالقيمتين 6 و7) ثم ينفذه |
exec divide.sh 30 | يجعل divide.sh يحل محل السكربت الحالي؛ فلا ينفذ أي شيء بعد exec |
تمارين
نحو 30 دقيقة. لكل تمرين، اكتب إجابتك على الورق أولا، ثم اكتب السكربت في ~/lab12 وشغله لتتحقق من نفسك. استخدم فقط ما غطاه هذا الدرس العملي والجزء 1.
التمرين 1: توقع خرج حلقتين
اكتب بالضبط ما يطبعه هذا السكربت (نحو 4 دقائق).
p1.sh
#!/bin/bash
counter=10
until [ $counter -lt 4 ]
do
echo -n "$counter "
((counter-=3))
done
echo
for value in {15..3..4}
do
echo -n "$value "
done
echoالتمرين 2: زوجي أم فردي
اكتب even_odd.sh، وهو سكربت يطبع الأعداد من 1 إلى 10، كل عدد في سطر مستقل، متبوعا بالعبارة is even أو is odd (نحو 5 دقائق). فمثلا، أول سطرين هما 1 is odd و2 is even.
التمرين 3: دالة التقدير
اكتب grade.sh (نحو 6 دقائق):
- دالة
gradeتقرأ وسيطها إلى متغير محلي اسمهmarkوتطبعAعندما تكون الدرجة 85 على الأقل، وBعندما تكون 65 على الأقل، وCفي غير ذلك. - حلقة for على الدرجات
92 70 40 85تلتقط كل حرف باستخدام$( )في متغير اسمهgوتطبع سطرا مثلmark 92 gets A.
التمرين 4: توقع النطاق
اكتب السطر الوحيد الذي يطبعه هذا السكربت (نحو 4 دقائق).
scope.sh
#!/bin/bash
count=0
add () {
local step=$1
count=$(( count + step ))
step=100
}
step=7
add 5
add 3
echo "count=$count step=$step"التمرين 5: التحقق من الصلاحية بالأمر return
اكتب valid.sh (نحو 6 دقائق). الدالة valid ترجع الحالة 0 عندما يكون وسيطها درجة من 0 إلى 100، والحالة 1 في غير ذلك. وحلقة for تستدعيها للقيم 45 120 100 -3، وتختبر $? بعد كل استدعاء، وتطبع 45 ok أو 120 invalid، وهكذا.
التمرين 6: ترقيم أسطر ملف
اكتب number_lines.sh، الذي يقرأ hardware.txt سطرا بسطر بالحلقة while read، ويطبع كل سطر بعد رقمه (1: Monitor)، وينتهي بالمجموع، Total: 5 lines (نحو 5 دقائق).
الإجابات
التمرين 1
$ bash p1.sh
10 7 4
15 11 7 3 until: يأخذcounterالقيم 10، 7، 4، ثم 1. والشرط1 -lt 4صحيح، فتتوقف الحلقة قبل طباعة 1.- يبدأ المدى من 15 ويعد تنازليا بخطوة 4، لأن 15 أكبر من 3: أي 15، 11، 7، 3. وتبلغ قيمة النهاية 3 تماما، ولذلك تطبع.
- كل
echo -nيترك مسافة بعد العدد ولا يضيف سطرا جديدا؛ وechoالمنفرد ينهي السطر.
التمرين 2
#!/bin/bash
# Print 1 to 10 and whether each number is even or odd
for n in {1..10}
do
if [[ $n%2 -eq 0 ]]
then
echo "$n is even"
else
echo "$n is odd"
fi
done$ bash even_odd.sh
1 is odd
2 is even
3 is odd
4 is even
5 is odd
6 is even
7 is odd
8 is even
9 is odd
10 is evenحلقة while بعداد من 1 إلى 10، أو حلقة for بأسلوب لغة C من 1 إلى 10، تعطي الخرج نفسه.
التمرين 3
#!/bin/bash
# Print the grade letter of a mark
grade () {
local mark=$1
if [ $mark -ge 85 ]
then
echo A
elif [ $mark -ge 65 ]
then
echo B
else
echo C
fi
}
for m in 92 70 40 85
do
g=$( grade $m )
echo "mark $m gets $g"
done$ bash grade.sh
mark 92 gets A
mark 70 gets B
mark 40 gets C
mark 85 gets Aيحصل 85 على A لأن الشرط هو -ge (على الأقل). ومع -gt كان سيحصل على B: وهذا خطأ آخر من أخطاء الفرق بواحد.
التمرين 4
$ bash scope.sh
count=8 step=7- ليس للمتغير
countنسخة محلية، ولذلك يغير الاستدعاءان كلاهما المتغير العام: 0 + 5 = 5، ثم 5 + 3 = 8. - المتغير
stepمحلي داخلadd، ولذلك لا يغيرstep=100إلا النسخة المحلية. ويبقىstepالعام 7.
التمرين 5
#!/bin/bash
# Return status 0 for a mark from 0 to 100, 1 otherwise
valid () {
if [[ $1 -ge 0 && $1 -le 100 ]]
then
return 0
else
return 1
fi
}
for m in 45 120 100 -3
do
valid $m
if [ $? -eq 0 ]
then
echo "$m ok"
else
echo "$m invalid"
fi
done$ bash valid.sh
45 ok
120 invalid
100 ok
-3 invalidيختبر $? في السطر الذي يلي valid $m مباشرة، فيظل يحمل حالة الدالة.
التمرين 6
#!/bin/bash
# Number the lines of a file
n=0
while read line
do
((n++))
echo "$n: $line"
done < hardware.txt
echo "Total: $n lines"$ bash number_lines.sh
1: Monitor
2: Keyboard
3: Mouse
4: Scanner
5: Printer
Total: 5 linesيعد n تكرارات الحلقة، تكرارا لكل سطر، فيحمل بعد done عدد الأسطر.