فصل 24- توابع

‎24.1‎- توابع پیچیده و پیچیدگی توابع

توابع می‌توانند شناسه‌های داده شده به آنها را پردازش نموده و برای پردازش بعدی یک کد خروج به اسکریپت برگشت بدهند.

function_name $arg1 $arg2

تابع به شناسه‌های داده شده به وسیله مکان آنها مراجعه می‌کند ( همچنانکه اگر آنها پارامترهای مکانی بودند)، یعنی، ‎$1‎‏، ‎$2‎، و الی آخر.

مثال ‎24-2‎. تابع پذیرنده پارامترها

#!/bin/bash
#توابع و پارامترها

DEFAULT=default                             #           مقدار پارامتر پیش‌فرض.

func2 () {
   if [ -z "$1" ]                           #آیا طول پارامتر شماره ‎1‎ صفر است؟
   then
     echo "-Parameter #1 is zero length.-"  # یا پارامتری عبور داده نشده است.
   else
     echo "-Parameter #1 is \"$1\".-"       
   fi

   variable=${1-$DEFAULT}                   # جایگزینی پارامتر چه نشان می‌دهد؟
   echo "variable = $variable"              
                                            #     ---------------------------
                                            # باعث تشخیص میان نبودن پارامتر و
                                            #+       پارامتر تهی خواهد گردید.

   if [ "$2" ]
   then
     echo "-Parameter #2 is \"$2\".-"
   fi

   return 0
}

echo
   
echo "Nothing passed."   
func2                          #      بدون پارامتر فراخوانی گردیده است
echo


echo "Zero-length parameter passed."
func2 ""                       #   با پارامتری به طول صفر فراخوانی شده
echo

echo "Null parameter passed."
func2 "$uninitialized_param"   #با پارامتر بدون مقدار اولیه، احضار شده
echo

echo "One parameter passed."   
func2 first           #   با یک پارامتر فراخوانی گردیده است
echo

echo "Two parameters passed."   
func2 first second    #      با دو پارامتر فراخوانی شده است
echo

echo "\"\" \"second\" passed."
func2 "" second       # با پارامتر اول به طول صفر و یک رشته
echo                  #اسکی به عنوان پارامتر دوم احضار شده.

exit 0

important

فرمان shift روی پارامترهای داده شده به توابع عمل می‌کند ( مثال ‎36-18‎ را ببینید ).

اما در مورد شناسه‌های خط فرمان داده شده به اسکریپت چطور؟ آیا تابع آنها را می‌بیند؟ بسیار خوب، اجازه بدهید آشفتگی را برطرف کنیم.

مثال ‎24-3‎. توابع و شناسه‌های خط فرمان عبور داده شده به اسکریپت

#!/bin/bash
# func-cmdlinearg.sh
# این اسکریپت را با یک شناسه خط فرمان
#+ فراخوانی کنید، چیزی مشابه  ‎$0 arg1‎

func ()

{
echo "$1"   #شناسه اول داده شده به تابع را نشان می‌دهد.
}           #  آیا یک شناسه خط فرمان را شناسایی می‌کند؟

echo "First call to function: no arg passed."
echo "See if command-line arg is seen."
func
#خیر! شناسه خط فرمان دیده نمی‌شود.

echo "============================================================"
echo
echo "Second call to function: command-line arg passed explicitly."
func $1
#اکنون دیده می‌شود!

exit 0
مترجم:  به طور کلی استفاده از نماد پارامترهای مکانی در داخل تابع به معنی پارامتر مکانی تابع خواهد بود و
در خارج از تابع به معنی پارامتر مکانی اسکریپت است.
بنابراین در احضار تابع به صورت ‎func $1‎  ابتدا ‎$1‎ به پارامتر مکانی اسکریپت بسط داده می‌شود و سپس
تابع اجرا می‌شود و آن پارامتر به پارامتر مکانی تابع تبدیل می‌شود و در تابع با نماد ‎$1‎ قابل دستیابی می‌گردد.



