پرسش و پاسخ شماره ۳۵ - آموزش اسکریپت نویسی
X
تبلیغات
رایتل

آموزش اسکریپت نویسی

آموزش اسکریپت نویسی پوسته گنو-لینوکس

#!/bin/bash

پرسش و پاسخ شماره ۳۵

پرسش و پاسخ شماره ۳۵

چگونه می‌توانم شناسه‌های( گزینه‌های ) خط‌فرمان را به آسانی مدیریت کنم؟

خوب، تا اندازه بسیاری بستگی به آن دارد که چه کاری می‌خواهید با آنها انجام بدهید. چندین راهکار ، هرکدام با ضعف و قوت‌های مربوط به خود، وجود دارد.

حلقه دستی

این راهکار هر مجموعه‌ای از گزینه‌های اختیاری را مدیریت می‌کند، زیرا تجزیه کننده را خود شما می‌نویسید. برای 90% برنامه‌ها، ساده‌ترین رویکرد است(به دلیل آنکه شما به ندرت به موارد تفننی نیاز دارید).

این مثال ترکیبی از گزینه‌های کوتاه و بلند را مدیریت خواهد کرد. توجه کنید چگونه هر دو شکل‎ "--file"‎ و ‎"--file=FILE"‎ اداره می‌شوند.

   1 #!/bin/sh
   2 # POSIX  ترکیب دستوری پوسته‎
   3 
   4 # تنظیم دوباره تمام متغیرهایی که باید برقرار شوند‎
   5 file=""
   6 verbose=0
   7 
   8 while :
   9 do
  10     case $1 in
  11         -h | --help | -\?)
  12             #  شما ‎usage()‎یا ‎Help()‎ فراخوانی تابع‎
  13             exit 0  # را به کارنبرید ‎exit 1‎ این یک خطا نیست، کاربر کمک خواسته‎
  14             ;;
  15         -f | --file)
  16             file=$2     #    را گرفته‌اید FILE می‌توانید بررسی کنید که واقعاً‎
  17             shift 2
  18             ;;
  19         --file=*)
  20             file=${1#*=}        #  "=" حذف هر چیز تا رسیدن به‎
  21             shift
  22             ;;
  23         -v | --verbose)
  24             # یکی به درازنویسی اضافه می‌کند ‎-v‎ هر نمونه از‎
  25             verbose=$((verbose+1))
  26             shift
  27             ;;
  28         --) # پایان تمام گزینه ها‎
  29             shift
  30             break
  31             ;;
  32         -*)
  33             echo "WARN: Unknown option (ignored): $1" >&2
  34             shift
  35             ;;
  36         *)  #  while گزینه دیگری موجود نیست، توقف حلقه ‎
  37             break
  38             ;;
  39     esac
  40 done
  41 
  42 #  .به فرض که گزینه‌هایی لازم باشند، کنترل آنکه آنها را گرفته‌ایم‎
  43 
  44 if [ ! "$file" ]; then
  45     echo "ERROR: option '--file FILE' not given. See --help" >&2
  46     exit 1
  47 fi
  48 
  49 # .بقیه برنامه در اینجا‎
  50 # اگر پس از گزینه‌ها، (به عنوان مثال) فایلهای ورودی موجود باشند، آنها‎
  51 # .باقی خواهند ماند‎"$@"‎ در پارامترهای مکانی ‎
  52 

این تجزیه کننده گزینه‌های جداگانه‌ای که به یکدیگر الحاق گردیده‌اند را مدیریت نمی‌کند(مانند ‎ -xvf‎ که به عنوان ‎ -x -v -f‎ قبول بشود). این مورد با تلاش می‌توانست افزوده شود، اما به عنوان یک تمرین برای خواننده واگذار گردید.

برخی برنامه نویسان Bash دوست دارند، برای هشیاری در برابر متغیرهای استفاده نشده، این کُد را در ابتدای اسکریپت‌های خودشان بنویسند:

    set -u     #  set -o nounset یا

استفاده از این دستور حلقه فوق را ناموفق می‌کند، چون ‎ "$1"‎ شاید درموقع ورود به حلقه برقرار نباشد. چهار راه حل برای این موضوع وجود دارد:

  1. عدم استفاده از ‎ -u‎.

  2. تعویض ‎ case $1 in‎ با ‎case ${1+$1} in‎ (تمام کُدهای پس از آن را که ‎ set -u‎ نقض می‌کند، به خوبی توانمند می‌سازد).

  3. تعویض ‎ case $1 in‎ با‎ case ${1-} in‎ (هر متغیری که امکان اعلان نشدن دارد، برای ممانعت از اثر ‎ set -u‎، می‌تواند به صورت‎ ${variable-}‎ نوشته بشود).

