آموزش‌های خط فرمانی

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

آموزش‌های خط فرمانی

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

قرار دادن فرمان در متغیر


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

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

    # مثالی که کار نمی‌کند
    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)


پرسش و پاسخ شماره ۴۹


چگونه می‌توانم به روزرسانی‌های دوره‌ای یا پیوست کردن‌ها به یک فایل راببینم؟(مثل: رشد فایل log )

tail -f‎ رشد یک فایل ثبت وقایع را به شما نشان می‌دهد. در بعضی سیستم‌ها(برای مثال OpenBSD)، این به طور خودکار تبدیل یک فایل لاگ به فایل جدیدی با همان نام را دنبال می‌کند(که به طور معمول همانست که شما می‌خواهید). برای به دست آوردن همین توانایی در سیستم‌های گنو، به جای آن از ‎ tail -F‎ استفاده کنید.

این فقط در صورتی مفید است که به روزرسانی‌های فایل پس از آخرین نوبتی که آنرا دیده‌اید، مورد نیازتان باشد.

# آغاز می‌شود n=1 با تنظیم 
   tail -n $n testfile; n="+$(( $(wc -l < testfile) + 1 ))"

هر بار فراخوانی این کُد، به روزرسانی فایل را از جایی که آخرین بار متوقف شده‌ایم ارائه می‌دهد. اگر شماره سطر جایی که می‌خواهید از آنجا شروع کنید را می‌دانید، n را برابر آن عدد قرار بدهید.


CategoryShell

پرسش و پاسخ 49 (آخرین ویرایش ‎ 2010-06-25 20:17:30 ‎ توسط MatthiasPopp)


فرمان eval و مسائل امنیت


فرمان eval و مسائل امنیت

فرمان eval برای سوءاستفاده به شدت قدرتمند و بینهایت آسان است.

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

مثالهای استفاده نامناسب eval

