Logo

كتابة سكربتات Bash - الجزء الثاني

14 دقيقة قراءة
شرائح الدرس
1 / 16

نظم التشغيل 1 - الدرس الثاني عشر

كتابة سكربتات Bash - الجزء الثاني

تكرار العمل بالحلقات، وتنظيم المنطق في دوال، واستدعاء سكربت من آخر، وبناء قوائم تفاعلية، والتعامل مع النصوص، ونقل البيانات عبر إعادة التوجيه والأنابيب.

بنهاية هذا الدرس ستكون قادرا على كتابة سكربتات Bash تكرر العمل باستخدام الـ loops، وتنظم المنطق في functions، وتستدعي سكربتا من سكربت آخر، وتبني menus تفاعلية، وتتعامل مع الـ strings، وتقرأ أو تكتب الملفات من خلال الـ redirection والـ pipes.

الأهداف

  • تكرار العمل باستخدام for على قائمة كلمات، و while مع عداد، و until؛ وتحديث العدادات بـ (( )).
  • كتابة الـ functions بصيغتي التعريف، وتمرير الـ arguments، والحفاظ على الـ variables كـ local، وإرجاع نتيجة من خلال global variable.
  • تشغيل سكربت من سكربت آخر باستخدام source و bash و eval و exec، ومعرفة الفرق بين كل واحد منها.
  • بناء menu بـ select، ومعالجة الـ arguments الآتية من سطر الأوامر بـ case، والتعامل مع الـ strings باستخدام IFS و read -a و ${var^^} و tr.
  • قراءة ملف سطرا بسطر، والكتابة بـ > و >>، وربط sort و grep و wc بالـ pipes.

المتطلبات السابقة

  • الدرس السابق عن كتابة سكربتات Bash: سطر الـ shebang، جعل السكربت قابلا للتنفيذ، الـ variables، read، $1/$2، $?، exit، حساب $(( ))، bc، و if/elif/else مع [[ ]].
  • طرفية Linux (تثبيت أصلي، أو جهاز افتراضي، أو WSL).
  • حاسبة bc، مطلوبة لمثال واحد على الـ functions. إذا لم تكن مثبتة، ثبتها بمدير الحزم الخاص بتوزيعتك.
  • مجلد عمل واحد لكل سكربتات هذا الدرس، لأن سكربت مهمة يستدعي سكربتات مساعدة لمهمة أخرى بالاسم.

معنى "أنشئ السكربت" فيما يلي: افتح السطر في محرر نصوص، والصقه، واحفظه بامتداد .sh، ثم شغله بـ bash name.sh (أو اجعله قابلا للتنفيذ مرة واحدة وشغله بعدها بـ ./name.sh). خطوتان قصيرتان في هذا الدرس تكرران عمدا سكربتات من الدرس السابق: هي مراجعة، وضعت حيث يبني هذا الدرس عليها.

المفهوم الأول: الـ Loops

التعريف. الـ loop يكرر مجموعة من الأوامر. عند Bash ثلاث كلمات مفتاحية للـ loops، for و while و until، وجسم كل loop يقع بين do و done.

الغرض. أي عمل يجب أن يحدث مرة واحدة لكل عنصر (كل يوم من أيام الأسبوع، كل كلمة من جملة، كل رقم من 1 إلى 20) يكتب مرة واحدة داخل الجسم ويشغله الـ loop بعدد المرات اللازم.

كيف يعمل. for يأخذ الكلمة التالية من قائمة في كل مرة يمر بها ويتوقف عندما تنتهي القائمة. while و until يختبران شرطا قبل كل مرة:

flowchart TD
    A[Start] --> B{Condition}
    B -- "while: true / until: false" --> C[Run the body between do and done]
    C --> D["Update the counter, for example counter++"]
    D --> B
    B -- "while: false / until: true" --> E[Continue after done]

الفرق الوحيد بين while و until: while يعمل ما دام الاختبار ينجح، until يعمل ما دام الاختبار يفشل.

for على قائمة كلمات

الصيغة الأكثر شيوعا تمر على قائمة كلمات، كلمة واحدة في كل مرة:

#!/bin/bash
# Read a weekday name in each iteration of the loop
for day in Monday Tuesday Wednesday Thursday Friday Saturday Sunday
do
    if [[ $day == 'Monday' || $day == 'Thursday' ]]
    then
        echo "Meeting on $day at 9:30 am"
    elif [[ $day == 'Tuesday' || $day == 'Wednesday' || $day == 'Friday' ]]
    then
        echo "Training on $day at 11:00 am"
    else
        echo "$day is Holiday"
    fi
