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

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

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

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

انتظار برای خاتمه یافتن پردازش فرزند


چطور در انتظار انجام چند پردازش فرزند بمانم؟

برای انجام این کار روشهای بیشماری موجود است، اما تمام آنها محدود به ابزارهای در دسترس هستند. من به چاره سازی‌های زیر رسیدم .

اگر می‌خواهید در انتظار تمام پردازشهای فرزند بمانید، به سادگی wait را بدون شناسه احضار کنید.

اگر فقط می‌خواهید منتظر برخی، اما نه تمام آنها، بمانید و نگران وضعیت خروج آنها نمی‌باشید، می‌توانید wait را با PIDهای چندگانه فراخوانی کنید:

wait $pid1 $pid2

اگر نیاز دارید، بدانید آیا فرزند موفق یا ناموفق بوده، آنوقت شاید:

waitall() { # PID...
  ## .انتظار برای خروج فرزند و نمایش آنکه آیا همه با وضعیت صفر خارج شده‌اند‎
  local errors=0
  while :; do
    debug "Processes remaining: $*"
    for pid in "$@"; do
      shift
      if kill -0 "$pid" 2>/dev/null; then
        debug "$pid is still alive."
        set -- "$@" "$pid"
      elif wait "$pid"; then
        debug "$pid exited with zero exit status."
      else
        debug "$pid exited with non-zero exit status."
        ((++errors))
      fi
    done
    (("$#" > 0)) || break
    # متوقف گردد؟ sleep وقتی فرزند خاتمه داده می‌شود، چطور این ‎:‎برای انجام ‎
    sleep ${WAITALL_DELAY:-1}
   done
  ((errors == 0))
}

debug() { echo "DEBUG: $*" >&2; }

pids=""
for t in 3 5 4; do
  sleep "$t" &
  pids="$pids $!"
done
waitall $pids

حلقه‌زنی بواسطه ‎kill -0‎ می‌تواند بسیار بی‌کفایت باشد.

اطلاعات مفیدتر می‌تواند در صفحه مدیریت پردازش یافت شود.


CategoryShell

پرسش و پاسخ 108 (آخرین ویرایش ‎2011-09-12 23:03:28‎ توسط GreyCat)


درج نشانه زمان در سطرهای یک جریان داده


چگونه به هر سطر یک جریان مُهرتاریخ بزنم؟

روشهای بیشماری برای انجام این کار هست، اما تمام آنها یا به ابزارهای در دسترس محدود می‌شوند، یا کُند هستند. ما چند نمونه را نشان خواهیم داد.

بیایید اول با روش آهسته قابل حمل شروع کنیم و با این نمونه آن را انجام بدهیم:

# POSIX
while IFS= read -r line; do
  echo "$(date +%Y%m%d-%H:%M:%S) $line"
done

و یکی دیگر که حتی کُندتر است:

awk '{system("printf \"`date +%T ` \">&2")}$0'

و سومی، که به طور جزئی سریعتر است، اما ممکن است بعضی از سطرهای ورودی را خُرد کند:

xargs -I@ -n1 date "+%T @"

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

روشهای متنوعی برای انجام این کار، بدون تولید پردازش فرزند(forking) به ازای هر سطر، وجود دارد، اما تمام آنها نیازمند ابزارهای غیر استاندارد یا پوسته‌های خاص، می‌باشند. ‎Bash 4.2‎ می‌تواند آن را با printf انجام بدهد:

# Bash 4.2
while read -r; do
  printf "%(%Y%m%d-%H:%M:%S)T %s\n" -1 "$REPLY"
done

مشخص کننده قالب ‎%(...)T‎ در bash نگارش ‎4.2‎ جدید است. شناسه ‎-1‎ به او می‌گوید از زمان جاری به جای زمان عبور داده شده به عنوان شناسه، استفاده کند. برای جزئیات، صفحه man مربوط را ملاحظه نمایید.

یک روش دیگر، نوشتن یک perl یک سطری است:

perl -p -e '@l=localtime; printf "%04d%02d%02d-%02d:%02d:%02d ", 1900+$l[5], $l[4], $l[3], $l[2], $l[1], $l[0]'

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

ابزارهای دیگری مخصوصاً برای نشانه‌گذاری زمان فایلهای ثبت وقایع و مشابه، وجود دارد. یکی از آنها multilog از daemontools می‌باشد، اما قالب نشانه‌گذاری زمان آن TAI64N است که قابل خواندن انسانی نیست. دیگری ts از بسته moreutils می‌باشد.