"eval" غلط املایی رایج evil(مترجم: زیانبار، ناشناخته) است. بخشی از این پرسش و پاسخ که به فاصله‌ها در نام فایلها می‌پردازد، قبلاً شامل اسکریپت زیر تحت عنوان «ابزار مفید(که احتمالاً به اندازه تکنیک ‎\0‎ امن نمی‌باشد)» آمده بود.

    Syntax : nasty_find_all <path> <command> [maxdepth]

    # !این کد زیانبار است و  هرگز نباید به کار برود
    export IFS=" "
    [ -z "$3" ] && set -- "$1" "$2" 1
    FILES=`find "$1" -maxdepth "$3" -type f -printf "\"%p\" "`
    # هشدار، شرارت
    eval FILES=($FILES)
    for ((I=0; I < ${#FILES[@]}; I++))
    do
        eval "$2 \"${FILES[I]}\""
    done
    unset IFS

این اسکریپت جستجوی بازگشتی فایلها و اجرای فرمان معین شده کاربر روی آنها، حتی اگر نام آنها شامل سطر جدید یا فاصله یا هردو باشد را فرض کرده بود. مؤلف گمان کرده بود که ‎find -print0 | xargs -0‎ برای مقاصدی از قبیل فرمانهای چندگانه مناسب نمی‌باشد. این اسکریپت با یک توضیح تعلیمی درهریک از تمام سطرهای در بر گیرنده همراه بود، که ما از آنها صرفنظر می‌کنیم.

در دفاع از اسکریپت، به این شکل کار می‌کند:

$ ls -lR
.:
total 8
drwxr-xr-x  2 vidar users 4096 Nov 12 21:51 dir with spaces
-rwxr-xr-x  1 vidar users  248 Nov 12 21:50 nasty_find_all

./dir with spaces:
total 0
-rw-r--r--  1 vidar users 0 Nov 12 21:51 file?with newlines
$ ./nasty_find_all . echo 3
./nasty_find_all
./dir with spaces/file
with newlines
$

اما این را ملاحظه نمایید:

$ touch "\"); ls -l $'\x2F'; #"

شما درست فایلی به نام ‎  "); ls -l $'\x2F'; #‎ ایجاد نموده‌اید.

حالا FILES شامل ‎ ""); ls -l $'\x2F'; #‎ خواهد بود. موقعی که ما ‎eval FILES=($FILES)‎ را اجرا می‌کنیم، تبدیل می‌شود به

FILES=(""); ls -l $'\x2F'; #"

که خودش می‌شود دوجمله ‎ FILES=(""); ‎ و ‎ ls -l / ‎ تبریک، شما دقیقاً اجرای دستورات دلخواه را مجاز نموده‌اید.

$ touch "\"); ls -l $'\x2F'; #"
$ ./nasty_find_all . echo 3
total 1052
-rw-r--r--   1 root root 1018530 Apr  6  2005 System.map
drwxr-xr-x   2 root root    4096 Oct 26 22:05 bin
drwxr-xr-x   3 root root    4096 Oct 26 22:05 boot
drwxr-xr-x  17 root root   29500 Nov 12 20:52 dev
drwxr-xr-x  68 root root    4096 Nov 12 20:54 etc
drwxr-xr-x   9 root root    4096 Oct  5 11:37 home
drwxr-xr-x  10 root root    4096 Oct 26 22:05 lib
drwxr-xr-x   2 root root    4096 Nov  4 00:14 lost+found
drwxr-xr-x   6 root root    4096 Nov  4 18:22 mnt
drwxr-xr-x  11 root root    4096 Oct 26 22:05 opt
dr-xr-xr-x  82 root root       0 Nov  4 00:41 proc
drwx------  26 root root    4096 Oct 26 22:05 root
drwxr-xr-x   2 root root    4096 Nov  4 00:34 sbin
drwxr-xr-x   9 root root       0 Nov  4 00:41 sys
drwxrwxrwt   8 root root    4096 Nov 12 21:55 tmp
drwxr-xr-x  15 root root    4096 Oct 26 22:05 usr
drwxr-xr-x  13 root root    4096 Oct 26 22:05 var
./nasty_find_all
./dir with spaces/file
with newlines
./
$

تعویض‎  ls -l ‎ با ‎ rm -rf ‎ یا وخیم‌تر، قدرت خلاقه زیادی نمی‌خواهد.

کسی ممکن است با خود بگوید اینها رویدادهای مشکوک است، اما کسی این نیرنگ را به کار نمی‌برد. تمام آن فقط یک کاربر بداندیش می‌خواهد، یا شاید محتمل‌تر، کاربر مبتدی که موقع رفتن به حمام ترمینال خود را قفل نشده ترک می‌کند، یا اسکریپت PHP نوشته شده برای ارسال فایل که سلامت نام فایل را کنترل نمی‌کند، یا فردی مرتکب اشتباهی مانند شخصی بشود که خودش اجازه اجرای کُد اختیاری را جایز نموده است(اکنون به جای محدود بودن به کاربران وب ، یک ضارب می‌تواند از ‎ nasty_find_all‎ برای عبور به محبس chroot و به دست آوردن امتیازات اضافی استفاده کند)، یا استفاده از یک سرویس گیرنده IRC یا IM که در پذیرش نام فایلها برای انتقال فایل یا لاگ‌های مکالمه بیش از حد آزادی‌خواه است، و غیره.

مثالهای استفاده مناسب eval

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

# در سیتم‌های قدیمی، بعد از تغییر اندازه پنجره، شخص  این را باید اجرا کند‎
eval `resize`

#          SSH کمتر ابتدایی: گرفتن  عبارت عبور برای یک کلید محرمانه ‎
#       اجرا می‌شود .xsession یا .profile این به طور نوعی از  یک نوع ‎
#  به تمام پردازشهای نشست کاربر ssh متغیرهای تولید شده توسط کارگزار‎
#           .احتمالی از آن ارث می‌برد ssh صادر می‌شوند، به طوری که یک ‎
eval `ssh-agent -s`

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

# POSIX
#
# تولید متغیرهای گزینه به طور پویا، احظار ش را امتحان کنید
#
#    sh -x example.sh --verbose --test --debug

for i in "$@"
do
    case "$i" in
       --test|--verbose|--debug)
            shift                   # حذف گزینه خط فرمان
            name=${i#--}            # حذف پیشوند گزینه
            eval "$name='$name'"    # از نو ساختن  متغیر
            ;;
    esac
done

echo "verbose: $verbose"
echo "test: $test"
echo "debug: $debug"

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

# !کُد خطرناک، این را به کار نبرید
for i in "$@"
do
    case "$i" in
       --test*|--verbose*|--debug*)
            shift                   # حذف گزینه خط فرمان ‎
            name=${i#--}            # حذف پیشوند گزینه‎
            eval "$name='$name'"    # درست کردن گزینه از نو‎
            ;;
    esac
done

تمام آنچه تغییر نموده آنست که ما سعی نموده‌ایم مثال خوب قبلی(که کار خیلی زیادی انجام نمی‌دهد) را به این طریق ، با اجازه دادن به دریافت مواردی مانند ‎ --test=foo‎ سودمند کنیم. اما نگاه کنید که چه چیزی را فعال می‌سازد:

$ ./foo --test='; ls -l /etc/passwd;x='
-rw-r--r-- 1 root root 943 2007-03-28 12:03 /etc/passwd

یکبار دیگر: با مجاز نمودن آنکه فرمان eval با ورودی فیلتر نشده کاربر استفاده بشود، ما اجرای فرمان دلخواه را مجاز کرده‌ایم.

اگر چه، تخصیص یک متغیر اختیاری توسط eval با استفاده از این ترکیب دستوری کاملاً بی خطر می‌باشد:

eval "$varname=\$whatever"

البته، این ترکیب فرض می‌کند که ‎$varname‎ نام یک متغیر معتبر می‌باشد.

جایگزین‌ها برای eval

  • آیا این نمی‌توانست با declare بهتر انجام بشود؟ به عنوان مثال:

     for i in "$@"
     do
        case "$i" in
           --test|--verbose|--debug)
                shift                   # حذف گزینه خط فرمان‎
                name=${i#--}            # حذف پیشوند گزینه‎
                declare $name=Yes       # تنظیم مقدار پیش فرض
                ;;
           --test=*|--verbose=*|--debug=*)
                shift
                name=${i#--}
                value=${name#*=}        #  کمیت جایی بعد از اولین کلمه و ‏=‏ است‎
                name=${name%%=*}        #  محدود نمودن نام به تنها یک کلمه ‎
    				    #  (حتی اگر یک  = دیگر در مقدار باشد)‎
                declare $name="$value"  #  درست کردن گزینه از نو‎
                ;;
        esac
     done

    توجه کنید که ‎ --name‎ برای حالت پیش فرض ، و ‎ --name=value قالب‌های لازم هستند.

    به نظر می‌رسد declare نوعی تفکیک کننده جادویی دارد، بیشتر مانند‎ [[ ‎. در اینجا آزمایشی هست که من در bash نگارش 3.1.17 انجام داده‌ام:

     griffon:~$ declare foo=x;date;x=Yes
     Sun Nov  4 09:36:08 EST 2007
     
     griffon:~$ name='foo=x;date;x'
     griffon:~$ declare $name=Yes
     griffon:~$ echo $foo
     x;date;x=Yes

    آشکار است که، حداقل در bash, فرمان declare خیلی مطمئن‌تر از eval است.

     attoparsec:~$ echo $BASH_VERSION 
     4.2.24(1)-release
     attoparsec:~$ danger='( $(printf "%s!\n" DANGER >&2) )'
     attoparsec:~$ declare safe=${danger}
     attoparsec:~$ declare -a unsafe
     attoparsec:~$ declare unsafe=${danger}
     DANGER!
     attoparsec:~$ 
    متغیرهای عادی ممکن است با declare بی خطر باشند، اما متغیرهای آرایه‌ای بی‌خطر نیستند.

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

کاربرد قوی eval

یک رویکرد دیگر آن است که کُد خطرناک می‌تواند در یک تابع پوشانیده بشود. برای مثال به جای انجام کاری مانند این:

    eval "${ArrayName}"'="${Value}"'

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

    ArrayName="echo rm -rf /tmp/dummyfolder/*; tvar"
    eval "${ArrayName}"'="${Value}"'

راه پیشگیری از این گونه حفره امنیتی ایجاد تابعی است که اجرایش مقدار معینی از امنیت را به شما می‌دهد و کُد پاکیزه‌تری را جایز می‌نماید.

  # check_valid_var_name VariableName
  function check_valid_var_name {
    case "${1:?Missing Variable Name}" in
      [!a-zA-Z_]* | *[!a-zA-Z_0-9]* ) return 3;;
    esac
  }
  # set_variable VariableName [<Variable Value>]
  function set_variable {
    check_valid_var_name "${1:?Missing Variable Name}" || return $?
    eval "${1}"'="${2:-}"'
  }
  set_variable "laksdpaso" "dasädöas# #-c,c pos 9302 1´ " 
  set_variable "echo rm -rf /tmp/dummyfolder/*; tvar" "dasädöas# #-c,c pos 9302 1´ " 
  # return Error

توجه: set_variable یک مزیت اضافه بر کاربرد declare دارد. مورد پایین را ملاحظه کنید.

   VariableName="Name=hhh"
   declare "${VariableName}=Test Value"         # Valid code, unexpected behavior
   set_variable "${VariableName}" "Test Value"  # return Error

چند مثال دیگر برای ارجاع

  # get_array_element VariableName ArrayName ArrayElement
  function get_array_element {
    check_valid_var_name "${1:?Missing Variable Name}" || return $?
    check_valid_var_name "${2:?Missing Array Name}" || return $?
    eval "${1}"'="${'"${2}"'["${3:?Missing Array Index}"]}"'
  }
  # set_array_element ArrayName ArrayElement [<Variable Value>]
  function set_array_element {
    check_valid_var_name "${1:?Missing Array Name}" || return $?
    eval "${1}"'["${2:?Missing Array Index}"]="${3:-}"'
  }
  # unset_array_element ArrayName ArrayElement
  function unset_array_element {
    unset "${1}[${2}]"
  }
  # unset_array_element VarName ArrayName
  function get_array_element_cnt {
    check_valid_var_name "${1:?Missing Variable Name}" || return $?
    check_valid_var_name "${2:?Missing Array Name}" || return $?
    eval "${1}"'="${#'"${2}"'[@]}"'
  }
  # push_element ArrayName <New Element 1> [<New Element 2> ...]
  function push_element {
    check_valid_var_name "${1:?Missing Array Name}" || return $?
    local ArrayName="${1}"
    local LastElement
    eval 'LastElement="${#'"${ArrayName}"'[@]}"'
    while shift && [ $# -gt 0 ] ; do
      eval "${ArrayName}"'["${LastElement}"]="${1}"'
      let LastElement+=1
    done
  }
  # pop_element ArrayName <Destination Variable Name 1> [<Destination Variable Name 2> ...]
  function pop_element {
    check_valid_var_name "${1:?Missing Array Name}" || return $?
    local ArrayName="${1}"
    local LastElement
    eval 'LastElement="${#'"${ArrayName}"'[@]}"'
    while shift && [[ $# -gt 0 && ${LastElement} -gt 0 ]] ; do
      let LastElement-=1
      check_valid_var_name "${1:?Missing Variable Name}" || return $?
      eval "${1}"'="${'"${ArrayName}"'["${LastElement}"]}"'
      unset "${ArrayName}[${LastElement}]" 
    done
    [[ $# -eq 0 ]] || return 8
  }
  # shift_element ArrayName [<Destination Variable Name>]
  function shift_element {
    check_valid_var_name "${1:?Missing Array Name}" || return $?
    local ArrayName="${1}"
    local CurElement=0 LastElement
    eval 'LastElement="${#'"${ArrayName}"'[@]}"'
    while shift && [[ $# -gt 0 && ${LastElement} -gt ${CurElement} ]] ; do
      check_valid_var_name "${1:?Missing Variable Name}" || return $?
      eval "${1}"'="${'"${ArrayName}"'["${CurElement}"]}"'
      let CurElement+=1
    done
    eval "${ArrayName}"'=("${'"${ArrayName}"'[@]:${CurElement}}")'
    [[ $# -eq 0 ]] || return 8
  }
  # unshift_element ArrayName <New Element 1> [<New Element 2> ...]
  function unshift_element {
    check_valid_var_name "${1:?Missing Array Name}" || return $?
    [ $# -gt 1 ] || return 0
    eval "${1}"'=("${@:2}" "${'"${1}"'[@]}" )'
  }

 # 1000 x { declare "laksdpaso=dasädöas# #-c,c pos 9302 1´ "       }  0m0.069s مدت زمان اجرا‎
 # 1000 x { set_variable laksdpaso "dasädöas# #-c,c pos 9302 1´ "  }  0m0.141s مدت زمان اجرا‎
 # 1000 x { get_array_element TestVar TestArray 1                  }  0m0.199s مدت زمان اجرا‎
 # 1000 x { set_array_element TestArray 1 "dfds  edfs fdf df"      }  0m0.174s مدت زمان اجرا‎
 # 1000 x { set_array_element TestArray 0                          }  0m0.167s مدت زمان اجرا‎
 # 1000 x { get_array_element_cnt TestVar TestArray                }  0m0.171s مدت زمان اجرا‎

 # با یک آرایه دو هزار عنصری اتجام شده‌اند push,pops,shifts,unshifts همه نوابع ‎
 # 1000 x { push_element TestArray "dsf sdf ss s"                  }  0m0.274s مدت زمان اجرا‎
 # 1000 x { pop_element TestArray TestVar                          }  0m0.380s مدت زمان اجرا‎
 # 1000 x { unshift_element TestArray "dsf sdf ss s"               }  0m9.027s مدت زمان اجرا‎
 # 1000 x { shift_element TestArray TestVar                        }  0m5.583s مدت زمان اجرا‎

توجه، shift_element و unshift_element کارایی ضعیفی دارند و مخصوصاً روی آرایه‌های بزرگ باید از آنها اجتناب گردد. مابقی کارایی قابل قبولی دارند و من به طور منظم آنها را به کار می‌برم.


CategoryShell

پرسش و پاسخ 48 (آخرین ویرایش ‎2013-03-07 03:51:23‎ توسط ChrisJohnson)


تغییر مسیر stderr به یک لوله


چطور می‌توانم stderr را به یک لوله تغییر مسیر بدهم؟

یک لوله فقط می‌تواند خروجی استاندارد‎ (stdout)‎ برنامه را حمل کند. برای عبور دادن خروجی استاندارد خطا ‎ (stderr)‎ از میان لوله، لازم است stderr را به همان مقصد stdout تغییر مسیر بدهید. به طور اختیاری برای دریافت stderr تنها، می‌توانید stdout را ببندید یا به ‎ /dev/null‎ تغییر مسیر بدهید. چند نمونه کُد:

# Bourne
# می‌نویسد stdout و هم در stderr برنامه‌ایست که هم در 'myprog' فرض کنید  ‎

# را به لوله تغییر مسیر می‌دهد درحالیکه stderr نگارش اول ‎
# خروجی استاندارد نیز  باقی می‌ماند. (هردو مخلوط می‌شوند)‏‎
myprog 2>&1 | grep ...

#  به لوله تغییر مسیر داده می‌شود، بدون  stderr نگارش دوم ‎
#  .تغییرمسیر داده می‌شود ‎/dev/null‎ که به stdout حفظ کردن‎
myprog 2>&1 >/dev/null | grep ...

#       .در یک فایل stdout همان ایده، این دفعه با ذخیره 
myprog 2>&1 >file | grep ...

یک مثال ساده دیگر از تغییر مسیر stdout و stderr:

# Bourne
{ command | stdout_reader; } 2>&1 | stderr_reader

برای توضیحات بیشتر در مورد چگونگی عملکرد متقابل تغییر مسیرها و لوله‌ها پرسش و پاسخ شماره 55 را ببینید.

این مورد کاربرد واضحی با برنامه‌هایی از قبیل dialog دارد، که(با استفاده از ncurses) پنجره‌ها را در نمایشگر(خروجی استاندارد) رسم می‌کند و نتایج را به stderr بر می‌گرداند. یک روش کار با این برنامه می‌تواند تغییر مسیر stderr به یک فایل موقتی باشد. اما این ضروری نیست -- پرسش و پاسخ شماره 40 را برای مثالهای استفاده از dialog ببینید!

در مثال فوق(همچنین پرسش و پاسخ شماره 40)، ما به طور کلی stdout را رها کردیم، یا آن را به دستگاه شناخته شده‌ای فرستادیم(‎/dev/tty‎ برای ترمینال کاربر). همچنین ممکن است کسی فقط stderr را لوله‌کشی نماید اما تعامل با stdout را حفظ نماید(بدون شناخت مستدل از جایی که خروجی اسکریپت می‌رود). این اندکی ماهرانه است.

# Bourne
#                بدون تغییر stdout به لوله و نگاه داشتنstderr تغییر مسیر‎

exec 3>&1                       #       ذخیره مقدار فعلی خروجی استاندارد‎
myprog 2>&1 >&3 | grep ...      #  3 شماره  FD  ارسال خروجی استاندارد به‎
exec 3>&-                       #    .حالا بستن آن برای باقیمانده اسکریپت‎

#     http://www.tldp.org/LDP/abs/html/io-redirection.html    با تشکر از 

همان کار می‌تواند بدون exec انجام بشود:

# POSIX
$ myfunc () { echo "I'm stdout"; echo "I'm stderr" >&2; }
$ { myfunc 2>&1 1>&3 3>&- | cat  > stderr.file 3>&-; } 3>&1
I'm stdout
$ cat stderr.file
I'm stderr

توصیف‌گر شماره 3 بسته می‌شود‎ (3>&-)‎ بنابراین فرمانها از آن ارث نمی‌برند. توجه کنید bash دونسخه‌ای نمودن و بستن در یک تغییر مسیر را اجازه می‌دهد: ‎ 1>&3- ‎
 می‌توانید در لینوکس با آزمایش زیر تفاوتها را کنترل کنید:

# Bash
{ bash <<< 'lsof -a -p $$ -d1,2,3'   ;} 3>&1
{ bash <<< 'lsof -a -p $$ -d1,2,3' 3>&-  ;} 3>&1

نمایش dialog یک سطری:

# Bourne
exec 3>&1
dialog --menu Title 0 0 0 FirstItem FirstDescription 2>&1 >&3 | sed 's/First/Only/'
exec 3>&-

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

یک نتیجه مشابه می‌تواند با جایگزینی پردازش حاصل شود:

# Bash
perl -e 'print "stdout\n"; warn "stderr\n"' 2> >(tr '[:lower:]' '[:upper:]')

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

این آموزش تغییر مسیر را ببینید(با یک مثال که stdout را به یک لوله تغییر مسیر می‌دهد و stderr را به لوله دیگری).


CategoryShell

پرسش و پاسخ 47 (آخرین ویرایش ‎ 2012-03-05 11:30:04 ‎ توسط pgas)


بررسی وجود یک کلمه در یک لیست


می‌خواهم بررسی کنم که آیا یک کلمه در یک لیست وجود دارد( یا یک عنصر عضوی از یک مجموعه هست).

اگر پرسش واقعی شما «چطور می‌توانم بررسی کنم که آیا یکی از پارامترهایم ‎ -v‎ است؟» می‌باشد، پس لطفاً پرسش و پاسخ شماره 35 را ملاحظه کنید.در غیر اینصورت، خواندن را ادامه دهید....

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

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

در یک آرایه سنتی معین، تنها روش صحیحِ انجام این کار، اجرای یک حلقه روی تمام عناصر آرایه و کنترل آنها برای عنصر مورد جستجوی شما می‌باشد. فرض کنیم آنچه در جستجوی آن هستیم bar است و لیست ما در آرایه foo قرار دارد:

  •    # Bash
       for element in "${foo[@]}"; do
          [[ $element = $bar ]] && echo "Found $bar."
       done

اگر نیاز دارید این کار را چندین مرتبه در اسکریپت خود انجام بدهید، ممکن است بخواهید منطق آنرا در یک تابع به کار ببرید:

  •    # Bash
       isIn() {
           local pattern="$1" element
           shift
    
           for element
           do
               [[ $element = $pattern ]] && return 0
           done
    
           return 1
       }
    
       if isIn "jacob" "${names[@]}"
       then
           echo "Jacob is on the list."
       fi

یا، اگر می‌خواهید تابع شما شماره شاخص عضوی که عنصر در آن پیدا شده را بازگرداند:

  •    # Bash 3.0 or higher
       indexOf() {
           local pattern=$1
           local index list
           shift
    
           list=("$@")
           for index in "${!list[@]}"
           do
               [[ ${list[index]} = $pattern ]] && {
                   echo $index
                   return 0
               }
           done
    
           echo -1
           return 1
       }
    
       if index=$(indexOf "jacob" "${names[@]}")
       then
           echo "Jacob is the ${index}th on the list."
       else
           echo "Jacob is not on the list."
       fi

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

  •    # Bourne
       set -f
       for element in $foo; do
          if test x"$element" = x"$bar"; then
             echo "Found $bar."
          fi
       done
       set +f

در اینجا کلمه به عنوان هر زیر رشته‌ای که با فضای سفید(یا به طور دقیق‌تر کاراکترهای فعلی در متغیر IFS) مرزبندی شده تعریف می‌شود. دستور ‎ set -f‎ از بسط glob در کلمات داخل لیست پیش‌گیری می‌کند. فعال کردن مجدد بسط glob با‎ (set +f)‎ اختیاری است.

اگر شما در حال کار با bash نسخه 4 یا ksh93 می‌باشید، به آرایه‌های انجمنی دسترسی دارید. اینها به شما اجازه می‌دهند مشکل را تجدید سازمان بدهید -- به جای ساختن لیستی از کلمات که مجاز هستند، شما می‌توانید یک آرایه انجمنی بسازید که در آن کلیدها کلماتی هستند که شما می‌خواهید در نظر بگیرید. مقادیر آنها -- بسته به طبیعت مشکل --می‌توانند با معنی باشند یا نباشند.

  •    # Bash 4
       declare -A good
       for word in "goodword1" "goodword2" ...; do
         good["$word"]=1
       done
    
       # مجاز است $foo کنترل اینکه  آیا 
       if ((${good[$foo]})); then ...

این هم یک روش دستیابی غیر موجه که شما نباید از آن استفاده کنید، اما به خاطر تکمیل مطلب ارائه می‌شود:

  •    # Bash
       if [[ " $foo " = *" $bar "* ]]; then
          echo "Found $bar."
       fi

( مشکل در اینجا آنست که فرض می‌شود space می‌تواند به عنوان جداکننده بین کلمات استفاده شود. عناصر شما ممکن است شامل فاصله باشند، که این روش را ناموفق می‌سازد!)

همان روش، برای پوسته‌های Bourne:

  •    # Bourne
       case " $foo " in
          *" $bar "*) echo "Found $bar.";;
       esac

همچنین می‌توانید از glob توسعه یافته با printf برای جستجوی کلمه در یک آرایه استفاده کنید. من این را به اندازه کافی تست نکرده‌ام، بنابراین ممکن است در بعضی موقعیت‌ها ناموفق شود --sn18

  •    # Bash
       shopt -s extglob
       # glob تبدیل آرایه به
       printf -v glob '%q|' "${array[@]}"
       glob=${glob%|}
       [[ $word = @($glob) ]] && echo "Found $word"
  • این کُد موقعی که یک عنصر آرایه شامل کاراکتر | باشد شکست می‌خورد. از اینرو، من این را به این پایین میان سایر روشها که به شیوه‌ای با محدودیت مشابه کار می‌کنند انتقال دادم. -- GreyCat

    • printf %q ‎  نیز کاراکتر | را نقل ‌قولی می‌کند، بنابراین احتمالاً اینطور نیست --sn18

grep گنو یک ویژگی ‎ \b‎ دارد که که بنا به گفته برخی بر لبه‌های کلمات مطابقت می‌کند (مرزهای کلمه). ممکن است شخصی سعی کند با کاربرد آن، رویکرد کوتاه‌تر استفاده شده فوق را بازآزمایی نماید، اما این کار توأم با خطر است:

  •    #  یکی از پارامترهای مکانی است؟ 'foo' آیا
       egrep '\bfoo\b' <<<"$@" >/dev/null && echo yes
    
       # یکی از پارامترهای مکانی است؟ '-v'  این جایی است که شکست می‌خورد: آیا
       egrep '\b-v\b' <<<"$@" >/dev/null && echo yes
       # را به صورت یک کلمه جداگانه می‌بیند "v" , \b متأسفانه ‎
       # را به چه جهنمی می‌فرستد ‎"-"‎ کسی نمی‌داند‎
    
       # هست؟ 'array' در آرایه "someword" آیا کلمه 
       egrep '\bsomeword\b' <<<"${array[@]}"
       # باشد نمی‌توانید از این استفاده کنید '-v' همان someword بدیهی است اگر کلمه

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

مقایسه عمده

این شیوه تلاش می‌کند رشته مورد نظر را با تمام محتویات آرایه مقایسه نماید. به طور بالقوه می‌تواند بسیار مؤثر باشد، اما بستگی به جداکننده دارد که نباید در کمیت مورد جستجو یا در آرایه موجود باشد. در اینجا ما از ‎$'\a'‎ ، کاراکتر زنگ، استفاده کرده‌ایم چون بینهایت غیر معمول است.

  •    # usage: if has "element" list of words; then ...; fi
       has() {
         local IFS=$'\a' t="$1"
         shift
         [[ $'\a'"$*"$'\a' == *$'\a'$t$'\a'* ]]
       }

انواع برشمرده(Enumerated types)زیرنویس 1

در ksh93t یا بعد از آن شاید کسی با استفاده از دستور داخلی enum انواع، متغیرها، یا ثابتهای enum ایجاد نماید. اینها به طور مشابه با enumهای C کار می‌کنند(و خصیصه معادل در سایر زبانها). اینها می‌توانند برای محدود نمودن مقادیری که شاید به متغیری اختصاص داده شود، به کار بروند، به طوری‌که از لزوم یک بررسی سنگین در هر نوبتی که یک متغیر آرایه‌ای تنظیم یا ارجاع می‌شود، اجتناب گردد. مانند انواع تولید شده با استفاده از ‎ typeset -T‎، نتیجه یک فرمان enum، فرمان تعریف جدیدی است که می‌تواند برای معرفی اقلام آن نوع به کار برود.

# ksh93
 $ enum colors=(red green blue)
 $ colors foo=green
 $ foo=yellow
ksh: foo:  invalid value yellow‏‏
مترجم: در کُد فوق، به این دلیل خطا صادر می‌شود که ما متغیر را چنان تعریف کرده‌ایم که مقادیر قابل تخصیص به آن، به سه رنگ نامبرده محدود گردیده است

typeset -a‎ همچنین می‌تواند در ترکیب با یک نوع enum برای پذیرفتن ثابتهای enum به عنوان زیرنویس به کار برود.

# ksh93
 $ typeset -a [colors] bar
 $ bar[blue]=test1
 $ typeset -p bar
typeset -a [colors] bar=([blue]=test)
 $ bar[orange]=test
ksh: colors:  invalid value orange

برای مثالهای بیشتر ‎src/cmd/ksh93/tests/enum.sh‎ در منبع AST را ببینید.


  1. مترجم: نوع برشمرده(Enumerated type) از ویژگی‌های زبانهای برنامه نویسی پیشرفته‌ای مانند C است، که در تعریف خود لیست فراگیری از تمام مقادیر ممکن برای آن نوع متغیر را در بر دارد، مانند متغیری با نوع «روزهای هفته» که تعریف آن لیستی شامل هفت عضو با مقادیر شنبه، یکشنبه، و ... می‌باشد. (1)

CategoryShell

پرسش و پاسخ 46 (آخرین ویرایش ‎ 2012-07-24 04:36:07 ‎ توسط ormaaj)