6.2 لغة البرمجة bash | كتاب لينكس الشامل | >> |
تحدثنا من قبل عن أصل كلمة bash وهي تعني
Bourne Again SHell
وهو نفسه مفسر الأوامر التي تطبعها والتي تحدثنا عنها من قبل في قسم
سطر الأوامر ليس مخيفا
وهو أيضا يمكنه أن يحاكي مفسر sh التقليدي (القديم)
وذلك بعمل وصلة link منه باسم sh ،
ولكن bash ليس أفضل مفسر للأوامر ولكنه الأكثر شهرة
وهناك الكثير غيره مثل ksh و csh و zsh
وتحدثنا في قسم نظرة تشريحية في لينكس
كيف تختار مفسر الأوامر الذي تريد
وذلك بتعديل آخر حقل من السطر المقابل للمستخدم الذي تريد من ملف
/etc/passwd
ولأن معظم التوزيعات تختار bash بشكل تلقائي أحببت أن أتحدث عنه
تكمن أهمية تعلم هذه الطريقة في أنك تتعلم مقدمة عن لغات البرمجة التفسيرية وكيفية التعامل معها في لينكس،وأيضا في القيام بالواجبات الموكولة إليك كمدير للنظام والتي تقوم بها بشكل متكرر ولتوفر على نفسك طباعة عدد كبير من الأوامر وتعتبر اللغات التفسيرية طريقة سريعة وقذرة ؛ سريعة ذلك أن عمل برنامج عليها لايحتاج سوى لكتابة ذلك البرنامج في أي محرر نصوص كما هو من دون تصنيف compile وقذرة لأن الأخطاء التي قد تحدث أثناء تنفيذ البرنامج run-time متوقعة وقد تحدث دون أن يعطيك تحذير عليها (لأنك لم تعمل compile) وأيضا لأن البرنامج قد ينفذ برامج خارجية قد تختلف اصدارتها عن الإصدار الذي صمم من أجله ولأنها ليست بلغة الآلة تكون أقل سرعة (هذا لايتناقض مع كون كتابة البرنامج لا تأخذ وقت)
ذكرنا سابقاً أنه عند تنفيذ ملف فإن لينكس
يعرف كيف سينفذ هذا الملف من بنيته الداخلية
وليس من الإمتداد،نفصل ذلك الآن فنقول إذا كان الملف يبدأ ب #!
والتي تسمى sha-bang فإن هذا الملف برنامج بلغة تفسيرية وما يأتي بعدها هو
اسم البرنامج المفسر مثلا ملف يحتوي على التالي
#!/bin/bash # filename.sh : this is a do-nothing script ... # bash code goes here ...
/bin/bash
وتمرير هذا الملف ليفسره
وسيكون له نفس التأثير لو كتبنا
bash$ /bin/bash filename.sh
#!/usr/bin/perl -w # filename.pl : this is a perl script ... # perl code goes here ...
bash$ /usr/bin/perl -w filename.pl
عند النقر المزدوج على ملفات ذات الامتداد الخاص بلغة تفسيرية
مثل hello.sh
في غنوم و KDE فإنه قد يفتحه في محرر نصوص
بدلاً من تنفيذه. إذا لم تحب هذا السلوك اجعله بدون امتداد مثلاً hello-sh
bash$ which bash /bin/bash
#!/usr/bin/env bash # filename.sh : this is a do-nothing script ... # bash code goes here ...
باختصار الخطوة الأول هي أن تفتح محرر النصوص المفضل لديك
ثم تكتب #!/bin/bash
ثم تكتب البرنامج ثم تخزنه بأي اسم تريد وأي امتداد تريد ولكن يفضل أن يكون ذا معنى مثل
.sh
الخطوة الثانية هي أن تسمح للجميع بتنفيذ هذا الملف وذلك بكتابة
chmod +x filename.sh
الآن لتنفيذ هذا البرنامج كل ما عليك هو كتابة اسمه والمسار
مثلا
~/my-scripts/filename.sh
واذا كنت في ذلك المجلد الذي يحتوي الملف
يكفي أن تكتب بدل المسار './'
dot-slash
والتي تعني الدليل الحالي
مثلاً ./filename.sh
واذا كان الدليل الحالي أي النقطة '.'
موجود ضمن المسارات
$PATH
كما هو الحال عادةً
فيمكنك كتابة اسم الملف filename.sh
ولكن يفضل أن تتعود على كتابة ./
حتى تتجنب الحالة التي يوجد فيها برنامج بهذا الاسم في مجلد
له أولوية أعلى في ترتيب ال $PATH
، وإذا أردت أن ينفذ برنامجك بدون مقدمات يجب أن تضعه في احدى المجلدات المكتوبة في
$PATH
لتعرف هذه المجلدات اكتب
bash$ echo $PATH /bin:/usr/bin:/sbin:/usr/sbin:/usr/X11R6/bin:.
كل ما بعد رمز ال hash
هو تعليق لا يتم تنفيذه
كل سطر هو عبارة عن اما أمر مبني ضمن bash
أو سلسلة من برامج لينكس التي تعلمناها سابقا مثل
echo
و mkdir
و cp
وغيرها من الأوامر
لهذا يكون أول برنامج لنا هكذا
#!/bin/bash # hello.sh : this is a hello-world script echo "hello world"
hello.sh
انظر النتيجة
لتعرف متغير اكتب اسم المتغير ثم $
ثم قيمة المتغير دون مسافات
ولتعوض قيمة المتغير ضع
$
قبل اسم المتغير
مثلا يصبح برنامجنا بالشكل التالي
#!/bin/bash # hello.sh : this is a hello-world script str1="hello world" echo "$str1"
$
هو تعويض قيمة المتغير مكان اسمه
هنا قيمة المتغير str1
هي hello world
فيصبح معنى
"$str1"
بابدال str1 مكان hello world
فيصبح الأمر هو
echo "hello world"
وقد وضعت "$str1"
داخل علامة التنصيص لأن echo يجب أن تأخذ النص
على شكل معامل واحد فإذا لم أضعها يصبح الأمر
echo hello world
وهذا خطأ
تسمى علامة التنصيص المزدوج التي استعملناها سابقا
بالتنصيص الضعيف لأنه بدلا من أخذ كل ما هو بينها كما هو
كما يوحي الاسم (التنصيص/الإقتباس) فهي تقوم
على عمل بعض التعديلات أحيانا كما في حالة تعويض قيمة المتغير
وهناك الكثير من الحالات الأخرى منها ما يسمى escaping
وهو استخدام رمز خاص هو back-slah '\'
متبوعاً بأحد ما يلي:
\n | سطر جديد | ||||||
\r | العودة لبداية السطر الحالي للكتابة فوقه | ||||||
\b | backspace حذف حرف للوراء | ||||||
\f | formfeed صفحة جديدة | ||||||
\a | تصدر صوت alert | ||||||
\t | tab أي مسافة جدولة | ||||||
\nnn | تعني الرمز المقابل ل nnn حيث nnn رقم بالثماني مثلا 33 هي escape و 101 تعني حرف A | ||||||
\xnn |
تعني الرمز المقابل ل nnn حيث nnn رقم بالست-عشري مثلا1b هي escape و 41 تعني حرف A
| ||||||
\ مع أي شيء آخر |
تعني الشيء الآخر نفسه مثلا
|
#!/bin/bash # hello.sh : this is another hello-world script str1="hello world" str2="hello world again" echo -e "$str1\n$str2"
hello world hello world again
\n
النوع الآخر من التنصيص هو التنصيص القوي ويكون عن طريق علامة التنصيص
المفردة '
وهي الموجودة فوق حرف الطاء
في لوحة المفاتيح الإنجليزية
حيث لا يتم تعويض أي من المتغيرات مثلا
str1="hello world" echo '$str1'
$str1
كيف تكتب أمر ليطبع it's good to see you
مستخدماً علامة تنصيص مفردة.
هناك بعض المتغيرات الخاصة التي يعرفها bash منها
|
|
إذا أردت أن تأخذ الخرج القياسي لبرنامج
وتعويضه في مكان معين مثل قيمة متغير
ويتم ذلك بوضع الأامر داخل علامة `
وهي الرمز الموجد عند حرف الذال العربي في لوحة المفاتيح الإنجليزية
أو بوضع الأمر بين قوسين () مسبوقين ب $
مثلا برنامج whoami
يكتب اسمك فإذا كنت تريد أن
يقول لك hello ahmad اكتب البرنامج التالي
#!/bin/bash # hello.sh : this is a hello-world script echo "hello `whoami`" #echo "hello $(whoami)"
#!/bin/bash # whereami.sh : this is a 'where am I' script str1="`whoami`" str2="`pwd`" echo "$str1 is now viewing the \"$str2\" folder"
ahmad is now viewing the "/home/ahmad/my-scripts/" folder
read
مثلاً read you_name
ويمكنك إجراء بعض الحسابات على الأعداد الصحيحة
والسلاسل النصية بأمر expr
اكتب expr --help
لترى ما هي العمليات التي يوفرها
هذا الأمر يستقبل الأرقام والعمليات على شكل معاملات منفصلة(مسافة مثلاً)
لهذا لا تنجح expr '12*2-7'
والصواب
expr 12 '*' 2 - 7
ولاحظ علامة التنصيص
حول * لمنع bash من تعويضها بأسماء الملفات
لنأخذ هذا المثال البسيط
#! /bin/bash # rect-area.sh : a script to find area of rectangle echo -n "Enter width: " read width echo -n "Enter height: " read height area=`expr $width '*' $height` echo "Area of rectangle=${width}x${height}=$area"
bash$ ./rect-area.sh Enter width: 15 Enter height: 3 Area of rectangle=15x3=45 bash$
{}
حول اسم المتغير بهذا نقول ل bash أن x ليست تابعة لاسم المتغير
أي أن اسم المتغير ليسwidthx
طبعاً يمكن تجنب هذا بوضع مسافة.
يمكننا أيضاً في bash ولكن ليس sh أن نستفيد من الأمر let
ضع let "area = width * height"
مكان السطر حيث area=`expr $width '*' $height`
.
يمكن أيضاً استخدام اسلوب التعويض الحسابي
وهو وضع قوسين مسبوقين ب ‘$‘ وهنا لا ضرورة لعلامة التنصيص
area = $(($width*$height))
العمليات التي يمكن أن يقوم بها BASH هي (وهي المألوفة لمبرمج سي)
# from BASH INFO `ID++ ID--' variable post-increment and post-decrement `++ID --ID' variable pre-increment and pre-decrement `- +' unary minus and plus `! ~' logical and bitwise negation `**' exponentiation `* / %' multiplication, division, remainder `+ -' addition, subtraction `<< >>' left and right bitwise shifts `<= >= < >' comparison `== !=' equality and inequality `&' bitwise AND `^' bitwise exclusive OR `|' bitwise OR `&&' logical AND `||' logical OR `expr ? expr : expr' conditional evaluation `= *= /= %= += -= <<= >>= &= ^= |=' assignment `expr1 , expr2' comma
area=`echo "$width * $height" | bc`
أو برنامج dc مثل نص لتحليل عدد لعوامله الأولية راجع فصل البرامج العلمية
#!/bin/bash # fact.sh factroize an integer to primes echo -n "Enter an integer: " read n echo "$n[p]s2[lip/dli%0=1dvsr]s12sid2%0=13sidvsr\ [dli%0=1lrli2+dsi!>.]ds.xd1<2" | dc
قبل أن نبدأ بالجمل الشرطية لنتذكر أن البرامج التي ننفذها تعيد رقم يمثل حالتان هما النجاح (0) والفشل (أي رقم غير الصفر وهو يمثل سبب الفشل) وهذا المنطق مقلوب بعض الشيء لأن العادة في لغات البرمجة أن الصفر خطأ و غير ذلك(1 مثلاً) صواب. هذا يجعل الشرط في bash غامضاً لدرء الغموض نستعمل هنا نجاح success لنرمز للصفر و فشل fail لغير الواحد . انظر هذا المثال يبحث عن كلمة vfat في ملف fstab
bash$ grep -e vfat /etc/fstab >/dev/null; echo $? 0
أسهل طرق الشرط هو استعمال && و || الأولى تنفذ ما بعدها إذا نجح ما قبلها والأخرى إذا لم ينجح
bash$ grep -e vfat /etc/fstab >/dev/null && > echo "yorika! I found It." || > echo "good luck! no FAT found" yorika! I found It.
# File related checks # From bash info page `-a FILE' `-e FILE' True if FILE exists. `-b FILE' True if FILE exists and is a block special file. `-c FILE' True if FILE exists and is a character special file. `-d FILE' True if FILE exists and is a directory. `-f FILE' True if FILE exists and is a regular file. `-g FILE' True if FILE exists and its set-group-id bit is set. `-h FILE' True if FILE exists and is a symbolic link. `-k FILE' True if FILE exists and its "sticky" bit is set. `-p FILE' True if FILE exists and is a named pipe (FIFO). `-r FILE' True if FILE exists and is readable. `-s FILE' True if FILE exists and has a size greater than zero. `-t FD' True if file descriptor FD is open and refers to a terminal. `-u FILE' True if FILE exists and its set-user-id bit is set. `-w FILE' True if FILE exists and is writable. `-x FILE' True if FILE exists and is executable. `-O FILE' True if FILE exists and is owned by the effective user id. `-G FILE' True if FILE exists and is owned by the effective group id. `-L FILE' True if FILE exists and is a symbolic link. `-S FILE' True if FILE exists and is a socket. `-N FILE' True if FILE exists and has been modified since it was last read. `FILE1 -nt FILE2' True if FILE1 is newer (according to modification date) than FILE2, or if FILE1 exists and FILE2 does not. `FILE1 -ot FILE2' True if FILE1 is older than FILE2, or if FILE2 exists and FILE1 does not. `FILE1 -ef FILE2' True if FILE1 and FILE2 refer to the same device and inode numbers.
bash$ [ -e /etc/fstab ] && > echo "you have an 'fstab' so what!" || > echo "Ooops! where is your fstab" you have an 'fstab' so what!
`INT1 -eq INT2' `INT1 -ne INT2' `INT1 -lt INT2' `INT1 -le INT2' `INT1 -gt INT2' `INT1 -ge INT2' return true if INT1 is equal to, not equal to, less than, less than or equal to, greater than, or greater than or equal to INT2, respectively. `-z STRING' True if the length of STRING is zero. `-n STRING' `STRING' True if the length of STRING is non-zero. `STRING1 == STRING2' True if the strings are equal. `=' may be used in place of `==' for strict POSIX compliance. `STRING1 != STRING2' True if the strings are not equal. `STRING1 < STRING2' True if STRING1 sorts before STRING2 lexicographically in the current locale. `STRING1 > STRING2' True if STRING1 sorts after STRING2 lexicographically in the current locale.
-o
أو ‘و‘ من خلال
-a
صيغة جملة if هي كما يلي
if TEST-COMMANDS; then CONSEQUENT-COMMANDS; elif TEST-COMMANDS; then CONSEQUENT-COMMANDS; else CONSEQUENT-COMMANDS; fi
case WORD in PATTERN ) CONSEQUENT-COMMANDS ;; PATTERN1 | PATTERN2 ) CONSEQUENT-COMMANDS ;; esac # PATTERN can have * ? [] .. etc
#!/bin/bash # if.sh: a sample if statment script read a read b if (( a < b)); then echo "$a is less than $b."; else echo "$a is greater than or equal to $b."; fi
#!/bin/bash # oddeven.sh: tell if a given number odd or even echo "enter a number" h=`read` let "remainder = h % 2" if [ "$remainder" -eq 0 ] # Even? then echo "$h is even" else echo "$h is odd" fi
#!/bin/bash # case.sh: a sample case statement script echo -n "enter a color : " read c case $c in blue ) echo "I like blue too.";; red ) echo "red is not bad.";; green ) echo "green is good.";; yellow | orange | cyan | magenta ) echo "$c is not a basic color.";; * ) echo "is $c a real color?";; esac
# from BASH INFO `*' Matches any string, including the null string. `?' Matches any single character. `[...]' Matches any one of the enclosed characters. A pair of characters separated by a hyphen denotes a RANGE EXPRESSION; any character that sorts between those two characters, inclusive, using the current locale's collating sequence and character set, is matched. If the first character following the `[' is a `!' or a `^' then any character not enclosed is matched. A `-' may be matched by including it as the first or last character in the set. A `]' may be matched by including it as the first character in the set. The sorting order of characters in range expressions is determined by the current locale and the value of the `LC_COLLATE' shell variable, if set. If the `extglob' shell option is enabled using the `shopt' builtin, several extended pattern matching operators are recognized. In the following description, a PATTERN-LIST is a list of one or more patterns separated by a `|'. Composite patterns may be formed using one or more of the following sub-patterns: `?(PATTERN-LIST)' Matches zero or one occurrence of the given patterns. `*(PATTERN-LIST)' Matches zero or more occurrences of the given patterns. `+(PATTERN-LIST)' Matches one or more occurrences of the given patterns. `@(PATTERN-LIST)' Matches exactly one of the given patterns. `!(PATTERN-LIST)' Matches anything except one of the given patterns.
هذا ملخص للحلقت التكرارية في bash
until TEST-COMMANDS; do CONSEQUENT-COMMANDS ; done while TEST-COMMANDS; do CONSEQUENT-COMMANDS ; done for NAME in WORDS; do CONSEQUENT-COMMANDS ; done for (( EXPR1 ; EXPR2 ; EXPR3 )) ; do CONSEQUENT-COMMANDS ; done #you may use 'break' or 'continue'
#!/bin/sh # gz-all.sh: replace all files with GZIPed version for i in ./* do gzip -9 "$i" done
#!/bin/bash # c-for.sh: c-like for loop echo "this is a counter" MAX=5 for ((i=1; i<=MAX; i++)) do echo "$i" done
الأجزاء التي تتكرر كثيراً في برنامج ما يمكن وضعها في وظيفة function يتم استدعؤها call عند الضرورة وتمرير لها بعض المعاملات وهي متغيرات يفترض أن تحدد سلوكات مختلفة لنفس الوظيفة. فيما يسمى بأسلوب البرمجة الهيكلية حيث يقسم البرنامج إلى أجزاء كل منها يقوم بمهمة بسيطة ولكن بكفاءة مما يسهل تتبع البرنامج لأن المتغيرات المحلية داخل تلك الوظئيفة لا تعتمد قيمتها على الأجزاء الخارجية ولأن ذلك يسمح لك بتجربة(فحص) كل وظيفة بشكل مستقل. عمل وظيفة يكون بوضع () بعد اسم الوظيفة ووضع الأوامر بين {} و يمكن أن تسبق اسم الوظيفة بكلمة function (إذا أردت).
#! /bin/sh # func.sh : a script using functions TellTime() { echo "It's `date +%I:%M` now" } function SayHello() { echo "Hello `whoami`" echo "It's a nice day" } # this is the main/global part SayHello TellTime
#! /bin/sh # rect-area.sh : a script to find area of rectangle # this function calc the rectangle area RectArea() { width="$1" height="$2" area=`echo "$width * $height" | bc` echo "$area" } # this is the main/global part echo -n "Enter width: " read width echo -n "Enter height: " read height area=`RectArea $width $height` echo "Area of rectangle=${width}x${height}=$area"
$1
والعرض $2
وليس معامل تنفيذ الscript كله
يمكنك أن تضع الأجزاء المكرر
أو الوظائف التي تستعملها بكثر في ملف منفصل (مكتبة) وتستدعيه
في ملف برنامجك وهذه الطريقة تسمى source وهي تشبه عملية include في سي.
تتم هذه العملية بكتابة source FILE
أو . FILE
تستخدم هذه الطريقة بكثرة في نصوص الإقلاع و الخدمات
/etc/rc.d
حيث يوجد ملف اسمه functions يحتوي على الوظائف الشائعة
التي تقوم بها الخدمات عند نجاح المهمة (يكتب كلمة OK في مكان معين
باللون الأخضر) أو فشلها (يكتب FAIL باللون الأحمر).
هذا المثال يوضح ذلك
# rect-tool.sh: this is a tool lib for rect-client.sh # it need not be executable (chmod -x) function RectArea() { width="$1" height="$2" area=$(echo "$width * $height" | bc) echo "$area" }
#!/bin/bash # rect-client.sh: main file of rectangle project! . ./rect-tool.sh # this is the main/global part echo -n "Enter width: " read width echo -n "Enter height: " read height area=$(RectArea $width $height) echo "Area of rectangle=${width}x${height}=$area"
البرنامج التالي يبحث عن الأقراص المدمجة ويضع
الوصلات links المناسبة في دليل dev.
هذا البرنامج أستعمله في التوزيعات
القديمة التي لا تتعرف على
الأجهزة المضافة تلقائياً فأجعل هذا البرنامج
ينفذ كلما أقلع لينكس. فيصبح /dev/cdrom
يشير إلى
/dev/hdb
مثلاً
#!/bin/sh # findcds.sh: find IDE cdroms and fix the lins in /dev if [ "$UID" = "0" ];then echo "Error: You must be a root run 'su' 1st." exit 1 fi [ ! -d /proc/ide ] && mount none /proc -t proc || exit 1 echo -n "finding IDE CDROMs : " cds="" for i in hda hdb hdc hdd hde hdf hdg hdi hdj do echo -n "." if [ -f /proc/ide/$i/media ];then dv=`cat /proc/ide/$i/media` [ "cdrom" = "$dv" ] && cds="$cds $i" fi echo -n "." done echo " OK. [$cds ]" CDS_LIST="`echo "$cds " | sed -e 's/^ //'`" echo -n -e "updating /dev/cdrom link ... " CD=`echo "$CDS_LIST" | cut -d ' ' -f 1` echo "/dev/$CD" ln -sf "/dev/$CD" /dev/cdrom CDS_LIST=`echo "$CDS_LIST" | cut -d ' ' -f 2-` N="2" for i in $CDS_LIST do echo -n -e "updating /dev/cdrom$N link ... " echo "/dev/$i" ln -sf "/dev/$i" "/dev/cdrom$N" N=`expr $N + 1` done
/dev/cdrom
والباقيات أعطها رقم مثلاً /dev/cdrom2
.
البرنامج التالي استخدمه في توليد قائمة المصطلحات فهو يعمل على حذف كل شيء ليس إنجليزي أو أرقام و ‘-‘ و ‘\‘ و ‘/‘ وتحويلها إلى سطر جديد لهذا تصبح كل كلمة الإنجليزية(أو مصطلح مثل apt-get) في سطر منفصل ثم حذف الأسطر التي لا تحتوي أحرف ثم ترتيبها أبجدياً وبقى تحريره وتنظيفه يدوياً.
#!/bin/sh rm /tmp/glassory.txt.tmp 2>/dev/null for i in ./*.html do echo -n "processing [$i] ... " cat $i | sed -e "s/[^0-9A-Za-z\\\/\-]/+/g" | tr A-Z a-z | tr -s "+" "\012" | sed -e "s/^[0-9\\\/ ]*//g" >> /tmp/glassory.txt.tmp echo " OK " done echo -n "sorting ... " cat /tmp/glassory.txt.tmp | sort | uniq > ./glassory.txt rm /tmp/glassory.txt.tmp echo "done!"
إذا كان البرنامج يقوم بعملية بتخزين ملفات مؤقتة عليه حذفها قبل الخروج.فإن خرج بضغط CTRL+C أو بأداة kill فإنه لن يصل للجزء الذي يقوم بحذف الملفات الزائدة. بعض النصوص البرمجية تقوم بحفظ نسخة احتياطية من ملفات الإعدادات قبل أن تبدأ ثم تعدلها فإن فشلت تعيد القديمة ولكن ماذا لو خرج المستخدم قبل إتمام الحسابات دون أن يستعيد النسخة الاحتياطية. حالة أخرى هي بأنك تريد إهمال بعض الإشارات التي يرسلها المستخدم والكثير من التطبيقات الأخرى التي تتطلب تتبع استقبال أي إشارة. يفور bash طريقة مرنة لذلك مثلاً لحذف ملف عند استلام إشارة الإنهاء بوساطة لوحة المفاتيح أو kill فإن الأمر
trap "rm /tmp/mytmp" 2 3 9
#!/bin/bash echo "Enter something, or try to send me a signal - CTRL+C -" # 18 20 24 and 17 19 23 for CTRL+Z # 2 3 9 for CTRL+C and kill -KILL trap "echo 'Hi, we have a signal'" 18 20 24 17 19 23 2 3 9 read a
signal(7)
مثلاً بكتابة man 7 signal
انظر BASH info pages و Advanced Bash-Scripting Guide
<< السابق | كتاب لينكس الشامل | التالي >> |