done
  • for day in ...: day هو الـ loop variable. في كل مرة يأخذ الكلمة التالية من القائمة بعد in.
  • do / done: الجسم. كل ما بينهما يعمل مرة واحدة لكل كلمة.
  • الـ if/elif/else داخل الجسم تقارن $day الحالية بقيم نصية، فيحصل كل يوم على رسالته الخاصة.

for على كلمات جملة

القائمة بعد in ليست بالضرورة مجموعة أسماء منفصلة. جملة تكتب هناك تنقسم عند الفراغات، وتصبح كل كلمة مرة واحدة من الـ loop، وهذه هي الطريقة التي يقرأ بها الـ for loop بيانات نصية كلمة بكلمة:

#!/bin/bash
# Read each word of a text by using for loop
for value in Bash Programming for the Beginners
do
    echo $value
done
  • for value in Bash Programming for the Beginners: الكلمات الخمس تشكل القائمة؛ value تستقبلها واحدة تلو الأخرى.
  • echo $value: يطبع الكلمة الحالية في سطرها الخاص، فيكون الناتج Bash وProgramming وfor وthe وBeginners، كلمة في كل سطر.

while مع عداد

#!/bin/bash
echo "Print the numbers which are even and divisible by 5"
counter=1
while [ $counter -le 20 ]
do
    if [[ $counter%2 -eq 0 && $counter%5 -eq 0 ]]
    then
        echo "$counter"
    fi
    ((counter++))
done
echo "Done"
  • counter=1: الـ loop variable يجب أن يضبط قبل الـ loop، وإلا يفشل الاختبار فورا.
  • while [ $counter -le 20 ]: الجسم يعمل ما دام counter أصغر من أو يساوي 20.
  • [[ $counter%2 -eq 0 && $counter%5 -eq 0 ]]: -eq داخل [[ ]] يقيم الطرفين حسابيا، فـ $counter%2 تصبح، مثلا، 10%2، وهي 0. % هو عامل باقي القسمة (remainder).
  • ((counter++)): يضيف 1 إلى counter. نسيان هذا السطر يعطي loop لا نهائي؛ اضغط Ctrl+C إذا حدث ذلك.

until

#!/bin/bash
n=20
until [ $n -lt 0 ]
do
    if [[ $n%2 -gt 0 ]]
    then
        echo $n
    fi
    ((n=$n-1))
done
  • until [ $n -lt 0 ]: الجسم يعمل حتى تصبح n أصغر من 0، بعبارة أخرى ما دامت n تساوي 0 أو أكبر.
  • [[ $n%2 -gt 0 ]]: باقي قسمة أكبر من 0 يعني أن n فردية، فالسكربت يطبع الأرقام الفردية عدا تنازليا من 19 إلى 1.
  • ((n=$n-1)): العداد ينزل بدلا من أن يصعد. هذه نفس خطوة التنقيص المسبق ((--N)) التي ظهرت سابقا.

المفهوم الثاني: الـ Functions

التعريف. الـ function هو مجموعة أوامر مسماة تعرفها مرة واحدة وتستدعيها بقدر ما تحتاج. استدعاؤه هو نفس كتابة اسمه كأمر.

الغرض. عندما تحتاج المجموعة نفسها في أكثر من مكان في السكربت، يحتفظ الـ function بها مرة واحدة، وكل مكان يستدعي الاسم بدلا من تكرار السطور.

كيف يعمل. التعريف يعطي الاسم، وأقواسا فارغة، والجسم بين أقواس معقوفة. الاستدعاء هو الاسم فقط، وقد يتبعه قيم؛ داخل الـ function تصبح هذه القيم $1 و $2، تماما كما هي الـ arguments التي يستقبلها سكربت من سطر الأوامر. تعود النتيجة إما كنص يطبعه الـ function أو كـ variable يضبطه الـ function ويقرؤه المستدعي بعد ذلك:

flowchart LR
    A[Main script] -- "call: calculate_area 5, the 5 becomes $1" --> B[Function body]
    B -- "sets a global variable, for example return_str" --> A
    B -- "prints a line with echo" --> T[Terminal]

صيغ التعريف، والـ Arguments، و local

#!/bin/bash
function print_message()
{
    echo "Bash programming for beginner"
}
 
function ret_strdata()
{
    return_str="Learn bash programming step by step"
}
 
function calculate_area()
{
    local radius=$1
    area=$(echo $radius*$radius*3.14 | bc)
    echo "Area of the circle is $area"
}
 