CategoryShell

پرسش و پاسخ 107 (آخرین ویرایش ‎2012-05-19 12:23:00‎ توسط GreyCat)


tee کردن stdout اسکریپت در فایل log


می‌خواهم از داخل اسکریپت stdout را به یک فایل ثبت وقایع tee نمایم. و همچنین شاید stderr را.

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

اجازه بدهید با ساده‌ترین حالت شروع کنیم: می‌خواهم خروجی استانداردم را علاوه بر صفحه نمایش به یک فایل ثبت رخداد tee نمایم.

این به معنای آنست که از هر آنچه به stdout ارسال می‌گردد دو نسخه می‌خواهیم -- یک نسخه برای صفحه نمایش(یا هر جایی که stdout موقعی که اسکریپت شروع شده به آنجا اشاره می‌نمود)، و یک نسخه برای فایل ثبت وقایع. برنامه tee برای این منظور استفاده می‌شود:

# Bash
exec > >(tee mylog)    

ترکیب دستوری جایگزینی پردازش، یک لوله با نام (یا چیزی قابل قیاس با آن) ایجاد می‌کند و برنامه tee را با خواندن از آن لوله در پس زمینه اجرا می‌کند. tee دو کپی از هر آنچه می‌خواند، ایجاد می‌کند -- یکی برای فایل mylog (که آن را باز می‌کند)، و یکی برای stdout، که از اسکریپت ارث می‌برد. سرانجام، exec خروجی استاندارد پوسته را به لوله تغییر مسیر می‌دهد.

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

# Bash
exec > >(tee mylog)

echo "A" >&2
cat file
echo "B" >&2

سطر A و B که در stderr نوشته می‌شوند به میان پردازش tee نمی‌روند - آنها مستقیماً به stderr ارسال می‌شوند. به هرحال، file که ما از cat به دست می‌آوریم، قبل از اینکه ما آن را ببینیم به میان لوله و tee فرستاده می‌شود. اگر ما این اسکریپت را بدون هر گونه تغییر مسیر در ترمینال اجرا نماییم، احتمالاً (تضمین نمی‌شود!) چیزی مانند این می‌بینیم:زیرنویس مترجم 1

~$ ./foo
A
B
~$ hi mom

حقیقتاً راهی برای اجتناب از این وجود ندارد. ما می‌توانستیم با امید به خوش‌اقبالی، stderr را به سَبک مشابهی به تأخیر اندازیم، اما تضمینی وجود ندارد که سطرها به طورمساوی به تأخیر اُفتند.

همچنین، توجه نمایید که محتویات فایل بعد از اعلان بعدی پوسته چاپ گردید. بعضی اشخاص آن را اختلال نظم احساس می‌کنند. یکبار دیگر، روش پاکیزه‌ای برای اجتناب از آن وجود ندارد، چون tee در یک پردازش پس‌زمینه، اما خارج از کنترل ما انجام شده است. حتی اضافه نمودن فرمان wait به اسکریپت تأثیری ندارد. برخی افراد جهت دادن شانس به فرمان tee پس زمینه برای پایان یافتن، فرمان ‎sleep 1‎ را به انتهای اسکریپت اضافه می‌کنند. این کار می‌کند(معمولاً)، اما بعضی اشخاص آن را بد‌تر از مشکل اصلی می‌دانند.

اگر از دستور زبان Bash دوری کنیم، و لوله با نام و یک پردازش پس زمینه خودمان را تنظیم کنیم، آنوقت کنترل را به دست می‌آوریم:

# Bash
mkdir -p ~/tmp || exit 1
trap 'rm -f ~/tmp/pipe$$; exit' EXIT
mkfifo ~/tmp/pipe$$
tee mylog < ~/tmp/pipe$$ & pid=$!
exec > ~/tmp/pipe$$

echo A >&2
cat bar
echo B >&2

exec >&-
wait $pid

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

~$ ./foo
A
B
hi mom
~$

این به گونه بعدی این پرسش منجر می‌گردد -- می‌خواهم stdout و stderr با حفظ هماهنگی سطرها، هردو را با یکدیگر، ثبت کنم.

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

# Bash
exec > >(tee mylog) 2>&1

echo A >&2
cat file
echo B >&2

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

