این صفحه اشتباهات رایجی را که برنامه نویسان Bash مرتکب میگردند نشان میدهد. مثالهای ذیل هر یک به نوعی معیوب هستند:
یکی از رایجترین اشتباهاتی که برنامهنویسان BASH مرتکب میشوند، نوشتن حلقهای مانند این است:
for i in $(ls *.mp3); do # Wrong! some command $i # Wrong! done for i in $(ls) # Wrong! for i in `ls` # Wrong! for i in $(find . -type f) # Wrong! for i in `find . -type f` # Wrong! files=($(find . -type f)) # Wrong! for i in ${files[@]} # Wrong!
هرگز بدون نقلقولها از یک جایگزینی فرمان -- از هر نوع! -- استفاده نکنید. در اینجا دو موضوع اصلی وجود دارد: استفاده از یک بسط غیر نقلقولی برای تفکیک خروجی به شناسهها، و تجزیه خروجی ls -- برنامهای که در هر صورت هرگز خروجیاش نباید تجزیه شود.
چرا؟ چون وقتی یک فایل در نام خود شامل فاصله باشد، این مورد ناموفق میشود. چرا؟ به علت آن که خروجی جایگزینی فرمانِ $(ls *.mp3) دستخوش تفکیک کلمه میگردد. فرض کنیم ما در دایرکتوری جاری دارای فایلی به نام 01 - Don't Eat the Yellow Snow.mp3 باشیم، حلقه for روی هر کلمه حاصل شده از نام فایل تکرار میشود: 01, -, Don't, Eat... وغیره.
شاید وخیمتر، رشتههایی که از مرحله تفکیک کلمه قبلی حاصل شدهاند سپس تحت تأثیر بسط نام مسیر واقع خواهند شد. به عنوان یک مثال، اگر ls هر خروجی شامل یک کاراکتر * تولید کند، کلمه حاوی آن به عنوان یک الگو شناسایی خواهد گردید و با لیستی از تمام نام فایلهایی که با آن مطابقت میکنند، جایگزین میشود.
همچنین شما نمیتوانید جایگزینی را با نقلقول دوگانه بپوشانید:
for i in "$(ls *.mp3)"; do # Wrong!
این باعث میگردد با کل خروجی فرمان ls به عنوان یک کلمه منفرد رفتار شود. به جای هر نوبت تکرار با هر نام فایل، حلقه فقط یک مرتبه با تمام نام فایلهای منگنه شده با یکدیگر اجرا خواهد شد.
علاوه بر این، استفاده از ls به طور قابل فهمی غیر ضروری است. این یک فرمان خارجی است که واقعاً برای انجام این کار لازم نیست. بنابراین، روش صحیح برای انجام آن، کدام است؟
for i in *.mp3; do # ... بهتر! و some command "$i" # ... شماره دو را ببینید pitfall برای اطلاعات بیشتر done
اجازه بدهید Bash لیست نام فایلها را برای شما بسط بدهد. بسط، در معرض تفکیک کلمه قرار نخواهد گرفت. با هر نام فایل که با یک glob به شکل *.mp3 منطبق میشود، به عنوان یک کلمه جداگانه رفتار میگردد، و حلقه برای هر نام فایل یکبار تکرار خواهد شد. (اگرنیاز دارید فایلها را به طور بازگشتی پردازش کنید، UsingFind را ببینید.)
سؤال: اگر هیچ نام فایل مطابق جانشین *.mp3 در دایرکتوری جاری نباشد چه میشود؟ آنوقت حلقه for یکبار با i="*.mp3" اجرا میشود، که رفتار مورد انتظار نمیباشد! چاره کار، بررسی آن است که آیا یک فایل منطبق وجود دارد:
# POSIX for i in *.mp3; do [ -e "$i" ] || continue some command "$i" done
اگر شما به سادگی همیشه نقلقولها را به کار ببرید و هرگز به هر دلیل از تفکیک کلمه استفاده نکنید، خود را از بسیاری از این دامها حفظ خواهید نمود! تفکیک کلمه یک سوءویژگی ورشکسته به ارث رسیده از پوسته Bourne است که به طور پیشفرض، اگر بسطها را نقلقولی نکنید ، پیش میرود. اکثریت وسیع تلهها به طریقی به بسطهای غیر نقلقولی و تفکیک کلمه و جانشینی( globbing ) بعدی آن نتیجه، مربوط میشوند. یک گونه دیگر در این زمینه سوءمصرف تفکیک کلمه و یک حلقه for برای خواندن سطرهای یک فایل است. این اشتباه است! چنانکه آن سطرها نامهای فایل باشند، این اشتباهی مضاعف( یا شاید سهبرابر) است.
به نقلقولهای اطراف $i در بدنه حلقه توجه نمایید. این مورد به دومین pitfall ما منجر میشود:
چه چیزی در دستور نشان داده شده فوق اشتباه است؟ خوب، اگر شما بر حسب اتفاق از قبل بدانید که $file و $target دارای فضای سفید یا فوقکاراکتر نیستند، هیچ چیز. هر چند، نتایج بسطها باز هم موضوع تفکیک کلمه و بسط نام مسیر هستند. همواره بسطهای پارامتر را نقلقول دوگانه نمایید.
اما اگر از قبل نمیدانید، یا اگر بدگمان هستید، یا اگر فقط در تلاش هستید که عادتهای مناسب را پرورش بدهید، آنوقت باید متغیرهای مرجع خود را برای پرهیز از تحت تأثیر تفکیک کلمه واقع گردیدن آنها، نقلقولی کنید.
cp -- "$file" "$target"
بدون نقلقول دوگانه، دستوری مانند cp 01 - Don't Eat the Yellow Snow.mp3 /mnt/usb خواهید داشت که به خطاهایی مشابه cp: cannot stat `01': No such file or directory منجر خواهد گردید. در صورتی که $file در خود فوقکاراکترهایی(* یا ? یا [) داشته باشد، و اگر فایلهایی وجود داشته باشند که با آنها منطبق گردند، آنها بسط داده خواهند شد. با نقلقولهای دوگانه، همه چیز بدون اشکال است، مگر اینکه "$file" اتفاقاً با یک کاراکتر -(خط تیره) شروع بشود، که در این حالت فرمان cp گمان میکند شما میخواهید گزینههای خط فرمان به او بدهید( pitfall #3 زیرین را ببینید.)
حتی در موقعیت نسبتاً کمیابی که میتوانید محتوای متغیرها را تضمین کنید، نقلقول کردن بسطهای پارامتر تکنیک راحت و مناسبی است، مخصوصاً اگر آنها شامل نام فایلها باشند. نویسندگان با تجربه اسکریپت همیشه از نقلقولها استفاده خواهند نمود، شاید غیر از موقعیتهای کم شماری که در آنها به طور مسلم از متنِ بلاواسطهِ کد آشکار است که یک پارامتر محتوی مقدار تضمین شدهِ بیخطر است. افراد خبره احتمالاً همیشه فرمان cp در عنوان این pitfall را اشتباه در نظر میگیرند. شما نیز باید.
نام فایلهایی با خط تیرههای پیشتاز میتوانند باعث مشکلات بسیاری گردند. Globهایی مانند *.mp3 در یک لیست بسط یافته(بر طبق منطقه شما) مرتب میشوند، و - در اکثر مناطق در ترتیب قبل از حروف قرار میگیرد. سپس آن لیست، به برخی فرمانها تحویل میشود، که ممکن است -filename به طور غیر صحیح به عنوان یک گزینه تفسیر گردد. دو راه حل اصلی برای این مورد وجود دارد.
یک راه حل، درج کردن -- میان فرمان(مانند cp) و شناسههایش خواهد بوذ. این کار به فرمان میگوید کاوش گزینهها را متوقف کند، و همه چیز سالم است:
cp -- "$file" "$target"
مشکلات بالقوهای در این رویکرد وجود دارد. شما باید از درج -- برای هر موردِ استعمال پارامتر در مضمونی که احتمالاً میتواند در آن مضمون به عنوان یک گزینه تفسیر گردد، مطمئن بشوید -- که فراموشی آن آسان است.
اکثر کتابخانههای تجزیه گزینه خوب نوشته شده این مورد را میفهمند، و برنامههایی که به طور صحیح از آنها استفاده کنند باید آزادانه آن ویژگی را ارث ببرند. به هر حال، باز هم آگاه باشید که در نهایت شناسایی انتهای گزینهها وابسته به برنامه کاربردی است. برخی برنامهها که تجزیه گزینهها را به طور دستی یا به طور نادرست انجام میدهند یا ازکتابخانههای ضعیف 3rd-party استفاده میکنند، ممکن است آن را شناسایی نکنند. برنامههای سودمند استاندارد با چند موردِ استثنا که توسط POSIX مشخص میشوند، باید تشخیص بدهند. برنامه echo یک نمونه است.
یک انتخاب دیگر تضمین کردن آن است که همیشه نام فایلهای شما بواسطه کاربرد نام مسیر مطلق یا نسبی با یک دایرکتوری شروع بشود.
for i in ./*.mp3; do cp "$i" /target ...
در این حالت، حتی اگر فایلی داشته باشیم که نام آن با - شروع بشود، جانشین(glob) تضمین خواهد نمود که همیشه متغیر شامل چیزی مانند ./-foo.mp3 خواهد بود، و این تا آنجا که به cp مربوط میشود، کاملاً ایمن است.
سرانجام، اگر شما میتوانید تضمین کنید که تمام نتایج دارای پیشوند یکسان خواهند بود و فقط چند نوبت در بدنه حلقه از متغیر استفاده میکنند، به سادگی میتوانید پیشوند را با بسط الحاق کنید.
for i in *.mp3; do cp "./$i" /target ... done
این بسیار مشابه موضوع pitfall شماره 2 است، اما من آن را تکرار میکنم زیرا, همچنان با اهمیت است. در مثال بالا، نقلقولها در محل اشتباه قرار دارند. شما در bash به نقلقول نمودن رشته لفظی نیاز ندارید (مگر اینکه محتوی فوق کاراکترها یا کاراکترهای الگو باشد). اما اگر شما اطمینان ندارید که آیا متغیرهایتان میتوانند شامل فضای سفید یا کاراکترهای عام باشند، باید آنها را نقلقولی کنید.
این ترکیب به چند دلیل میتواند ناموفق بشود:
اگر یک متغیر مورد ارجاع در [ موجود نباشد، یا تهی باشد، آنوقت فرمانِ [ به چنین موردی منجر میگردد:
[ = "bar" ] # !اشتباه
...و خطای unary operator expected را بروز میدهد. (عملگر = از نوع binary(دوگانی) است، نه unary(یگانی)، بنابراین فرمانِ [ از دیدن آن در آنجا شوکه میشود.)
اگر متغیر شامل فضای سفید داخلی باشد، آنوقت تفکیک به کلمات جداگانه، قبل از آنکه فرمانِ [ آن را ببیند انجام میشود. بدین ترتیب:
[ چند کلمه جداگانه در اینجا = "bar" ]
در حالیکه ممکن است از نظر شما صحیح باشد، تا آنجا که به [ مربوط میگردد، خطای دستوری است. روش صحیح برای نوشتن این مورد، چنین خواهد بود: :
# POSIX [ "$foo" = bar ] # !صحیح
این ترکیب در سیستمهای موافق با POSIX حتی اگر $foo با کاراکتر - شروع بشود نیز به خوبی کار میکند, چون فرمان [ در POSIX رفتارش را به نسبت تعداد شناسههای عبور داده شده به آن، تعیین میکند. فقط پوستههای خیلی قدیمی با این روش مشکل دارند، و شما وقتی کد جدیدی مینویسید نباید نگران آن باشید (راه چاره x"$foo" پایین را ببینید).
در Bash و بسیاری از سایر پوستههای ksh-مانند، یک جایگزین ممتاز وجود دارد که از کلمه کلیدی [[ استفاده میکند.
# Bash / Ksh [[ $foo == bar ]] # !صحیح
در [[ ]] نیازی نیست شما متغیرهای مورد اشاره در طرف چپ عملگر تخصیصِ = را نقلقولی کنید، زیرا آنها دستخوش تفکیک کلمه یا globbing قرار نمیگیرند، و حتی متغیرهای تهی نیز به طور صحیح مدیریت میشوند. از طرف دیگر، نقلقولی نمودن آنها هم به چیزی آسیب نخواهد رسانید. بر خلاف [ و test، میتوانید از عملگر همسانی == نیز استفاده کنید. به هر حال توجه کنید که با استفاده از [[، مقایسهها انطباق الگو در برابر رشته موجود در طرف راست را انجام میدهند، نه فقط یک مقایسه رشتهای ساده. در صورتی که در رشته طرف راست هر کاراکتری که دارای معنی ویژهای در مضمون انطباق الگو باشد به کار برود، برای لفظی کردن رشته طرف راست، باید آن را نقلقولی کنید.
# Bash و Ksh در match=b*r [[ $foo == "$match" ]] # نیز مطابقت شود b*r خوب! نقلقولی نشده میتواند در برابر الگوی
ممکن است کُدی مانند این را دیده باشید:
# POSIX / Bourne [ x"$foo" = xbar ] # صحیح اما معمولاً غیر لازم
ترفند x"$foo" در جایی لازم میشود که کُدی باید در پوسته های قدیمی فاقد [[ و دارای فرمان پیشین [ اجرا گردد، که اگر $foo با یک - شروع بشود این فرمان سر در گم میگردد. باز هم در سیستمهای قدیمیترِ بیان شده، [ توجه نمیکند که آیا نشانه طرف راستِ = با یک - شروع میشود. فقط آن را به صورت لفظی به کار میبرد. درست سمت چپ عملگر تساوی است که نیازمند هشیاری فوقالعاده میباشد.
توجه نمایید پوستههایی که محتاج این ترفند هستند، مطابقِ POSIX نیستند. حتی پوسته موروثی Bourne به این احتیاج ندارند (شاید همزاد غیر POSIX پوسته Bourne که هنوز در سطح وسیع به عنوان یک پوسته سیستم استفاده میشود). به ندرت چنین قابلیت حمل فوقالعادهای نیاز است و قابلیت خواندن کد شما را کمتر (و آن را زشتتر) میسازد.
باز هم این یک خطای نقلقولی دیگر. به طوریکه با یک بسط متغیر، نتیجه یک جایگزینی فرمان دستخوش تفکیک کلمه و بسط نام مسیر قرار میگیرد. بنابراین باید آن را نقلقولی کنید:
cd "$(dirname "$f")"
آنچه در اینجا واضح نیست، چگونگی نقلقولهای درونی است. یک برنامهنویس C که این را بخواند، انتظار خواهد داشت نقلقولهای دوگانه اول و دوم با یکدیگر، و سپس نقلقولهای سوم و چهارم همگروه باشند. اما در Bash این حالت نیست. Bash با نقلقولهای دوگانه داخل جایگزینی فرمان به عنوان یک زوج، و با نقلقولهای دوگانه خارج از جایگزینی به عنوان یک زوج دیگر، رفتار میکند.
یک روش دیگرِ نوشتنِ این مطلب: تجزیهکننده با جایگزینی فرمان به عنوان یک «مرحله درونی» رفتار میکند، و نقلقولهای داخل آن از نقلقولهای بیرون از آن جدا میشوند.
توجه نمایید که این رفتار نقلقول درونی برای حالت نقلقولهای برعکس( backticks) نیست. همیشه باید ترکیب "$(...)" برگزیده شود!
[ "
شما نمیتوانید && را در درون فرمان test قدیمی( یا [) به کار ببرید. تجزیهکننده Bash عملگر && را خارج از [[ ]] یا (( )) میبیند و دستور شما را به دو فرمان قبل و بعد از && تجزیه میکند. به جای آن یکی از این موارد را استفاده کنید:
[ bar = "$foo" ] && [ foo = "$bar" ] # !صحیح (POSIX) [[ $foo = bar && $bar = foo ]] # !این نیز صحیح (Bash / Ksh)
(توجه نمایید که ما متغیر و ثابت را در داخل [ به واسطه دلایلی که در چاله شماره ۴(pitfall #4) بحث گردید، جابجا نمودهایم. همچنین میتوانستیم مورد [[ را جابجا کنیم، اما برای ممانعت از تفسیر شدن بسطها به عنوان یک الگو، لازم میشد که آنها را نقلقولی کنیم.) همان کار در مورد || اِعمال میشود. از [[، یا دو فرمان [ استفاده کنید.
از این مورد پرهیز نمایید:
[ bar = "$foo" -a foo = "$bar" ] # .غیر قابل حمل
عملگرهای -a و -o باینری، و ( / ) (گروهبندی)، ضمایم XSI به استاندارد POSIX هستند. تمام آنها در POSIX-2008 به عنوان از رده خارج علامت گذاری شدند. نباید آنها در کدهای جدید به کار بروند. یکی از مشکلات عملی با [ A = B -a C = D ] (یا -o) آنست که POSIX نتایج فرمان test یا [ را با بیش از چهار شناسه تعریف نکرده است. احتمالاً با اکثر پوستهها کار میکند، اما نمیتوانید به آن استناد کنید. اگر لازم است شما برای پوستههای POSIX بنویسید، به جای آن باید دو فرمان test یا [ همراه با && مابین آنها را به کار ببرید.
در اینجا چند موضوع وجود دارد. نخست، فرمان [[ نباید به تنهایی برای ارزیابی عبارتهای حسابی به کار برود. باید برای عبارتهای بررسی شامل یکی از عملگرهای پشتیبانی شده test استفاده گردد. با آنکه به طور تکنیکی میتوانید با استفاده از بعضی عملگرهای [[ حساب کنید، انجام چنین موردی فقط در تلفیق با یکی ازعملگرهای غیرحسابی در جایی از عبارت، قابل قبول است. اگر شما میخواهید دقیقاً یک مقایسه عددی(یا هر محاسبه دیگرپوسته) را انجام بدهید، خیلی بهتر است به جای آن از (( )) استفاده کنید:
# Bash یا Ksh در پوسته ((foo > 7)) # !صحیح [[ foo -gt 7 ]] # .کار میکند، اما بیهوده است # .را به کار ببرید let یا ((...)) بیش از همه اشتباه در نظر گرفته میشود.به جای آن
اگر عملگر > را داخل [[ ]] به کار ببرید، به عنوان مقایسه رشتهای(بررسی مقابله ترتیب توسط منطقه) با آن رفتار میشود، نه یک مقایسه عدد صحیح. این ممکن است گاهی اوقات کار کند، اما وقتی کمترین توقع را دارید ناموفق خواهد شد. اگر شما > را داخل [ ] به کار ببرید، حتی وخیمتر است: یک تغییر مسیر خروجی است. شما فایلی به نام 7 در دایرکتوری جاری خود به دست خواهید آورد، و بررسی نیز مادامیکه $foo تهی نباشد، موفق خواهد بود.
اگر همنوایی اکید با POSIX مورد نیاز است و (( در دسترس نیست، آنوقت جایگزین صحیح، کاربرد [ سَبکِ قدیم است:
# POSIX [ "$foo" -gt 7 ] # Also right! [ $((foo > 7)) -ne 0 ] # جهت عملگرهای عمومیتر حساب ،(( برایPOSIX معادل موافق
توجه کنید که اگر $foo یک عدد صحیح نباشد، فرمان test ... -gt به روشهای جالبتوجهی ناموفق میگردد. بنابراین، غیر از استفاده برای نشان دادن و محدود کردن شناسهها به یک کلمه منفرد به منظور کاهش احتمال آثار جانبی نامفهوم در برخی پوستهها، امتیاز زیادی در نقلقول کردن صحیح آن وجود ندارد.
اگر ورودی به هر مضمون حسابی( از جمله (( یا let)، یا عبارت بررسی [ شامل مقایسه حسابی، نتواند تضمین بشود، آنوقت همواره شما باید قبل از ارزیابی، عبارت ورودی را تأیید اعتبار نمایید.
# POSIX case $foo in *[^[:digit:]]*) printf '$foo expanded to a non-digit: %s\n' "$foo" >&2 exit 1 ;; *) [ $foo -gt 7 ] esac
grep foo bar
کُد فوق در نگاه اول به نظر صحیح میآید، چنین نیست؟ به طور یقین، فقط یک کاربردِ ضعیفِ grep -c است، اما به عنوان مثال سادهای در نظر گرفته شده است. تغییرات count به خارج از حلقه while گسترش نمییابد زیرا هر فرمان در یک خطلوله در یک پوسته فرعی مستقل اجرا میشود. این مورد تقریباً هر نوآموز Bash را متعجب میسازد.
POSIX مشخص نمیکند آیا آخرین عنصر یک خطلوله در یک پوسته فرعی ارزیابی میشود یا خیر. برخی پوستهها از قبیل ksh93 و Bash >= 4.2 با shopt -s lastpipe فعال شده، حلقه while در این مثال را درپردازش اصلی پوسته اجرا میکنند، و تأثیرگذاری همه اثرات جانبی درون آن را مجاز میکنند. بنابراین، اسکریپتهای قابل حمل باید به طریقی نوشته بشوند که متکی به یکی از این رفتارها نباشند. برای راهکارهای عبور از این مورد، لطفاً پرسش و پاسخ شماره 24 را ببینید. بیش از آن طولانی است که مناسب اینجا باشد.
بسیاری از نوآموزان یک بینش نادرست در باره جملات if دارند که معلول دیدن الگوی بسیار رایجی است که یک کلمه کلیدی if بلاواسطه با یک [ یا [[ دنبال میشود. این مطلب اشخاص را متقاعد میسازد که آن [ به طریقی قسمتی از ترکیب دستوری جمله if است، درست مانند پرانتزهای به کار رفته در جمله if زبان C.
این حالت نیست! if یک فرمان میپذیرد. [ یک فرمان است، نه علامت گذار ترکیب دستوری برای جمله if. معادلی برای فرمان test است، به استثنای آن که شناسه انتهایی آن باید ] باشد. برای مثال موارد زیر:
# POSIX if [ false ]; then echo "HELP"; fi if test false; then echo "HELP"; fi
معادل هستند، هر دو کنترل میکنند که شناسه "false" غیر تهی است. در هر دو مورد با متعجب نمودن برنامهنویسان سایر زبانها در باره گرامر پوسته، همواره HELP چاپ خواهد شد.
ترکیب دستوری یک جمله if چنین است:
if COMMANDS then <COMMANDS> elif <COMMANDS> # اختیاری then <COMMANDS> else <COMMANDS> # اختیاری fi # ضروری
یکبار دیگر، [ یک فرمان است. مانند هر فرمان ساده معمولی دیگر شناسهها را میپذیرد. if یک فرمان مرکب است که فرمانهای دیگر را در بر میگیرد -- و در ترکیب آن [ وجود ندارد!
ممکن است هیچ یا چند بخش elif، و یک بخش اختیاری else وجود داشته باشد.
فرمان مرکب if از دو یا چند بخش شامل لیستهای فرمانها که هر یک به وسیله یک کلمه کلیدی then، elif، یا else تعیین حدود شدهاند، تشکیل میگردد و توسط کلمه کلیدی fi پایان داده میشود. کد خروج فرمان انتهایی بخش اول و هر بخش بعدی elif تعیین میکند آیا بخش then متناظر آن ارزیابی بشود. تا وقتی یکی از بخشهای then اجرا بشود، یک elif دیگر ارزیابی میگردد. اگر هیچ بخش then ارزیابی نشود، آنوقت انشعاب else پذیرفته میشود، یا اگر بخش else معین نشده باشد، بلوک if کامل است و کل فرمان if صفر(صحیح) را برگشت میدهد.
اگر شما میخواهید بر اساس خروجی فرمان grep تصمیمسازی کنید، نیازی به محصور نمودن آن در پرانتزها، قلابها، یا هر گونه دیگری از علامتگذاری دستوری، ندارید! فقط از grep به جای دستورات بعد از if استفاده کنید، مانند این:
if grep -q fooregex myfile; then ... fi
اگر grep سطری از myfile را مطابقت بدهد، آنوقت کد خروج 0 (صحیح) خواهد بود، و قسمت then اجرا خواهد شد. در غیر اینصورت، اگر هیچ مطابقتی وجود نداشته باشد، grep کد غیرصفر برگشت میدهد و کل فرمان if صفر خواهد بود.
See also:
[bar="$foo"] # !اشتباه [ bar="$foo" ] # !باز هم اشتباه
به طوری که در مثال قبلی تشریح شد، [ یک فرمان است. درست همانند با هر فرمان دیگر، Bash انتظار دارد فرمان(ساده) با یک کاراکتر فاصله دنبال شود، آنوقت شناسه اول، سپس فاصله دیگر، و غیره. شما نمیتوانید تمام اقلام را با یکدیگر بدون قرار دادن فاصله بین آنها اجرا کنید! این هم روش صحیح آن:
if [ bar = "$foo" ]; then ...
هر یک از موارد bar، =، بسط "$foo"، و ] یک شناسه جداگانه برای فرمان [ هستند. میان هر زوج از شناسهها باید فضای سفید وجود داشته باشد، به طوریکه پوسته بداند هر شناسه کجا شروع و ختم میشود.
دوباره راهی اینجا شدیم. [ یک فرمان است. یک علامتگذار ترکیب دستوری که میان if و بعضی انواع شرط C-شکل مینشیند، نیست. برای گروهبندی هم به کار نمیرود. شما نمیتوانید دستورات C-شکل if را گرفته و آنها را فقط با تعویض پرانتزها با قلابهای گوشهدار به فرمانهای Bash ترجمه کنید!
اگر میخواهید یک شرط مرکب را بیان نمایید، چنین کنید:
if [ a = b ] && [ c = d ]; then ...
توجه نمایید که ما بعد از if دو دستور متصل شده به وسیله یک عملگر && ( AND منطقی، میانبر ارزیابی) داریم. این دقیقاً همانند است با:
if test a = b && test c = d; then ...
اگر فرمان test اول غلط را باز گرداند، وارد بدنه جمله if نمیشود. اگر صحیح برگرداند، آنوقت فرمان test دوم اجرا میشود، و اگر آن یک نیز صحیح را برگرداند، سپس وارد بدنه جمله if خواهد شد. (برنامهنویسان C از قبل با && آشنا هستند. Bash همان مسیر کوتاه ارزیابی را استفاده میکند. به همچنین، || میانبر ارزیابی برای عملگر OR است.)
کلمه کلیدی [[ استفاده از && را مجاز میکند، بنابراین کُد فوق میتواند به این طریق نیز نوشته شود:
if [[ a = b && c = d ]]; then ...
برای یک تله مرتبط با تستهای ترکیب شده با عملگرهای شرطی pitfall شماره ۶ را ببینید.
نمیتوانید از یک کاراکتر $ قبل از نام متغیر در فرمان read استفاده کنید. اگر میخواهید دادهها را در متغیری به نام foo قرار بدهید، آن را به این شکل انجام بدهید:
read foo
یا به طور مطمئنتر:
IFS= read -r foo
read $foo یک سطر از ورودی را خوانده و آن را در متغیری که نام آن در $foo نگهداری میشود قرار خواهد داد. اگر شما واقعاً در نظر دارید foo یک مرجع برای برخی متغیرهای دیگر باشد، شاید این مفید باشد، اما در اکثریت حالتها، این در حقیقت یک باگ است.
cat file
شما نمیتوانید از یک فایل بخوانید و در همان خط لوله در آن فایل بنویسید. بر اساس آنکه لوله شما چه کاری انجام میدهد، ممکن است فایل پاک بشود(به صفر بایت، یا شاید به تعداد بایتهای برابر با اندازه بافر خطلوله سیستم عامل شما)، یا ممکن است تا آنجا رشد نماید که تمام فضای قابل استفاده دیسک را پُر کند، یا به محدودیت اندازه فایل در سیستم عامل شما، یا سهمیه شما از فضای دیسک برسد، و غیره.
اگر میخواهید به طور ایمن فایل را تغییر بدهید، غیر از الحاق نمودن به انتهای آن، باید یک فایل موقتی ایجاد شده در جایی(*) وجود داشته باشد. برای مثال، این کد کاملاً قابل حمل میباشد:
sed 's/foo/bar/g' file > tmpfile && mv tmpfile file
مورد پایین فقط در GNU sed 4.x کار میکند:
sed -i 's/foo/bar/g' file(s)
توجه نمایید که این مورد نیز یک فایل موقتی ایجاد میکند، و همان نوع تغییرنام نیرنگ آمیز را انجام میدهد -- فقط آن را به صورت ناپیدا مدیریت میکند.
و فرمان معادل زیرین، به پرل نگارش 5.x (که احتمالاً به طور گستردهتری نسبت به GNU sed 4.x در دسترس است)، نیاز دارد:
perl -pi -e 's/foo/bar/g' file(s)
برای تفصیل بیشتر در باره تعویض محتویات فایلها، لطفاً پرسش و پاسخ شماره 21 را ببینید.
sed '...' file | grep '...' | sponge file
به جای استفاده از یک فایل موقتی به اضافه یک mv اتمی
این فرمان به ظاهر نسبتاً صاف و ساده، به دلیل اینکه $foo نقلقولی نگردیده، نابسامانی عظیمی را موجب میگردد. این عبارت فقط در معرض تفکیک کلمه نخواهد بود، بلکه در معرض جانشینی فایل نیز هست. این مورد برنامهنویسان Bash را موقعی که در حقیقت متغیرهایشان صحیح است، به طرف تصور اشتباه بودن محتوای متغیرهایشان گمراه میکند --تنها، تفکیک کلمه یا بسط نام فایل، آن چیزی است که قضاوت آنها را در مورد آنچه رخ میدهد، مختل میسازد.
msg="Please enter a file name of the form *.zip" echo $msg
این پیغام به کلمات تجزیه میشود و هر جانشین از قبیل *.zip بسط داده میشود. کاربران شما چه فکری خواهند نمود، وقتی این پیغام را ببینند:
Please enter a file name of the form freenfss.zip lw35nfss.zip
جهت نمایش تشریحی:
var="*.zip" # میباشد"zip"شامل یک ستاره، یک نقطه و کلمه var echo "$var" # را مینویسد *.zip echo $var # ختم میشوند را مینویسد .zip لیست فایلهایی که به
در حقیقت، فرمان echo در اینجا نمیتواند با ایمنی مطلق به کار برود. برای مثال اگر متغیر شامل -n باشد، echo در عوض دادههایی که باید چاپ گردند، آن را به عنوان یک گزینه در نظر میگیرد. تنها روش مطلقاً مطمئن برای چاپ مقدار یک متغیر، استفاده از printf است:
printf "%s\n" "$foo"
خیر، متغیر را با قرار دادن یک $ در جلوی نام متغیر اختصاص ندهید. این پرل نیست.
نه، وقتی به متغیر مقدار اختصاص میدهید، نمیتوانید در اطراف = فاصله قرار بدهید. این C نیست. موقعی که شما مینویسید foo = bar پوسته آن را به سه کلمه تفکیک میکند. اولین کلمه، foo، به عنوان نام فرمان قبول میشود.کلمه دوم و سوم شناسههای آن فرمان میشوند.
بعلاوه، موارد زیر نیز اشتباه هستند:
foo= bar # !اشتباه foo =bar # !اشتباه $foo = bar; # !کاملاً اشتباه foo=bar # .صحیح foo="bar" # .صحیحتر
یک here document ابزار مفیدی برای تعیبیه بلوکهای بزرگ دادههای متنی در یک اسکریپت است. این ابزار موجب تغییر مسیر سطرهای متن در یک اسکریپت به ورودی استاندارد فرمان میشود. متأسفانه، echo فرمانی نیست که از ورودی استاندارد بخواند.
# :این کد اشتباه است echo <<EOF Hello world How's it going? EOF # :این است آنچه شما سعی کردید انجام بدهید cat <<EOF Hello world How's it going? EOF # یا، از نقلقولها استفاده کنید که میتوانند سطرهای چندگانه را پوشش بدهند # :(یک دستور داخلی است echo کارآمد، فرمان ) echo "Hello world How's it going?"
استفاده از نقلقولها به این صورت خوب است -- در تمام پوستهها، به خوبی کار میکند -- اما به شما اجازه فقط رها کردن یک بلوک از سطرها به داخل اسکریپت را نمیدهد. روی سطر اول و آخر علامتگذاری ترکیبی وجود دارد. اگر میخواهید سطرهای شما تحت تأثیر ترکیب دستوری پوسته قرار نگیرند، و نمیخواهید بذر یک فرمان cat را بکارید، یک پیشنهاد دیگر این است:
# (مؤثر، این فرمان نیز دستور داخلی است) printf یا استفاده از printf %s "\ Hello world How's it going? "
در مثال printf، کاراکتر \ در سطر اول، از یک سطر جدید اضافه در ابتدای بلوک متن پیشگیری مینماید. در انتها یک سطرجدید لفظی وجود دارد( به علت اینکه نقلقول انتهایی در یک سطر جدید است). فقدان \n درشناسه قالب فرمان printf افزودن یک سطر جدید توسط printf در انتها را مانع میشود. ترفند \ در نقلقولهای منفرد کار نمیکند. اگر نقلقولهای منفرد در اطراف متن را میخواهید یا نیاز دارید، دو انتخاب دارید، که هر دو سرایت دادن ترکیب دستوری پوسته به دادههایتان را ایجاب میکنند:
printf %s \ 'Hello world ' printf %s 'Hello world '
این ترکیب دستوری تقریباً صحیح است. مشکل آنست که، در بسیاری پلاتفرمها، su یک شناسه -c قبول میکند، اما آنچه شما میخواهید نیست. برای مثال، روی OpenBSD:
$ su -c 'echo hello' su: only the superuser may specify a login class
شما میخواهید -c 'some command' را به پوسته عبور بدهید، و این یعنی شما یک نام کاربری قبل از -c لازم دارید.
su root -c 'some command' # اکنون درست است
وقتی نام کاربری از قلم انداخته شود su نام کاربری را root فرض میکند، اما وقتی شما میخواهید پس از آن یک فرمان به پوسته عبور بدهید شکست میخورد. در این حالت شما باید نام کاربری را ارائه بدهید.
اگر شما خطاهای فرمان cd را کنترل نکنید، شاید اجرای bar در محل اشتباهی انجام شود. اگر به عنوان مثال bar بر حسب اتفاق rm -f * باشد، این میتواند یک مصیبت عمدهای بشود.
شما همواره باید خطاهای فرمان cd را کنترل نمایید. سادهترین راه انجام آن چنین است:
cd /foo && bar
اگر بیش از یک فرمان تنها، بعد از cd وجود داشته باشد، میتوانید این مورد را برگزینید:
cd /foo || exit 1 bar baz bat ... # دستورات زیاد
cd درماندگی از تغییر دایرکتوریها را با پیغامی از قبیل "bash: cd: /foo: No such file or directory" در
cd /net || { echo "Can't read /net. Make sure you've logged in to the Samba network, and try again."; exit 1; } do_stuff more_stuff
توجه کنید یک فاصله ضروری میان { و echo، و یک ; الزامی، قبل از بستن} وجود دارد.
بعضی اشخاص نیز دوست دارند برای لغو کردن اسکریپتهایشان با هر فرمانی که کد غیر صفر برمیگرداند، set -e را فعال کنند، اما برای انجام آن به طور صحیح این مورد میتواند بیشتر ماهرانه باشد( چون بسیاری فرمانهای رایج ممکن است برای یک وضعیت هشدار دهنده کد غیر صفر را برگشت بدهند، که ممکن است نخواهید با آن به عنوان مورد مصیبآمیز رفتار کنید).
ضمناً، اگر به فراوانی در یک اسکریپت Bash تعویض دایرکتوریها را انجام میدهید، به طور قطع راهنمای Bash در مورد pushd و popd، و dirs را بخوانید. شاید تمام آن کدی که شما برای مدیریت cdها و pwdها نوشتهاید، کاملاً غیر ضروری باشد.
در ارتباط با هر کدام، این مورد را:
find ... -type d -print0 | while IFS= read -r -d '' subdir; do here=$PWD cd "$subdir" && whatever && ... cd "$here" done
با این مقایسه کنید:
find ... -type d -print0 | while IFS= read -r -d '' subdir; do (cd "$subdir" || exit; whatever; ...) done
اجبار به یک پوسته فرعی در اینجا باعث میشود cd فقط در پوسته فرعی رخ بدهد، برای تکرار بعدی حلقه، ما صرفنظر از اینکه آیا cd موفق یا ناموفق گردیده است، به محل عادی خود باز میگردیم. نیاز نیست به طور دستی به عقب برگردیم، و گرفتار رشته بیپایان منطق ... && ...، که باز دارنده استفاده از سایر شرطیهاست، نمیشویم. نگارش پوسته فرعی سادهتر و واضحتر (ولو اینکه مقدار جزئی آهستهتر) است.
عملگر == برای فرمان [ معتبر نیست. از = یا به جای آن از کلمه کلیدی [[ استفاده کنید.
[ bar = "$foo" ] && echo yes [[ bar == $foo ]] && echo yes
شما نمیتوانید یک ; را بلافاصله بعد ازیک & قرار بدهید. فقط ; نامربوط را به کلی حذف کنید.
for i in {1..10}; do ./something & done
یا:
for i in {1..10}; do ./something & done
& از قبل به عنوان خاتمه دهنده فرمان عمل میکند، درست مانند آنچه ; انجام میدهد. و نمیتوانید این دو را ادغام کنید.
به طور کلی، یک ; میتواند با یک سطرجدید تعویض شود، اما تمام سطرهای جدید نمیتوانند با ; تعویض بشوند.
برخی اشخاص مایل به استفاده از && و || به عنوان ترکیب دستوری میانبر برای if ... then ... else ... fi هستند. در بسیاری حالتها، این ترکیب کاملاً مطمئن است:
[[ -s $errorlog ]] && echo "Uh oh, there were some errors." || echo "Successful."
هرچند، این ساختار در حالت کلی کاملاً معادل if ... fi نیست، زیرا فرمانی که بعد از && میآید نیز یک کد خروج تولید میکند. و اگر آن کد خروج «صحیح»(0) نباشد، آنوقت فرمانی که پس از || میآید نیز فراخوانی خواهد شد. برای مثال:
i=0 true && ((i++)) || ((i--)) echo $i # صفر را چاپ میکند
در اینجا چه اتفاقی رخ میدهد؟ به نظر میرسد i باید 1باشد، اما به 0 ختم میگردد. چرا؟ به دلیل آنکه مورد i++ و i-- هر دو اجرا شدهاند. فرمان ((i++)) یک وضعیت خروج دارد، و وضعیت خروج آن از یک ارزیابی C-شکل و عبارت درون پرانتزها استنتاج میشود. مقدار عبارت اتفاقاً صفر است(مقدار اولیه i)، و در C، یک عبارت با مقدار عدد صحیح 0 به عنوان false در نظر گرفته میشود. بنابراین ((i++)) (وقتی i صفر است)یک وضعیت خروج 1 (غلط) دارد، و از اینرو فرمان ((i--)) هم اجرا میگردد.
اگر ما از عملگر پیشافزایش استفاده کنیم این امر رخ نمیدهد، چون وضعیت خروج ++i صحیح است:
i=0 true && (( ++i )) || (( --i )) echo $i # را چاپ میکند 1
اما نکتهای از مثال نا مشهود است. این فقط حسب اتفاق به صورت همانندی کار میکند، و اگر y شانسی برای شکست داشته باشد شما نمیتوانید به آن x && y || z استناد کنید! (اگر مقدار i را به جای 0 با -1 ارزش گذاری کنیم این مثال ناموفق میشود.)
اگر میخواهید ایمن باشید، یا اگر شما واقعاً اطمینان ندارید که چطور کار میکند، یا اگر هر چیزی در پاراگرافهای قبلی کاملاً برای شما روشن نیست، لطفاً فقط از ترکیب دستوری ساده if ... fi در برنامههایتان استفاده کنید.
i=0 if true; then ((i++)) else ((i--)) fi echo $i # را چاپ میکند 1
این قسمت در پوسته Bourne نیز صدق میکند، این کدی است که چگونگی آن را تشریح مینماید:
true && { echo true; false; } || { echo false; true; }
خروجی به جای یک سطر منفرد "true"، دو سطر "true" و "false" است
مشکل در اینجا آن است که، در یک پوسته Bash محاورهای، خطایی مانند این خواهید دید:
bash: !": event not found
چنین است زیرا، در تنظیمات پیشفرض برای یک پوسته محاورهای، Bash با استفاده از نشانه تعجب، بسط تاریخچه به شیوه csh انجام میدهد. این مورد در اسکریپتهای پوسته مشکلی نیست، تنها در پوستههای محاورهای مسئله است.
متأسفانه، کوشش بدیهی برای تعمیر آن کار نخواهد کرد:
$ echo "hi\!" hi\!
آسانترین راه حل غیر فعال کردن گزینه histexpand است: این کار میتواند از طریق set +H یا set +o histexpand انجام شود.
پرسش: چرا متوسل شدن به histexpand مناسبتر از نقلقولهای منفرد است؟
من شخصاً موقعی به این موضوع وارد شدم که داشتم فایلهای صوتی را با فرمانهایی مانند این، دستکاری میکردم:
mp3info -t "Don't Let It Show" ... mp3info -t "Ah! Leah!" ...
کاربرد نقلقولهای منفرد به شدت ناجور بود، به دلیل آپاستروف در عنوان تمام فایلها . استفاده از نقلقولهای دوگانه موضوع بسط تاریخچه را به کار انداخت. (و فایلی را تصور کنید که دارای هر دو کاراکتر در نامش باشد. نقلقول کردن بیرحم خواهد بود.) چون من هرگز به طور واقعی بسط تاریخچه را استفاده نمیکنم، اولویت شخصی من، خاموش کردن آن در ~/.bashrc بود. -- GreyCat
این راه حل عمل خواهد نمود:
echo 'Hello World!'
set +H echo "Hello World!"
یا:
histchars=
بسیاری افراد برای غیر فعال نمودن دائمی بسط تاریخچه، به سادگی قرار دادن set +H یا set +o histexpand در فایل ~/.bashrc خودشان را انتخاب میکنند. به هر حال، این یک اولویت شخصی است، و شما باید هر کدام که برایتان بهتر عمل میکند را انتخاب نمایید.
راه حل دیگر این است:
exmark='!' echo "Hello, world$exmark"
Bash (مانند تمام پوستههای Bourne) دارای یک ترکیب دستوری ویژه برای ارجاع به لیست پارامترهای مکانی به صورت یک به یک میباشد، و آن ترکیب نه ترکیب $* است و نه ترکیب $@ است. این هر دو به یک لیست از کلماتِ پارامترهای اسکریپت شما بسط مییابند، نه هر پارامتر به عنوان یک کلمه جداگانه.
صحیح آن این است:
for arg in "$@" # یا به سادگی: for arg
چون در اسکریپتها، حلقه زنی روی پارامترهای مکانی مورد رایجی است، for arg پیش فرض for arg in "$@" است. "$@" نقلقول دوگانه شده، افسون خاصی است که باعث میشود هر پارامتر به عنوان یک کلمه منفرد (یا یک تکرار منفرد حلقه) به کار برود. همان کاری است که حداقل 99% اوقات شما باید انجام بدهید.
این هم یک مثال:
# نگارش نادرست for x in $*; do echo "parameter: '$x'" done $ ./myscript 'arg 1' arg2 arg3 parameter: 'arg' parameter: '1' parameter: 'arg2' parameter: 'arg3'
باید به این صورت نوشته شود:
# نگارش صحیح for x in "$@"; do echo "parameter: '$x'" done $ ./myscript 'arg 1' arg2 arg3 parameter: 'arg 1' parameter: 'arg2' parameter: 'arg3'
این در برخی پوستهها کار میکند، اما در سایرین خیر. شما موقع تعریف یک تابع هرگز نباید کلمه کلیدی function را باپرانتزها () ترکیب نمایید.
Bash (حداقل در بعضی از نگارشها) ترکیب این دو را اجازه میدهد. اکثر پوستهها آن را قبول نمیکنند (برای مثال zsh 4.x و شاید بالاتر). برخی پوستهها function foo را میپذیرند، اما برای قابلیت حمل حداکثر، همواره باید از این شکل استفاده کنید:
foo() { ... }
بسط مد تنها موقعی اَعمال میگردد که کاراکتر
نقلقول پارامترهای مسیری که نسبت به دایرکتوری خانگی کاربر بیان میگردند، باید با استفاده از $HOME به جای '~' انجام بشوند. برای نمونه وضعیتی را در نظر بگیرید که در آن $HOME برابر است با"/home/my photos".
"~/dir with spaces" # بسط داده میشود "~/dir with spaces" به ~"/dir with spaces" # بسط مییابد "~/dir with spaces" به ~/"dir with spaces" # بسط مییابد "/home/my photos/dir with spaces" به "$HOME/dir with spaces" # بسط مییابد "/home/my photos/dir with spaces" به
موقع تعریف یک متغیر محلی در تابع، local به عنوان یک فرمان بر سمت راست خودش اثر میکند. این امر گاهیاوقات میتواند به طور غریبی با بقیه سطر فعل و انفعال کند -- برای مثال، اگر شما بخواهید وضعیت خروج ($?) جایگزینی فرمان را اخذ کنید، نمیتوانید این کار را انجام دهید. کد خروج local گرفته میشود.
بهترین کار استفاده از دستورات جداگانه است:
local varname varname=$(command) rc=$?
تله بعدی موضوع دیگری با این ترکیب دستوری را شرح میدهد:
انجام بسط مد ( با یا بدون نام کاربری) تنها برای موقعی تضمین میشود که مد(~) در ابتدای کلمه واقع شود، خودش به تنهایی یا با یک
به هر حال، فرمانهای export و local یک تخصیص را تشکیل نمیدهند. بنابراین، در برخی پوستهها(مانند Bash)، export foo=~/bar دستخوش بسط مد واقع خواهد شد، در سایرین(مانند dash)، واقع نخواهد شد.
foo=~/bar; export foo # !صحیح export foo="$HOME/bar" # !صحیح
در نقلقولهای منفرد، بسط پارامترهای bash مانند $foo بسط نمییابند. این به قصد استفاده از نقلقولهای منفرد برای محافظت از کاراکترهایی مانند $ در برابر پوسته است.
نقلقولها را به نقلقولهای دوگانه تغییر بدهید:
foo="hello"; sed "s/$foo/good bye/"
اما به خاطر داشته باشید، اگر از نقلقولهای دوگانه استفاده میکنید، شاید به کاراکترهای گریز بیشتری نیاز داشته باشید. صفحه نقلقولها را ببینید.
در اینجا(حداقل) سه مورد اشتباه وجود دارد. اولین مشکل آنست که [A-Z] و [a-z] توسط پوسته به عنوان globها دیده میشوند. اگر هیچ نام فایل منفرد لفظی در دایرکتوری کاری خودتان ندارید، به نظر خواهد آمد که دستور صحیح است، اما اگر داشته باشید، چیزهایی اشتباه خواهد بود. احتمالاً در ساعت سه بامداد یک روز تعطیل.
دومین مشکل آن است که واقعاً این نشانهگذاری برای tr صحیح نیست. آنچه این دستور واقعاً انجام میدهد، ترجمه '[' به '['، هر چیز در محدوده A-Z به a-z، و ']' به ']' است. بنابراین حتی شما به آن براکتها نیاز ندارید، و مشکل اول برطرف میشود.
سومین مشکل آن است که بر اساس منطقه، A-Z یا a-z ممکن است بیست و شش کاراکتر اسکی که شما انتظار دارید را به شما ندهد. در حقیقت، در برخی مناطق z در میانه الفبا قرار دارد! راه حل برای این مورد بستگی دارد به آنچه شما میخواهید رخ بدهد:
# اگر میخواهید حالت ۲۶ کاراکتر لاتین را تغییر بدهید از این استفاده کنید LC_COLLATE=C tr A-Z a-z # اگر تبدیل حالت بر اساس منطقه را میخواهید، که بیشتر میتواند # .مانند آنچه کاربر انتظار دارد، باشد، از این استفاده کنید tr '[:upper:]' '[:lower:]'
در دستور دوم نقلقولها برای اجتناب از globbing مورد نیاز هستند.
مشکل اساسی در اینجا آن است که نام یک پردازش در حال اجرا به طور ذاتی غیرقابل استناد است. میتواند بیش از یک پردازش مشروع gedit وجود داشته باشد. چیز دیگری میتواند خودش را به عنوان gedit تغییر قیافه داده باشد( تغییر نام گزارش شده یک فرمان اجرا شده، پیش پا افتاده است). برای پاسخهای راستین این مورد، صفحه مدیریت پردازش را ببینید.
موارد زیر نوع سریع و نامساعد آن است.
جستجو برای PID (به عنوان مثال) gedit را، بسیاری افراد اینطور شروع میکنند
$ ps ax | grep gedit 10530 ? S 6:23 gedit 32118 pts/0 R+ 0:00 grep gedit
که، وابسته به یک وضعیت مسابقه است، که غالباً grep خودش به عنوان یک نتیجه حاصل میشود. برای فیلتر کردن grep :
ps ax | grep -v grep | grep gedit # کار میکند، اما بد ریخت
یک جایگزین برای آن استفاده از این است:
ps ax | grep [g]edit
این کد در جدول پردازشی که [g]edit در آن است و grep برای جستجوی gedit آن را بررسی میکند، از خود grep صرف نظر میکند.
در گنو-لینوکس، پارامتر -C به جای فیلتر نام فرمان میتواند به کار برود:
$ ps -C gedit PID TTY TIME CMD 10530 ? 00:06:23 gedit
اما چرا دردسر، موقعی که میتوانید به جای آن فقط از pgrep استفاده نمایید؟
$ pgrep gedit 10530
اکنون در مرحله دوم، PID غالباً به وسیله awk یا cut استخراج میشود:
$ ps -C gedit | awk '{print $1}' | tail -n1
اما حتی آن نیز میتواند با چند تریلیون پارامتر برای ps سر و کار داشته باشد:
$ ps -C gedit -opid= 10530
اگر شما در 1992 زمینگیر شدهاید و از pgrep استفاده نمیکنید، میتوانید در عوض از pidof (فقط در گنو-لینوکس) باستانی، متروک، و خوار شمرده شده استفاده کنید:
$ pidof gedit 10530
و اگر شما به PID برای کُشتن پردازش احتیاج دارید، شاید pkill برایتان جالب باشد. توجه کنید که به هر حال، برای مثال، pgrep ssh یا pkill ssh نیز میتوانند پردازشهایی به نام sshd را پیدا کند، و ممکن است نخواهید آنها را kill کنید.
متأسفانه بعضی برنامهها با نامشان اجرا نمیشوند، برای مثال firefox اغلب در نتیجه firefox-bin شروع میشود، که, شما نیاز خواهید داشت با ps ax | grep firefox آن را کشف کنید. یا میتوانید با pgrep و افزودن برخی پارامترها آن را انجام بدهید:
$ pgrep -fl firefox 3128 /usr/lib/firefox/firefox 7120 /usr/lib/firefox/plugin-container /usr/lib/flashplugin-installer/libflashplayer.so -greomni /usr/lib/firefox/omni.ja 3128 true plugin
لطفاً مدیریت پردازش را بخوانید. به طور جدی.
این به علت نقلقولها اشتباه نیست، بلکه به علت قالب رشتهِ قابل سوء استفاده، اشتباه است. اگر $foo به طور دقیق تحت نظارت شما نیست، آنوقت هر کاراکتر \ یا % در متغیر ممکن است موجب رفتار نامطلوب بشود.
همیشه قالب رشته خودتان را ارائه کنید:
printf %s "$foo" printf '%s\n' "$foo"
تجزیه کننده Bash قبل از هر بسط یا جایگزینی دیگر، بسط ابرو را انجام میدهد. بنابراین کد بسط ابرو قسمت $n بسط نیافته را میبیند، که عددی نیست، و به همین جهت داخل ابروها را به لیستی از اعداد بسط نمیدهد. این مطلب استفاده از بسط ابرو را برای ایجاد لیستهایی که اندازه آنها فقط موقع اجرا معلوم است، تقریباً غیر ممکن میسازد.
به جای آن این کار را انجام بدهید:
for ((i=1; i<=n; i++)); do ... done
اصلاً در وضعیت تکرار ساده روی اعداد صحیح، یک حلقه for محاسباتی، تقریباً همیشه نسبت به بسط ابرو ارجح خواهد بود، زیرا بسط ابرو هر شناسهای را که میتواند آرامتر باشد نیز از قبل بسط میدهد و به طور غیر ضرور حافظه را مصرف میکند.
موقعی که در داخل [[ سمت راست یک عملگر = نقلقولی نشده باشد، bash به جای رفتار با آن به عنوان رشته، انطباق الگو را در مقابل آن انجام میدهد. بنابراین، اگر در کُد فوق، bar محتوی * باشد، نتیجه ارزیابی همواره صحیح خواهد بود. اگر میخواهید برابری رشتهها را بررسی کنید، طرف راست تساوی باید نقلقولی بشود:
if [[ $foo = "$bar" ]]
اگر میخواهید انطباق الگو انجام بدهید، شاید انتخاب نام متغیرهایی که نشان بدهند طرف راست شامل یک الگو است، خردمندانه باشد. یا از توضیحات استفاده کنید.
همچنین با ارزش است اشاره شود که اگر طرف راست =~ را نقلقولی کنید، آن نیز به جای انطباق عبارت منظم، مجبور به مقایسه رشتهای میشود. این مطلب ما را به تله بعد سوق میدهد:
نقلقولهای اطراف طرف راست عملگر =~ باعث میگردد طرف راست به جای یک عبارت منظم یک رشته شود. اگر میخواهید یک عبارت منظم بلند یا پیچیده به کار ببرید و از پوشش زیاد با کاراکتر گریز اجتناب کنید، آن را در یک متغیر قرار بدهید:
re='some RE' if [[ $foo =~ $re ]]
این مطلب همچنین تفاوت چگونگی کارکرد =~ در میان نگارشهای مختلف bash را پشت سر میگذارد. کاربرد یک متغیر از برخی مشکلات محیل و نامطبوع پیشگیری میکند.
همان مشکل با انطباق الگو در داخل [[ رخ میدهد:
[[ $foo = "*.glob" ]] # به عنوان یک رشته لفظی رفتار میشود. *.glob با !اشتباه [[ $foo = *.glob ]] # مانند رفتار میگردد-glob به عنوان الگوی *.glob با .صحیح
موقع استفاده از فرمان [، شما باید هر «جایگزینی» را که به آن میدهید، نقلقولی کنید. در غیر اینصورت، $foo میتواند به صفر کلمه، یا 42 کلمه، یا هر تعداد کلمه که یک کلمه نیست، بسط یابد، که ترکیب دستوری را ساقط میکند.
[ -n "$foo" ] [ -z "$foo" ] [ -n "$(some command with a "$file" in it)" ] # :تفکیک کلمه یا بسط جانشین را انجام نمیدهد، پس میتوانستید این را نیز به کار ببرید [[ [[ -n $foo ]] [[ -z $foo ]]
این تست پیوندهای نمادین را تعقیب میکند، از این جهت اگر یک پیوند نمادین خراب باشد، به عنوان مثال به فایلی اشاره کند که موجود نیست، ولواینکه پیوند وجود دارد، test -e کد 1 را برای آن باز میگرداند.
برای پشت سر گذاشتن این مشکل(و آمادگی در برابر آن) باید از این استفاده کنید:
[[ -e "$broken_symlink" || -L "$broken_symlink" ]]
ed file
مشکل ساز است زیرا ed صفر را برای \{0,3\} قبول نمیکند.
میتوانید بررسی کنید که کُد زیر کار میکند:
ed file <<<"g/d\{1,3\}/s//e/g"
توجه نمایید این مشکل واقع میشود، ولواینکه آن BRE (که عبارت منظم مورد استفاده ed است) که POSIX وضع کرده باید صفر را به عنوان کوچکترین عدد وقوع قبول کند(بخش 5را ببینید).
expr sub-string برای کلمه "match" ناموفق است
این به طور قابل قبولی خوب کار میکند
اکثر اوقات
word=abcde expr "$word" : ".\(.*\)" bcde
اما برای کلمه "match" ناموفق است
word=match expr "$word" : ".\(.*\)"
مشکل این است که "match" یک کلیدواژه است. راه حل(فقط در گنو) پیشوند نمودن آن با یک '+' است
word=match expr + "$word" : ".\(.*\)" atch
یا، بله میدانم, توقف استفاده از expr. هر کاری را که expr انجام میدهد میتوانید با استفاده از بسط پارامتر انجام بدهید. آنچه در بالا سعی میکنید انجام بدهید چیست؟ حذف اولین حرف از کلمه؟ آن کار در پوستههای POSIX میتواند با استفاده از PE یا بسط جزئی رشته انجام شود:
$ word=match $ echo "${word#?}" # PE(بسط پارامتر) atch $ echo "${word:1}" # SE(بسط جزئی از رشته) atch
به طور جدی، هیچ عذری برای استفاده از expr مورد قبول نیست مگر اینکه شما در Solaris با /bin/sh ناسازگارش با POSIX باشید. expr یک پردازش خارجی است، بنابراین بسیار کُندتر از دستکاری رشته درون-پردازش است. و چون هیچکس آن را به کار نمیبرد، هیچکس درک نمیکند که چکار میکند، از این جهت کد شما مبهم و برای نگهداری دشوار میگردد.
On UTF-8 and Byte-Order Marks (BOM)
به طور کلی: متن UTF-8 در Unix از BOM [1] استفاده نمیکند. کُدگذاری یک فایل متن ساده توسط منطقه یا به وسیله انواع mime [2] یا سایر فوق دادهها تعیین میشود. در حالیکه حضور یک BOM در سند UTF-8 آن را برای خواندن توسط انسان، معیوب نخواهد کرد، در هر فایل متن مورد نظر برای تفسیر شدن توسط پردازشهای خودکار از قبیل اسکریپتها، کد منبع، فایلهای پیکربندی، و غیره، مسئلهساز است. فایلهایی که با BOM شروع میشوند باید به یک درجه همچون آنهایی که شامل سطرهای معیوب MS-DOS هستند، نامطلوب درنظر گرفته شوند.
در اسکریپتنویسی پوسته: 'جایی که UTF-8 به طور ناپیدا در محیطهای 8-bit استفاده میشود، استفاده از یک BOM با هر قرارداد یا قالب فایلی که کاراکترهای اسکی ویژهای را در ابتدای فایل انتظار دارد، مانند توقع استفاده از کاراکترهای "#!" در ابتدای اسکریپهای پوسته یونیکس، تداخل میکند.'
http://unicode.org/faq/utf_bom.html#bom5
هیچ مورد اشتباهی در این عبارت وجود ندارد، اما شما باید آگاه باشید که جایگزینیهای فرمان (تمام اشکال: `...` و $(...) و $(<file) و `<file` و ${ ...; } (در ksh)) همه سطرجدیدهای دنباله را حذف میکنند.این امر اغلب بی اهمیت یا حتی خوشآیند است، امادر صورتی که شما بایدخروجی لفظی به انضمام تمام سطرهای جدید ممکن را حفظ کنید، این مهارت آمیز است زیرا شما راهی برای دانستن آنکه خروجی شامل آنها هست یا چندتا، ندارید. یک راه رفع و رجوع نازیبا اما قابل استفاده افزودن یک پسوند در داخل جایگزینی فرمان و حذف آن در خارج جایگزینی است:
absolute_dir_path_x=$(readlink -fn -- "$dir_path"; printf x) absolute_dir_path=${absolute_dir_path_x%x}
راه حل کمتر قابل حمل اما شاید زیباتر، استفاده از read با یک جداکننده تهی است.
# (فعال شده lastpipe با bash 4.2+ یا) Ksh در readlink -fn -- "$dir_path" | IFS= read -rd '' absolute_dir_path
جنبه منفی این روش آن است که read همواره false را برگشت خواهد داد مگر اینکه برونداد فرمان یک بایت تهی باشد که باعث خوانده شدن فقط بخشی از جریان داده میگردد. تنها روش برای به دست آوردن کد خروج فرمان از طریق PIPESTATUS است. شما میتوانستید برای مجبور نمودن read به برگشت دادن true، به طور عمد نیز یک بایت تهی تولید و از pipefail استفاده کنید.
set -o pipefail { readlink -fn -- "$dir_path"; printf '\0x'; } | IFS= read -rd '' absolute_dir_path
این مختصری از نابسامانی قابلیت حمل است، به طوری که Bash از هر دو pipefail و PIPESTATUS پشتیبانی میکند، ksh93 تنها از pipefail، و mksh فقط از PIPESTATUS پشتیبانی میکند. به طور افزونتر، برای اینکه read در بایت تهی متوقف شود، یک نگارش خیلی جدید ksh93 لازم میشود.
یک روش برای بازداشتن برنامهها از تفسیر نام فایلهای عبور داده شده به آنها به عنوان گزینه، استفاده از نام مسیر است ( pitfall شماره ۳ فوق را بینید). برای فایلهای تحت دایرکتوری جاری، نامها میتوانند با یک نام مسیر نسبی ./ پیشوند بشوند.
به هر حال، در مورد الگویی مشابه *.*، مشکلاتی میتواند ایجاد بشود زیرا این الگو با رشتهای بر شکل ./filename منطبق میشود. در یک حالت ساده،میتوانید مستقیماً از یک glob برای تولید انطباقهای مطلوب استفاده کنید. به هر حال اگر یک مرحله انطباق الگوی جداگانه مورد نیاز است (به عنوان مثال، نتایج پردازش گردیدهاند و در یک آرایه اندوخته شدهاند، و لازم است فیلتر بشوند)، این مورد میتواند با در نظر گرفتن پیشوند در الگو: [[ $file != ./*.* ]]، یا توسط پاک کردن الگو از تطبیق، حل بشود.
# Bash در پوسته shopt -s nullglob for path in ./*; do [[ ${path##*/} != *.* ]] && rm "$path" done # یا حتی بهتر for file in *; do [[ $file != *.* ]] && rm "./${file}" done # یا باز هم بهتر for file in *.*; do rm "./${file}" done
یک امکان دیگر ارسال علامتپایان گزینهها با شناسه -- است. (در Pitfall شماره ۳ پوشش داده شده).
shopt -s nullglob for file in *; do [[ $file != *.* ]] && rm -- "$file" done
این مورد یکی از رایجترین اشتباهاتی است که با تغییر مسیرها ارتباط دارد، به طور نوعی توسط اشخاصی انجام میشود که میخواهند هر دو خروجی استاندارد و خطا را به یک فایل یا لوله هدایت کنند، این مورد را امتحان میکنند و درک نخواهند نمود که چرا stderr باز هم در ترمینالشان نشان داده میشود. اگر شما توسط این مطلب بهت زده شدهاید، احتمالاً چگونگی شروع به کار تغییر مسیرها یا احتمالا توصیفگرهای فایل را درک نکردهاید. تغییر مسیرها قبل از آنکه فرمان اجرا گردد از چپ به راست ارزیابی میشوند. اصلاً از نظر معنایی این کد نادرستی است: «اول خطای استاندارد به جایی که خروجی استاندارد در حال اشاره کردن به آن است(tty) تغییر مسیر داده میشود، سپس خروجی استاندارد به logfile تغییر مسیر داده میشود». درست برعکس است. خطای استاندارد از قبل به tty میرود. به جای آن، از این مورد استفاده کنید:
somecmd >>logfile 2>&1
توضیح ژرفکاوانهتر، و تشریح کپی توصیفگر، و بخش تغییر مسیر در راهنمای BashGuide را ببینید.
$? فقط در صورتی لازم میشود که شما به بازیابی کامل وضعیت فرمان قبلی نیاز داشته باشید. اگر تنها بخواهید موفقیت یا شکست(وضعیت غیر صفر) را بررسی کنید، فقط به طور مستقیم فرمان را بررسی کنید. به عنوان مثال:
if cmd; then ... fi
کنترل یک وضعیت خروج در مقابل لیستی از پیشنهادها میتواند از الگویی مانند این پیروی کند:
cmd status=$? case $status in 0) echo success >&2 ;; 1) echo 'Must supply a parameter, exiting.' >&2 exit 1 ;; *) echo 'Unknown error, exiting.' >&2 exit $status esac
دامهای Bash (آخرین ویرایش 2013-10-18 15:45:47 توسط GreyCat)