آموزش اسکریپت نویسی

آموزش اسکریپت نویسی پوسته گنو-لینوکس

آموزش اسکریپت نویسی

آموزش اسکریپت نویسی پوسته گنو-لینوکس

پرسش و پاسخ شماره ۶



پرسش و پاسخ شماره ۶

چطور می‌توانم از متغیرهای متغیر( متغیرهای غیرمستقیم، اشاره‌گرها، مرجع‌ها ) یا آرایه‌های انجمنی استفاده کنم؟

این یک صفحه مختلط می‌باشد، به علت آنکه موضوع آن پیچیده است. به طور کلی به سه بخش تقسیم شده است: آرایه‌های انجمنی، ارزیابی متغیرهای غیر مستقیم، و اختصاص متغیرهای غیر مستقیم. در سرتاسر آن مباحث موضوعات برنامه‌نویسی و مفاهیم پراکنده وجود دارد.

آرایه های انجمنی

ما اول آرایه‌های انجمنی را معرفی می‌کنیم، زیرا در عمده موقعیت‌هایی که افراد سعی در تخصیص و ارزیابی متغیرهای غیرمستقیم می‌نمایند، به جای آن باید از آرایه‌های انجمنی استفاده کنند. برای نمونه، ما بارها کسانی را دیده‌ایم که می‌پرسند چطور می‌توانند یک گروه متغیرهای وابسته مانند IPaddr_hostname1، IPaddr_hostname2 و به همین ترتیب، داشته باشند. روش مناسبتر برای ذخیره این داده، یک آرایه انجمنی به نام IPaddr است که با hostname(نام میزبان) شاخص‌گذاری شده باشد.

برای طرح‌ریزی از یک رشته به دیگری، به آرایه شاخص‌گذاری شده توسط رشته به جای عدد، نیاز دارید. این در AWK به عنوان «آرایه‌های انجمنی» موجود است، در پرل به عنوان«hashes»، و در Tcl به سادگی به عنوان«arrays». آنها همچنین در ksh93 وجود دارند، که در آنجا شما از آن به این شکل استفاده می‌کنید:

  •  # ksh93
     typeset -A homedir             # تعریف می‌کند ksh93 آرایه انجمنی
     homedir[jim]=/home/jim
     homedir[silvia]=/home/silvia
     homedir[alex]=/home/alex
    
     for user in "${!homedir[@]}"   # تمام شاخص‌ها(نامهای کاربری) را به شما می‌آورد
     do
         echo "Home directory of user $user is ${homedir[$user]}"
     done

BASH از نگارش 4 و بالاتر، از آنها پشتیبانی می‌کند:

  •  # Bash 4 and up
     declare -A homedir
     homedir[jim]=/home/jim
     # یا
     homedir=( [jim]=/home/jim
               [silvia]=/home/silvia
               [alex]=/home/alex )
     ...

در نسخه‌های Bash قبل از نگارش 4 یا اگر نمی‌توانید از ksh93 استفاده کنید، گزینه‌های شما محدود است. یا به سوی سایر مفسرها(awk، perl، python، ruby، tcl، ...) بروید یا مشکل خود را با ساده سازی آن ارزیابی مجدد نمایید.

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

فرض کنید چند میزبان خادم با مختصر تفاوتی در پیکربندی آنها داریم، و می‌خواهیم با ssh در هر یک از آنها فرمانهایی با تفاوت جزئی را اجرا نماییم. یک روشی که می‌توانیم به کار ببریم، تنظیم یک گروه فرمانهای ssh، که به آسانی نمی‌توانند تغییر کنند(hard-code)، در توابع هر نام میزبان در یک اسکریپت منفرد و فقط اجرای آنها به طور سری یا موازی. (فوراً این را رد نکنید! ساده خوب است.) روش دیگر می‌تواند ذخیره هر گروه از دستورات به عنوان یک عضو آرایه انجمنی شاخص‌گذاری شده با نام میزبان باشد:

  •  source "$conf"
     for host in "${!commands[@]}"; do
         ssh "$host" "${commands[$host]}"
     done
    
     # :فایلی مشابه این است  "$conf" که در آن
     declare -A commands
     commands=( [host1]="mvn clean install && cd webapp && mvn jetty:run"
                [host2]="..."
     )

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