print_message
ret_strdata
echo $return_str
echo "Enter the radius value"
read rad
calculate_area $rad
  • function print_message() { ... }: صيغة التعريف الأولى. الكلمة المفتاحية function اختيارية، فـ print_message() { ... } هي الصيغة الثانية وتعني الشيء نفسه تماما. كلتاهما تحتاج الأقواس الفارغة والأقواس المعقوفة.
  • ret_strdata تضبط return_str بدون local. الـ variables تكون global بشكل افتراضي، فيستطيع السكربت الرئيسي طباعتها بعد الاستدعاء: أسلوب "الإرجاع عبر global variable"، وهذه هي الطريقة التي يعيد بها function نصا في Bash.
  • calculate_area يستقبل الـ argument الخاص به بالطريقة التي يستقبلها سكربت: القيمة الأولى بعد اسم الـ function تصبح $1. local radius=$1 تنسخها إلى variable موجود فقط داخل الـ function؛ بدون local، function يضبط اسما يستخدمه السكربت الرئيسي يكتب فوقه.
  • $(echo $radius*$radius*3.14 | bc): (( )) تتعامل مع الأعداد الصحيحة فقط، فالضرب العشري يمرر بـ pipe إلى bc، و $( ) يلتقط ما يطبعه bc. النجمة * المفردة قد يوسعها الـ shell إلى أسماء ملفات؛ هنا النمط 5*5*3.14 لا يطابق أي ملف فيصل إلى bc بلا تغيير، إلا أن وضع التعبير بين علامتي اقتباس كـ "$radius * $radius * 3.14" هو العادة الأسلم.
  • print_message، ret_strdata، calculate_area $rad: الاستدعاءات. التعريفات يجب أن تسبق الاستدعاءات؛ Bash يقرأ من الأعلى إلى الأسفل ولا يعرف function قبل أن يمر بتعريفه.

الـ Scope: global بشكل افتراضي، و local عند الطلب

تستحق الكلمة المفتاحية local من السطر السابق عرضا خاصا بها، لأن الفرق لا يظهر إلا عندما يستخدم function والسكربت الرئيسي الاسم نفسه:

#!/bin/bash
counter=100
show_local()
{
    local counter=5
    echo "Inside function counter = $counter"
}
 
show_local
echo "Outside function counter = $counter"
  • counter=100: global variable يضبطه السكربت الرئيسي.
  • local counter=5: داخل الـ function، counter ثانية ومنفصلة موجودة فقط حتى ينتهي الـ function.
  • show_local ثم echo "Outside ...": الـ function يطبع 5؛ السكربت الرئيسي يطبع 100 رغم ذلك، لأن الـ global variable لم يلمس أبدا. أزل كلمة local وكلا السطرين يطبعان 5.

بين سكربتين كاملين، تنتقل النتيجة كـ exit code بدلا من ذلك: السكربت ينتهي بـ exit 0 أو exit 1، والسكربت الذي شغله يفحص $?.

المفهوم الثالث: استدعاء سكربت من سكربت آخر

التعريف. يمكن لسكربت أن يبدأ سكربتا آخر بالطريقة نفسها التي تبدأه بها من سطر الأوامر. أربعة أوامر تفعل ذلك، وتختلف في أي shell يشغل السكربت الثاني وماذا يحدث للسكربت الأول.

الغرض. تقسيم العمل إلى عدة ملفات يبقي كل واحد قصيرا، والمستدعي هو من يقرر إن كانت الـ variables الخاصة بالملف الثاني تبقى، وإن كانت الـ arguments تمرر، وإن كان المستدعي يستمر بعد ذلك.

كيف يعمل. الـ child shell هو عملية bash ثانية يبدأها الأول؛ يحصل على نسخته الخاصة من الـ variables، وكل ما يضبطه يختفي عند خروجه. source فقط يتجنب الـ child shell:

flowchart TD
    S[callpro.sh in shell A] --> S1["source add.sh: same shell, variables stay"]
    S --> S2["bash subtract.sh 50 20: child shell B, returns to A"]
    S --> S3["eval bash multiply.sh $a $b: build the command text, then run it"]
    S --> S4["exec full/path/divide.sh 30: shell A replaced, nothing after runs"]
  • source file: يقرأ أوامر الملف داخل الـ shell الحالي؛ أي variable يضبطه الملف يبقى موجودا بعد ذلك. source تستقبل الاسم كـ file argument، فـ add.sh بلا مسار في المجلد الحالي يعمل.
  • bash file args: يبدأ child shell. الـ arguments تصبح $1 و $2 داخله؛ الـ variables الخاصة به تختفي عند انتهائه. مثل source، يستقبل bash الاسم كـ file argument، فـ subtract.sh بلا مسار يعمل.
  • eval bash file $a $b: eval يجمع أولا الـ arguments الخاصة به في سطر نص واحد، ثم يشغل ذلك النص كأمر. هذا مهم عندما يبنى نص الأمر فقط وقت التشغيل، مثلا عندما يعيش الأمر كله داخل variable. مع variables عادية، كما في الأمثلة هنا، تكون النتيجة مطابقة تماما لكتابة bash multiply.sh 6 7؛ السطر يعرض eval فقط لأجل صيغته.
  • exec file args: يستبدل السكربت الجاري تشغيله بالبرنامج المستهدف، فالسطور بعد exec لا تنفذ أبدا. exec يبحث عن هدفه في PATH تماما كأمر مكتوب، فـ divide.sh بلا مسار يفشل بالرسالة exec: divide.sh: not found؛ مسار كامل أو صيغة ./divide.sh كلاهما يعمل. $PWD يحمل نفس النص الذي يطبعه أمر pwd: المسار الكامل للمجلد الحالي.

المفهوم الرابع: الـ Menus بـ select والـ Arguments بـ case

التعريف. select name in list يطبع القائمة كـ menu مرقم، ويظهر السطر التوجيهي #?، ويقرأ رقما، ويخزن الكلمة المطابقة في name. case يقارن قيمة واحدة بعدة أنماط ويشغل فرع أول مطابقة.

الغرض. select يعطي السكربت menu تفاعلي في سطر واحد. case يستبدل سلسلة طويلة من if/elif عندما يجب مطابقة قيمة واحدة مع عدة كلمات ثابتة.

كيف يعمل. select هو loop: يتكرر حتى يخرج السكربت، فالجسم يختبر خيار "Exit" وينادي exit 0:

flowchart TD
    A[Print the numbered list] --> B["Show the #? prompt"]
    B --> C[Read a number]
    C --> D[Store the matching word in language]
    D --> E{Is it Exit?}
    E -- yes --> F[exit 0]
    E -- no --> G[echo the selected word]
    G --> B

مع cut، يتعامل case مع الـ arguments بصيغة key=value:

for arg in "$@"
do
    key=$(echo $arg | cut -f1 -d=)
    value=$(echo $arg | cut -f2 -d=)
    case $key in
        name) echo "Student's name = $value";;
        mark) echo "Obtained mark = $value";;
        *)
    esac
done
  • "$@": كل الـ arguments من سطر الأوامر، كل واحدة تبقى كلمة واحدة حتى لو احتوت فراغات (فـ name="Ahmed Ali" تصل كاملة).
  • cut -f1 -d=: قطع النص عند = (-d= تضبط الفاصل) والاحتفاظ بالحقل 1؛ -f2 تحتفظ بالحقل 2.
  • case $key in ... esac: كل نمط ينتهي بـ )، وكل فرع ينتهي بـ ;;، و *) هو الافتراضي الذي يطابق أي شيء. الفرع الافتراضي هنا فارغ، فمفتاح غير معروف يتجاهل بصمت.

المفهوم الخامس: الـ Strings

التعريف. الـ string هو قطعة نص محفوظة في variable. لا توجد عند Bash string functions منفصلة؛ العمليات الشائعة تتم بالاقتباس (quoting)، والتوسيعات (expansions)، وأوامر صغيرة.

الغرض. السكربتات تدمج النصوص، وتقارنها، وتقسمها إلى كلمات، وتغير حالة أحرفها باستمرار: بناء رسالة، فحص إجابة، قراءة سطر إدخال كلمة بكلمة.

كيف يعمل.

  • الدمج (concatenation): وضع الـ variables جنبا إلى جنب، echo "$string1$string2".
  • المقارنة: if [ "$text" == "Python" ]. ضع الـ variable بين علامتي اقتباس حتى لا يكسر إدخال فارغ الاختبار.
  • التقسيم وتغيير حالة الأحرف هما العمليتان الجديدتان، تعرض كل منهما بسطر أدناه.

تقسيم string إلى array

الـ array هو variable واحد يحمل عدة قيم، مرقمة من 0: arr[0] هي العنصر الأول، و "${arr[@]}" تسرد كل عنصر كل كلمة منفصلة. read -a arr يخزن كل كلمة من إدخاله كالعنصر التالي في arr.