در مقایسه با برخی زبان‌های برنامه‌نویسی دیگر، اسکریپت‌های پوسته به طور معمول فقط مقدار پارامترها را به توابع عبور می‌دهند. نام متغیرها (که در واقع اشاره گر هستند)، اگر به صورت پارامتر به تابع داده شوند، به عنوان رشته‌های لفظی تلقی خواهند گردید. توابع شناسه‌هایشان را به طور لفظی تفسیر می‌کنند.

ارجاع‌های متغیر غیرمستقیم ( مثال ‎37-2‎ را ببینید) یک نوع مکانیسم ناهنجار برای عبور دادن اشاره‌گرهای متغیر به توابع فراهم می‌کنند.

مثال ‎24-4‎. عبور دادن یک مرجع غیرمستقیم به یک تابع

#!/bin/bash
# ind-func.sh:    عبور دادن یک مرجع غیرمستقیم به یک تابع.

echo_var ()
{
echo "$1"
}

message=Hello
Hello=Goodbye

echo_var "$message"            #                    Hello
#          اکنون، بیایید یک مرجع غیرمستقیم به تابع بدهیم.
echo_var "${!message}"         #                  Goodbye

echo "-------------"

#اگر محتویات متغیر hello را تغییر بدهیم چه اتفاقی می‌افتد؟
Hello="Hello, again!"
echo_var "$message"            #                    Hello
echo_var "${!message}"         #            Hello, again!

exit 0

پرسش منطقی بعدی آن است که آیا بعد از اینکه پارامترها به تابع عبور داده شدند، می‌توانند dereference بشوند.
(مترجم: dereference عبارت است از دستیابی اطلاعات از طریق نشانی موجود در یک متغیر)

مثال ‎24-5‎.‏ dereference کردن پارامتر عبور داده شده به یک تابع

#!/bin/bash
# dereference.sh
#dereference کردن پارامتر عبور داده شده به یک تابع.
#                      اسکریپت نوشته ‎Bruce W. Clare‎

dereference ()
{
     y=\$"$1"          # نام متغیر (نه محتوای آن!).
     echo $y           # ‎$Junk‎

     x=`eval "expr \"$y\" "`
     echo $1=$x
     eval "$1=\"Some Different Text \""  #تخصیص مقدار جدید.
}

Junk="Some Text"
echo $Junk "before"    #‎Some Text before‎

dereference Junk
echo $Junk "after"     #‎Some Different Text after‎

exit 0

مثال ‎24-6‎. بار دیگر، dereference کردن پارامتر عبور داده شده به یک تابع

#!/bin/bash
#  ref-params.sh: dereference کردن پارامتر عبور داده شده به یک تابع.
#  (مثال پیچیده)

ITERATIONS=3  #تعداد دریافت‌های ورودی.
icount=1

my_read () {
  #  ‎my_read فراخوانی شده با نام متغیر، مقدار قبلی را به عنوان مقدار
  #+ پیش‌فرض، میان براکت‌ها بیرون می‌دهد، سپس مقدار جدید را جویا می‌شود.

  local local_var

  echo -n "Enter a value "
  eval 'echo -n "[$'$1'] "'  #                           مقدار قبلی.
# eval echo -n "[\$$1] "     #  فهمیدن آن آسانتر است، اما در اعلان به
                             #+کاربر، فاصله انتهایی را از دست می‌دهد.
  read local_var
  [ -n "$local_var" ] && eval $1=\$local_var

  #  ‎And-list‎: اگر local_var موجود است، آنوقت ‎$1‎ مساوی مقدار آن بشود.
}

echo

while [ "$icount" -le "$ITERATIONS" ]
do
  my_read var
  echo "Entry #$icount = $var"
  let "icount += 1"
  echo
done  

#  تشکر از ‎Stephane Chazelas‎ برای در اختیار گذاشتن این مثال آموزنده.

exit 0

خروج و برگشت دادن

وضعیت خروج

