این صفحه مقولهای را تشریح مینماید که احتمالاً مهمترین مبحث در باره برنامهنویسی پوسته است و بیش از همه به طور نادرست فهمیده شده است.
بدون قید وشرط واجب است شما قبل از انجام هر کار مهم در پوسته، به طور کامل تمام آنچه در اینجا تشریح میگردد را درک کنید . درست نفهمیدن آنکه شناسهها کدامند و تفکیک کلمه چطور عمل میکند، حتی در کُدی که شما کنترل کردهاید و به نظر میرسد به خوبی کار میکند، منجر به باگهای غیر منتظره، و در حالتهای وخیمتر، انحراف شدید و از دست رفتن دادهها میگردد.
پوسته یک میانجی بین شما(یا اسکریپت شما) و هسته(کرنل) میباشد. پوسته اجازه میدهد شما به جای احضار مستقیم فراخوانهای سیستمزیرنویس 1 ، دستورات را با استفاده از ترکیب دستوری مشخص شده اجرا کنید.
آنچه حقیقتاً پوسته برای شما انجام میدهد، ترجمه ترکیب دستوری(گرامر) خودش به فراخوانهای سیستمی میباشد. بدین علت، اهمیت دارد که ما حداقل مبانی آن را درک کنیم، که وقتی پوسته در حال خواندن دستورات شما و انجام خواست شما میباشد چه چیزی رخ میدهد.
اجرای دستورات از طریق فراخوان سیستم execve(2) روی میدهد. این فراخوان به سه بخش از اطلاعات نیاز دارد:
جهت تخصیص یک زمینه برای برنامه و گفتن آنچه باید انجام بدهد، ما آن برنامه را با یک آرایه از شناسهها آماده میکنیم. این یعنی، ما یک مجموعه رشته به آن میدهیم. هر یک از این رشتههامیتواند شامل هر کاراکتر(بایت) (به استثنای بایت NUL) باشد. به معنای آنکه، هر شناسه میتواند یک کلمه، یک جمله، یا بیشتر باشد.
اگر ما برای مثال، مایل به حذف یک کتاب معین هستیم ، میتوانیم برنامه rm را با فراهم نمودن نام مسیرهای فایلهایی که مایل به حذف آنها هستیم، فراخوانی کنیم:
Execute File: [ "/bin/rm" ] With arguments: [ "James, P.D. - Children of Men - Chapter 1.pdf" ] [ "James, P.D. - Children of Men - Chapter 2.pdf" ] ...
فرمان rm این دو شناسه را برای تعیین آنکه کدام فایلها باید حذف شوند به کار خواهد برد. در این حالت، هر شناسه را unlink(2) خواهد نمود.
(در واقعیت، موقع احضار فراخوان سیستمی execve(2)، ما یک شناسه فوقالعاده نیز عبور میدهیم. این صفرمین شناسه نامی است که مایل هستیم به پردازش بدهیم. اینکه به طور دقیق به چه معنا میباشد و برای چه به کار میرود، تنها به طور مبهم تعریف گردیده, و با این مطلب نامربوط است. گفتن آنکه bash بعد از تفکیک کلمه، اولین قطعه(قطعه نام فرمان) را به عنوان شناسه صِفرُم به کار میبرد کفایت میکند. من از این پس در اینجا هرگونه اشاره به آن را از قلم خواهم انداخت.)
بنابراین به خاطر بسپارید: شناسهها رشتههایی از کاراکترها هستند، هر یک از آنها میتواند شامل هر کاراکتر(غیر از یک بایت NUL ) باشد و ما موقعی که فایلی را اجرا میکنیم، میتوانیم چندین شناسه به آن بدهیم.
به منظور آسانتر نمودن کار شما برای بیان درخواست خودتان، جهت انجام یک عملکرد سیستم، پوستهها وجود دارند، که ترکیب دستوری تعریف شده خود را به فراخوانهای سیستم ترجمه میکنند. اگر میخواهیم از اشتباهات و باگها اجتناب نماییم، این امری الزامآور است که ما به طور صحیح این ترکیب دستوری را درک نماییم.
ترکیب دستوری پوسته برای فراهم نمودن یک روش شهودی ارتباط ما با سیستم، ساخته شده است. از تکنیکهایی مانند تفکیک کلمه و کلیدواژههای انگلیسی استفاده میکند تا به ما اجازه دهد خواستههای خودمان را به زبانی بیان کنیم، که به طور نزدیکی شبیه به روشی است که برای یکدیگر مینویسیم. با وجود این فریب نخورید: این ترکیب دستوری بسیار دقیق است و پوستهها انسان نیستند، اگر شما خودتان به طور واضح و غیرمبهم بیان نکنید، آنها نمیتوانند در مورد آنچه ممکن است مقصود شما باشد حدس بزنند. بر اساس دریافت ناگهانی، ترکیب دستوری پوسته را حدس نزنید. آن را درک کنید، و سپس آنچه را مقصودتان است، به طور دقیق بنویسید.
برای اجرای یک فرمان ساده rm در پوسته، ما باید جملهای به همین واضحی بنویسیم:
rm myfile myotherfile
این ترکیب دستوری پوسته را آگاه خواهد نمود، که دو فایل راحذف(remove) کند: myfile، و myotherfile. چگونه پوسته این مطلب را میفهمد؟ چطور یک جمله را به فراخوانهای سیستم تبدیل میکند؟ کلید این موضوع تفکیک کلمه میباشد.
برای یک پوسته، فضای سفید به طور غیرقابل باوری اهمیت دارد. بنابراین با این اندیشه که یک فاصله یا tab بیشتر یا کمترتفاوت زیادی ایجاد نمیکند، نادانی نکنید. و تصور نکنید که چون فضای سفید در C یا Java خیلی مطرح نیست، همان مطلب در پوسته شما نیز صدق میکند. فضای سفید برای رخصت دادن به پوسته جهت آنکه شما را درک کند ضروری است.
پوسته سطر کُد شما را میگیرد و آنرا از جایی که رشتههای گرامری فضای سفید وجود دارد به تکهها خُرد میکند. فرمان فوق به صورت زیر تفکیک میشود:
rm myfile myotherfile ^ ^ [rm] [myfile] [myotherfile]
به طوری که میتوانید ببینید، تمام فضاهای سفید گرامری حذف گردیدهاند. بعد از اینکه تفکیک کلمه روی سطر دستور شما انجام شده است، فضای سفیدی باقی نمانده است. حقیقتاً ما دارای سه تکه کاملاً جداگانه از کاراکترها میباشیم: یکی rm، دیگری myfile، و آخری myotherfile. حالا پوسته این تکهها را برای ساختن فراخوان سیستمیexecve(2) به کار میبرد.
پوسته از شناسهها، یک آرایه برای عبور دادن به سیستمعامل میسازد. عناصر (رشتهها) در این آرایه، rm و myfile و myotherfile میباشند. یک فراخوان execve(2) با تحویل این آرایه احضار میگردد. سیستم عامل روی آن عمل میکند، متغیر محیطی PATH را برای برنامهای به نام rm جستجو میکند، و آنرا با شناسههای داخل آرایه اجرا میکند. سپس rm آن فایلها را unlink(2) میکند.
(BASH به طور واقعی نخست خودش PATH را جستجو میکند، و محل rm را در یک hash ذخیره میکند. سایر پوستهها ممکن است آنرا انجام ندهند، و به راستی در این لحظه این اهمیت ندارد.)
Bash دارای یک متغیر خاصمیباشد، IFS، که برای تعیین آنکه کدام کاراکترها به عنوان جداکنندهها تفکیک کلمه استفاده میشوند، به کار میرود. به طور پیشفرض، IFS برقرار نیست(unset است) و چنان عمل میکند که اگر به کاراکترهای فاصله، tab و سطر جدید (`$' \t\n'`)) تنظیم میگردید. نتیجه این مورد آن است که شما هر یک از این اشکال فضای سفید را میتوانید برای جدا کردن کلمات به کار ببرید.
در حین تفکیک کلمه از IFS فقط برای تفکیک کلمه دادهها استفاده میشود. برای نمونه،این مثال هیچ دادهای را تفکیک نمیکند. فقط کد اسکریپت وجود دارد. یک سطر از کد اسکریپت همواره فقط از محل فضای سفید تفکیک میشود:
IFS=: rm myfile myother:file ^ ^ [rm] [myfile] [myother:file]
از طرف دیگر، در صورت که فایلها از متغیری که بسط داده میشود حاصل شوند، تفکیک کلمه بر دادههایی که از یک متغیر ناشی میگردند انجام میشود. در این حالت محتوای IFS استفاده میشود:
# .کد نامناسب IFS=: files='myfile myother:file' rm $files ^ # :پس از بسط پارامتر rm myfile myother:file ^ ^ [rm] [myfile myother] [file]
توجه کنید در اینجا چطور دو شکل جداگانه تفکیک کلمه را ادامه میدهیم. نخست، فرمان از جایی که فضای سفید وجود دارد تفکیک میشود. سپس، بسط با استفاده از IFS تفکیک میشود. در این حالت،فضای سفید به کلمات تفکیک نمیکند، تنها کاراکتر کولن این کار را انجام میدهد.
اهمیت دارد که شما خطر تغییر وضع رفتارهای پوسته را درک کنید. اگر شما IFS را ویرایش کنید، از آن به بعد تفکیک کلمه در یک حالت غیر پیشفرض رخ میدهد. برخی به شما پیشنهاد خواهند نمود که IFS را ذخیره نموده وبعداً آن را به مقدار پیشفرض برگردانید. سایرین پیشنهاد اجرای unset IFS بعد از آنکه تفکیک کلمه سفارشی را انجام دادهاید میکنند.
شخصاً، ترجیح میدهم به شما پیشنهاد کنم که در سطح اسکریپت IFS را ویرایش نکنید. هرگز. اول از همه، استفاده واقعی از تفکیک کلمه سطح داده(data-level) تکنیک بسیار نامناسبی . تفکیک کلمه یک تکنیک بسیار پرمخاطره است، مخصوصاً زیرا همراه با آن بسط نام مسیر ناخواسته میآید(bash هر فایلی را که نام آن با کلمات اخیراً بسط یافته شما منطبق گردد را جستجو میکند و واقعاً کلمات شما را با هر فایلی که با آن تطبیق کند تعویض مینماید -- این در کمین است و هرگز شما واقعاً نمیخواهید چنین موردی برای کلمات شما رخ بدهد). ثانیاً؟، تنظیم مجدد IFS معمولاً برای محدود نمودن دامنه تفکیک کلمه سفارشی شما کافی نیست.مثال زیر را ملاحظه کنید:
# .کد نامساعد IFS=, names=Tom,Jack for name in $names do ...; done unset IFS
در اینجا، لازم است IFS به , تنظیم گردد برای اینکه رشته names را به عناصر جدا شده با کاما تفکیک کند. به هر حال انجام چنین موردی به معنای آن است که تمام بدنه حلقه for تحت یک IFS غیر پیشفرض اجرا خواهد گردید، که ممکن است باعث بُروز دادن اثرات نامطلوب بشود.
روش صحیح برای انجام این مورد یکی از این ساختارها میباشد:
# آرایهها names=( Tom Jack ) for name in "${names[@]}" # است read سفارشی تنها محدود به فرمان IFS تفکیک کلمه بیخطر(بدون بسط نام مسیر) IFS=, read -ra namesArray <<< "$names" for name in "${namesArray[@]}" # while-read یک حلقه while read -rd, name do ...; done <<< "$names,"
به طوری که میتوانید ببینید، تنظیم یک IFS سفارشی میتواند قابل استفاده باشد، فقط به بهترین وجه همراه فرمان اجرا میگردد، به طوری که فقط بر همان فرمان یگانه اِعمال میشود. تنها تعداد معینی از فرمانهای داخلی واقعاً میتوانند از IFS به این طریق استفاده کنند. فرمان read یکی از آنها میباشد. شما در این روش نمیتوانید بسط غیر نقلقولی را مهار کنید، اما اشکالی ندارد، زیرا از ابتدا نیز واقعاً نمیتوانستید این کار را انجام بدهید!
names=Tom,Jack # کُد نامناسب IFS=, namesArray=( $names ) # .را محدود کنید IFS به این طریق نمیتوانید # .هر دو را برای سرتا سر اسکریپت برقرار میکند namesArray و IFS در حقیقت IFS=,; namesArray=( $names ); unset IFS # .کار میکند اما کد نامناسبی است # تفکیک کلمه نا امن رخ میدهد(بسط نام مسیر ضمنی) # کد خوب IFS=, read -ra namesArray <<< "$names" # محدود 'read' فقط به فرمان IFS # .میگردد و بدون بسط نام مسیر، نام متغیرها بدون خطر تجزیه میشوند
اکنون بیایید به عقب به اولین مثال بازگردیم: ما میخواستیم فایلهای فصل را از ebook خودمان حذف نماییم. به نظر میرسد انجام این کار توسط پوسته مشکلساز باشد، زیرا نام فایلهای فصل شامل فضای سفید هستند. این مورد برای فراخوان سیستم هیچگونه مشکلی نیست، اما مشکل بزرگی برای پوسته است. پوسته از قبل فضای سفید را برای امر بسیار مهمی استفاده میکند : تعیین آنکه کدام تکههای جمله ما به عنوان شناسههای جداگانه عبور داده شوند.
اگر ما از روی بی تجربگی بدون هر گونه تفکر و ملاحظهای در مورد ترکیب دستوری پوسته به آن گفته بودیم، فصل اول را حذف کند، این است آنچه رخ میداد:
rm James, P.D. - Children of Men - Chapter 1.pdf ^ ^ ^ ^ ^ ^ ^ ^ ^ [rm] [James,] [P.D.] [-] [Children] [of] [Men] [-] [Chapter] [1.pdf]
پوسته شما 9 نام فایل برای حذف به برنامه rm ارائه میکرد، که هیچکدام آنها نام فایل مورد نظر ما نبود. rm برای حذف هر فایل دارای آن نامها تلاش میکرد. اگر شما بدشانس بودید، شاید rm بعضی از فایلهای شما را که هرگز مایل نبودید حذف گردند، بر حسب تصادف حذف میکرد.
به واسطه بخش تفکیک کلمه فوق، اکنون شما میدانید که چرا پوسته اینطور عمل میکند. اما چطور به پوسته برای فهمیدن آنکه ماحقیقتاً مایل هستیم چه کاری را به انجام برساند، کمک کنیم؟
مشکل آنست که فضای سفید برای پوسته ترکیب دستوری میباشد. به معنای آنست که پوسته روی آن عمل کند. ما نمیخواهیم فضای سفید در نام فایل ما هیچ معنایی برای پوسته داشته باشد، دقیقاً میخواهیم بخشی از قطعه بزرگ دادهها باشد، درست مانند هر یک از کاراکترهای دیگر. درست مانند بایتهای معمولی، ساده، و آسوده. ما میخواهیم فضاهای سفیدمان فضای سفید لفظی باشند.
تغییر چیزی از دستوری به داده لفظی دو فرآیند را در بر میگیرد: نقلقول کردن یا گریز. نقلقولی کردن بایتهای ما به وسیله بستهبندی با علامتهای نقلقول در اطراف آنها انجام میگردد. گریز با مقدم کردن کاراکتر گریز
به جا آوردن یک توجه خاص: این علائم نقلقول نباید لفظی باشند، درست مانند فضاهای سفید ما در بالا، این نقلقولها باید غیر نقلقولی و پوشش نیافته با کاراکتر گریز باشند، که دستوری باقی بمانند(یعنی، قدرت خاص آنها حفظ بشود).
# :گریز یافته rm James,\ P.D.\ -\ Children\ of\ Men\ -\ Chapter\ 1.pdf # :نقل قولی شده rm James," "P.D." "-" "Children" "of" "Men" "-" "Chapter" "1.pdf # :میتوانیم با نقلقولی کردن تمام کاراکترهای شناسه، این سطر را زیباتر کنیم rm "James, P.D. - Children of Men - Chapter 1.pdf" ^ [rm] [James, P.D. - Children of Men - Chapter 1.pdf]
هر بایتی که در نقلقولهای دستوری تعبیه گردد دیگر به صورت دستوری در نظر گرفته نمیشود(با برخی استثناهای مخصوص نقلقول که اکنون وارد آن نمیشوم). این به آن معنا میباشد که اگر ما رشته foo bar را نقلقولی کنیم، هر کاراکتر در آن رشته هرگونه معنی و مقصود برای پوسته را از دست خواهد داد. پوسته آنها را مانند کاراکترهای معمولی میبیند و آنها را همراه با قطعهای که در حال کار روی آنست عبور میدهد.
چون نقلقولها(یا
برای اطلاعات بیشتر در باره نقلقولها و اینکه آنها دقیقاً چطور رفتار میکنند، صفحه نقلقولها را ببینید.
حالا باید شناسهها و نقلقولها را خوب فهمیده باشید. اجازه بدهید یک مفهوم دیگر که در اسکریپتنویسی پوسته بسیار همگانی است و بازهم خیلی اوقات سوءتعبیر شده است را معرفی کنیم.
پارامترها ظرفهایی در حافظه هستند که رشتهها را برای ما نگهداری میکنند. ما میتوانیم بعداً این رشتهها را در فرمانهای پوسته به کار ببریم بدون نیاز به تکرار دادهها: ما دادهها را از ظرفهای حافظه به دستور تخلیه میکنیم. این تخلیه(باراندازی) بسط نامیده میشود، عبارت بسط پارامتر از این جهت است .
متغیرها نوعی پارامتر میباشند. آنها پارامترهایی با نام مشخص میباشند و برای تخصیص دادهها آسان هستند. نام یک متغیر تنها میتواند شامل کاراکترهای الفبایی-عددی(و به طور اختیاری، یک خط زیر) باشد. نام شامل علامت دلار نیست.
بسط یک پارامتر با پیشوند کردن علامت دلار به نام آن رخ میدهد. عمل بسط باعث میگردد دادههای درون آن پارامتر داخل جمله جاری تزریق بشود، تقریباً مثلاینکه شما خودتان بسط پارامتر را با مقداری جمله تعویض کنید.
$ place=lawn $ echo Welcome to my $place. Welcome to my lawn.
به هرحال، ضروری است درک شود که نقلقول کردن و Escaping قبل از واقع شدن بسط پارامتر در نظر گرفته میشوند، در حالیکه تفکیک کلمه بعد از آن انجام میشود. این به معنای آنست که این ضرورت مطلقاً باقی میماند: در حالتی که ممکن است پارامترها به کمیتهایی بسط داده شوند که محتوی فضاهای سفید دستوری باشند، که سپس در مرحله بعدی تفکیک کلمه میشوند، ما بسط پارامترهایمان را نقلقولی کنیم.
تقریباً هرگز قرار دادن فضای سفید دستوری در پارامترها، پسندیده نیست. شاید شما بخواهید تکههای چندتایی دادهها را در یک پارامتر قرار بدهید، به هرحال، موقعی که لازم باشد، اهمیت دارد که شما یک پارامتر رشتهای استفاده نکنید، بلکه به جای آن یک آرایه به کار ببرید. (پوستههای POSIX لزوماً دارای آرایهها نیستند، اما Bash و KornShell هستند.)
اگر پارامتری که دادههایش شامل فضای سفید است را بدون نقلقولی نمودن بسط بدهیم، این است آنچه اتفاق میافتد:
book="Children of Men.pdf" rm $book # :پس از بسط پارامتر rm Children of Men.pdf ^ ^ ^ [rm] [Children] [of] [Men.pdf]
نقلقولی نمودن بسط پارامتر باعث میگردد دادههایش در یک زمینه نقلقولی شده بسط داده شوند، یعنی فضاهای سفید آن، ارزش دستوری خود را از دست داده و لفظی خواهند شد:
book="Children of Men.pdf" rm "$book" # :بعد از بسط پارامتر rm [Children of Men.pdf] ^ # کُد کاذب هستند،آنها واقعاً آنجا نیستند، بلکه نماد هستند [ و ] # که این بایتها توسط نقلقولهای فوق به عنوان لفظی علامت خورده بودند [rm] [Children of Men.pdf]
یک اشتباه رایج دیگر که بسیاری اشخاص موقع دیدن خطاهای تفکیک کلمه مرتکب میشوند، تلاش برای قرار دادن نقلقولها داخل دادههای پارامترهایشان است. این کار نمیکند، به این دلیل ساده که این نقلقولهای درون پارامترها نقلقولهای لفظی هستند، نه دستوری. زمانیکه bash مقادیر پارامترها را بسط میدهد، قبلاً کار با نقلقولها به عنوان عناصر دستوری را متوقف نموده است.
book='"Children of Men.pdf"' rm $book # :بعد از بسط پارامتر rm "Children of Men.pdf" ^ ^ ^ # راننوشتم [ و ] نقلقولها در اینجا لفظی هستند، نه دستوری. من # فوق وجود نداشت rm چون نقلقول دستوری در فرمان [rm] ["Children] [of] [Men.pdf"]
توجه نمایید که چون شما نقلقولهای لفظی را بسط دادهاید، این نقلقولها اکنون قسمتی از تکهها هستند، درست مانند هر بایت لفظی دیگر. هر چند که، فضای سفید لفظی نیست. چون در پردازش، تفکیک کلمه روی فضای سفید(در واقع، IFS) به آن اندازه دیر انجام میشود، بازهم پوسته آنها را به عنوان جداکننده در نظر میگیرد، و آشفتگی نشان داده شده در بالا را تولید می کند.
شاید این مبحث برای اینکه تمام آن را به یکباره اخذ کنید و خوب بفهمید اندکی زیاد باشد. اگر فکر میکنید بعداً برگشت به این صفحه و خواندن مجدد آن به شما کمک خواهد نمود، لطفاً این صفحه را نشانهگذاری( bookmark) کنید.
برای ساده، بادوام و اطمینان بخش نمودن امور برای خود، شما باید راهنماییهای زیرین را دنبال نمایید:
"نقلقول" کردن هر شناسهای که محتوی دادههایی است که پیش میآید ترکیب دستوری پوسته نیز باشند.
نقلقولی نمودن بسط تمام پارامترها در شناسهها. شما هرگز واقعاً نمیدانید یک پارامتر ممکن است به چه چیز بسط داده شود، و حتی اگر فکر میکنید به بایتهایی که ترکیب دستوری پوسته باشند بسط نخواهد یافت، نقلقولی کردن ثبات آینده کُد شماست و آن را ایمنتر و با ثباتتر میسازد.
و برخی نکات مرتبط اضافی:
اگر به ذخیره اقلام چندگانه همراه یکدیگر نیاز دارید، از یک آرایه استفاده کنید:
files=( 1.pdf 2.pdf "1 and a half.pdf" ) rm "${files[@]}"
سعی نکنید دستورات را داخل پارامترها قرار دهید، نمیتوانید شناسهها را به طور صحیح نقلقولی کنید. در عوض از تابع استفاده کنید:
search() { cd /foo; find . -name "$1"; } search '*.pdf'; search '*.jpg'
شناسهها (آخرین ویرایش 2012-01-09 15:55:00 توسط static-74-101-128-34)