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

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

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

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

چرا ‎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


توابع سودمند warn و die


توابع سودمند همگانی(warn و die)

(اگر شما در جستجوی گزینه پردازش بوده‌اید، پرسش و پاسخ شماره ۳۵ را ملاحظه کنید.) در مورد توابع ذیل بارها در ‎#bash‎ سؤال شده است، بنابراین امیدواریم آنها برایتان مفید باشد.

##
# warn: چاپ می‌کند stderr پیغامی در‎
# warn "format" ["arguments"...] :طرز استفاده ‎
#
warn() {
  local fmt="$1"
  shift
  printf "script_name: $fmt\n" "$@" >&2
}

###
### زیرین "die" سه تابع‎
### فوق "warn" بر اساس تابع‎
###

##
# چاپ می‌کند stderr یک پیغام در ‎:‎ (نگارش ساده) die ‎
# .و با وضعیت خروج آخرین فرمان خارج می‌شود‎
# 
# ‎some_command || die "پیغام شما" ‎["arguments"...]‎   :‎طرز استفاده‎
#
die () {
  local st="$?"
  warn "$@"
  exit "$st"
}

##
#  stderr یک پیغام در‏ ‎:‎ (نگارش وضعیت صریح) die‎
#  .چاپ می‌کند و با وضعیت معین شده خارج می‌شود‎
# if فلان; then die status_code "پیغام"  ‎["arguments"...]‎; fi : طرز استفاده ‎
#
die() {
  local st="$1"
  shift
  warn "$@"
  exit "$st"
}

##
#  stderr پیغامی در‏‎:‎ (نگارش وضعیت اختیاری) die ‎
#  چاپ می‌کند و با وضعیت خروج تعیین شده یا‎
# .وضعیت خروج آخرین فرمان خارج می‌شود‎
#  some_command || die [status code] "message" ["arguments"...] :طرز استفاده ‎
#
die() {
  local st="$?"
  if [[ "$1" != *[^0-9]* ]]; then
    st="$1"
    shift
  fi
  warn "$@"
  exit "$st"
}


CategoryShell

پرسش و پاسخ 101 (آخرین ویرایش ‎2013-08-27 00:04:03‎ توسط 162)