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

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

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

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

پرسش و پاسخ شماره ۵۰

پرسش و پاسخ شماره ۵۰

من سعی دارم دستوری را در یک متغیر قرار بدهم، اما موارد پیچیده همیشه ناموفق است!

برخی اشخاص کوشش می‌کنند مواردی مشابه این کد را انجام بدهند:

    # مثالی که کار نمی‌کند
    args="-s 'The subject' $address"
    mail $args < $body

این کُد به دلیل تفکیک کلمه و به علت آنکه نقل‌قول‌های منفرد داخل متغیر لفظی هستند نه گرامری، شکست می‌خورد. وقتی که ‎$args‎ بسط داده می‌شود، چهار کلمه می‌شود. ‎ 'The‎ دومین کلمه، و ‎ subject'‎ سومین کلمه است.

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

بنابراین، چطور این کار را انجام بدهیم؟ تمام آن بستگی به این دارد که چه کاری است!

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

من تلاش می‌کنم فرمانی را ذخیره کنم به طوری که بعداً بدون تکرار آن در هر نوبت، آن را اجرا کنم

اگر شما می‌خواهید فرمانی را برای استفاده بعدی آن در ظرفی قرار بدهید، تابع را به کار ببرید. متغیرها داده را نگاه می‌دارند، توابع کُدها را نگاه می‌دارند.

    pingMe() {
        ping -q -c1 "$HOSTNAME"
    }

    [...]
    if pingMe; then ..

من می‌خواهم فرمانی را بر اساس اطلاعاتی که فقط در زمان اجرا معلوم است طرح ریزی کنم

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

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

    # این را انجام ندهید
    args=$recipient
    if [[ $subject ]]; then
        args+=" -s $subject"
    fi
    mail $args < $bodyfilename

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

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

    # یا بالاتر bash 3.1 مثال کارگر در
    args=("$recipient")
    if [[ $subject ]]; then
        args+=(-s "$subject")
    fi
    mail "${args[@]}" < "$bodyfilename"

(برای جزئیات بیشتر در مورد ترکیب دستوری آرایه پرسش و پاسخ شماره 5 را ببینید.)

اغلب، این پرسش موقعی می‌رسد که شخصی در حال تلاش برای استفاده از dialog برای ساختن یک منوی متحرک می‌باشد. فرمان dialog نمی‌تواند hard-coded زیرنویس 1 بشود، به دلیل آنکه پارامترهایش بر اساس داده‌هایی که تنها در زمان اجرا در دسترس هستند، فراهم می‌شود(به عنوان مثال تعداد اقلام منو). برای یک مثال از چگونگی انجام این کار به طور صحیح، پرسش و پاسخ شماره 40 را ببینید.

می‌خواهم یک وظیفه را عمومی کنم در حالتی که ابزار سطح پایین بعداً تغییر می‌کند

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

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

کاری که احتمالاً باید انجام دهید، این است:

    #  POSIX

    #  ارسال ایمیل به کسی‎
    #  بدنه نامه را از ورودی استاندارد می‌خواند‎
    #
    #  sendto address [subject]

    #
    sendto() {
	# unset -v IFS
        # mail ${2:+-s "$2"} "$1"
        MailTool ${2:+--subject="$2"} --recipient="$1"
    }

    sendto "$address" "The Subject" <"$bodyfile"

اینجا، بسط پارامتر کنترل می‌کند ‎$2‎ (موضوع اختیاری) به چیزی بسط یافته است. اگر اینطور باشد، بسط ‎ -s "$2"‎ را به فرمان mail اضافه می‌کند. اگر نه، به هیچ وجه بسط، گزینه ‎ -s‎ را اضافه نمی‌کند.

پیاده‌سازی اصلی اسکریپت، فرمان استاندارد یونیکس،‎ mail(1)‎ را به کار می‌برد. بعداً اسکریپت توضیح‌گذاری شد و با چیزی به نام MailTool که به طور فی‌البداهه مخصوص این مثال ساخته شده بود، تعویض گردید. اما به روشن ساختن این مفهوم خدمت می‌کرد که: حتی اگر ابزار پشتیبان تغییر نماید، فراخوانی تابع تغییر نمی‌کند. همچنین توجه نمایید که، مثال ‎mail(1)‎ فوق، برای جداکردن گزینه شناسه از بسط پارامتر نقل‌قولی شده داخلی به تفکیک کلمه استناد می‌کند. این یک استثنای قابل توجه است که در آن تفکیک کلمه قابل قبول و مطلوب است. این مورد بی خطر است زیرا گزینه کُد شده به طور ایستا، شامل هیچ کاراکتر glob نمی‌باشد، و بسط پارامتر برای ممانعت از globbing بعدی نقل‌قولی می‌شود. شما باید مطمئن شوید که IFS به منظور به دست آوردن نتایج مورد انتظار، به یک مقدار معقول تنظیم گردیده است.

