همواره شرایطی ماورای کنترل ما وجود دارد، که ما را به سمت انجام مواردی میراند، که اگر به عهده خودمان بود هرگز انجام آنها را انتخاب نمیکردیم. این مدخل FAQ یکی از آن موقعیتها را تشریح میکند.
یک برنامه CGI میتواند با پارامترهایی که توسط مرورگر شبکه ارسال گردیده، فراخوانی بشود . دو روش(حداقل) برای فراخوانی برنامه CGI وجود دارد: شیوه "GET" و شیوه "POST" . در شیوه "GET" ، پارامترها در یک متغیر محیطی به نام QUERY_STRING برای برنامه CGI فراهم میشوند. پارامترها قالب تعاریف KEY=VALUE را میگیرند(یعنی user=george)، با برخی کاراکترهایی که به هگزادسیمال کدگذاری شدهاند، فاصلهها به عنوان علامت بهاضافه کُد شدهاند، و همه آنها با کاراکترهای
البته اینک ما میدانیم که شما هرگز نمیخواهید یک اسکریپت CGI در Bash بنویسید. بنابراین برای اهداف مورد نظر این مدخل، ما فرض خواهیم نمود که تروریستها همسر و فرزندان شما را دزدیدهاند و اگر شما تقاضای آنان را درنوشتن چنین اسکریپتی اجابت نکنید، آنها را شکنجه میکنند، ضرب و جرح میسازند، و میکشند، «یا وخیمتر».
(موقعیت «یا وخیمتر» شاید به وضوح چیزی مشابه مجبور کردن شما به استفاده از نرمافزارهای مایکروسافت باشد.)
بنابراین، برای یک متغیر معین QUERY_STRING، شاید بخواهیم کلیدها(متغیرها) و مقادیر آنها را استخراج کنیم، به طوری که بتوانیم آنها را در اسکریپت به کار ببریم.
روش سریع، آسان، و خطرناک برای پردازش QUERY_STRING، تبدیل &ها به ;ها و سپس استفاده از فرمان eval برای انجام تخصیص آنها میباشد. به هر حال استفاده از eval به شدت دلسرد کننده است. این است که ما همیشه میگوییم، اگر راه دیگری برای انجام آن وجود دارد، از eval پرهیز نمایید.
# cgi در خواندن رشته ورودی در if [ "$QUERY_STRING" ]; then foo=$QUERY_STRING else read foo fi # (میماند برای تمرین خواننده) "&" تبدیل مقداری رشته کُد شده و مواردی مانند # روی رشته eval اجرای eval $foo # را در یک فیلد فرم وب قرار "/bin/rm -rf /" کنار بنشینید و تماشاکنید، کاربر # .نباشد میتواند به بخشی از سیستمفایل صدمه وارد کند root داده است، که حتی اگر # .یک رشته خطرناک دیگر می تواند بمب خوشهای باشد
به جای گفتن آنکه پوسته هر کُدِ فراهم شده توسط کاربر در پارامترها را اجرا کند، رویکرد بهتر استخراج هر زوج متغیر/مقدار، و تخصیص آنها در متغیرهای پوسته، به طور یک به یک، بدون اجرای آنها میباشد. این کار یک تخصیص متغیر غیر مستقیم نیاز دارد، که به معنی استفاده از بعضی ترفندکاریهای مختص پوسته میباشد. ما این ترفند را با ترکیب دستوری Bash مینویسیم، تبدیل آن به پوسته ksh یا Bourne به عنوان تمرین واگذار میشود.
# Bash # cgi خواندن رشته ورودی در if [ "$QUERY_STRING" ]; then foo=$QUERY_STRING else read -r foo fi # میباشد name=Fred+Flintstone&city=Bedrock شامل موردی مشابه foo # متصل شدهاند رفتار میکند & که با key=value این مانند یک لیست از عبارتهای # تکرار روی عناصر لیست و انجام عمل تخصیص هریک IFS='&'; set -f for i in $foo; do declare "$i" done unset IFS # یک متغیر پوسته با همان نام خواهد شد CGI اکنون هر پارامتر # .بهتر است شما بدانید نامها کدام هستند، چون آنها را پیگردی نمیکنیم # است. فاصله به عنوان + کُد شده است "urlencoded" هر متغیر بازهم # هگزادسیمال است xx کُد شدهاند که %xx اقلام مختلف به عنوان # را به کار ببریم "name" فرض کنید میخواهیم پارامتری به نام # .اول رمزگشایی فاصلهها name=${name//+/ } # ما ترفند دیگری برای انجام آن به کار میبریم %xx اکنون رمز گشایی کاراکترهای # تعویض میکنیم \x را با % اول تمام علائم # ها میشود\xxx را به کار میبریم برای آنکه موجب ارزیابی تمام echo -e دوم، دستور name=${name//\%/\\x} name=$(echo -e "$name") # این کار را قبل از تکرار و تخصیص حلقه انجام ندادیم چون اگر این کار را میکردیم # کُد شده (یا هر کاراکتر بدخواهانه) است & آنوقت یک پارامتر که شامل یک کاراکتر # .موجب اندوه بسیار خواهد شد. ما باید این کار را در اینجا انجام بدهیم # انجام بدهید "name" حالا هر کاری مایل هستید با
در حالیکه شاید این روش قدری کمتر واضح باشد، از مشکل امنیتی بزرگی اجتناب میکند که eval دارای آن است: اجرای هر فرمان دلخواهی که ممکن بود کاربر، علاقمند به ورود آن از طریق یک فُرم وِب باشد. به طور واضح این یک بهبود است.
در این نگارش هنوز کاستیهایی وجود دارد. برای مثال، ما برای تضمین معتبر یا ایمن بودن نام متغیر پوسته، هیچگونه اعتبارسنجی روی طرف چپ(نام متغیر) در هر زوج key=value انجام نمیدهیم. اگر کاربر یک PATH= را در یک پارامتر استعلام وارد کند چه؟
حتی یک رویکرد بهتر، میتواند قرار دادن زوجهای کلید\متغیر داخل یک آرایه انجمنی باشد. آرایههای انجمنی در ksh93 و bash 4.0 معتبر هستند، اما در POSIX یا پوستههای Bourne خیر. آنهابرای نگهداری زوجهای key/value طراحی شدهاند که در آن کلیدها میتوانند رشتههای اختیاری باشند، بنابراین به نظر میرسد برای این کار مناسب هستند.
# Bash 4+ # cgi خواندن رشته ورودی if [ "$QUERY_STRING" ]; then foo=$QUERY_STRING else read -r foo fi # .برقراری آرایه انجمنی برای نگهداری پارامترهای پرس وجو declare -A q # key=value+%41%42%43 تکرار روی عناصر # .جدا سازی کلید و کمیت، و انجام رمزگشایی کمیت IFS='&'; set -f for i in $foo; do IFS='=' read key value <<< "$i" # هاbackslash حذف -- اول پاک سازی : مراحل رمزگشایی # .دوم، علامتهای بعلاوه تبدیل به فاصله میشوند # .میشوند \x سوم، علائم درصدتبدیل به # .را موجب گردد printf چیزی باقی نمیگذارد که بتواند به طور غیر منتظره یک بسط # .ها مال ما هستند و هیچ علامت درصد باقی نماندهbackslashe تمام value=${value//\\/} value=${value//+/ } value=${value//\%/\\x} printf -v final -- "$value" q["$key"]="$final" done unset IFS # .به کار ببریم q اکنون میتوانیم پارامترها را از آرایه انجمنی با نام # ${!q[*]} اگر لیستی از کلیدها را لازم داشته باشیم، این است
در اینجا مرحله پاکسازی بینهایت مهم است. بدون آن اقدام احتیاطی، فرمان printf میتواند به واسطه یک رشته قالببندی مهاجم، آسیب پذیر باشد. گزینه printf -v varname در هر نگارش از bash که از آرایههای انجمنی پشتبانی کند، معتبر است، بنابراین میتوانیم از آن در اینجا استفاده کنیم. خیلی بیشتر از فراخوانی پوسته فرعی مؤثر است. همچنین از مشکلات بالقوه echo -e در صورتی که اتفاقاً value موردی مانند -n باشد، اجتناب نمودهایم.
به طور تکنیکی، خصوصیات CGI چندین نمونه از یک کلید را در یک استعلام منفرد اجازه میدهد. برای مثال، group=managers&member=Alice&member=Charlie یک رشته کاملاً مشروع پرس وجو میباشد. هیچ یک از رویکردها در این صفحه این حالت را مدیریت نمیکند(حداقل نه به طریقی که ما احتمالاً آنرا روش صحیح در نظر بگیریم). خوشبختانه، غالباً اینطور نیست که شما بخواهید یک اسکریپت CGI مانند این بنویسید، و در هر حال، شما مجبور نیستید برای انجام این وظیفه از bash استفاده کنید.
پرسش و پاسخ 92 (آخرین ویرایش 2010-04-16 23:58:26 توسط GreyCat)
COLUMNS و LINES در وضعیت محاورهای توسط BASH تنظیم میشوند، آنها به طور پیشفرض در یک اسکریپت در دسترس نیستند. در اکثر سیستمها خودتان میتوانید با ترمینال پرس و جو کنید:
unsup() { echo "Your system doesn't support retrieving $1 with tput. Giving up." >&2; exit 1; } COLUMNS=$(tput cols) || unsup cols LINES=$(tput lines) || unsup lines
Bash به طور خودکار متغیرهای COLUMNS و LINES را موقعی که پوسته محاورهای تغییر اندازه داده میشود، به هنگام میکند. اگر شما این متغیرها را در اسکریپت تنظیم میکنید و میخواهید موقعی که اندازه ترمینال تغییر میکند، آنها به هنگام شوند، یعنی به مجرد دریافت سیگنال SIGWINCH، میتوانید خودتان یک trap تنظیم کنید:
trap 'COLUMNS=$(tput cols) LINES=$(tput lines)' WINCH
همچنین میتوانید در سرآیند اسکریپت، پوسته را به عنوان محاورهای تنظیم کنید:
#!/bin/bash -i echo $COLUMNS
به هر حال، این مورد دارای اشکالاتی هست:
بررسی بواسطه گزینه -i به منظور تعیین آنکه آیا پوسته محاورهای است، و سپس انصراف یا درست رفتار نکردن، برای اسکریپتها خیلی غیر معمول نیست، اگرچه بهترین کار نمیباشد. هیچ روش کاملاً خالی از نقصی برای بررسی این مطلب، وجود ندارد، بنابراین بعضی از اسکریپتها ممکن است به سبب آن ناموفق شوند.
اجرا با گزینه -i فایل .bashrc را منبع میکند، و گزینههای مختلفی از قبیل job-control را تنظیم میکند که شاید آثار جانبی ناخواسته داشته باشند.
اگر چه از لحاظ تکنیکی میتوانید -i را در میان اسکریپت تنظیم کنید، اما تأثیری بر تنظیم COLUMNS و LINES ندارد -- -i باید ابتدا، موقعی که Bash احضار میشود برقرار باشد.
معمولاً Bash موقعی که ترمینال شما سیگنال SIGWINCH نشان دهنده تغییر اندازه را ارسال میکند،COLUMNS و LINES را به روز رسانی میکند. برخی ترمینالها ممکن است این کار را انجام ندهند، بنابراین اگر متغیرهای شما حتی موقعی که یک پوسته محاورهای در حال اجرا است به روز رسانی نمیشوند، استفاده از shopt -s checkwinsize را امتحان کنید. این دستور باعث استعلام ترمینال توسط Bash پس از هر فرمان میشود، بنابراین تنها در صورتی که واقعاً لازم است از آن استفاده کنید.
البته، tput به ترمینال نیاز دارد. مطابق POSIX، اگر خروجی استاندارد tty نباشد، نتایج تعیین شده نیستند، و stdin استفاده نمیشود، اگر چه بعضی پیادهسازیها ممکن است سعی کنند به هرحال از آن استفاده کنند. در لینوکس OpenBSD و Gentoo (و ظاهراً حداقل برخی لینوکسهای دیگر)، دست کم یکی از stdout یا stderr باید tty باشد، وگرنه tput فقط بعضی مقادیر پیشفرض را برمیگرداند.
linux$ tput -S <<<$'cols\nlines' 2>&1 | cat 80 24 openbsd$ tput cols lines 2>&1 | cat 80 24
پرسش و پاسخ 91 (آخرین ویرایش 2013-01-11 22:11:44 توسط GreyCat)
نمیتوانید آنرا به تنهایی با تغییر مسیر bash انجام دهید، برعکسِ>> وجود ندارد....
برای درج محتویات در ابتدای فایل، میتوانید از یک ویرایشگر استفاده کنید، برای مثال ex:
ex file << EOF 0a header line 1 header line 2 . w EOF
یا ed:
printf '%s\n' 0a "line 1" "line 2" . w | ed -s file
ex همچنین در صورت فقدان کاراکتر سطرجدید در انتهای فایل آن را به فایل اضافه میکند.
یا با استفاده از مواردی از این قبیل میتوانید فایل را بازنویسی کنید:
{ echo line; cat file ;} >tmpfile && mv tmpfile file echo line | cat - file > tmpfile && mv tmpfile file
برخی اشخاص اصرار دارند از چکش sed برای کوبیدن تمام پیچها استفاده کنند:
sed "1iTEXTTOPREPEND" filename > tmp && mv tmp filename
راه حلهای بسیار دیگری نیز وجود دارد.
پرسش و پاسخ 90 (آخرین ویرایش 2011-03-15 17:02:32 توسط GreyCat)
خواندن سطر به سطر فایل، اگر یک دستور در داخل حلقه نیز stdin را بخواند، میتواند فایل ورودی را تهی کند، برای مثال:
# مثالی که کار نمیکند while IFS= read -r file; do ffmpeg -i "$file" -vcodec libxvid -acodec libfaac -ar 32000 "${file%.avi}".mkv done < <(find . -name '*.avi')
# مثالی که کار نمیکند while read host; do ssh "$host" some command done <hostslist
در اینجا چه اتفاقی رخ میدهد؟ اجازه بدهید مثال اول راببینیم. read سطری از ورودی استاندارد(FD 0) میخواند،آن را در پارامتر file قرار میدهد، و بعد ffmpeg اجرا میشود. مانند هر برنامه دیگری که شما از BASH اجرا میکنید، ffmpeg نیز ورودی استاندارد را به ارث میبرد، که بنا به دلایلی از آن میخواند. من نمیدانم چرا. اما در هر وضعیتی، موقعی که ffmpeg از stdin میخواند، تمام ورودی را از فرمان find جذب میکند، حلقه از گرسنگی میمیرد.
این هم چگونه کارآمد ساختن آن:
while IFS= read -r file; do ffmpeg -i "$file" -vcodec libxvid -acodec libfaac -ar 32000 "${file%.avi}".mkv </dev/null done < <(find . -name '*.avi')
به تغییر مسیر در سطر ffmpeg توجه نمایید: </dev/null. مثال ssh میتواند به همین ترتیب ، یا با گزینه -n اصلاح بشود (حداقل با OpenSSH).
گاهی اوقات شاید کار کردن با آنچه از ورودی استاندارد خوانده میشود، با حلقههای بزرگ دشوار گردد، یا ممکن است برنامه موقعی که شما </dev/null را به آن اضافه میکنید رفتارش را تغییر بدهد. در این وضعیت شما میتوانید کاری کنید که فرمان read از توصیفگر فایل متفاوتی استفاده کند، که کمتر محتمل است یک برنامه تصادفی از آن بخواند:
while read <&3 line; do ...... done 3<file
یا از گزینه -u فرمان read استفاده کنید(POSIX نیست):
# Bash while read -u 3 line; do ...... done 3<file
پرسش و پاسخ 89 (آخرین ویرایش 2013-05-17 20:58:31 توسط GreyCat)
این روش برای آن طراحی شده تا به شما امکان بدهد ثبت وقایع کامل دوستانهای از تمام فرمانهای اجرا شده توسط کاربر را ذخیره کنید، مقصود از آن حسابرسی امن فرمانها نمیباشد -صفحه امنیت bash در برابر پاکسازی تاریخچه را ببینید.
به طور پیشفرض، Bash تاریچهاش رافقط موقع خروج به روزرسانی میکند، و تاریخچه موجود را با یک نگارش جدیدتر بازنویسی میکند. این مطلب به دو دلیل شما را از داشتن یک فایل ثبت وقایع تاریخچه محروم میکند:
برای حل مشکل نخست، ما گزینه histappend پوسته را تنظیم میکنیم که باعث میشود تمام سطرهای تاریخچه جدید در فایل پیوست بشوند، و تضمین میشود که تاریخچه ورود(login)های متعدد یکدیگر را رونویسی نمیکنند.
برای ممانعت از مفقود شدن سطرهای تاریخچه در وضعیتی که Bash به طور غیرعادی خارج شود، لازم است مطمئن شویم که سطرها بعد از هر دستور نوشته میشوند. می توانیم از دستور داخلی پوسته به شکل history -a استفاده کنیم که باعث نوشته شدن فوری تمام سطرهای تاریخچه میگردد، و ما میتوانیم با افزودن آن به متغیر PROMPT_COMMAND اجرای آن را خودکار نماییم. این متغیر محتوی فرمانی برای اجرا شدن قبل از نمایش هر اعلان فرمان جدید است، و بنابراین بعد از هر فرمان پوسته محاورهای، اجرا میشود.
توجه نمایید که اجرای 'history -a' بعد از هر فرمان تأثیر دوجانبه دارد:
برای انجام تمام این موارد، کد زیر را در فایل ~/.bashrc خودتان به کار ببرید:
HISTFILESIZE=400000000 HISTSIZE=10000 PROMPT_COMMAND="history -a" export HISTSIZE PROMPT_COMMAND shopt -s histappend
در کد فوق همچنین حداکثر تعداد سطرهای تاریخچه را که در حافظه ذخیره میشوند، افزایش دادهایم و هر محدودیتی برای خود فایل تاریخچه را حذف نمودیم. پیشفرض اینها 500 سطر است، که اگر شما کاربر فعالی هستید باعث میشود به سرعت شروع کنید به از دست دادن سطر فرمانهای تاریخچه. با تنظیم کردن HISTFILESIZE به یک کمیت درشت، یک فایل به اندازه کافی بزرگ را به طوری که عملاً نامحدود است تأمین میکنیم - و با تنظیم $HISTSIZE، تعداد این سطرها که در حافظه نگهداری میشوند را محدود نمودهایم. متأسفانه، bash کل فایل تاریخچه را قبل از آنکه کوتاه شده آن به اندازه $HISTSIZE را به حافظه کپی کند، میخواند - بنابراین اگر فایل تاریخچه فرمان شما خیلی بزرگ شود، زمان شروع اولیه bash شما میتواند به طور رنجشآوری بالا برود. حتی بدتر، بارگیری فایل تاریخچه بزرگ و بعد کوتاهسازی آن به اندازه $HISTSIZE منجر به نفخ منابع مورد استفاده میشود، bash حافظه بسیار بیشتری از وقتی که فایل تاریخچه فقط به اندازه $HISTSIZE سطر باشد، مصرف میکند. بنابراین اگر انتظار دارید فایل تاریخچه شما خیلی بزرگ شود، برای مثال بالای 20,000 سطر، باید به طور متناوب آنرا بایگانی کنید. بایگانی فایلهای تاریخچه در پایین را ببینید.
PROMPT_COMMAND ممکن است قبلاً در تنظیمات شما به کار رفته باشد، برای مثال شامل کدهای کنترل برای به روزرسانی یک نوار نمایش XTerm با اعلان فرمان شما. اگر مال شما از قبل در استفاده است میتوانید به این طریق به آن پیوست کنید: PROMPT_COMMAND="${PROMPT_COMMAND:-:} ; history -a"
همچنین شاید بخواهید متغیرهای HISTIGNORE و HISTCONTROL را برای کنترل آنچه ذخیره میشود، برای مثال حذف سطرهای تکراری، به کار ببرید - به هر حال انجام آن شما را از دیدن آن که چندبار یک فرمان معین توسط کاربری اجرا شده باز میدارد، و به طور دقیق وقتی( HISTTIMEFORMAT نیز برقرار شده باشد).
سرانجام، توجه کنید که به علت اجرای PROMPT_COMMAND درست قبل از اینکه اعلان فرمان چاپ شود، ممکن است شما آخرین سطر فرمان را در صورتی که پوسته در اثنای انجام این فرمان فسخ بشود، از دست بدهید. به عنوان یک مثال، ملاحظه کنید: this_cmd_is_never_written_to_history ; kill -9 $$
نتیجه عمل فوق، یک فایل تاریخچه با مقدار بسیاری فرمانهای تکراری است. پیوست نمودن تاریخچه باعث میشود فایل تاریخچه شما به وسیله همه تاریخچههای بارگذاری شده پوسته در هر نوبت، رشد کند.
به طور مهمتر، مطلب عمدهای که با حساسیت نسبت به تاریخچه برای ما اهمیت دارد آن است که قادر به یافتن دستوراتی باشیم که قبلاً اجرا شدهاند. اسکریپت زیر تمام دستوراتی که از قبل در فایل تاریخچه هستند را از آن حذف میکند، در حالیکه ترتیب دستورات دست نخورده حفظ میگردد، به طریقی که آخرین دستورات اجرا شده در انتهای فایل باقی میمانند (یعنی حفظ آخرین وقوع یک فرمان، نه اولین مورد).
awk 'NR==FNR && !/^#/{lines[$0]=FNR;next} lines[$0]==FNR' "$HISTFILE" "$HISTFILE" > "$HISTFILE.compressed" && mv "$HISTFILE.compressed" "$HISTFILE"
پس از چند ماه، این اسکریپت فایل تاریخچهام را از 761474 به 2349 سطر فشرده نمود.
یکبار که شما این روشها را فعال کنید، متوجه خواهید شد که تاریخچه bash خیلی بیشتر ارزشمند میشود، میسر نمودن فراخوانی هر فرمانی که هر زمانی اجرا کردهاید. همینطور، شما باید مطمئن شوید که فایل(های) تاریخچهتان به پشتیبانهای مقرر شما ضمیمه میگردد.
همچنین ممکن است برای ممانعت از بارگیری تاریخچه کامل در حافظه، توسط هر پوسته bash جدید، بخواهید بایگانی منظم فایل تاریخچه خود را فعال نمایید. با یک فایل تاریخچه شامل 10,000 مدخل، bash در Solaris 10 تقریباً از 5.5MB حافظه استفاده میکند، با تأخیر غیرقابل ارزیابی در شروع اولیه( من فرض کنم با $HOME روی یک دیسک محلی؟ -- GreyCat). با تاریخچهای به اندازه 100,000 مدخل این به 10MB با تأخیر قابل ملاحظه 3-5 ثانیه در زمان شروع میرسد. بایگانی متناوب برای پاک کردن قدیمیترین سطرهای گزارش و برای اجتناب از هرز دادن منابع قابل توصیه است، مخصوصاً اگر RAM بسیار ارزشمند باشد. (بزرگترین ~/.bash_history من بعد از 1.5 ماه 7500 مدخل است.)
این امر به بهترین وجه از طریق ابزاری که میتواند بخشی از فایل را بایگانی کند انجام شده است. یک اسکریپت ساده برای انجام این کار اینطور خواهد بود:
#!/bin/bash umask 077 max_lines=10000 linecount=$(wc -l < ~/.bash_history) if (($linecount > $max_lines)); then prune_lines=$(($linecount - $max_lines)) head -$prune_lines ~/.bash_history >> ~/.bash_history.archive \ && sed -e "1,${prune_lines}d" ~/.bash_history > ~/.bash_history.tmp$$ \ && mv ~/.bash_history.tmp$$ ~/.bash_history fi
این اسکریپت سطرهای کافی از بالای فایل تاریخچه را برای کوتاه کردن آن به اندازه X سطر پاک میکند، باقیمانده را در فایل ~/.bash_history.archive درج میکند. این از توانایی هرسکردن HISTFILESIZE تقلید میکند، اما باقیمانده را به جای حذف کردن، بایگانی میکند - تضمین میکند شما همواره میتوانید تاریخچه گذشته را با grep ~/.bash_history* جستجو نمایید.
چنین اسکریپتی میتواند به طور شبانه یا هفتگی از crontab شخصی شما برای فعال کردن تهیه بایگانی دورهای به کار برود. توجه نمایید که چندین کاربر را مدیریت نمیکند و فقط تاریخچه کاربر جاری را بایگانی خواهد نمود - توسعه دادن آن جهت اجرا برای تمام کاربران(همچون root) به عنوان تمرین برای خواننده باقی گذاشته شد.
پرسش و پاسخ 88 (آخرین ویرایش 2013-07-21 20:03:09 توسط ppp091138132034)