#!/bin/bash
echo "Enter a string value"
read text
IFS=' '
read -a arr <<< "$text"
for value in "${arr[@]}";
do
    printf "$value\n"
done
  • read text: السطر المكتوب كاملا يصل إلى text.
  • IFS=' ': يضبط الـ Internal Field Separator، الحرف الذي يقسم عليه read، إلى فراغ.
  • read -a arr <<< "$text": <<< يغذي الـ string إلى read كما لو كتب، و -a يخزن كل كلمة كالعنصر التالي في arr.
  • for value in "${arr[@]}";: الـ loop يمر على العناصر، عنصرا في كل مرة. الفاصلة المنقوطة ; النهائية قبل نهاية السطر اختيارية وغير ضارة.
  • printf "$value\n": printf يطبع الـ format string كما هو بالضبط، فيجب كتابة \n صراحة للحصول على سطر جديد؛ echo يضيف سطرا جديدا من تلقاء نفسه. كتابة Learn Bash programming تطبع الكلمات الثلاث في ثلاثة سطور.

الـ loop الذي قسم الجملة في المفهوم الأول قسم الكلمات بالطريقة نفسها، لكن كلماتها كانت ثابتة في السكربت؛ هذا السطر يقسم نصا يكتب وقت التشغيل.

تغيير حالة الأحرف

#!/bin/bash
text1='[email protected]'
echo "${text1^^}"
text2='Bash Programming Basics'
echo $text2 | tr '[:upper:]' '[:lower:]'
  • ${text1^^}: توسيع Bash الخاص بالأحرف الكبيرة (uppercase)؛ يطبع [email protected].
  • echo $text2 | tr '[:upper:]' '[:lower:]': tr يحول كل حرف كبير يمر عبر الـ pipe إلى حرف صغير، فيعطي bash programming basics. ضع تعبيرات الأقواس المربعة بين علامتي اقتباس حتى لا يعاملها الـ shell كأنماط أسماء ملفات.

المفهوم السادس: الملفات والـ Pipes

التعريف. المخرجات القياسية (standard output) هي ما يطبعه أمر على الطرفية؛ المدخلات القياسية (standard input) هي ما يقرؤه من لوحة المفاتيح. كلاهما يمكن إعادة توجيهه: > و | ينقلان المخرجات إلى مكان آخر، و read و | يغذيان المدخلات من مكان آخر.

الغرض. الـ redirection تتيح للسكربت الاحتفاظ بالنتائج في ملف وقراءتها بعد ذلك؛ الـ pipe يتيح لعدة أوامر أن تعمل كواحد بدون ملف مؤقت.

كيف يعمل. قراءة ملف:

#!/bin/bash
echo "Reading file using cat command"
content=`cat hardware.txt`
echo $content
 
echo "Reading file line by line using loop"
filename='hardware.txt'
while read line;
do
    echo $line
done<$filename
  • content=`cat hardware.txt`: علامات الاقتباس الخلفية (backticks) هي الصيغة القديمة لـ $( )؛ الملف كاملا يصل إلى variable واحد. echo $content بدون علامات اقتباس يطبعه في سطر واحد، لأن الأسطر الجديدة تصبح فراغات.
  • while read line;: read line تنجح ما دام هناك سطر متبق، فتتوقف while عند نهاية الملف. الفاصلة المنقوطة النهائية قبل نهاية السطر اختيارية، تماما كما في الـ for loop في المفهوم الخامس.
  • done<$filename: يعيد توجيه الملف إلى الـ loop الكامل، فكل read يأخذ السطر التالي من الملف بدلا من لوحة المفاتيح، ويطبع كل سطر منفصلا.

الكتابة: echo $str1 > test.txt يخلق الملف أو يكتب فوقه؛ echo $str2 >> test.txt يضيف إليه.

الـ pipe (|) ينقل المخرجات القياسية لأمر إلى المدخلات القياسية للأمر التالي:

flowchart LR
    A[marks.txt] --> B[sort] -- sorted lines --> C["grep 'Keya'"] -- matching lines --> D[wc -l] --> E[2]

المهمة الأولى: أربع حلقات في سكربت واحد

  1. أنشئ loops.sh بالحلقات الأربع من المفهوم الأول مجمعة في ملف واحد:
#!/bin/bash
# Read a weekday name in each iteration of the loop
for day in Monday Tuesday Wednesday Thursday Friday Saturday Sunday
do
    if [[ $day == 'Monday' || $day == 'Thursday' ]]
    then
        echo "Meeting on $day at 9:30 am"
    elif [[ $day == 'Tuesday' || $day == 'Wednesday' || $day == 'Friday' ]]
    then
        echo "Training on $day at 11:00 am"
    else
        echo "$day is Holiday"
    fi