بنابراین، اغلب سزاوار است یک قدم به عقب برگردیم و به جای سایر زبانهای برنامه‌نویسی، به ضوابط پوسته ها فکر کنیم . آیا در حال اجرای اسکریپت در یک میزبان راه دور نیستیم؟ پس چرامجموعه پیکربندی‌ها را در اسکریپت‌ها ذخیره نمی‌کنیم؟ آنوقت ساده است:

  •  # مشخص برای میزبانهایی که دستورات باید در آنها اجرا شوند conf یک سری فایلهای 
     for conf in /etc/myapp/*; do
         host=${conf##*/}
         ssh "$host" bash < "$conf"
     done
    
     # /etc/myapp/hostname is just a script:
     mvn clean install &&
     cd webapp &&
     mvn jetty:run

اکنون ما نیاز به آرایه‌های انجمنی ، و همچنین نیاز به حل گروهی از مسائل بسیار ناگوار نقل‌قولی را برطرف نموده‌ایم. موازی‌سازی با Parallel گنو نیز آسان است:

  •  parallel ssh {/} bash "<" {} ::: /etc/myapp/*

آرایه انجمنی در پوسته‌های قدیمی‌تر قابل مدیریت است

قبل از اندیشیدن به استفاده از eval برای تقلید آرایه‌های انجمنی در یک پوسته‌ قدیمی‌تر(احتمالاً با ایجاد مجموعه‌ای از نام متغیرها مشابه homedir_alex)، فکر کردن به رویکرد ساده‌تر یا کاملاً متفاوتی که می‌توانید به جای آن به کار ببرید را امتحان کنید. اگر بازهم به نطر می‌رسد که این دستیابی بهترین کار است،معایب ذیل را ملاحظه نمایید:

  1. حقیقتاً خواندن، پیگردی نمودن، و نگهداری آن دشوار است.
  2. نامهای متغیر باید با عبارت منظم ^[a-zA-Z_][a-zA-Z_0-9]* منطبق باشد-- برای مثال، یک نام متغیر نمی‌تواند شامل کاراکترهای دلخواه باشد بلکه فقط حروف، ارقام و خط‌زیر مجاز می‌باشند. نمی‌توانیم متغیری شامل نامهای کاربری یونیکس داشته باشیم، برای نمونه، -- نام کاربری hong-hu را ملاحظه کنید. خط تیره '-'نمی‌تواند بخشی از نام متغیر باشد،بنابراین تمام تلاش برای ایجاد متغیری به نام homedir_hong-hu از ابتدا محکوم به شکست است.

  3. نقل‌قول برای حصول نتیجه صحیح دشوار است. اگر محتوای رشته(نه نام متغیر) می‌تواند شامل کاراکترهای فضای سفید و نقل‌قول باشد،نقل‌قولی نمودن به طور صحیح برای حفظ آن در هر دو تجزیه پوسته دشوار است. و آن فقط برای ثابت‌ها که در زمان نوشتن برنامه معلوم هستند، شدنی است. (فرمان ‎ printf %q‎ پوسته Bash کمک می‌کند، اما مورد قابل مقایسه‌ای در پوسته‌های POSIX در دسترس نیست

  4. اگر برنامه گندزدایی، ورودی کاربر را اداره نکند، این می‌تواند خیلی خطرناک باشد!

بخش آرایه‌ها از راهنما یا پرسش و پاسخ 5 را برای توضیح عمقی و مثالهایی از چگونگی استفاده از آرایه‌ها در Bash، بخوانید.

اگر شما نیاز به یک آرایه انجمنی دارید اما پوسته شما آنها را پشتیبانی نمی کند، لطفاً به جای آن استفاده از AWK را در نظر بگیرید.

غیر مستقیم

اندیشیدن قبل از به کاربردن غیرمستقیم

قرار دادن نام متغیرها یا هر ترکیب دستوری bash درون پارامترهای دیگر به طور کلی ایده نامساعدی است. این کار از جدایی بین کُد و داده تخطی می‌کند، و بدین ترتیب شما را در شیب لغزنده‌ای به طرف باگها، مسائل امنیت، و غیره قرار می‌دهد. حتی وقتی می‌دانید که «کاملاً می‌فهمید»، زیرا شما به «طور دقیق می‌دانید و می‌فهمید که چه کار می‌کنید»، باگها برای همگی ما پیشامد می‌کنند و سزاوار است رعایت جدایی را تمرین کنیم تا دامنه زیانهایی را که می‌توانند موجب گردند، به حداقل برسانیم.

گذشته از آن، کُد شما را نیز نامفهوم و غیر شفاف می‌سازد.

به طور طبیعی، در اسکریپت‌نویسی bash، شما ابداً نیازی به ارجاع‌های غیر مستقیم ندارید. عموماً، اشخاص زمانی به این راه حل متوجه می‌شوند که در مورد آرایه‌های Bash نمی‌دانند یا درک نمی‌کنند، یا سایر ویژگی‌های Bash از قبیل توابع را به طور کامل ملاحظه نکرده‌اند.

ارزیابی متغیرهای غیرمستقیم/مرجع‌ها

BASH به شما اجازه می‌دهد پارامتر را به طور غیرمستقیم بسط دهید--یعنی،یک متغیر ممکن است شامل نام متغیر دیگری باشد:

  •  # Bash
     realvariable=contents
     ref=realvariable
     echo "${!ref}"   # محتویات متغیر اصلی را چاپ می‌کند

(ksh93) پوسته Korn تفاوت کلی دارد، ترکیب دستوری قدرتمندتر-- دستور nameref (همچنین به عنوان ‎typeset -n‎ نیز شناخته می‌شود):

  •  # ksh93
     realvariable=contents
     nameref ref=realvariable
     echo "$ref"      #  محتویات متغیر اصلی را چاپ می‌کند

متأسفانه، برای پوسته‌های غیر از Bash و ksh93 هیچ ترکیب دستوری برای ارزیابی متغیر مرجع شده، موجود نیست. شما باید از eval استفاده کنید، که یعنی شما، جهت اجتناب از عاقبت آن، باید متحمل اقدامات بسیار زیادی برای سترون سازی داده‌های خود بشوید.

تصور یک استفاده عملی از این، که به همان آسانی کاربرد آرایه انجمنی ، انجام بشود مشکل است. اما اشخاص دائماً آنرا می‌پرسند(این به طور مؤثق سؤال مکرراً پرسیده شده‌ای می‌باشد).

فرمان nameref پوسته ksh93 به ما اجازه می‌دهد با ارجاع‌ها به آرایه‌ها به همان خوبی متغیرهای معمولی کار کنیم. برای مثال:

  •  # ksh93
     myfunc() {
       nameref ref=$1
       echo "array $1 has ${#ref[*]} elements"
     }
     realarray=(...)
     myfunc realarray

ما از هیچ ترفندی که بتواند رونوشتی از آن توانایی در پوسته‌های POSIX یا Bourne ایجاد کند آگاه نمی‌باشیم(غیر از کاربرد eval، که اجرای آن به طور امن به شدت دشوار است). Bash تقریباً می‌تواند آنرا انجام بدهد -- برخی ترفندهای آرایه غیرمستقیم کار می‌کنند، و برخی دیگر خیر، و ما نمی‌دانیم که آیا ترکیب مورد بحث در انتشارهای آینده پایدار باقی می‌ماند. پس این هک را با مسؤلیت خودتان استفاده کنید.

  •  # Bash -- trick #1.  به نظر می‌رسد در نگارش ۲ و بالاتر کار می‌کند
     realarray=(...) ref=realarray; index=2
     tmp="$ref[$index]"
     echo "${!tmp}"                    # عضو آرایه با شاخص ۲ را می‌دهد
    
     # Bash -- trick #2.  ظاهراً در نگارش ۳ و بالاتر کار می‌کند‎
     #  کار نمی‌کند bash 2.05b در نگارش  
     tmp="$ref[@]"
     printf "<%s> " "${!tmp}"; echo    # .در تمام طول آرایه تکرار می‌شود

بازیابی شاخص‌های آرایه به طور مستقیم با استفاده از بسط غیر مستقیم‎ ${!var}‎ پوسته Bash امکان پذیر نیست.

تخصیص متغیرهای غیرمستقیم/مرجع

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

اختصاص مقدار از طریق مرجع(یا اشاره‌گر، یا متغیر غیرمستقیم، یا هر چه شما می‌خواهید آنرا بنامید -- من از اینجا به بعد می‌خواهم از "ref" استفاده کنم) به طور گسترده‌تری ممکن است، اما توانایی انجام آن به شدت مختص پوسته است.

قبل از اینکه شروع کنیم، باید اشاره کنیم که شما باید مقدار ref را کنترل کنید. یعنی، شما باید فقط از آن ref که مقدار آن را خودتان داخل برنامه تخصیص داده‌اید، یا از ورودی تصدیق شده، استفاده کنید. اگر یک کاربرنهایی بتواند متغیر ref را با رشته‌های دلخواه اشغال کند، نتیجه می‌تواند تزریق پیش‌بینی نشده کُد باشد. در انتها مثالی از این مورد را نشان خواهیم داد.

در ksh93، فقط می‌توانیم دوباره از nameref استفاده کنیم:

  •  # ksh93
     nameref ref=realvariable
     ref="contents"
     #   می‌باشد "contents" شامل رشته  realvariable اکنون

در Bash، می‌توانیم از read و ترکیب دستوری here string ویژه Bash استفاده کنیم:

  •  # Bash
     ref=realvariable
     IFS= read -r $ref <<< "contents"
     #   می‌باشد "contents" شامل رشته  realvariable اکنون

به هرحال، این فقط اگر سطر جدیدی در محتویات موجود نباشد، کار می‌کند. اگر به تخصیص دادن سطرهای چندتایی نیاز دارید به خواندن ادامه دهید.

ترفند مشابهی نیز برای متغیرهای آرایه‌ای Bash کار می‌کند:

  •  # Bash
     aref=realarray
     read -r -a $aref <<< "words go into array elements"
     echo "${realarray[1]}"   # را چاپ می‌کتد "go"

(یک بار دیگر، سطر جدید در ورودی این ترفند را نقض خواهد نمود. متغیر IFS برای جداکردن کلمان به کار می‌رود، بنابراین شما شاید به تنظیم آن احتیاج داشته یا نداشته باشید.)

ترفند دیگر مورد استفاده، فرمان ‎ printf -v‎ در Bash است(فقط در نگارش‌های اخیر در دسترس می‌باشد):

  •  #  یا بالاتر Bash 3.1 نگارش 
     ref=realvariable
     printf -v $ref %s "contents"

اگر محتویات شامل یک رشته ثابت نباشد، بلکه ترجیحاً تولید شده به طور پویا باشد، ترفند ‎printf -v‎ سودمند است. از تمام امکانات قالب‌بندی printf می‌توانید استفاده کنید. این ترفند هر محتوایی را نیز برای رشته اجازه می‌دهد، از جمله سطرهای‌جدید تعبیه شده(اما بایتهای NUL خیر -هیچ نیرویی در جهان نمی‌تواند بایتهای تهی را به طور قابل استفاده‌ای در رشته‌های شل قرار بدهد). اگر شما در bash نگارش 3.1 یا بالاتر هستید، این بهترین ترفند است.

بازهم یک ترفند دیگر فرمان typeset پوسته Korn یا فرمان declare پوسته Bash است. اینها تقریباً هم‌ارز یکدیگر هستند. هر دوی اینها اگر داخل یک تابع به کار بروند باعث قلمرو محلی متغیر می‌شوند، اما اگر در خارج از تابع استفاده شوند، می‌توانند متغیرهای سراسری را به کار اندازند.

  •  # Korn تمام نگارشهای پوسته
     typeset $ref="contents"
    
     # Bash:
     declare $ref="contents"

نگارش 4.2 Bash گزینه ‎ declare -g‎ را اضافه نموده، که می‌تواند حتی از داخل تابع متغیر را در زمینه سراسری قرار بدهد.

مزیت استفاده از typeset یا declare نسبت به eval آنست که سمت راست تخصیص توسط شل تفکیک نمی‌شود. اگر شما در اینجا از eval استفاده کنید، باید اول تمام سمت راست تخصیص راپاکیزه کنید(پوشش بدهید). این ترفند محتویات را نیز به طور دقیق حفظ می‌نماید،از جمله سطرهای جدید را، بنابراین، اگر در bash قبل از 3.1 (یا ksh88) می‌باشید بهترین ترفند است و در مورد تغییر تصادفی حوزه متغیرها نگران نباشید(به عنوان مثال، در حال استفاده از آن در داخل تابع نیستید).

در هرحال، با bash، هنوز هم شما باید در مورد سمت چپ تخصیص مراقب باشید. داخل کروشه‌ها، بازهم بسط ها انجام می‌شوند، بدین معنی که با ref آلوده، declare می‌تواند درست به خطرناکی eval باشد:

  •  # Bash:
     ref='x[$(touch evilfile; echo 0)]'
     ls -l evilfile   # چنین فایل یا دایرکتوری موجود نیست
     declare "$ref=value"
     ls -l evilfile   # ! حالا موجود است

این مشکل با typeset در mksh و pdksh نیز وجود دارد، اما ظاهراً در ksh93 نیست. این است چرای آنکه مقدار ref باید همیشه تحت کنترل شما باشد.

اگر شما از پوسته Bash یا Korn استفاده نمی‌کنید، می‌توانید تخصیص متغیرهای مرجع شده را با استفاده ازترکیب دستوری HereDocument انجام دهید:

  •  # Bourne
     ref=realvariable
     read $ref <<EOF
     contents
     EOF

(افسوس، با روش read ما برای گرفتن حداکثر تنها یک سطر محتوا عقب‌گرد می‌کنیم. این قابل حمل‌ترین ترفند است، اما محدود به یک سطر منفرد محتوا شده است.)

به خاطر بیاورید که، موقع استفاده از here document، اگر کلمه نگهبان(EOF در مثال ما) نقل‌قولی نشده باشد، آن وقت بسط پارامتر درون بدنه انجام خواهد شد. اگر نگهبان نقل‌قولی بشود، سپس بسط پارامتر انجام نمی‌شود. هر کدام که برای کار شما مناسبتر است به کار ببرید.

سرانجام، بعضی ها دقیقاً نمی‌توانند با پرتاب eval داخل یک عکس نیز مخالفت کنند:

  •  # Bourne
     ref=myVar
     eval "$ref=\$value"

این به جمله‌ای که اجرا می‌شود بسط می‌یابد:

  •  myVar=$value

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

خبر خوب آن است که اگر شما طرف راست را به طور صحیح سترون نمایید، این ترفند کاملاً قابل حمل است،هیچ مسئله ناشی از دامنه متغیر ندارد، و تمام محتویات از جمله سطرهای جدید را اجازه می‌دهد. خبر بد آنکه اگر شما در سترون‌سازی طرف راست به طور صحیح، موفق نباشید، حفره حجیم امنیتی دارید. از eval با ریسک خودتان استفاده کنید.

See Also


CategoryShell

پرسش و پاسخ شماره ۶ (آخرین ویرایش ‎ 2012-05-28 18:03:15 ‎ توسط ormaaj)

نظرات 0 + ارسال نظر
ایمیل شما بعد از ثبت نمایش داده نخواهد شد