getopts

هرگز از ‎ getopt(1)‎ استفاده نکنید. getopt نمی‌تواند شناسه‌های رشته‌ای تهی، یا شناسه‌های دارای فضای سفید را اداره کند. لطفاً اصلاً وجود آن را فراموش کنید.

پوسته POSIX (و سایرین) به جای آن getopts را ارائه می‌نمایند که برای استفاده مطمئن است. این هم یک مثال ساده getopts:

   1 #!/bin/sh
   2 
   3              #  POSIX یک متغیر‎
   4 OPTIND=1     #  قبلاً در پوسته به کار رفته باشد getopts تنظیم مجدد در حالتی که  ‎
   5 
   6              # :ارزش گذاری اولیه متغیرهای خودمان
   7 output_file=""
   8 verbose=0
   9 
  10 while getopts "h?vf:" opt; do
  11     case "$opt" in
  12         h|\?)
  13             show_help
  14             exit 0
  15             ;;
  16         v)  verbose=1
  17             ;;
  18         f)  output_file=$OPTARG
  19             ;;
  20     esac
  21 done
  22 
  23 shift $((OPTIND-1))
  24 
  25 [ "$1" = "--" ] && shift
  26 
  27 echo "verbose=$verbose, output_file='$output_file', Leftovers: $@"
  28 
  29 # انتهای فایل
  30 

مزایای getopts عبارتند از:

  1. قابل حمل است، و به عنوان مثال در dash نیز کار می‌کند.
  2. مواردی مانند ‎ -vf filename‎ که روش یونیکسی مورد انتظار می‌باشد را به طور خودکار مدیریت می‌کند.

اشکال getopts آن است که فقط گزینه‌های کوتاه را مدیریت می‌کند(‎-h‎، نه ‎--help‎).

یک آموزش getopts وجود دارد که شرح می‌دهد هر یک از ترکیبات دستوری و متغیرها به چه معنی هستند. در bash، همچنین‎ help getopts‎ نیز وجود دارد، که می‌تواند آموزنده باشد.

همچنین بازهم در فراخوانی getopts آن اشکال وجود دارد که گزینه‌ها در حداقل دو، یا شاید سه مکان کُد می‌شوند، در جمله case که آنها را پردازش می‌کند و احتمالاً درپیغام راهنمایی که یکی از این روزها خواهان نوشتن آن می‌شوید. این یک فرصت کلاسیک برای رسوخ خطاها در کُدهای نوشته و نگهداری شده -غالباً هنوز زیاد توسعه نیافته، یا خیلی قدیمی‌تر- است. این مورد با استفاده از توابع فراخوانی برگشتی، می‌تواند پیش‌گیری بشود، اما این رویکرد نوعی خنثی نمودن هدف استفاده از getopts می‌باشد.

1. گزینه بلند ماهرانه getops

این هم یک مثال که مدعی تجزیه گزینه‌های بلند با getopts می‌باشد. ایده اصلی کاملاً ساده است: فقط ‎ "-:"‎ را در optstring قرار بدهید. این ترفند به پوسته‌ای نیاز دارد که گزینه-شناسه را اجازه دهد(یعنی نام فایل در ‎ "-f filename"‎ به صورت‎ "-ffilename"‎ به گزینه الحاق بشود). استاندارد POSIX می‌گوید یک فاصله باید بین آنها باشد، bash و dash نوع‎ "-ffilename"‎ را اجازه می‌دهند، اما اگر کسی می‌خواهد اسکریپت قابل حمل بنویسد، نباید به این ارفاق تکیه کند.

   1 #!/bin/bash
   2 # .استفاده می‌کند. به طوری که نوشته شده قابل حمل نیست bash از ملحقات‎
   3 
   4 optspec=":h-:"
   5 
   6 while getopts "$optspec" optchar
   7 do
   8     case "${optchar}" in
   9         -)
  10             case "${OPTARG}" in
  11               loglevel)
  12                   eval val="\$${OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
  13                   echo "Parsing option: '--${OPTARG}', value: '${val}'" >&2
  14                   ;;
  15               loglevel=*)
  16                   val=${OPTARG#*=}
  17                   opt=${OPTARG%=$val}
  18                   echo "Parsing option: '--${opt}', value: '${val}'" >&2
  19                   ;;
  20             esac
  21             ;;
  22         h)
  23             echo "usage: $0 [--loglevel[=]<value>]" >&2
  24             exit 2
  25             ;;
  26     esac
  27 done
  28 
  29 # End of file
  30 