~$ ./foo
A
hi mom
B
~$ cat mylog
A
hi mom
B
~$ 

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

~$ ./foo
A
hi mom
~$ B

(این مورد می‌تواند با همان راه حلِ لوله با نام و پردازش پس زمینه که قبلاً نشان دادم حل بشود.)

سومین گونه از این پرسش نیز نسبتاً ساده است: من می‌خواهم خروجی استاندارد را در یک فایل ثبت کنم، و stderr را در فایل دیگر. این ساده است، زیرا آن محدودیت اضافی را نداریم که می‌باید هماهنگی مابین دو جریان روی ترمینال را اداره نماییم. فقط نویسنده‌های فایل‌های log را تنظیم می‌کنیم:

exec > >(tee mylog.stdout) 2> >(tee mylog.stderr >&2)

echo A >&2
cat bar
echo B >&2

و اکنون جریانهای ما به طور جداگانه ثبت می‌شوند. چون فایلهای لاگ مجزا می‌باشند، ترتیبی که سطرها در آنها نوشته می‌شوند، اهمیت ندارد. هرچندکه، در ترمینال نتایجِ درهم ریخته حاصل خواهیم نمود:

~$ ./foo
A
hi mom
B
~$ ./foo
hi mom
A
B
~$

اما بعضی اشخاص نمی‌خواهند از دست دادن تمایز میان خروجی استاندارد و stderr، یا ناهماهنگی سطرها را قبول کنند. آنها در مورد کلمات وسواس دارند، و بنابراین متقاضی مشکل‌ترین گونه این پرسش هستند -- من می‌خواهم stdout و stderr را با یکدیگر داخل یک فایل منفرد ثبت نمایم، اما همچنین می‌خواهم آنها را در مقصدهای جداگانه اصلی آنها نگهداری کنم.

به منظور انجام این مورد، نخست باید چند نکته را متذکر شویم:

  • اگر آنها می‌خواهند دو جریان جداگانه stdout و stderr باشند، آنوقت پردازش معینی باید در هریک از آنها بنویسد.
  • راهی برای نوشتن یک پردازش در اسکریپت پوسته‌ای که از دو توصیف‌گر فایل جداگانه بخواند که یکی از آنها ورودی در دسترس دارد، وجود ندارد زیرا پوسته دارای میانجی ‎poll(2)‎ یا ‎select(2)‎ نیست.

  • بنابراین، ما به دو پردازش نویسنده جداگانه نیاز داریم.

  • تنها روش حفظ نمودن خروجی دو نویسنده جداگانه، از خراب شدن با یکدیگر، ایجاد اطمینان از آن است که هردو نویسنده، خروجی‌ خود را در وضعیت افزودن (درج) باز کنند. یک FD که در وضعیت افزودن باز می‌شود صفت تضمین کننده‌ای دارد، که هر گاه داده می‌خواهد در آن نوشته شود، نخست به انتها می‌پرد.

بنابراین:

# Bash
> mylog
exec > >(tee -a mylog) 2> >(tee -a mylog >&2)

echo A >&2
cat file
echo B >&2

این تضمین می‌کند که فایل ثبت رخداد صحیح است. تضمین نمی‌کند که نویسنده‌ها قبل از اعلان فرمان بعدی خاتمه یابند:

~$ ./foo
A
hi mom
B
~$ cat mylog
A
hi mom
B
~$ ./foo
A
hi mom
~$ B

می‌توانستیم همان ترفند لوله با نام بعلاوه waitرا استفاده کنیم که قبلاً به کار بردیم(به عنوان تمرین برای خواننده واگذار شد).

این صفحه پرسش «آیا سطرهایی که در ترمینال ظاهر می‌گردند تضمین می‌شود که به ترتیب صحیح ظاهر ‌شوند» را باقی می‌گذارد. در حال حاضر: حقیقتاً من نمی‌دانم.


CategoryShell

پرسش و پاسخ 106 (آخرین ویرایش ‎2013-03-28 16:00:45‎ توسط GreyCat)


  1. مترجم: در اینجا فرض شده است که شما دارای فایلی به نام file در پوشه جاری هستید که محتوی رشته hi mom می‌باشد و همچنین کد فوق را در اسکریپتی به نام foo ذخیره نموده‌اید. پس از اجرای اسکریپت فایلی به نام mylog با همان محتوا در دایرکتوری جاری خواهید داشت، و این نشان دهنده آن است که محتوای فایل file در دونسخه یکی برای خروجی استاندارد و دیگری به فایل mylog ارسال گردیده است. (بازگشت)