توابع یک مقدار را که وضعیت خروج نامیده می‌شود برگشت می‌دهند. این مورد، قابل قیاس با وضعیت خروج برگشت شده به وسیله یک فرمان است. وضعیت خروج می‌تواند به طور صریح با یک دستور return مشخص بشود، در غیر اینصورت برابر با وضعیت خروج آخرین فرمان اجرا شده در تابع خواهد بود (‎‎0‎ اگر موفق باشد، و یک کد خطای غیرصفر اگر موفق نباشد). این وضعیت خروج با مراجعه به آن به عنوان ‎$?‎ می‌تواند در اسکریپت استفاده گردد. این ساز و کار به طور موثر اجازه می‌دهد که توابع اسکریپت، دارای یک «مقدار برگشتی» مشابه توابع ‎C‎ باشند.

return

یک تابع را خاتمه می‌دهد. یک فرمان return‎[1]‎ به طور اختیاری یک شناسه عدد صحیح می‌پذیرد، که به عنوان «وضعیت خروج» به اسکریپت احضار کننده تابع برگشت داده می‌شود، این وضعیت خروج به متغیر ‎$?‎ تخصیص داده می‌شود.

مثال ‎24-7‎. ماکزیمم دو عدد

#!/bin/bash
# max.sh:  ماکزیمم دو عدد صحیح.

E_PARAM_ERR=250    #     اگر کمتر از دو پارامتر به تابع داده شده باشد.
EQUAL=251          #   مقدار برگشتی در صورتیکه دو پارامتر مساوی باشند.
#     مقادیر خطا خارج از محدوده اعدادی است که می‌توان به تابع عبور داد.

max2 ()             #         از بین دوعدد، عدد بزرگتر را برگشت می‌دهد.
{                   #توجه: اعداد مورد مقایسه باید کوچکتر از ‎250‎ باشند.
if [ -z "$2" ]
then
  return $E_PARAM_ERR
fi

if [ "$1" -eq "$2" ]
then
  return $EQUAL
else
  if [ "$1" -gt "$2" ]
  then
    return $1
  else
    return $2
  fi
fi
}

max2 $1 $2
return_val=$?

if [ "$return_val" -eq $E_PARAM_ERR ]
then
  echo "Need to pass two parameters to the function."
elif [ "$return_val" -eq $EQUAL ]
  then
    echo "The two numbers are equal."
else
    echo "The larger of the two numbers is $return_val."
fi  

  
exit 0

#                  تمرین (آسان):
#              -------------------
#این اسکریپت را به یک اسکریپت محاوره‌ای تبدیل کنید،
#+  یعنی، اسکریپت (دو عدد) ورودی را درخواست نماید.

tip

برای اینکه تابع یک رشته یا آرایه را برگشت بدهد، از یک متغیر اختصاصی استفاده کنید.

count_lines_in_etc_passwd()
{
  [[ -r /etc/passwd ]] && REPLY=$(echo $(wc -l < /etc/passwd))
  #    اگر ‎/etc/passwd‎ قابل خواندن است، ‎REPLY‎ برابر با تعداد سطرهایش بشود.
  #                        مقدار پارامتر و اطلاعات وضعیت، را برگشت می‌دهد.
  #به نظر می‌رسد ‎echo‎ غیر ضروری است، اما فضای سفید زائد خروجی را حذف می‌کند.
}

if count_lines_in_etc_passwd
then
  echo "There are $REPLY lines in /etc/passwd."
else
  echo "Cannot count lines in /etc/passwd."
fi  

# با تشکر از ‎S.C.‎

مثال ‎24-8‎. تبدیل عددها به اعداد رومی

#!/bin/bash

#تبدیل اعداد عربی به اعداد رومی
#               محدوده: ‎0 - 200‎
#    ناپخته است، اما کار می‌کند.

#بسط محدوده و بهبودبخشی آن به عنوان یک تمرین واگذار شده است.