done
 
# Read each word of a text by using for loop
for value in Bash Programming for the Beginners
do
    echo $value
done
 
echo "Print the numbers which are even and divisible by 5"
counter=1
while [ $counter -le 20 ]
do
    if [[ $counter%2 -eq 0 && $counter%5 -eq 0 ]]
    then
        echo "$counter"
    fi
    ((counter++))
done
echo "Done"
 
n=20
until [ $n -lt 0 ]
do
    if [[ $n%2 -gt 0 ]]
    then
        echo $n
    fi
    ((n=$n-1))
done
  1. شغل bash loops.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
Bash
Programming
for
the
Beginners
Print the numbers which are even and divisible by 5
10
20
Done
19
17
15
13
11
9
7
5
3
1
  1. غير until [ $n -lt 0 ] إلى until [ $n -lt 10 ] وشغل مرة أخرى. المتوقع: الكتلة الأخيرة تتوقف الآن عند 11، لأن الـ loop ينتهي بمجرد أن تنزل n تحت 10.
  2. أزل سطر ((counter++)) من كتلة while، شغل السكربت، واضغط Ctrl+C بعد ثانية. المتوقع: "Done" لا يطبع أبدا، لأن counter تبقى عند 1 للأبد. أعد السطر كما كان.

المهمة الثانية: الـ Functions، الـ Local Variables، والـ Exit Codes

  1. أنشئ func.sh من الدوال الثلاث في المفهوم الثاني وشغل bash func.sh، مكتبا 5 عند السطر التوجيهي. المتوقع:
Bash programming for beginner
Learn bash programming step by step
Enter the radius value
5
Area of the circle is 78.50
  1. أنشئ scope.sh من سطر show_local في المفهوم الثاني وشغل bash scope.sh. المتوقع:
Inside function counter = 5
Outside function counter = 100
  1. احذف الكلمة local في show_local وشغل مرة أخرى. المتوقع: يعرض السطران الآن 5، لأن الـ function كتب فوق global variable خاص بالسكربت.
  2. مراجعة من الدرس السابق: يسلم سكربت نتيجة إلى من استدعاه عبر exit code. أنشئ الملفين أدناه، ثم شغل bash second.sh مرتين، مكتبا 55 ثم 110.

first.sh:

#!/bin/bash
echo "Enter a numeric value"
read n
if [[ $n -le 100 ]]
then
    exit 0
else
    exit 1
fi

second.sh:

#!/bin/bash
bash "first.sh"
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

المتوقع للتشغيلين:

Enter a numeric value
55
The input number is less than or equal to 100
Enter a numeric value
110
The input number is greater than 100

second.sh لا يرى العدد أبدا، بل فقط exit code الخاص بـ first.sh.

المهمة الثالثة: استدعاء السكربتات بـ source و bash و eval و exec

  1. أنشئ السكربتات المساعدة الأربعة. كل سطر ملف كامل.

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"
  1. اجعل السكربتات الأربعة قابلة للتنفيذ، لأن exec يحتاج هدفه قابلا للتنفيذ.
  2. أنشئ callpro.sh. exec يبحث عن هدفه كأمر مكتوب، فيحتاج المسار الكامل لـ divide.sh؛ $PWD يوفر المسار الذي يطبعه pwd، فيعمل السكربت بغض النظر عن المجلد الذي أنشئ فيه:
#!/bin/bash
script1="add.sh"
script2="subtract.sh"
script3="multiply.sh"
script4="$PWD/divide.sh"
 
source "$script1"
echo "After source, result is still visible here: $result"
 
bash $script2 50 20
 
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 because exec replaced the shell"
  1. شغل bash callpro.sh وأدخل 6 ثم 7. المتوقع:
The addition of 60+40=100
After source, result is still visible here: 100
The subtraction of 50-20=30
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

السطر الثاني يعرض 100 لأن source شغل add.sh في الـ shell نفسه. سطر eval يطبع نفس ما تطبعه bash multiply.sh 6 7، كما يوضح المفهوم الثالث. سطر echo الأخير لا يظهر أبدا لأن exec استبدل ذلك الـ shell بـ divide.sh.

  1. غير source "$script1" إلى bash "$script1" وشغل مرة أخرى. المتوقع: السطر الثاني ينتهي مباشرة بعد النقطتين، لأن add.sh عمل في child shell (عملية bash ثانية) وضاع result الخاص به عند خروجه.
  2. غير script4="$PWD/divide.sh" إلى script4="divide.sh" وشغل مرة أخرى. السطر الأخير المتوقع: callpro.sh: line 18: exec: divide.sh: not found، لأن exec بحث في PATH والمجلد الحالي ليس فيه. أعد صيغة $PWD.