چرا ‎set -e‎ مطابق انتظار عمل نمی‌کند‎


چرا ‎ set -e‎(یا ‎ set -o errexit‎ یا ‎ trap ERR‎) آنچه را انتظار دارم انجام نمی‌دهد؟

دستور ‎set -e‎ کوششی برای افزودن تشخیص خطای خودکار به پوسته بود. هدف آن بود که موجب گردد هرگاه هر خطایی رخ داد، پوسته لغو بشود، بنابراین شما ‎ || exit 1‎ را نباید بعد از هر دستور مهم قرار بدهید.

آن هدف کاملاً غیرعملی بود، زیرا بسیاری از فرمانها به طور عمدی عدد غیر صفر را برمی‌گردانند. برای مثال:

  if [ -d /foo ]; then ...; else ...; fi

به طور واضح، موقعی که جمله شرطی ‎[ -d /foo ]‎ مقدار غیر صفر را (به دلیل موجود نبودن دایرکتوری) باز می‌گرداند، ما نمی‌خواهیم انصراف حاصل شود -- اسکریپت ما می‌خواهد قسمت else آن را به کار ببرد. بنابراین، مجریان تصمیم گرفتند یک گروه قواعد ویژه بسازند، مانند «فرمانهایی که بخشی از یک بررسی if می‌باشند، مصون هستند»، یا «فرمانها در داخل یک خط‌لوله غیر از آخرین دستور، مصون هستند».

این قواعد به شدت درهم تابیده هستند، و حتی در بعضی حالت‌های فوق‌العاده ساده، بازهم قابل فهمیدن نیستند. حتی وخیم‌تر، قواعد از یک نگارش Bash به دیگری تغییر می‌کنند، چون Bash تلاش می‌کند تعریف بینهایت لغزنده POSIX از این ویژگی را دنبال کند. موقعی که یک پوسته فرعی درگیر می‌شود این مطلب بازهم وخیم‌تر می‌شود -- رفتار نسبت به اینکه آیا Bash در وضعیت سازگار با POSIX احضار می‌شود، تغییر می‌کند. یک wiki دیگر صفحه‌ای دارد که این مورد را با تفصیل بیشتری پوشش می‌دهد. حتماً پیش‌بینی‌های احتیاطی را بررسی کنید.

تمرین برای خواننده: چرا این مثال چیزی چاپ نمی‌کند؟

   1 #!/bin/bash
   2 set -e
   3 i=0
   4 let i++
   5 echo "i is $i"

تمرین 2: چرا این یکی گاهی اوقات کار می‌کند؟ در کدام نگارش bash کار می‌کند، و در کدام نگارش با شکست مواجه می‌شود؟

   1 #!/bin/bash
   2 set -e
   3 i=0
   4 ((i++))
   5 echo "i is $i"

تمرین 3: چرا این دو اسکریپت عیناً مثل هم نیستند؟

   1 #!/bin/bash
   2 set -e
   3 test -d nosuchdir && echo no dir
   4 echo survived

   1 #!/bin/bash
   2 set -e
   3 f() { test -d nosuchdir && echo no dir; }
   4 f
   5 echo survived

تمرین 4: چرا این اسکریپت‌ها معادل هم نیستند؟

   1 set -e
   2 f() { test -d nosuchdir && echo no dir; }
   3 f
   4 echo survived

   1 set -e
   2 f() { if test -d nosuchdir; then echo no dir; fi; }
   3 f
   4 echo survived

(پاسخ پرسش‌ها)

پیشنهاد شخصی GreyCat ساده است: از ‎set -e‎ استفاده نکنید. به جای آن کنترل خطای خودتان را اضافه نمایید.

پیشنهاد شخصی rking پیش به سوی استفاده از ‎set -e‎، اما بر حذر بودن از گاف‌های احتمالی است. معناشناسی سودمندی دارد، بنابراین حذف آن از جعبه ابزار به مثابه کوته‌بینی است.


CategoryShell

پرسش و پاسخ 105 (آخرین ویرایش ‎2013-01-14 19:54:51‎ توسط GreyCat)


چرا ‎foo=bar echo "$foo"‎ کار نمی‌کند


چرا ‎ foo=bar echo "$foo"‎ رشته bar را چاپ نمی‌کند؟