#نحوه کاربرد:       ‎roman number-to-convert‎

LIMIT=200
E_ARG_ERR=65
E_OUT_OF_RANGE=66

if [ -z "$1" ]
then
  echo "Usage: `basename $0` number-to-convert"
  exit $E_ARG_ERR
fi  

num=$1
if [ "$num" -gt $LIMIT ]
then
  echo "Out of range!"
  exit $E_OUT_OF_RANGE
fi  

to_roman ()   #تابع باید قبل از اولین فراخوانی آن، تعریف بشود.
{
number=$1
factor=$2
rchar=$3
let "remainder = number - factor"
while [ "$remainder" -ge 0 ]
do
  echo -n $rchar
  let "number -= factor"
  let "remainder = number - factor"
done  

return $number
       #                    تمرین‌ها:
       #            ------------------------
       #             ‎(1‎ چگونگی کارکرد تابع را تشریح کنید.
       #          اشاره: جداسازی به وسیله تفریق پی در پی.
       #                   ‎(2‎ محدوده تابع را گسترش بدهید.
       #    اشاره: ‎echo‎ و جایگزینی فرمان را به کار ببرید.
}
   

to_roman $num 100 C
num=$?
to_roman $num 90 LXXXX
num=$?
to_roman $num 50 L
num=$?
to_roman $num 40 XL
num=$?
to_roman $num 10 X
num=$?
to_roman $num 9 IX
num=$?
to_roman $num 5 V
num=$?
to_roman $num 4 IV
num=$?
to_roman $num 1 I
#         فراخوانی‌های متوالی تابع تبدیل!
#براستی این لازم است؟؟؟ می‌تواند ساده شود؟

echo

exit

مثال ‎11-29‎ را هم ببینید.

important

بزرگترین عدد صحیح مثبتی که یک تابع می‌تواند برگشت بدهد ‎255‎ است. فرمان return دقیقاً وابسته به مفهوم وضعیت خروج است، که موجب این محدودیت خاص است. خوشبختانه، برای آن موقعیت‌هایی که نیازمند برگشت دادن عدد صحیح بزرگ از یک تابع هستند، راه‌حل‌های موقتی متنوعی وجود دارد.

مثال ‎24-9‎. آزمایش مقادیر برگشتی بزرگ در یک تابع

#!/bin/bash
# return-test.sh

# بزرگترین عدد صحیح مثبت که یک تابع می‌تواند برگشت بدهد ‎255‎ است.

return_test ()         #هر چه به آن عبور داده شود، برگشت می‌دهد.
{
  return $1
}

return_test 27         #          قبول است.
echo $?                # ‎27‎ را برگشت می‌دهد.
  
return_test 255        #   باز هم قبول است.
echo $?                #‎255‎ را برگشت می‌دهد.

return_test 257        #               خطا!
echo $?                #‎1‎ را برگشت می‌دهد(کد برگشتی خطاهای مختلف).

#============================================================
return_test -151896    #آیا اعداد صحیح بزرگ منفی کار می‌کنند؟
echo $?                # آیا عدد ‎-151896‎ را برگشت خواهد داد؟
                       #            خیر! ‎168‎ را برگشت می‌دهد.
#   ‎Bash‎ قبل از نگارش ‎2.05b‎ مقادیر برگشتی عدد صحیح بزرگ منفی
#+      را اجازه می‌داد. اتفاق می‌افتاد که ویژگی سودمندی باشد.
# نگارش‌های جدیدتر ‎Bash‎ متاسفانه جلوی این راه گریز را گرفتند.
#         هشدار! این ممکن است در اسکریپت‌های قدیمی‌تر نقض گردد
#============================================================

exit 0

یک راه حل موقت برای به دست آوردن «مقادیر برگشتی» صحیح بزرگ، فقط تخصیص دادن «مقدار برگشتی» به یک متغیر سراسری است.

Return_Val=   #متغیر سراسری جهت نگهداری مقدار برگشتی بزرگ از تابع.