المهمة الرابعة: menu وحاسبة سطر أوامر

  1. أنشئ menu.sh:
#!/bin/bash
echo "Select your favorite language"
select language in C# Java PHP Python Bash Exit
do
    if [[ $language == "Exit" ]]
    then
        exit 0
    else
        echo "Selected language is $language"
    fi
done
  • select language in C# Java PHP Python Bash Exit: الكلمات الست هي الـ menu؛ الكلمة المختارة تصل إلى language.
  • if [[ $language == "Exit" ]] ثم exit 0: الطريقة الوحيدة للخروج من الـ loop.
  • echo "Selected language is $language": أي اختيار آخر يطبع ويعود السطر التوجيهي #?.
  1. شغل bash menu.sh، اكتب 3، ثم 6. المتوقع:
Select your favorite language
1) C#
2) Java
3) PHP
4) Python
5) Bash
6) Exit
#? 3
Selected language is PHP
#? 6

الـ menu يطبع مرة واحدة، والسطر التوجيهي #? يعود بعد كل اختيار، و 6 ينهي السكربت من خلال exit 0.

  1. مراجعة: أنشئ cl1.sh، الذي يقرأ ثلاثة وسائط: عدد، و operator، وعدد. x تمثل الضرب لأن * مفردة في سطر الأوامر قد يوسعها الـ shell إلى أسماء ملفات.
#!/bin/bash
echo "Argument values are: $1 $2 $3"
operand1=$1
operand2=$3
operator=$2
 
if [[ $operator == '+' ]]
then
    ((result=$operand1+$operand2))
elif [[ $operator == '-' ]]
then
    ((result=$operand1-$operand2))
elif [[ $operator == 'x' ]]
then
    ((result=$operand1*$operand2))
elif [[ $operator == '/' ]]
then
    ((result=$operand1/$operand2))
fi
echo -e "Result is = $result"

echo -e يفعل رموز الهروب (backslash escapes) مثل \n داخل النص؛ هنا لا يوجد أي منها في النص، فلا يظهر فرق مرئي و echo العادي كان سيطبع السطر نفسه.

  1. شغله أربع مرات: bash cl1.sh 6 + 3، bash cl1.sh 6 - 3، bash cl1.sh 6 x 3، bash cl1.sh 6 / 3. المتوقع:
Argument values are: 6 + 3
Result is = 9
Argument values are: 6 - 3
Result is = 3
Argument values are: 6 x 3
Result is = 18
Argument values are: 6 / 3
Result is = 2
  1. أنشئ cl2.sh من سطر case في المفهوم الرابع، مع #!/bin/bash و echo "Argument values are: $1 $2" كأول سطرين له. شغل bash cl2.sh name="Ahmed Ali" mark=90. المتوقع:
Argument values are: name=Ahmed Ali mark=90
Student's name = Ahmed Ali
Obtained mark = 90
  1. عدل الفرع الافتراضي الفارغ: استبدل السطر *) بـ *) echo "Unknown argument: $key";;. شغل bash cl2.sh name="Ahmed Ali" mark=90 age=20. المتوقع:
Argument values are: name=Ahmed Ali mark=90
Student's name = Ahmed Ali
Obtained mark = 90
Unknown argument: age

السطر الأول لا يزال يعرض وسيطين فقط لأنه يطبع $1 $2؛ الـ loop على "$@" يرى الثلاثة، و age يصل إلى الفرع *).

المهمة الخامسة: الـ Strings والملفات

  1. أنشئ split.sh من سطر التقسيم في المفهوم الخامس. شغله واكتب Learn Bash programming. المتوقع:
Enter a string value
Learn Bash programming
Learn
Bash
programming

الإدخال يحتوي ثلاث كلمات، فالـ array يحتوي ثلاثة عناصر والـ loop يطبع ثلاثة سطور.

  1. أنشئ case.sh من سطر تغيير حالة الأحرف في المفهوم الخامس وشغل bash case.sh. المتوقع:
[email protected]
bash programming basics
  1. أنشئ hardware.txt بكلمة واحدة في كل سطر: Monitor، Keyboard، Mouse، Scanner، Printer. أنشئ readfile.sh من سطر المفهوم السادس وشغل 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
  1. أنشئ writefile.sh، شغله، واكتب Hello Bash ثم Second line:
#!/bin/bash
echo "Enter some text"
read str1
echo $str1 > test.txt
echo "Enter some other text"
read str2
echo $str2 >> test.txt
echo `cat test.txt`
  • read str1 ثم echo $str1 > test.txt: النص الأول يخلق الملف (أو يكتب فوقه).
  • read str2 ثم echo $str2 >> test.txt: النص الثاني يضاف كسطر جديد.
  • echo `cat test.txt`: يطبع محتوى الملف؛ علامات الاقتباس الخلفية بدون اقتباس تحول السطرين إلى سطر واحد، كما في المفهوم السادس.

الجلسة المتوقعة:

Enter some text
Hello Bash
Enter some other text
Second line
Hello Bash Second line

ثم شغل cat test.txt وتوقع السطرين في صفين منفصلين: > خلق الملف و >> أضاف إليه. شغل السكربت مرة أخرى بكلمات مختلفة: الملف يحتفظ فقط بالزوج الجديد، لأن > كتب فوقه.

المهمة السادسة: الـ Pipes

  1. أنشئ marks.txt بهذه الأسطر الستة تماما (الأعمدة مفصولة بفراغات أو tabs):
Asraf   CSE-409         79
Kabir   CSE-304         95
Keya    CSE-101         67
Asraf   CSE-304         88
Keya    CSE-409         90
Asraf   CSE-101         92
  1. اعمل المهمة بالطريقة الطويلة، ثلاثة أوامر وملف مؤقت:
sort marks.txt
grep 'Keya' marks.txt > temp.txt
wc -l temp.txt

المتوقع لناتج الأوامر الثلاثة:

Asraf   CSE-101         92
Asraf   CSE-304         88
Asraf   CSE-409         79
Kabir   CSE-304         95
Keya    CSE-101         67
Keya    CSE-409         90
2 temp.txt

sort يرتب السطور أبجديا (السطور الثلاثة لـ Asraf حسب رمز المقرر)، و grep يكتب سطري Keya في temp.txt، و wc -l يعدهما.

  1. اعملها بالطريقة القصيرة: sort marks.txt | grep 'Keya' | wc -l. المتوقع: 2. لا حاجة إلى temp.txt، فكل مخرج ينتقل مباشرة إلى الأمر التالي.
  2. غير الـ pipeline ليعد Asraf بدلا من ذلك. المتوقع: 3.

الخلاصة

  • for word in list يمر على قائمة، سواء كانت القائمة مجموعة أسماء أو كلمات جملة؛ while يعمل ما دام الاختبار ينجح، و until ما دام يفشل. كل جسم يقع بين do ... done.
  • حدث العدادات بـ ((counter++)) أو ((n=$n-1))؛ loop بدون تحديث لا ينتهي أبدا.
  • الـ functions تعرف بصيغة name() { } مع أو بدون الكلمة المفتاحية function، ويجب أن تعرف قبل استدعائها، وتستقبل الـ arguments كـ $1 و $2. الـ variables تكون global بشكل افتراضي؛ local تحفظ واحدا داخل الـ function فقط. الـ function يسلم نصا عبر ضبط global variable؛ السكربتات الكاملة تسلم نتيجة بـ exit N، تقرأ عبر $?.
  • source يحفظ الـ variables الخاصة بالسكربت المستدعى؛ bash يشغله في child shell؛ eval يبني نص الأمر أولا ويتصرف كـ bash عندما يكون النص عاديا؛ exec يستبدل الـ shell الحالي نهائيا ويحتاج مسارا كاملا مثل $PWD/divide.sh.
  • select يبني menu مرقم بالسطر التوجيهي #? ويتكرر حتى exit؛ case ... esac يطابق الأنماط، و *) هو الافتراضي؛ cut -f1 -d= يقسم key=value.
  • الـ Strings: الدمج بالتجاور، المقارنة بـ [ "$a" == "$b" ]، التقسيم إلى array بـ IFS مع read -a والمرور عليه بـ "${arr[@]}"، تغيير حالة الأحرف بـ ${var^^} أو tr. printf يحتاج \n صريحة.
  • اقرأ الملفات بـ while read line; do ...; done<file، اكتب بـ > (الكتابة فوق الملف) و >> (الإضافة)، وسلسل الأوامر بـ |، الذي يربط المخرجات القياسية لأمر بالمدخلات القياسية للأمر التالي.