این تله است، و باید به ترتیب دقیقی که، تفکیک کنندهBash هر مرحله را انجام می‌دهد، توجه شود.

بسیاری اشخاص وقتی ابتدا ترکیب ‎var=value command‎ را کشف می‌کنند و پی‌می‌برند که چطور در طول اجرای فرمان به طور موقتی متغیر را تنظیم می‌نماید، نهایتاً یک مثال مشابه این یکی می‌سازند و سردرگم می‌شوند که چرا آنچه را انتظار دارند انجام نمی‌دهد.

به عنوان یک توضیح:

$ unset foo
$ foo=bar echo "$foo"

$ echo "$foo"

$ foo=bar; echo "$foo"
bar

علت اینکه دستور اول یک سطر خالی چاپ می‌کند به خاطر ترتیب این مراحل است:

  • بسط پارامتر ‎$foo‎ اول انجام می‌شود. یک رشته تهی برای عبارت نقل‌قولی شده جایگزین می‌شود.

  • بعد از آن، Bash محیط موقتی را برقرار می‌کند و ‎foo=bar‎ را در آن قرار می‌دهد.

  • فرمان echo با یک رشته تهی به عنوان شناسه، و foo=bar در محیطش اجرا می‌شود. اما چون فرمان echo به متغیرهای محیط توجه ندارد، آن را نادیده می‌گیرد.

این نگارش آنطور که ما انتظار داریم عمل می‌کند:

$ unset -v foo
$ foo=bar bash -c 'echo "$foo"'
bar

در این حالت، مراحل زیر انجام می‌شوند:

  • یک محیط موقت همراه با ‎foo=bar‎ داخل آن، تنظیم می‌شود.

  • bash از داخل آن محیط فراخوانی می‌شود، و ‎-c‎ و ‎echo "$foo"‎ به عنوان دو شناسه‌اش به آن تحویل می‌شوند.

  • Bash پردازش فرزند ‎$foo‎ را با مقدار آن در محیط بسط می‌دهد و آن مقدار را تحویل echo می‌دهد.

در تمام حالت‌ها کاملاً واضح نیست که چرا اشخاص این سؤال را از ما می‌پرسند.اکثراً به نظر می‌رسد آنها به جای تلاش برای حل یک مشکل معین، درباره رفتار آن کنجکاو می‌باشند، بنابراین، من سعی نمی‌کنم مثالهایی در باره «روش صحیح انجام مواردی مانند این» تحویل بدهم، چون یک مشکل حقیقی برای حل کردن وجود ندارد.

موقعیت‌های ویژه‌ای در Bash وجود دارد که در آنها درک این مطلب می‌تواند مفید باشد. مثال زیر را ملاحظه کنید:

  arr=('Var 1' 'Var 2' 'Var 3' 'Var 4')

  #  ";" وصل کردن هر عنصر آرایه با‎
  #   نمودن unset سپس  و IFS تنظیم متغیر ‎:‎روش سنتی  ‎
  IFS=\;
  joinedVariable="${arr[*]}"
  unset -v IFS

‎  #  ‏به طور موقت تنظیم شود. eval برای مدت اجرای IFS  روش جایگزین، متغیر‎
  #  ‎4.3.0‎ در نگارش‌های کوچکتر از Bash راهکار نقل‌قول دوگانه یک باگ‎
  IFS=\; command eval 'JoinedVariable="${arr[*]}"'

در اینجا، پیشنهاد eval ساده‌تر و بیشتر برازنده است(در نظر شما! -- GreyCat). برای تضمین امنیت موقع به کارگیری eval باید دقت مناسب به عمل آید. پیشوند command برای تمام پوسته‌های غیر از Bash به اضافه Bash در حالت POSIX لازم است‎( http://wiki.bash-hackers.org/commands/builtin/eval#using_the_environment)‎ را ببینید. این مورد در نگارشهای اخیر zsh به سبب یک پسرفت آشکار، (رفتار مستند شده جهش به سوی ‎setopt POSIX_BUILTINS‎)، یا busybox به علت یک باگ که در این حالت تخصیص‌های محیط برای پخش شدن ناموفق می‌گردند، کار نخواهد کرد. (هر کس به اندازه کافی علاقمند است با خیال راحت باگ‌ها را اصلاح کند).


CategoryShell

پرسش و پاسخ 104 (آخرین ویرایش ‎2013-07-23 12:04:16‎ توسط GreyCat)