من یک ثبت وقایع از عملیات اسکریپت خود می‌خواهم

دلیل دیگری که مردم سعی می‌کنند فرمانها را در متغیرها قرار بدهند این است که آنها می‌خواهند اسکریپت‌هایشان هر فرمان را قبل از اجرای آن چاپ کند. اگر تمام آنچه شما می‌خواهید این است، پس به سادگی از دستور ‎set -x‎ استفاده کنید، یا اسکریپت خود را با ‎ #!/bin/bash -x‎ یا ‎bash -x ./myscript‎ احضار کنید. توجه کنید که می‌توانید این حالت را با استفاده از ‎ set +x‎ و ‎set -x‎ خاموش و روشن کنید.

شایان توجه است که نمی‌توانید یک خط لوله دستور را داخل یک متغیر آرایه‌ای قرار بدهید و بعد با استفاده از تکنیک‎ "${array[@]}"‎ آن را اجرا کنید. تنها راه ذخیره یک خط لوله در یک متغیر، اضافه نمودن(با احتیاط!) لایه‌ای از نقل‌قولها در صورت لزوم ، ذخیره در یک متغیر رشته‌ای، و سپس استفاده از eval یا sh برای اجرای متغیر می‌باشد. این به دلایل امنیتی پیشنهاد نمی‌شود. همان مورد با فرمانهای در بر گیرنده تغییر مسیر، جملات if یا while، وغیره فراهم می‌گردد.

بعضی اشخاص دچار دردسر می‌شوند به علت آنکه می‌خواهند اسکریپتی داشته باشند که دستوراتشان از جمله تغییر مسیرها را قبل از اجرای آنها چاپ کند. ‎set -x‎ دستور را بدون تغییرمسیرها نمایش می‌دهد. اشخاص سعی می‌کنند با انجام موردی مانند کُد زیر آن را رفع و رجوع کنند:

    # مثالی که عمل نمی‌کند
    command="mysql -u me -p somedbname < file"
    ((DEBUG)) && echo "$command"
    "$command"

(این به قدری رایج است که من به طور صریح آن را قرار می‌دهم، ولواینکه گمان می‌کنم تکرار مطلبی است که قبلاً نوشتم.)

یکبار دیگر، این کار نمی‌کند. حتی یک آرایه در اینجا کار نمی‌کند. تنها موردی که در اینجا عمل می‌کند پوشش دادن فرمان با دقت زیاد برای مطمئن شدن از آنکه فوق کاراکترها باعث مشکلات خطیر امنیتی نخواهند شد، و سپس استفاده از eval یا sh برای خواندن مجدد دستور است. لطفاً این کار را انجام ندهید! یک روش برای ثبت وقایع کامل فرمان بدون متوسل شدن به استفاده از eval یا sh، استفاده از ‎DEBUG trap‎ می‌باشد. یک نمونه کد عملی:

   trap 'printf %s\\n "$BASH_COMMAND" >&2' DEBUG 

با فرض اینکه شما در حال ثبت کردن خطای استاندارد می‌باشید.

توجه کنید که نمایندگی تغییر مسیر، به وسیله ‎BASH_COMMAND‎ باز هم تحت تأثیر این باگ قرار می‌گیرد. به نظر می‌رسد تا اندازه‌ای در git تعمیر شده، اما کامل نیست. انتظار نداشته باشید صحیح باشد.

اگر شما به اندازه‌ای سرتان با آن جایتان بازی می‌کند که باز هم فکر می‌کنید نیاز دارید هر دستوری که در صدد اجرای آن می‌باشید قبل از اجرایش در خروجی نوشته شود، و تمام تغییر مسیرها را نیز شامل شود، پس تنها این کار را انجام بدهید:

    # مثال کارگر
    echo "mysql -u me -p somedbname < file"
    mysql -u me -p somedbname < file

به هیچ وجه متغیر استفاده نکنید. دقیقاً فرمان را کپی و paste کنید، یک لایه نقل‌قول اضافی در اطراف آن قرار بدهید(گاهی اوقات مهارت‌آمیز)، و یک echo پیش از آن ضمیمه کنید.

پیشنهاد شخصی من دقیقاً استفاده از ‎ set -x‎ و دلواپس آن نبودن است.


CategoryShell

پرسش و پاسخ 50 (آخرین ویرایش ‎2013-04-13 18:52:30‎ توسط geirha)


  1. مترجم: hard-coded به رفتاری گفته می‌شود که داده ‌ها به طور مستقیم داخل برنامه و احتمالاً در چندین محل نوشته می‌شوند به طوری که به آسانی نمی‌توانند اصلاح شوند. (1)