alt_return_test ()
{
  fvar=$1
  Return_Val=$fvar
  return     # صفر (موفقیت) را برگشت می‌دهد.
}

alt_return_test 1
echo $?                              #    0
echo "return value = $Return_Val"    #    1

alt_return_test 256
echo "return value = $Return_Val"    #  256

alt_return_test 257
echo "return value = $Return_Val"    #  257

alt_return_test 25701
echo "return value = $Return_Val"    #25701

یک شیوه بیشتر برازنده، داشتن تابعی که «مقدار برگشتی» را به خروجی استاندارد، echo می‌کند و سپس ضبط نمودن آن به وسیله جایگزینی فرمان است. گفتار در این مورد از بخش ‎36.7‎ را ملاحظه نمایید.

مثال ‎24-10‎. مقایسه دو عدد صحیح بزرگ

#!/bin/bash
#         max2.sh:         عدد بزرگتر بین دو عدد صحیح بزرگ.

                    # این مثال قبلی ‎max.sh‎ است که برای مجاز کردن
                    #    مقایسه دو عدد صحیح بزرگ ویرایش شده است.

EQUAL=0             #مقدار برگشتی در صورتیکه دو عدد مساوی باشند.
E_PARAM_ERR=-99999  #    پارامترهای کافی به تابع عبور داده نشده.
#           ^^^^^^     خارج از حدود پارامترهای احتمالی داده شده.

max2 ()             #  عدد بزرگتر از میان دو عدد را برگشت می‌دهد.
{
if [ -z "$2" ]
then
  echo $E_PARAM_ERR
  return
fi

if [ "$1" -eq "$2" ]
then
  echo $EQUAL
  return
else
  if [ "$1" -gt "$2" ]
  then
    retval=$1
  else
    retval=$2
  fi
fi

echo $retval        #به جای برگشت دادن مقدار، (در ‎stdout‎) منعکس می‌کند.
                    # چرا؟
}


return_val=$(max2 33001 33997)
#            ^^^^              نام تابع
#                 ^^^^^ ^^^^^  پارامترهای رد شده به تابع
#         این در حقیقت شکلی از جایگزینی فرمان است:
#+با تابع همانطور که اگر یک فرمان بود رفتار می‌کند،
#+و ‎stdout‎ تابع را به متغیر ‎return_val‎ تخصیص می‌دهد


# ========================= OUTPUT ========================
if [ "$return_val" -eq "$E_PARAM_ERR" ]
  then
  echo "Error in parameters passed to comparison function!"
elif [ "$return_val" -eq "$EQUAL" ]
  then
    echo "The two numbers are equal."
else
    echo "The larger of the two numbers is $return_val."
fi
# =========================================================
  
exit 0

#                                  تمرین‌ها:
#                           ------------------------
#  ‎(1‎ یک روش برازنده‌تر برای آزمایش پارامترهای رد شده به تابع پیدا کنید.
#                         ‎(2‎ ساختار ‎if/then‎ در ‎OUTPUT‎ را ساده‌سازی کنید.
#  ‎(3‎ اسکریپت را برای گرفتن ورودی از پارامترهای خط فرمان بازنویسی کنید.

این هم یک مثال دیگر از ضبط کردن «مقدار برگشتی» یک تابع. فهمیدن آن نیازمند مقداری آگاهی از awk است.