در عمل، این مثال به قدری ابهام آلود است، که اگر این تنها دلیل استفاده از getopts باشد، شاید افزودن پشتیبانی از گزینه الحاق شده به حلقه تجزیه دستی (مانند‎ -vf filename‎)، نسبت به آن قابل ترجیح باشد.

در اینجا نگارش بهبود یافته و عمومی‌تری از تلاش فوق برای افزودن، پشتیبانی ازگزینه‌های بلند هنگام استفاده از getopts، آورده‌ایم:

   1 #!/bin/bash
   2 # .استفاده می‌کند. به صورت نوشته شده قابل حمل نیست bash از ملحقات‎
   3 
   4 declare -A longoptspec
   5 longoptspec=( [loglevel]=1 ) #   استفاده از آرایه انجمنی برای تعیین آنکه ‎
  # longlevelگزینه بلند چند شناسه را قبول می‌کند، در این حالت تعریف می‌کنیم که ‎
  #یک شناسه دارد یامی‌پذیرد، گزینه‌های بلندی که به این طریق لیست نشده‌اند به طور‎
  # پیش فرض تعداد شناسه آنها صفر است‎
   6 optspec=":h-:"
   7 while getopts "$optspec" opt; do
   8 while true; do
   9     case "${opt}" in
  10         -)   # می‌باشد value= نام گزینه بلند یا نام گزینه بلند OPTARG‎
  11             if [[ "${OPTARG}" =~ .*=.* ]] 
                 #فقط یک شناسه امکان پذیر است ‎--key=value‎ با این شکل‎
  12             then
  13                 opt=${OPTARG/=*/}
  14                 OPTARG=${OPTARG#*=}
  15                 ((OPTIND--))
  16             else 
                 # شناسه‌های چندتایی امکان پذیر است ‎--key value1 value2‎ با ساختار ‎
  17                 opt="$OPTARG"
  18                 OPTARG=(${@:OPTIND:$((longoptspec[$opt]))})
  19             fi
  20             ((OPTIND+=longoptspec[$opt]))
  21             continue  # تنظیم گردیدند می‌توانیم همان طور opt و OPTARG اکنون که‎
            # گرفته بودیم، آنها را پردازش نماییم getopts  که اگر شناسه بلند را از‎
  22             ;;
  23         loglevel)
  24           loglevel=$OPTARG
  25             ;;
  26         h|help)
  27             echo "usage: $0 [--loglevel[=]<value>]" >&2
  28             exit 2
  29             ;;
  30     esac
  31 break; done
  32 done
  33 
  34 # End of file
  35 

با این نسخه می‌توانید گزینه‌های کوتاه و بلند را در کنار هم داشته باشید ونیازی به ویرایش کُد از سطر 10 تا 22 نخواهید داشت. این راه حل همچنین می‌تواند شناسه‌های چندگانه برای گزینه‌های بلند را اداره کند، فقط از ‎ ${OPTARG}‎ یا ‎${OPTARG[0]}‎ برای اولین شناسه استفاده کنید، و از ‎ ${OPTARG[1]}‎ برای دومین،‎ ${OPTARG[2]}‎ برای سومین شناسه و به همین ترتیب. این مورد نیز همان کمبود نمونه ماقبل خود را دارد و قابل حمل نبوده، مختص bash است.

پیمایش ساده تکراری brute-force زیرنویس ۱

یک رویکرد دیگر کنترل گزینه‌ها در دستورات if می‌باشد. تابعی مانند این یکی شاید مفید باشد:

   1 #!/bin/bash
   2 
   3 HaveOpt ()
   4 {
   5     local needle=$1
   6     shift
   7 
   8     while [[ $1 == -* ]]
   9     do
  10         # به معنی پایان گزینه‌ها می‌باشد ‎"--"‎ مطابق قرارداد‎
  11         case "$1" in
  12             --)      return 1 ;;
  13             $needle) return 0 ;;
  14         esac
  15 
  16         shift
  17     done
  18 
  19     return 1
  20 }
  21 
  22 HaveOpt --quick "$@" && echo "Option quick is set"
  23 
  24 # End of file
  25 

