gotchas

فصل ‎34‎- ‏Gotchaها‎[۱]‎

 

Turandot: Gli enigmi sono tre, la morte una!

Caleph: No, no! Gli enigmi sono tre, una la vita!

--Puccini

اینها هم شیوه‌هایی از اسکریپت‌نویسی (توصیه نشده) هستند که به زندگی یکنواخت بدون حضور خودشان، هیجان می‌بخشند.

  • تخصیص کلمات یا کاراکترهای رزرو شده به نام متغیرها.

  • case=value0       #                                         باعث مشکلات می‌شود.
    23skidoo=value1   #                                     این نیز مشکل ساز است.
    #          نام متغیرهایی که با یک رقم شروع می‌شوند به وسیله پوسته رزرو شده‌اند.
    #‎_23skidoo=value1‎ را امتحان کنید. شروع شدن نام متغیرها با یک خط زیر قبول است.
    
    #                    اما . . .  استفاده از فقط یک خط زیر تنها کار نخواهد کرد.
    _=25
    echo $_           #‎$_‎ یک متغیر ویژه است که معادل آخرین شناسه آخرین فرمان است.
    #                           اما . . .      ‎ _‎ برای تابع یک نام قابل قبول است!
    
    xyz((!*=value2    #                                   باعث مشکلات شدیدی می‌شود.
    #                             از Bash نگارش ‎3‎، نقطه در نام متغیرها مجاز نیست.
    

  • به کار بردن خط تیره یا سایر کاراکترهای رزرو شده در نام یک متغیر (یا نام تابع).

  • var-1=23
    #               به جای آن از ‎var_1‎ استفاده کنید.
    
    function-whatever ()               #         خطا
    #  در عوض، ‎function_whatever ()‎ را به کار ببرید.
    
     
    #از نگارش ‎3‎ در Bash، نقطه در نام تابع مجاز نیست.
    function.whatever ()               #         خطا
    # ‎functionWhatever ()‎ را به جای آن استفاده کنید.

  • استفاده از نام یکسان برای یک متغیر و یک تابع. این کار می‌تواند فهمیدن اسکریپت را دشوار سازد.

  • do_something ()
    {
      echo "This function does something with \"$1\"."
    }
    
    do_something=do_something
    
    do_something do_something
    
    # تمام این‌ها مجاز، اما بسیار گیج کننده هستند.

  • استفاده از فضای سفید به طور نامناسب. در مقایسه با سایر زبان‌های برنامه‌نویسی، Bash می‌تواند در مورد فضای سفید کاملاً بهانه‌گیر باشد.

  • var1 = 23         #            ‎var1=23‎ صحیح است.
    #     در سطر بالا Bash تلاش می‌کند فرمان ‎var1‎ را با
    #                شناسه‌های «=» و «‎23‎» اجرا نماید.
    	
    let c = $a - $b   
    #  به جای آن: ‎let c=$a-$b‎  یا  ‎let "c = $a - $b"‎
    
    if [ $a -le 5]    #    ‎if [ $a -le 5 ]‎ صحیح است.
    #   حتی ‎if [ "$a" -le 5 ]‎ بهتر است.
                      #‎[[ $a -le 5 ]]‎ نیز کار می‌کند.

  • خاتمه ندادن فرمان پایانی یک بلوک کد داخل براکت‌های کمانی با سمی‌کالن.

  • { ls -l; df; echo "Done." }
    # 
    
    { ls -l; df; echo "Done."; }
    #     # فرمان انتهایی سمی‌کالن لازم دارد.##

  • فرض کردن آنکه متغیرهای ارزش‌گذاری نشده (متغیرها قبل از آنکه یک مقدار به آنها تخصیص یابد) صفر هستند. یک متغیر ارزش‌گذاری نشده دارای کمیت null(تهی) است، نه کمیت صفر.

  • #!/bin/bash
    
    echo "uninitialized_var = $uninitialized_var"
    # 
    
    #                      به هر حال . . .
    # اگر ‎$BASH_VERSION  ≥ 4.2‎ باشد، آنوقت
    
    if [[ ! -v uninitialized_var ]]
    then
      uninitialized_var=0   # ارزش‌گذاری آن به کمیت صفر!
    fi
    

  • اشتباه کردن = و ‎-eq‎ در یک تست. به خاطر داشته باشید که = برای مقایسه متغیرهای لفظی است و ‎-eq‎ برای مقایسه اعداد صحیح است.

  • if [ "$a" = 273 ]         #  آیا ‎$a‎ یک عدد صحیح است یا یک رشته؟
    if [ "$a" -eq 273 ]       #            اگر ‎$a‎ یک عدد صحیح باشد.
    
    #گاهی اوقات می‌توانید بدون پیامدهای نامساعد، ‎-eq‎ و ‎=‎ تعویض کنید.
    
    
    a=273.0   # یک عدد صحیح نیست.
    	   
    if [ "$a" = 273 ]
    then
      echo "Comparison works."
    else  
      echo "Comparison does not work."
    fi        # 
    
    #              همان نتیجه با ‎a=" 273"‎ و ‎a="0273"‎ به دست می‌آید.
    
    
    # به همچنین، وجود مشکلات در به کار بردن ‎-eq‎ با مقادیر غیر صحیح.
    	   
    if [ "$a" -eq 273.0 ]
    then
      echo "a = $a"
    fi        #      با یک پیغام خطا بی‌نتیجه می‌ماند.  
    # 

  • استفاده نادرست از عملگرهای مقایسه رشته.

    مثال ‎34-1‎. مقایسه رشته‌ای و عددی معادل هم نیستند

  • #!/bin/bash
    #  آزمایش به کار بردن یک مقایسه رشته‌ای روی اعداد صحیح.
    
    echo
    number=1
    
    # حلقه ‎while‎ پایین دارای دو خطا است:
    #+       یکی آشکار، و دیگری نامحسوس.
    
    while [ "$number" < 5 ]    # اشتباه! باید ‎while [ "$number" -lt 5 ]‎ باشد
    do
      echo -n "$number "
      let "number += 1"
    done  
    #  با شکست مواجه شده و پیغام خطای زیر را صادر می‌کند:
    #+  
    #  در درون براکت‌های منفرد ‎<‎ باید escape شود و حتی در
    #+ آنصورت باز هم برای مقایسه اعداد صحیح، اشتباه است.
    
    echo "---------------------"
    
    while [ "$number" \< 5 ]    # 
    do                          #
      echo -n "$number "        # به نظر می‌رسد کار می‌کند، اما. . .
      let "number += 1"         #+ در حقیقت به جای یک مقایسه عددی،
    done                        #+    یک مقایسه ASCII انجام می‌دهد.
    
    echo; echo "---------------------"
    
    #   این می‌تواند باعث مشکلاتی بشود. برای مثال:
    
    lesser=5
    greater=105
    
    if [ "$greater" \< "$lesser" ]
    then
      echo "$greater is less than $lesser"
    fi                      # 
    # در حقیقت، در مقایسه رشته‌ای (به ترتیب اسکی)
    #+                ‎105‎ واقعاً کوچکتر از ‎5‎ است.
    
    echo
    
    exit 0
  • اقدام به استفاده از let جهت تنظیم متغیرهای رشته‌ای.

  • let "a = hello, you"
    echo "$a"   # 

  • گاهی اوقات لازم است متغیرهای «test» داخل براکت‌ها ‎([ ])‎ نقل‌قولی (نقل‌قول دوگانه) بشوند. غفلت در انجام آن ممکن است باعث رفتار غیر منتظره‌ای بشود. مثال ‎7-6‎، مثال ‎20-5‎، و مثال ‎9-6‎ را مشاهده نمایید.

  • نقل‌قول کردن یک متغیر شامل فضای سفید مانع تقسیم شدن آن می‌شود. این عمل گاهی اوقات پیامدهای ناخواسته‌ای تولید می‌کند.

  • اجرای فرمان‌های صادر شده از یک اسکریپت ممکن است به علت اینکه مالک اسکریپت فاقد مجوز اجرای آنها باشد، با شکست مواجه گردد. اگر کاربری از خط فرمان نتواند یک فرمان را فراخوانی نماید، آنوقت قرار دادن آن در اسکریپت نیز به شکست منجر خواهد شد. تغییر دادن صفات فرمان مورد بحث، حتی شاید تنظیم بیت ‎suid‎ ( البته، به عنوان root) را امتحان کنید.

  • اقدام به استفاده از کاراکتر - به عنوان یک عامل تغییر مسیر (که نیست) به طور معمول منجر به یک غافل‌گیری ناخوشایند می‌گردد.

  • command1 2> - | command2
    # تلاش برای تغییرمسیر خطای خروجی ‎command1‎ به یک لوله. . .
    # . . . کار نخواهد کرد.	
    
    command1 2>& - | command2        #    این هم بیهوده است.
    
    # با تشکر از ‎S.C.‎

  • استفاده از توانایی Bash نگارش ‎2+‎ ممکن است باعث یک دردسر همراه با پیغام‌های خطا بشود. ماشین‌های لینوکس قدیمی‌تر ممکن است به عنوان پیش‌فرض نصب، دارای Bash نگارش ‎1.XX‎ باشند.

  • #!/bin/bash
    
    minimum_version=2
    #  چون ‎Chet Ramey‎ به طور مداوم ویژگی‌هایی به Bash اضافه می‌کند، شما باید
    #   ‎$minimum_version‎ را به ‎2.XX‎‏، ‎3.XX‎ یا هر چه که مناسب است تنظیم کنید.
    E_BAD_VERSION=80
    
    if [ "$BASH_VERSION" \< "$minimum_version" ]
    then
      echo "This script works only with Bash, version $minimum or greater."
      echo "Upgrade strongly recommended."
      exit $E_BAD_VERSION
    fi
    
    ...

  • استفاده از توانایی‌های مختص Bash در یک اسکریپت پوسته Bourne ‏(‎#!/bin/sh‎) روی یک ماشین غیر لینوکس می‌تواند باعث رفتار غیر منتظره بشود. یک سیستم لینوکس به طور معمول ‎sh‎ را مستعار bash می‌سازد، اما این مطلب لزوماً برای هر ماشین یونیکسی صحت ندارد.

  • استفاده از ویژگی‌های مستندسازی نشده در ‎Bash‎ می‌تواند رویه خطرناکی باشد. در انتشارهای قبلی این کتاب چند اسکریپت‌ وجود داشتند که مبتنی بر خصیصه‌ای بودند که اگرچه حداکثر مقدار برگشتی یک exit یا return برابر با ‎255‎ بود، اما آن محدودیت برای اعداد صحیح منفی صدق نمی‌کرد. متاسفانه، در نگارش ‎2.05b‎ و پس از آن این روزنه ناپدید شد. مثال ‎24-9‎ را ببینید.

  • در برخی مضمون‌ها، ممکن است وضعیت خروج گمراه کننده‌ای برگشت داده شود. این اتفاق ممکن است موقع تنظیم یک متغیر محلی در درون یک تابع یا هنگام تخصیص دادن یک کمیت حسابی به یک متغیر رخ بدهد.

  • وضعیت خروج یک عبارت حسابی معادل با یک کد خطا نیست.

  • var=1 && ((--var)) && echo $var
    #اینجا لیست and با وضعیت خروج ‎1‎ خاتمه می‌یابد.
    # ‎$var‎ نمایش داده نمی‌شود!
    echo $?   # 

  • اجرای اسکریپتی با سطرهای جدید به سبکِ ‎DOS‎ ‏(یعنی ‎\r\n‎) ناموفق خواهد شد، چون ‎#!/bin/bash\r\n‎ شناخته شده نیست، آنچه مورد انتظار است یعنی ‎#!/bin/bash\n‎ نیست. چاره کار، تبدیل سطرهای اسکریپت به سبکِ یونیکس است.

  • #!/bin/bash
    
    echo "Here"
    
    unix2dos $0    # اسکریپت خودش را به قالب DOS تغییر می‌دهد.
    chmod 755 $0   #        باز گرداندن مجوز اجرا به اسکریپت.
                   #   فرمان unix2dos مجوز اجرا را حذف می‌کند.
    
    ./$0           #        اسکریپت اقدام به اجرای خود می‌کند.
                   #     اما به عنوان یک فایل ‎DOS‎ کار نمی‌کرد.
    
    echo "There"
    
    exit 0

  • یک اسکریپت پوسته با سرآیند ‎#!/bin/sh‎ در وضعیت سازگاری کامل با Bash کار نمی‌کند. ممکن است برخی عملکردهای مختص ‎Bash‎ غیر فعال بشوند. اسکریپت‌هایی که نیاز به دسترسی کامل به تمام الحاقیه‌های مختص ‎Bash‎ دارند، باید با سطر ‎#!/bin/bash‎ شروع بشوند.

  • قرار دادن فضای سفید قبل از رشته نگهبانِ خاتمه دهنده یک ‎here document‎ در اسکریپت باعث رفتار غیر منتظره می‌گردد.

  • قرار دادن بیش از یک جمله echo در تابعی که خروجی‌اش ضبط می‌شود.

  • add2 ()
    {
      echo "Whatever ... "   # این سطر را حذف کنید!
      let "retval = $1 + $2"
        echo $retval
        }
    
        num1=12
        num2=43
        echo "Sum of $num1 and $num2 = $(add2 $num1 $num2)"
    
    #  
    # 
    
    #  به هم پیوستن «echo»ها.
    این تابع درست عمل نخواهد کرد.

  • یک اسکریپت نمی‌تواند متغیرها را به پشت سر خود، پردازش پدر، پوسته، یا محیط، export نماید. همانطور که در بیولوژی آموخته‌ایم، پردازش فرزند می‌تواند از پدر ارث ببرد، نه برعکس.

  • WHATEVER=/home/bozo
    export WHATEVER
    exit 0
    bash$ echo $WHATEVER
    
    bash$ 

    همچنانکه پیش‌بینی می‌شود، ‎$WHATEVER‎ در اعلان فرمان تنظیم نشده باقی می‌ماند.

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

  • مثال ‎34-2‎. دام‌های پوسته فرعی

    #!/bin/bash
    # دام‌های متغیرها در یک پوسته فرعی.
    
    outer_variable=outer
    echo
    echo "outer_variable = $outer_variable"
    echo
    
    (
    #      شروع پوسته فرعی
    
    echo "outer_variable inside subshell = $outer_variable"
    inner_variable=inner  #                      تنظیم نمودن متغیر
    echo "inner_variable inside subshell = $inner_variable"
    outer_variable=inner  # کمیت را به طور سراسری تغییر خواهد داد؟
    echo "outer_variable inside subshell = $outer_variable"
    
    # آیا ‎export‎ کردن تفاوتی ایجاد می‌کند؟
    # 
    # 
    # امتحان کنید و ببینید.
    
    #      پایان پوسته فرعی
    )
    
    echo
    echo "inner_variable outside subshell = $inner_variable"  #   تنظیم نشده.
    echo "outer_variable outside subshell = $outer_variable"  # تغییر نیافته.
    echo
    
    exit 0
    
    # اگر سطر ‎19‎ و ‎20‎ را از حالت توضیح خارج کنید
    #       چه می‌شود؟ آیا تفاوتی به وجود می‌آورد؟
  • لوله‌کشی خروجی echo به یک read ممکن است نتایج غیر منتظره تولید کند. در این سناریو، read چنانکه گویی در یک پوسته فرعی در حال اجرا است، عمل می‌کند. به جای آن، فرمان set را (همچون در مثال ‎15-18‎) به کار ببرید.

  • مثال ‎34-3‎. لوله‌کشی خروجی echo به یک فرمان read

    #!/bin/bash
    # 
    #    کوشش برای استفاده از echo و read جهت
    #+    تخصیص متغیرها به صورت غیر محاوره‌ای.
    
    # 
    
    a=aaa
    b=bbb
    c=ccc
    
    echo "one two three" | read a b c
    #  تخصیص دوباره ‎a‎‏، b، و c را امتحان کنید.
    
    echo
    echo "a = $a"    # a = aaa
    echo "b = $b"    # b = bbb
    echo "c = $c"    # c = ccc
    #           تخصیص مجدد ناموفق گردیده است.
    
    #                         با این وجود. . .
    #                  غیر توضیح نمودن سطر ‎6‎:
    # 
    #+                  مشکل را برطرف می‌سازد!
    #این یک ویژگی جدید در ‎Bash‎ نگارش ‎4.2‎ است.
    
    # ---------------------------------------
    
    #           جایگزین پایین را امتحان کنید.
    
    var=`echo "one two three"`
    set -- $var
    a=$1; b=$2; c=$3
    
    echo "-------"
    echo "a = $a"    #  a = one
    echo "b = $b"    #  b = two
    echo "c = $c"    #  c = three 
    #             تخصیص مجدد موفق گردیده است.
    
    # ---------------------------------------
    
    # همچنین توجه نمایید که echo برای یک read داخل پوسته فرعی کار می‌کند.
    #                   اما، مقدار متغیر فقط داخل پوسته فرعی تغییر می‌کند.
    
    a=aaa            # شروع دوباره از ابتدا.
    b=bbb
    c=ccc
    
    echo; echo
    echo "one two three" | ( read a b c;
    echo "Inside subshell: "; echo "a = $a"; echo "b = $b"; echo "c = $c" )
    # a = one
    # b = two
    # c = three
    echo "-----------------"
    echo "Outside subshell: "
    echo "a = $a"  # a = aaa
    echo "b = $b"  # b = bbb
    echo "c = $c"  # c = ccc
    echo
    
    exit 0

    در حقیقت، همچنانکه ‎Anthony Richardson‎ توضیح می‌دهد، لوله‌کشی هر حلقه‌ای می‌تواند باعث مشکل مشابهی بشود.

    #     لوله‌کشی حلقه درد سر ایجاد می‌کند.
    # این مثال نوشته ‎Anthony Richardson‎ با
    #+    افزایش‌های ‎Wilbert Berendsen‎ است.
    
    
    foundone=false
    find $HOME -type f -atime +30 -size 100k |
    while true
    do
       read f
       echo "$f is over 100KB and has not been accessed in over 30 days"
       echo "Consider moving the file to archives."
       foundone=true
       # ------------------------------------
         echo "Subshell level = $BASH_SUBSHELL"
       # 
       #    بله، ما داخل یک پوسته فرعی هستیم.
       # ------------------------------------
    done
       
    #‎foundone‎ در اینجا همواره false خواهد بود
    #+ چون در پوسته فرعی به true تبدیل می‌شود.
    if [ $foundone = false ]
    then
       echo "No files need archiving."
    fi
    
    ##====================اکنون، این هم روش درست:===================
    
    foundone=false
    for f in $(find $HOME -type f -atime +30 -size 100k)  # No pipe here.
    do
       echo "$f is over 100KB and has not been accessed in over 30 days"
       echo "Consider moving the file to archives."
       foundone=true
    done
       
    if [ $foundone = false ]
    then
       echo "No files need archiving."
    fi
    
    # #====================و این هم یک جایگزین دیگر=================
    
    # بخشی از اسکریپت را که متغیرها را می‌خواند در داخل یک بلوک کد
    #+     قرار می‌دهد، بنابراین آنها در یک پوسته فرعی شرکت دارند.
    #  با تشکر از ‎W.B.‎
    
    find $HOME -type f -atime +30 -size 100k | {
         foundone=false
         while read f
         do
           echo "$f is over 100KB and has not been accessed in over 30 days"
           echo "Consider moving the file to archives."
           foundone=true
         done
    
         if ! $foundone
         then
           echo "No files need archiving."
         fi
    }

    هنگام اقدام برای نوشتن خروجی استاندارد یک ‎tail -f‎ لوله‌کشی شده به grep مشکل مشابهی رخ می‌دهد.

    tail -f /var/log/messages | grep "$ERROR_MSG" >> error.log
    
    # فایل ‎error.log‎ دارای مورد نوشته شده‌ای در درونش نخواهد بود.
    #  به طوری که ‎Samuli Kaipiainen‎ اشاره می‌کند، این مشکل در اثر
    #+     آن است که grep خروجی‌اش را میانگیری (buffering) می‌کند.
    #     چاره آن افزودن  پارامتر «‎--line-buffered‎» به grep است.
    
    

  • به کار بردن فرمان‌ «suid» در داخل اسکریپت‌ها پرمخاطره است، چون ممکن است امنیت سیستم را در معرض خطر قرار بدهد. ‎[1]‎

  • استفاده از اسکریپت‌های پوسته برای برنامه‌نویسی CGI ممکن است مشکل‌آفرین باشد. متغیرهای اسکریپت پوسته «ضامن نوع» نیستند و این تا جاییکه به CGI مربوط است می‌تواند باعث رفتار نامطلوب بشود. علاوه بر این، «cracker-proof» کردن اسکریپت‌های پوسته دشوار است.

  • Bash رشته اسلاش‌های دوتایی (//) را به طور صحیح مدیریت نمی‌کند.

  • اسکریپت‌های Bash نوشته شده برای سیستم‌های لینوکس یا BSD ممکن است برای اجرا بر روی ماشین یونیکس تجاری نیازمند اصلاح کردن باشند. چنین اسکریپت‌هایی بیشتر اوقات مجموعه‌ای از فرمان‌ها و فیلترهای گنو را به کار می‌برند، که دارای توانایی‌های بیشتری نسبت به همتاهای نوع یونیکسی آنها هستند. این مطلب به طور خاص در مورد برنامه‌های پردازش متن از قبیل tr صحت دارد.

  • با تاسف بسیار، به روز رسانی‌ها برای خود Bash نیز اسکریپت‌های قدیمی‌تری را که کاملا درست کار می‌کردند، نقض نموده‌اند. اجازه بدهید دوباره یادآور شوم استفاده از ویژگی‌های مستندسازی نشده Bash چطور پر مخاطره هستند.

Danger is near thee --

Beware, beware, beware, beware.

Many brave hearts are asleep in the deep.

So beware --

Beware.

--A.J. Lamb and H.W. Petrie

 

یادداشت‌ها

‎[1]‎

تنظیم مجوز suid روی خود اسکریپت تاثیری روی لینوکس و اکثر گرایش‌های دیگر یونیکس ندارد.



  1. مترجم: یک سوء ویژگی در زبان برنامه‌نویسی که استفاده از آن آسان، اما متمایل به تولید اشتباه و نتایج ناخواسته است. به عنوان نمونه مورد زیر یک gotcha در Bash است

  2. #!/bin/bash
     a=2
     if [ $a > 12 ]
    	then
    	echo "a is greater than 12"
     else
    	echo "a is less than 12"
     fi
    

    اجرای آن را امتحان کنید. نتیجه‌اش ‎a is greater than 12‎ خواهد بود!

    (برگشت)