month_length ()          # شماره ماه را به عنوان یک شناسه دریافت می‌کند.
{                        #             تعداد روزهای ماه را برگشت می‌دهد.
monthD="31 28 31 30 31 30 31 31 30 31 30 31"      # تعریف به صورت محلی؟
echo "$monthD" | awk '{ print $'"${1}"' }'        #         مهارت آمیز.
#                             ^^^^^^^^^
#               پارامتر رد شده به تابع (‎$1‎ -- شماره ماه)، و سپس به ‎awk‎.
#‎Awk‎ این را به صورت «‎print $1 . . . print $12‎» می‌بیند (نسبت به عدد ماه)
#                 الگو برای تجزیه یک پارامتر با اسکریپت ‎awk جاسازی شده‎:
#  $'"${script_parameter}"'

#                                      این هم یک ساختار کمی ساده‌تر ‎awk‎:
# echo $monthD | awk -v month=$1 '{print $(month)}'
# از گزینه ‎-v‎ در ‎awk‎ استفاده می‌کند، که مقدار متغیر را قبل از اجرای بلوک
#+                              برنامه ‎awk‎ تخصیص می‌دهد. با تشکر از ‎Rich‎

#به کنترل خطای محدوده پارامتر صحیح ‎(1-12)‎ و فوریه سال کبیسه احتیاج دارد
}

#----------------------------------------------
#                             مثال نحوه کاربرد:
month=4        #برای مثال، آوریل (چهارمین ماه).
days_in=$(month_length $month)
echo $days_in  #  30
# ----------------------------------------------

همچنین مثال ‎A-7‎ و مثال ‎A-37‎ را ببینید.

تمرین: با استفاده از آنچه اکنون آموخته‌ایم، مثال اعداد رومی قبل را برای پذیرفتن دلخواهانه عدد ورودی بزرگ توسعه بدهید.

تغییر مسیر

تغییر مسیر دادن ‎stdin‎ یک تابع

در اصل تابع یک بلوک کد است که می‌تواند از طریق ورودی استانداردش تغییر مسیر داده شود (همچون در مثال ‎3-1‎).

مثال ‎24-11‎. نام واقعی از روی نام کاربری

#!/bin/bash
# realname.sh
#
# بواسطه نام کاربری، نام واقعی را از ‎/etc/passwd‎ به دست می‌آورد.


ARGCOUNT=1          #                 تعداد شناسه‌های مورد نیاز.
E_WRONGARGS=85

file=/etc/passwd
pattern=$1

if [ $# -ne "$ARGCOUNT" ]
then
  echo "Usage: `basename $0` USERNAME"
  exit $E_WRONGARGS
fi  

file_excerpt ()    #پویش فایل برای الگو، سپس چاپ بخش مناسب سطر.
{                  
  while read line  #    ‎while‎ لزوماً به ‎[ condition ]‎ نیاز ندارد
  do
    echo "$line" | grep $1 | awk -F":" '{ print $5 }'
                   # ‎awk‎ وادار به استفاده از جداکننده «:» می‌شود.
  done
} <$file           #                  تغییر مسیر به ‎stdin‎ تابع.

file_excerpt $pattern

#     بله، تمام این اسکریپت می‌تواند کاهش داده شود به
#grep PATTERN /etc/passwd | awk -F":" '{ print $5 }'
#                      یا
# awk -F: '/PATTERN/ {print $5}'
#                       یا
#awk -F: '($1 == "username") { print $5 }' 
#                        نام واقعی از روی نام کاربری
#    به هر حال، شاید این به آن اندازه آموزنده نباشد.

exit 0

یک شیوه دیگر و شاید کمتر گیج کننده برای تغییر مسیر دادن stdin تابع وجود دارد. این شیوه، مستلزم تغییر مسیر دادن stdin به یک بلوک کد است که در براکت‌ها قرار گرفته و در داخل تابع جاسازی شده است.

Function ()    #          به جای:
{
 ...
 } < file

#             این را امتحان کنید:
Function ()
{
  {
    ...
   } < file
}

#                   به طور مشابهی

Function ()    #این هم کار می‌کند.
{
  {
   echo $*
  } | tr a b
}

Function ()    #  این کار نمی‌کند.
{
  echo $*
} | tr a b  
#اینجا بلوک کد تودرتو الزامی است.

# با تشکر از ‎S.C.‎


فایل bashrc نمونه ‎Emmanuel Rouat‎ شامل برخی مثال‌های آموزنده از توابع است.

یادداشت‌ها

‎[1]‎

فرمان return یک فرمان داخلی Bash است.