و این در صورتی کار می‌کند که اسکریپت به این شکل اجرا بشود:

  • ./script --quick
  • ./script -other --quick

اما بدون "-" یا با "--" در اولین شناسه متوقف می‌شود:

  • ./script -bar foo --quick
  • ./script -bar -- --quick

البته، این رویکرد(تکرار روی لیست شناسه در هر نوبتی که می‌خواهید یک مورد را کنترل کنید،) نسبت به فقط یکبار تکرار و تنظیم نشانه متغیرها، خیلی کمتر کارامد است.

همچنین گزینه‌ها را در سرتاسر برنامه پخش می‌کند. گزینه لفظی ‎--quick‎ ممکن است، دور از هر نام گزینه دیگر، در صدها سطر از بدنه اصلی برنامه ظاهر شود. این کابوسی برای حفظ ونگهداری است.

برنامه‌های پیچیده افزودنی غیر استاندارد

bhepple استفاده از process-getopt ( با مجوز GPL ) را پیشنهاد می‌کند و این نمونه کد را ارائه می‌نماید:

PROG=$(basename $0)
VERSION='1.2'
USAGE="A tiny example using process-getopt(1)"

# برای تعریف تعدادی گزینه process-getopt فراخوانی توابع 
source process-getopt

SLOT=""
SLOT_func()   { [ "${1:-""}" ] && SLOT="yes"; }   # SLOTفراخوان برگشتی برای گزینه 
add_opt SLOT "boolean option" s "" slot

TOKEN=""
TOKEN_func()  { [ "${1:-""}" ] && TOKEN="$2"; }   # TOKEN فراخوانی برگشتی گزینه 
add_opt TOKEN "this option takes a value" t n token number

add_std_opts     #  و غیره --help تعریف گزینه‌های استاندارد 

TEMP=$(call_getopt "$@") || exit 1
eval set -- "$TEMP" #  getopt(1)درست مثل ‎

# حذف گزینه ها از سطر دستور
process_opts "$@" || shift "$?"

echo "SLOT=$SLOT"
echo "TOKEN=$TOKEN"
echo "args=$@"

اینجا برای بسیار آسانتر نمودن تألیف و نگهداری، تمام اطلاعات در مورد هر گزینه فقط در یک محل تعریف می‌شود.مقدار زیادی از چرکینی کار به طور خودکار زدوده می‌شود و از استانداردهای ‎ getopt(1)‎ متابعت می‌نماید، زیرا getopt را برای شما فراخوانی می‌کند.

  • در حقیقت، آنچه نویسنده غفلت کرده که بگوید، آن است که به جای getopt، واقعاً از معناشناسی getopts استفاده می‌شود. من این تست را اجرا کردم:

     ~/process-getopt-1.6$ set -- one 'rm -rf /' 'foo;bar' "'"
     ~/process-getopt-1.6$ call_getopt "$@"
      -- 'rm -rf /' 'foo;bar' ''\'''
  • به نظر می‌رسد به اندازه کافی برای اداره گزینه‌های تهی، گزینه‌های شامل فضای سفید، و گزینه‌های شامل نقل‌قول منفرد، هوشمند باشد، به نحوی که باعث می‌گرددeval پیش روی شما متوقف نشود. اما این ظهرنویسی برای نرم‌افزار process-getopt به طور کلی نیست، من به اندازه کافی در باره‌اش نمی‌دانم. -GreyCat

این برنامه نوشته شده و تست شده در لینوکس است، جایی که ‎ getopt(1)‎ از گزینه‌های بلند پشتیبانی می‌کند. برای قابلیت انتقال، در حین اجرا ‎ getopt(1)‎ محلی را تست می‌کند و اگریک نمونه غیر-گنو پیدا کند(یعنی نمونه‌ای که برای ‎getopt --test‎ عدد 4 برنمی‌گرداند) فقط گزینه‌های کوتاه را پردازش می‌کند. از دستور داخلی bash به نام‎ getopts(1)‎ استفاده نمی‌کند. -bhepple


CategoryShell

پرسش و پاسخ 35 (آخرین ویرایش ‎ 2012-10-22 20:41:23 ‎ توسط GreyCat)


  1. مترجم: brute-force یک سَبکِ برنامه‌نویسی است که در آن برنامه نویس به جای استفاده از هوشمندی خود برای ساده‌سازی مسئله، به قدرت پردازشی سیستم تکیه می‌کند. می‌توان آن را برنامه‌نویسی زمخت و بی‌ملاحظه نیز نامید (1)