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

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

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

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

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)


تعیین محدوده تاریخی ویرایش فایل


چطور بررسی نمایم که فایل دریک ماه معین یا در یک محدوده تاریخی ویرایش شده است؟

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

برنامه ‎stat(1)‎ وجود دارد، اما حتی در میان سیستم‌عامل‌های مختلف گنو نیز خیلی غیرقابل حمل است. در حالیکه اکثر ماشین‌ها یک stat دارند، تمام آنها شناسه‌ها و ترکیب‌دستوری متفاوتی دارند. بنابراین، اگر اسکریپت باید قابل‌حمل باشد، شما نباید به ‎stat(1)‎ تکیه کنید. یک نمونه دستور داخلی قابل بارگیری به نام finfo وجود دارد که فوق داده‌های فایل را بازیابی می‌کند، اما آن هم به طور پیش‌فرض در دسترس نمی‌باشد.

آنچه ما باید داشته باشیم test (یا ‎[[‎) که می‌تواند بررسی نماید آیا یک فایل قبل یا بعد از فایل دیگری ویرایش گردیده است(با استفاده از ‎-nt‎ یا ‎-ot‎) و touch که می‌تواند فایلهایی با تاریخ ویرایش معین شده ایجاد کند، می‌باشند. با ترکیب اینها می‌توانیم به مقدار قابل ملاحظه‌ای این کار را انجام بدهیم.

برای مثال، تابعی برای بررسی آنکه آیا فایلی در یک بازه معین شده زمانی ویرایش گردیده است:

inTime() {
 set -- "$1" "$2" "${3:-1}" "${4:-1}" "$5" "$6" # Default month & day to 1.
 local file=$1 ftmp="${TMPDIR:-/tmp}/.f.$$" ttmp="${TMPDIR:-/tmp}/.t.$$"
 local fyear=${2%-*} fmonth=${3%-*} fday=${4%-*} fhour=${5%-*} fminute=${6%-*}
 local tyear=${2#*-} tmonth=${3#*-} tday=${4#*-} thour=${5#*-} tminute=${6#*-}

 touch -t "$(printf '%02d%02d%02d%02d%02d' "$fyear" "$fmonth" "$fday" "$fhour" "$fminute")" "$ftmp"
 touch -t "$(printf '%02d%02d%02d%02d%02d' "$tyear" "$tmonth" "$tday" "$thour" "$tminute")" "$ttmp"

 (trap 'rm "$ftmp" "$ttmp"' RETURN; [[ $file -nt $ftmp && $file -ot $ttmp ]])
}

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

این هم یک مثال کاربردی:

 $ touch -t 198404041300 file
 $ inTime file 1984 04-05 && echo "file was last modified in April of 1984"
 file was last modified in April of 1984
 $ inTime file 2010 01-02 || echo "file was not last modified in January 2010"
 file was not last modified in Januari 2010
 $ inTime file 1945-2010 && echo "file was last modified after The War"
 file was last modified after The War


CategoryShell

پرسش و پاسخ 103 (آخرین ویرایش ‎2010-10-08 21:21:20‎ توسط pool-71-247-30-161)


محاسبه اختلاف بین دو تاریخ معین


چگونه اختلاف بین دو تاریخ را به دست آورم؟

بهترین کار آنست که در سراسر کُد خود با نشانه‌های زمان (timestamps) کار کنید، و سپس برای خروجی، این نشانه‌ها را به شکل قابل خواندن انسانی تبدیل نمایید. اگر شما با ورودی قابل خواندن انسانی سر و کار دارید، پس به چیزی که بتواند آنها را تجزیه کند نیاز خواهید داشت.

استفاده از date گنو، برای مثال:

# (وقت محلی) ‎Jan 1, 2010‎ به دست آوردن ثانیه‌های سپری شده از‎
then=$(date -d "2010-01-01 00:00:00" +%s)
now=$(date +%s)
echo $(($now - $then))

برای چاپ مدت زمان به صورت قابل خواندن انسانی، نیاز به انجام مقداری محاسبه خواهید داشت:

# تعریف تعدادی از ثابت‌ها‎
minute_secs=60 hour_secs=$((60 * minute_secs)) day_secs=$((24 * hour_secs))
# به دست آوردن کل مدت‎
seconds_since=$(($now - $then))
# تفکیک‎
days=$((seconds_since / day_secs))
hours=$((seconds_since % day_secs / hour_secs))
minutes=$((seconds_since % day_secs % hour_secs / minute_secs))
seconds=$((seconds_since % day_secs % hour_secs % minute_secs))
# چاپ شکیل‎
echo "$days days, $hours hours, $minutes minutes and $seconds seconds."

یا، بدون برچسب‌های تفصیلی:

# Bash/ksh
((duration = now - then))
((days = duration / 86400))
((duration %= 86400))
((hours = duration / 3600))
((duration %= 3600))
((minutes = duration / 60))
((seconds = duration % 60))
echo "$days days, $hours hours, $minutes minutes and $seconds seconds."

برای تبدیل نشانه‌های زمان به تاریخ قابل خواندن انسانی، استفاده از نگارشهای اخیرdate گنو:

date -d "@$now"

( برای اطلاعات بیشتر در باره تبدیل نشانه‌های زمان یونیکس به تاریخ قابل خواندن انسانی پرسش و پاسخ شماره 70 را ببینید.)


CategoryShell

پرسش و پاسخ 102 (آخرین ویرایش ‎2010-07-30 13:46:05‎ توسط GreyCat