توی این درس یاد میگیری اسکریپتی بسازی که مثل ابزارهای واقعی لینوکس فلگ (flag) و گزینه (option) بپذیرد: backup.sh -v -o /backups -k 7 /data. با دستور داخلی getopts گزینهها را یکییکی میخوانی، مقدار گزینه را از OPTARG برمیداری، با OPTIND و shift به بقیهی آرگومانها میرسی، یک پیام راهنما (-h) استاندارد مینویسی و ورودی را اعتبارسنجی میکنی تا اسکریپت با ورودی غلط، پیام روشن و کد خروج درست بدهد. گزینههای بلند مثل --help را هم با یک حلقهی ساده پشتیبانی میکنی.
مسئله: آرگومانهای بینام
Section titled “مسئله: آرگومانهای بینام”اسکریپت بکاپی که در درسهای قبل نوشتیم کمکم گزینههای زیادی گرفته است. اگر همه را آرگومان جایگاهی ($1، $2، …) بگیریم، صدا زدنش اینطور میشود:
./backup.sh /data /backups 7 yes no7 چیست؟ روز نگهداری؟ سطح فشردهسازی؟ yes و no کداماند؟ ترتیب را هم نمیشود عوض کرد و اگر فقط بخواهی گزینهی چهارم را بدهی، باید سه تای قبلی را هم بنویسی. ابزارهای واقعی این مشکل را با گزینههای اسمدار حل کردهاند:
./backup.sh -v -o /backups -k 7 /dataهر گزینه اسم دارد، ترتیبشان مهم نیست، هر کدام را که لازم نداری نمینویسی و -h میگوید اسکریپت چه میپذیرد.
تشبیه: فرم سفارش رستوران
Section titled “تشبیه: فرم سفارش رستوران”آرگومانهای جایگاهی مثل این است که سفارش را بدون توضیح پشت هم بگویی: «دو، بزرگ، بله، نه». گزینهها مثل فرم سفارش است: چند تیک (فلگهایی مثل -v که فقط روشن یا خاموشاند) و چند فیلد که باید پر شوند (گزینههایی مثل -o /backups که مقدار میخواهند). فرم را به هر ترتیبی پر کنی، آشپز میفهمد. getopts همان کسی است که فرم را میخواند و به آشپز (اسکریپت تو) تحویل میدهد.
ساختار خط فرمان
Section titled “ساختار خط فرمان”قراردادهای ابزارهای لینوکس (POSIX) برای خط فرمان اینها هستند:
| قسمت | نمونه | توضیح |
|---|---|---|
| فلگ (گزینهی بدون مقدار) | -v |
فقط روشن یا خاموش |
| گزینه با مقدار | -o /backups یا -o/backups |
مقدار بعد از گزینه، با فاصله یا چسبیده |
| ترکیب فلگها | -vn |
یعنی -v -n؛ آخرین حرف میتواند مقدار بگیرد: -vo /backups |
| پایان گزینهها | -- |
هر چه بعد از آن بیاید، گزینه حساب نمیشود |
| عملوند (operand) | /data |
آرگومانهای عادی، بعد از گزینهها |
getopts یک دستور داخلی Bash (و همهی shellهای POSIX) است که دقیقاً همین قواعد را پیاده میکند. شکل استفاده:
while getopts "رشتهی-گزینهها" opt; do case $opt in ...) ... ;; esacdoneshift $((OPTIND - 1))رشتهی گزینهها (optstring) فهرست حرفهای مجاز است؛ هر حرفی که بعدش : بیاید، مقدار میخواهد. مثلاً "hvo:k:" یعنی -h و -v فلگاند و -o و -k مقدار میگیرند. هر بار که حلقه میچرخد، getopts گزینهی بعدی را در متغیر opt میگذارد، مقدارش را (اگر داشت) در OPTARG، و شمارهی آرگومان بعدی را در OPTIND. وقتی گزینهها تمام شوند، با کد غیرصفر برمیگردد و حلقه تمام میشود.
مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: اولین فلگها
Section titled “مثال ۱: اولین فلگها”سادهترین حالت: دو فلگ بدون مقدار، -h برای راهنما و -v برای «پرحرف (verbose)»:
#!/usr/bin/env bash# hello.sh: two simple flags with getoptsverbose=false
while getopts "hv" opt; do case $opt in h) echo "usage: hello.sh [-h] [-v]"; exit 0 ;; v) verbose=true ;; *) exit 2 ;; esacdone
echo "Hello!"if [[ $verbose == true ]]; then echo "(verbose mode: running as $USER on $(hostname))"ficd ~/bashlabchmod +x hello.sh./hello.shecho "---"./hello.sh -vecho "---"./hello.sh -hecho "---"./hello.sh -x; echo "کد خروج: $?"Hello!---Hello!(verbose mode: running as ali on pc)---usage: hello.sh [-h] [-v]---./hello.sh: illegal option -- xکد خروج: 2- مقدار پیشفرض (
verbose=false) را قبل از حلقه میگذاریم؛ گزینه فقط وقتی داده شود عوضش میکند. - برای گزینهی ناشناخته (
-x)،getoptsخودش پیام خطا داد (illegal option -- x) و درoptعلامت?گذاشت که شاخهی*)آن را گرفت و با کد ۲ (استفادهی غلط) بیرون رفتیم.
مثال ۲: گزینهی دارای مقدار و OPTARG
Section titled “مثال ۲: گزینهی دارای مقدار و OPTARG”حرفی که بعدش : بیاید، مقدار میخواهد و مقدار در OPTARG است:
#!/usr/bin/env bash# opts.sh: show what getopts sees for each optionwhile getopts "vo:k:" opt; do echo "opt=$opt OPTARG=${OPTARG:-(none)} OPTIND=$OPTIND"donecd ~/bashlabchmod +x opts.shecho '=== -v -o /backups -k 7'./opts.sh -v -o /backups -k 7echo '=== -o/backups (چسبیده)'./opts.sh -o/backupsecho '=== -vk 7 (ترکیب فلگ و گزینهی مقداردار)'./opts.sh -vk 7echo '=== -o بدون مقدار'./opts.sh -o=== -v -o /backups -k 7opt=v OPTARG=(none) OPTIND=2opt=o OPTARG=/backups OPTIND=4opt=k OPTARG=7 OPTIND=6=== -o/backups (چسبیده)opt=o OPTARG=/backups OPTIND=2=== -vk 7 (ترکیب فلگ و گزینهی مقداردار)opt=v OPTARG=(none) OPTIND=1opt=k OPTARG=7 OPTIND=3=== -o بدون مقدار./opts.sh: option requires an argument -- oopt=? OPTARG=(none) OPTIND=2همهی شکلهای استاندارد کار کردند: با فاصله، چسبیده (-o/backups)، و ترکیبی (-vk 7، که آخرین حرف مقدار گرفت). به OPTIND نگاه کن: بعد از -v شد ۲، بعد از -o /backups شد ۴ (چون مقدار جدا هم یک آرگومان خورد). برای -o بدون مقدار، getopts خودش خطا داد و opt را ? کرد.
مثال ۳: OPTIND، shift و عملوندها
Section titled “مثال ۳: OPTIND، shift و عملوندها”بعد از گزینهها معمولاً عملوند (مثل پوشهی مبدأ) میآید. getopts به اولین آرگومانی که با - شروع نشود میرسد و میایستد. در آن لحظه OPTIND شمارهی همان آرگومان است، پس shift $((OPTIND - 1)) همهی گزینههای خواندهشده را کنار میگذارد و فقط عملوندها در $@ میمانند:
#!/usr/bin/env bash# operands.sh: options first, then operands in "$@"verbose=falseoutput=.while getopts "vo:" opt; do case $opt in v) verbose=true ;; o) output=$OPTARG ;; *) exit 2 ;; esacdoneshift $((OPTIND - 1))
echo "verbose=$verbose output=$output"echo "operands ($#):"for arg in "$@"; do echo " [$arg]"donecd ~/bashlabchmod +x operands.shecho '=== گزینهها و بعد دو عملوند'./operands.sh -v -o /backups /data "/home/ali/my docs"echo '=== گزینه بعد از عملوند: دیگر گزینه نیست'./operands.sh /data -vecho '=== -- پایان گزینهها: فایلی که اسمش با - شروع میشود'./operands.sh -v -- -report.txt=== گزینهها و بعد دو عملوندverbose=true output=/backupsoperands (2): [/data] [/home/ali/my docs]=== گزینه بعد از عملوند: دیگر گزینه نیستverbose=false output=.operands (2): [/data] [-v]=== -- پایان گزینهها: فایلی که اسمش با - شروع میشودverbose=true output=.operands (1): [-report.txt]- در اجرای اول، دو عملوند (حتی اسم دارای فاصله) سالم در
"$@"ماندند. - در اجرای دوم،
getoptsبه/dataرسید و ایستاد؛ پس-vبعد از آن یک عملوند است، نه گزینه. (برخلاف خیلی از ابزارهای GNU مثلlsکه گزینهها را هر جا باشند میخوانند؛getoptsاز قاعدهی POSIX پیروی میکند.) --بهgetoptsمیگوید «گزینهها تمام شد». برای اسمی که با-شروع میشود لازم است (همان کاری کهrm -- -fileمیکند).
مثال ۴: پیام خطای سفارشی با حالت بیصدا
Section titled “مثال ۴: پیام خطای سفارشی با حالت بیصدا”پیامهای پیشفرض getopts (illegal option -- x) کمی خشکاند. اگر رشتهی گزینهها با : شروع شود (":hvo:")، getopts ساکت میشود و دو حالت خطا را به تو میسپارد:
- گزینهی ناشناخته:
optمیشود?و خودِ حرف اشتباه درOPTARGاست. - گزینهی بدون مقدار:
optمیشود:و حرف گزینه درOPTARGاست.
#!/usr/bin/env bash# quiet.sh: custom error messages with a leading ':' in the optstringwhile getopts ":hvo:" opt; do case $opt in h) echo "usage: quiet.sh [-h] [-v] [-o DIR]"; exit 0 ;; v) echo "verbose on" ;; o) echo "output: $OPTARG" ;; \?) echo "quiet.sh: unknown option '-$OPTARG' (try -h)" >&2; exit 2 ;; :) echo "quiet.sh: option '-$OPTARG' needs a value, e.g. -$OPTARG /backups" >&2; exit 2 ;; esacdonecd ~/bashlabchmod +x quiet.sh./quiet.sh -v -o /tmp./quiet.sh -x; echo "کد خروج: $?"./quiet.sh -v -o; echo "کد خروج: $?"verbose onoutput: /tmpquiet.sh: unknown option '-x' (try -h)کد خروج: 2verbose onquiet.sh: option '-o' needs a value, e.g. -o /backupsکد خروج: 2دقت کن در case، علامت ? را باید \? بنویسی؛ ? تنها در الگوی case یعنی «هر یک کاراکتر» و با همهی گزینهها جور میشود.
مثال ۵: تابع usage و پیام راهنمای استاندارد
Section titled “مثال ۵: تابع usage و پیام راهنمای استاندارد”هر ابزار خوب با -h توضیح کوتاهی از خودش میدهد. قرارداد رایج:
- خط Usage با شکل کلی (گزینههای اختیاری داخل
[ ]، عملوندها با حروف بزرگ). - فهرست گزینهها با توضیح و مقدار پیشفرض.
- با
-h: راهنما روی stdout و کد ۰ (درخواست کاربر بوده). با ورودی غلط: راهنما (یا یک خط از آن) روی stderr و کد ۲.
#!/usr/bin/env bash# usage-demo.sh: a standard -h help message with a here-docset -euo pipefail
prog=$(basename "$0")
usage() { cat <<EOFUsage: $prog [-h] [-v] [-o DIR] [-k DAYS] SOURCE...
Archive one or more SOURCE directories into DIR.
Options: -o DIR where to write archives (default: ./backups) -k DAYS delete archives older than DAYS days (default: 7) -v print every step -h show this help and exit
Exit codes: 0 ok, 2 usage errorEOF}
while getopts ":hvo:k:" opt; do case $opt in h) usage; exit 0 ;; v|o|k) ;; \?) echo "$prog: unknown option -$OPTARG" >&2; usage >&2; exit 2 ;; :) echo "$prog: -$OPTARG needs a value" >&2; exit 2 ;; esacdoneshift $((OPTIND - 1))if [[ $# -eq 0 ]]; then echo "$prog: at least one SOURCE is required (see -h)" >&2 exit 2fiecho "would back up: $*"cd ~/bashlabchmod +x usage-demo.sh./usage-demo.sh -h; echo "کد خروج: $?"echo "==="./usage-demo.sh; echo "کد خروج: $?"echo "==="./usage-demo.sh -h > /dev/null && echo "help went to stdout (hidden), exit 0"./usage-demo.sh -z 2> /dev/null || echo "error went to stderr (hidden), exit $?"Usage: usage-demo.sh [-h] [-v] [-o DIR] [-k DAYS] SOURCE...
Archive one or more SOURCE directories into DIR.
Options: -o DIR where to write archives (default: ./backups) -k DAYS delete archives older than DAYS days (default: 7) -v print every step -h show this help and exit
Exit codes: 0 ok, 2 usage errorکد خروج: 0===usage-demo.sh: at least one SOURCE is required (see -h)کد خروج: 2===help went to stdout (hidden), exit 0error went to stderr (hidden), exit 2$prog اسم واقعی اسکریپت است (حتی اگر تغییر نامش بدهی). here-doc بدون کوتیشن، $prog را باز کرد. آخرین دو خط ثابت میکنند راهنمای -h روی stdout است (با > /dev/null پنهان شد) و پیام خطا روی stderr (با 2> /dev/null پنهان شد)؛ پس کسی که خروجی را در فایل میریزد، پیام خطا را قاطی دادهاش نمیکند.
مثال ۶: اعتبارسنجی ورودی
Section titled “مثال ۶: اعتبارسنجی ورودی”getopts فقط شکل را بررسی میکند، نه معنی را: -k abc را با کمال میل قبول میکند. اعتبارسنجی کار توست و بهترین جایش بلافاصله بعد از حلقه، قبل از هر کار واقعی است:
#!/usr/bin/env bash# validate-opts.sh: validate option values before doing any workset -euo pipefailprog=$(basename "$0")
die_usage() { echo "$prog: $*" >&2 echo "try '$prog -h' for help" >&2 exit 2}
output=./backupskeep=7mode=""
while getopts ":ho:k:m:" opt; do case $opt in h) echo "Usage: $prog [-o DIR] [-k DAYS] -m full|incr SOURCE"; exit 0 ;; o) output=$OPTARG ;; k) keep=$OPTARG ;; m) mode=$OPTARG ;; \?) die_usage "unknown option -$OPTARG" ;; :) die_usage "-$OPTARG needs a value" ;; esacdoneshift $((OPTIND - 1))
# 1) numbers: only digits, and in a sensible range[[ $keep =~ ^[0-9]+$ ]] || die_usage "-k must be a whole number of days, got '$keep'"(( keep >= 1 && keep <= 365 )) || die_usage "-k must be between 1 and 365, got $keep"# 2) required option and allowed values[[ -n $mode ]] || die_usage "-m is required"case $mode in full|incr) ;; *) die_usage "-m must be 'full' or 'incr', got '$mode'" ;;esac# 3) exactly one operand, and it must exist(( $# == 1 )) || die_usage "expected exactly one SOURCE, got $#"[[ -d $1 ]] || die_usage "SOURCE '$1' is not a directory"
echo "OK: mode=$mode source=$1 output=$output keep=$keep days"cd ~/bashlabchmod +x validate-opts.shfor args in "-k abc -m full /etc" "-k 900 -m full /etc" "-m weekly /etc" "/etc" "-m full" "-m full /nope" "-m full -k 14 /etc"; do echo "=== ./validate-opts.sh $args" ./validate-opts.sh $args; echo " (کد خروج: $?)"done=== ./validate-opts.sh -k abc -m full /etcvalidate-opts.sh: -k must be a whole number of days, got 'abc'try 'validate-opts.sh -h' for help (کد خروج: 2)=== ./validate-opts.sh -k 900 -m full /etcvalidate-opts.sh: -k must be between 1 and 365, got 900try 'validate-opts.sh -h' for help (کد خروج: 2)=== ./validate-opts.sh -m weekly /etcvalidate-opts.sh: -m must be 'full' or 'incr', got 'weekly'try 'validate-opts.sh -h' for help (کد خروج: 2)=== ./validate-opts.sh /etcvalidate-opts.sh: -m is requiredtry 'validate-opts.sh -h' for help (کد خروج: 2)=== ./validate-opts.sh -m fullvalidate-opts.sh: expected exactly one SOURCE, got 0try 'validate-opts.sh -h' for help (کد خروج: 2)=== ./validate-opts.sh -m full /nopevalidate-opts.sh: SOURCE '/nope' is not a directorytry 'validate-opts.sh -h' for help (کد خروج: 2)=== ./validate-opts.sh -m full -k 14 /etcOK: mode=full source=/etc output=./backups keep=14 days (کد خروج: 0)ترتیب بررسیها: اول نوع (فقط رقم؟)، بعد بازه، بعد گزینههای اجباری و مقدارهای مجاز (با case)، و آخر عملوندها. همه قبل از اینکه حتی یک فایل ساخته شود. ($args را باز هم عمداً بدون کوتیشن نوشتیم تا رشته به چند آرگومان بشکند؛ فقط در این حلقهی آزمایشی.)
مثال ۷: گزینههای بلند (--help) با حلقهی دستی
Section titled “مثال ۷: گزینههای بلند (--help) با حلقهی دستی”getopts فقط گزینههای یکحرفی را میفهمد. برای --help یا --output=DIR، رایجترین راه یک حلقهی سادهی while و case روی $1 است:
#!/usr/bin/env bash# long.sh: short and long options with a manual loopset -euo pipefailverbose=falseoutput=./backups
while (( $# > 0 )); do case $1 in -h|--help) echo "Usage: long.sh [-v|--verbose] [-o DIR|--output DIR|--output=DIR] SOURCE"; exit 0 ;; -v|--verbose) verbose=true ;; -o|--output) [[ $# -ge 2 ]] || { echo "long.sh: $1 needs a value" >&2; exit 2; } output=$2 shift ;; --output=*) output=${1#*=} ;; --) shift; break ;; -*) echo "long.sh: unknown option $1" >&2; exit 2 ;; *) break ;; esac shiftdone
echo "verbose=$verbose output=$output operands: $*"cd ~/bashlabchmod +x long.sh./long.sh --verbose --output /mnt/backup /data./long.sh -v --output=/mnt/backup /data /etc./long.sh -o; echo "کد خروج: $?"./long.sh --colour /data; echo "کد خروج: $?"./long.sh -- --weird-nameverbose=true output=/mnt/backup operands: /dataverbose=true output=/mnt/backup operands: /data /etclong.sh: -o needs a valueکد خروج: 2long.sh: unknown option --colourکد خروج: 2verbose=false output=./backups operands: --weird-name- هر دور،
$1بررسی و در پایان باshiftکنار گذاشته میشود. گزینهای که مقدار دارد (-o DIR) یکshiftاضافه میزند تا مقدارش هم برود. --output=*با گسترش پارامتر${1#*=}(درس کار با رشتهها) هر چه بعد از=است را برمیدارد.*)اولین عملوند را میبیند و باbreakحلقه را تمام میکند؛ عملوندها در"$@"میمانند.- این حلقه ترکیب فلگها (
-vo DIR) را نمیفهمد. اگر هر دو را میخواهی، ابزارgetopt(بدون s، از بستهی util-linux) هر دو را پشتیبانی میکند، ولی فقط روی لینوکس با همین شکل کار میکند (روی macOS نسخهی قدیمی دیگری است). برای اسکریپتهای کوچک،getoptsیا حلقهی دستی کافی و قابلحمل است.
پشت پرده: getopts حالتش را کجا نگه میدارد؟
Section titled “پشت پرده: getopts حالتش را کجا نگه میدارد؟”getopts یک دستور داخلی (builtin) شل است، نه یک برنامهی جدا، و تنها حافظهاش متغیر OPTIND است (که شل اول کار روی ۱ میگذارد). هر بار صدا زده میشود، از آرگومان شمارهی OPTIND ادامه میدهد:
type getoptsecho "OPTIND at start: $OPTIND"set -- -a -b filegetopts "ab" o; echo "1st call: o=$o OPTIND=$OPTIND"getopts "ab" o; echo "2nd call: o=$o OPTIND=$OPTIND"getopts "ab" o; echo "3rd call: status=$? o=$o OPTIND=$OPTIND (stopped at 'file')"getopts is a shell builtinOPTIND at start: 11st call: o=a OPTIND=22nd call: o=b OPTIND=33rd call: status=1 o=? OPTIND=3 (stopped at 'file')set -- آرگومانهای جایگاهی شل فعلی را عوض میکند (اینجا برای شبیهسازی -a -b file). در فراخوانی سوم، getopts به file رسید، کد ۱ برگرداند و opt را ? گذاشت؛ همین کد ۱ است که حلقهی while را تمام میکند. OPTIND=3 یعنی «اولین عملوند، آرگومان سوم است»، برای همین shift $((OPTIND - 1)) دو تا را کنار میگذارد.
این حافظهی سراسری یک دام دارد: اگر getopts را داخل تابع به کار ببری و تابع را دو بار صدا بزنی، بار دوم از جایی که بار اول تمام شد ادامه میدهد:
parse() { local opt while getopts "v" opt; do echo " got -$opt"; done}parse_fixed() { local opt OPTIND=1 while getopts "v" opt; do echo " got -$opt"; done}echo "parse (بدون reset):"parse -vparse -vecho "parse_fixed (local OPTIND=1):"parse_fixed -vparse_fixed -vparse (بدون reset):parse_fixed (local OPTIND=1): got -v got -vبار دوم parse هیچ گزینهای ندید، چون OPTIND از بار قبل ۲ مانده بود. راهحل: داخل تابع local OPTIND=1 (یا قبل از هر بار، OPTIND=1).
جدولهای مرجع
Section titled “جدولهای مرجع”نحو رشتهی گزینهها:
| رشته | معنی |
|---|---|
"hv" |
-h و -v فلگ بدون مقدار |
"o:" |
-o مقدار میخواهد (-o DIR یا -oDIR) |
":hvo:" |
: اول: حالت بیصدا، پیام خطا با خودت |
| در حالت عادی، خطا | opt = ?، پیام را getopts چاپ میکند، OPTARG خالی |
| در حالت بیصدا، گزینهی ناشناخته | opt = ? و OPTARG = حرف ناشناخته |
| در حالت بیصدا، مقدار جاافتاده | opt = : و OPTARG = حرف گزینه |
متغیرهای getopts:
| متغیر | معنی |
|---|---|
opt (اسم دلخواه) |
حرف گزینهی فعلی (یا ? / :) |
OPTARG |
مقدار گزینه، یا در حالت بیصدا حرف مشکلدار |
OPTIND |
شمارهی آرگومان بعدی که باید خوانده شود؛ بعد از حلقه: shift $((OPTIND - 1)) |
OPTERR |
اگر ۰ باشد، پیامهای خطای getopts چاپ نمیشوند (معادل : اول رشته) |
سه راه خواندن گزینهها:
| روش | گزینهی کوتاه | ترکیب -vo |
گزینهی بلند --help |
قابلحمل |
|---|---|---|---|---|
getopts |
✅ | ✅ | ❌ | ✅ همهی shellها |
حلقهی دستی while/case |
✅ | ❌ (مگر خودت بنویسی) | ✅ | ✅ |
getopt (util-linux) |
✅ | ✅ | ✅ | ❌ فقط لینوکس |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) فراموش کردن shift $((OPTIND - 1))
Section titled “۱) فراموش کردن shift $((OPTIND - 1))”cd ~/bashlabcat > noshift.sh <<'EOF'#!/usr/bin/env bashwhile getopts "v" opt; do :; doneecho "first operand is: $1"EOFbash noshift.sh -v /datafirst operand is: -vبدون shift، $1 هنوز -v است. راهحل: بلافاصله بعد از حلقه shift $((OPTIND - 1)).
۲) جاانداختن : برای گزینهی مقداردار
Section titled “۲) جاانداختن : برای گزینهی مقداردار”cd ~/bashlabcat > nocolon.sh <<'EOF'#!/usr/bin/env bashout=defaultwhile getopts "o" opt; do case $opt in o) out=$OPTARG ;; esacdoneshift $((OPTIND - 1))echo "out=[$out] operands=[$*]"EOFbash nocolon.sh -o /backups /dataout=[] operands=[/backups /data]بدون :، -o فلگ حساب شد، OPTARG خالی ماند و /backups بهعنوان عملوند رفت. راهحل: "o:".
۳) گزینهی مقدارداری که گزینهی بعدی را میبلعد
Section titled “۳) گزینهی مقدارداری که گزینهی بعدی را میبلعد”cd ~/bashlab./operands.sh -o -v /dataverbose=false output=-voperands (1): [/data]کاربر مقدار -o را فراموش کرد و getopts با خیال راحت -v را مقدار -o گرفت. راهحل: در اعتبارسنجی، مقداری که با - شروع میشود را مشکوک بدان: [[ $output == -* ]] && die_usage "-o needs a directory".
۴) ? بدون بکاسلش در case
Section titled “۴) ? بدون بکاسلش در case”cd ~/bashlabcat > noescape.sh <<'EOF'#!/usr/bin/env bashwhile getopts ":hv" opt; do case $opt in ?) echo "unknown option" ;; h) echo "help" ;; v) echo "verbose" ;; esacdoneEOFbash noescape.sh -h -vunknown optionunknown option-h و -v معتبر بودند ولی هر دو «unknown» گزارش شدند، چون الگوی ?) با هر حرفی جور میشود و اول آمده است. راهحل: \?).
۵) گزینه بعد از عملوند
Section titled “۵) گزینه بعد از عملوند”مثال ۳: ./operands.sh /data -v گزینه را عملوند میبیند. راهحل: در راهنما بنویس «گزینهها قبل از عملوندها»، یا اگر واقعاً لازم است، حلقهی دستی بنویس که عملوندها را جمع کند و ادامه دهد.
۶) getopts در تابع بدون local OPTIND=1
Section titled “۶) getopts در تابع بدون local OPTIND=1”پشت پرده: بار دوم هیچ گزینهای خوانده نمیشود. راهحل: local OPTIND=1.
اسکریپت greet.sh بنویس که با -n NAME اسم بگیرد (پیشفرض World)، با -u پیام را با حروف بزرگ چاپ کند و با -h راهنما بدهد. گزینهی ناشناخته پیام و کد ۲ بدهد.
دیدن جواب
cd ~/bashlabcat > greet.sh <<'EOF'#!/usr/bin/env bash# greet.sh: greet.sh [-h] [-u] [-n NAME]set -euo pipefailname=Worldupper=falsewhile getopts ":hun:" opt; do case $opt in h) echo "usage: greet.sh [-h] [-u] [-n NAME]"; exit 0 ;; u) upper=true ;; n) name=$OPTARG ;; \?) echo "greet.sh: unknown option -$OPTARG" >&2; exit 2 ;; :) echo "greet.sh: -$OPTARG needs a value" >&2; exit 2 ;; esacdonemsg="Hello, $name!"if [[ $upper == true ]]; then msg=${msg^^}fiecho "$msg"EOFchmod +x greet.sh./greet.sh./greet.sh -n Sara./greet.sh -un "Ali Rezaei"./greet.sh -q; echo "کد خروج: $?"Hello, World!Hello, Sara!HELLO, ALI REZAEI!greet.sh: unknown option -qکد خروج: 2${msg^^} (درس رشتهها) کل رشته را بزرگ میکند. -un "Ali Rezaei" ترکیب فلگ و گزینهی مقدارداری است که مقدارش فاصله دارد؛ چون کوتیشن دارد، یک آرگومان است.
تمرین اصلی درس: به اسکریپت بکاپ، فلگهای -h (راهنما)، -o DIR (پوشهی مقصد؛ پیشفرض ./backups) و -v (چاپ هر قدم) اضافه کن. عملوندها پوشههایی هستند که باید بکاپ گرفته شوند (حداقل یکی). ورودی را اعتبارسنجی کن (مبدأها وجود داشته باشند، مقدار -o با - شروع نشود) و برای هر مبدأ یک آرشیو tar.gz بساز.
دیدن جواب
cd ~/bashlabcat > backup.sh <<'EOF'#!/usr/bin/env bash# backup.sh: archive directories, with -h, -o and -vset -euo pipefailprog=$(basename "$0")
usage() { cat <<USAGEUsage: $prog [-h] [-v] [-o DIR] SOURCE...
Create a .tar.gz archive for every SOURCE directory.
Options: -o DIR where to put the archives (default: ./backups) -v print every step -h show this help and exitUSAGE}die_usage() { echo "$prog: $*" >&2; echo "try '$prog -h'" >&2; exit 2; }
output=./backupsverbose=falselog() { if [[ $verbose == true ]]; then echo "[$(date +%T)] $*"; fi; }
while getopts ":hvo:" opt; do case $opt in h) usage; exit 0 ;; v) verbose=true ;; o) output=$OPTARG ;; \?) die_usage "unknown option -$OPTARG" ;; :) die_usage "-$OPTARG needs a value" ;; esacdoneshift $((OPTIND - 1))
[[ $output != -* ]] || die_usage "-o needs a directory, got '$output'"(( $# >= 1 )) || die_usage "at least one SOURCE is required"for src in "$@"; do [[ -d $src ]] || die_usage "SOURCE '$src' is not a directory"done
mkdir -p "$output"log "writing archives to $output"stamp=$(date +%F)for src in "$@"; do name="$(basename "$src")-$stamp.tar.gz" log "archiving $src -> $output/$name" tar -czf "$output/$name" -C "$(dirname "$src")" "$(basename "$src")"doneecho "$prog: $# archive(s) written to $output"EOFchmod +x backup.shmkdir -p data/site data/dbecho "<h1>hi</h1>" > data/site/index.htmlecho "dump" > data/db/dump.sql./backup.sh -hecho "==="./backup.sh -v -o /tmp/lx-bk data/site data/dbls /tmp/lx-bkecho "==="./backup.sh -o -v data/site; echo "کد خروج: $?"./backup.sh -o /tmp/lx-bk data/nothing; echo "کد خروج: $?"./backup.sh data/siterm -rf /tmp/lx-bk backupsUsage: backup.sh [-h] [-v] [-o DIR] SOURCE...
Create a .tar.gz archive for every SOURCE directory.
Options: -o DIR where to put the archives (default: ./backups) -v print every step -h show this help and exit===[11:45:48] writing archives to /tmp/lx-bk[11:45:48] archiving data/site -> /tmp/lx-bk/site-2026-10-04.tar.gz[11:45:48] archiving data/db -> /tmp/lx-bk/db-2026-10-04.tar.gzbackup.sh: 2 archive(s) written to /tmp/lx-bkdb-2026-10-04.tar.gzsite-2026-10-04.tar.gz===backup.sh: -o needs a directory, got '-v'try 'backup.sh -h'کد خروج: 2backup.sh: SOURCE 'data/nothing' is not a directorytry 'backup.sh -h'کد خروج: 2backup.sh: 1 archive(s) written to ./backupsساختار، همان الگوی این درس است: پیشفرضها، حلقهی getopts با حالت بیصدا، shift، اعتبارسنجی همهی ورودیها قبل از شروع کار، و بعد خودِ کار. تابع log فقط وقتی -v داده شده چاپ میکند؛ پس بدون -v فقط خط خلاصه دیده میشود.
اسکریپت clean-logs.sh بنویس که هم گزینهی کوتاه و هم بلند بپذیرد: -d DAYS یا --days DAYS یا --days=DAYS (پیشفرض ۷)، -n یا --dry-run (فقط چاپ کن چه چیزی پاک میشد)، -h یا --help؛ و یک عملوند: پوشهی لاگها. فایلهای *.log قدیمیتر از DAYS روز را پاک کند (با find -mtime). روی پوشهای امتحان کن که فایلهای قدیمی و جدید دارد (تاریخ فایلها را با touch -d عقب ببر) و اول با --dry-run اجرا کن.
دیدن جواب
cd ~/bashlabcat > clean-logs.sh <<'EOF'#!/usr/bin/env bash# clean-logs.sh: delete *.log files older than N daysset -euo pipefailprog=$(basename "$0")die_usage() { echo "$prog: $*" >&2; exit 2; }
days=7dry_run=falsewhile (( $# > 0 )); do case $1 in -h|--help) echo "Usage: $prog [-n|--dry-run] [-d DAYS|--days DAYS] LOGDIR"; exit 0 ;; -n|--dry-run) dry_run=true ;; -d|--days) (( $# >= 2 )) || die_usage "$1 needs a value" days=$2 shift ;; --days=*) days=${1#*=} ;; --) shift; break ;; -*) die_usage "unknown option $1" ;; *) break ;; esac shiftdone
[[ $days =~ ^[0-9]+$ ]] || die_usage "DAYS must be a number, got '$days'"(( $# == 1 )) || die_usage "expected one LOGDIR"[[ -d $1 ]] || die_usage "'$1' is not a directory"
if [[ $dry_run == true ]]; then echo "dry run: these files would be deleted:" find "$1" -name '*.log' -type f -mtime +"$days" -print | sortelse count=$(find "$1" -name '*.log' -type f -mtime +"$days" -print -delete | wc -l) echo "deleted $count file(s) older than $days days"fiEOFchmod +x clean-logs.shmkdir -p logsfor d in 1 5 10 30; do touch -d "$d days ago" "logs/app-$d.log"donetouch -d "30 days ago" logs/keep.txtls logsecho "=== --dry-run با پیشفرض ۷ روز:"./clean-logs.sh --dry-run logsecho "=== --days=3 -n:"./clean-logs.sh --days=3 -n logsecho "=== اجرای واقعی با -d 7:"./clean-logs.sh -d 7 logsls logs./clean-logs.sh --days abc logs; echo "کد خروج: $?"app-1.logapp-10.logapp-30.logapp-5.logkeep.txt=== --dry-run با پیشفرض ۷ روز:dry run: these files would be deleted:logs/app-10.loglogs/app-30.log=== --days=3 -n:dry run: these files would be deleted:logs/app-10.loglogs/app-30.loglogs/app-5.log=== اجرای واقعی با -d 7:deleted 2 file(s) older than 7 daysapp-1.logapp-5.logkeep.txtclean-logs.sh: DAYS must be a number, got 'abc'کد خروج: 2-mtime +7 یعنی «آخرین تغییر بیش از ۷ روز پیش» (درس پیدا کردن فایلها). --dry-run همان find را بدون -delete اجرا میکند؛ عادت خوبی است که هر اسکریپت پاککننده چنین حالتی داشته باشد. فایل keep.txt با اینکه قدیمی است، چون .log نیست، دستنخورده ماند.
آزمونک
Section titled “آزمونک”در رشتهی گزینههای getopts "vo:k" کدام گزینه مقدار میخواهد؟
فقط حرفی که بعدش : آمده (اینجا o) مقدار میگیرد؛ مقدار در OPTARG است.
بعد از حلقهی getopts، چرا shift $((OPTIND - 1)) میزنیم؟
OPTIND شمارهی اولین عملوند است؛ OPTIND-1 تعداد آرگومانهای گزینه است.
رشتهی گزینهها با : شروع شده (":ho:"). کاربر -x داده است. opt و OPTARG چه هستند؟
در حالت بیصدا، ? یعنی گزینهی ناشناخته و : یعنی مقدار جاافتاده؛ حرف مشکلدار در OPTARG است.
./script.sh /data -v با getopts چه میکند؟
getopts از قاعدهی POSIX پیروی میکند: گزینهها قبل از عملوندها.
چرا در case برای گزینهی ناشناخته باید \?) نوشت و نه ?)؟
بدون بکاسلش، اگر ?) اول بیاید همهی گزینهها را میگیرد.
کاربر ./backup.sh -o -v /data نوشته (مقدار -o را فراموش کرده). با "vo:" چه میشود؟
getopts آرگومان بعدی را بیقیدوشرط مقدار میداند؛ [[ $output == -* ]] را بررسی کن.
تابعی که داخلش getopts دارد، بار دوم که صدا زده میشود هیچ گزینهای نمیخواند. راهحل؟
OPTIND سراسری است و از بار قبل جلو رفته است.
جمعبندی
Section titled “جمعبندی”- گزینههای اسمدار (
-v،-o DIR) اسکریپت را خوانا، بیترتیب و قابلکشف (-h) میکنند. قراردادها: ترکیب-vo DIR، مقدار چسبیده-oDIR، پایان گزینهها با--، گزینهها قبل از عملوندها. - الگو: پیشفرضها ←
while getopts ":hvo:" opt; do case $opt in ... esac; done←shift $((OPTIND - 1))← اعتبارسنجی ← کار اصلی. :بعد از حرف یعنی مقدار میخواهد (درOPTARG).:اول رشته یعنی حالت بیصدا:\?)برای گزینهی ناشناخته و:)برای مقدار جاافتاده، با پیام خودت.- تابع
usageبا here-doc: با-hروی stdout و کد ۰؛ با خطا روی stderr و کد ۲. getoptsفقط شکل را میسنجد؛ نوع، بازه، اجباریبودن، مقدارهای مجاز و وجود فایل را خودت بسنج، قبل از هر کار واقعی.- گزینهی بلند (
--help،--output=DIR) با حلقهیwhile (( $# > 0 )); do case $1 in ...; esac; shift; done. getoptsیک builtin با حافظهیOPTINDاست؛ در تابعlocal OPTIND=1.
| دستور | کاری که میکند |
|---|---|
while getopts ":hvo:" opt; do case $opt in ... esac; done | حلقهی استاندارد خواندن گزینهها |
o) output=$OPTARG ;; | مقدار گزینهی دارای : |
\?) echo "unknown -$OPTARG" >&2; exit 2 ;; | گزینهی ناشناخته (حالت بیصدا) |
:) echo "-$OPTARG needs a value" >&2; exit 2 ;; | مقدار جاافتاده (حالت بیصدا) |
shift $((OPTIND - 1)) | کنار گذاشتن گزینهها؛ عملوندها در "$@" |
./script.sh -v -- -file | پایان گزینهها |
usage() { cat <<EOF ... EOF; } | پیام راهنما |
[[ $n =~ ^[0-9]+$ ]] || die_usage "..." | اعتبارسنجی عدد |
while (( $# > 0 )); do case $1 in --help) ...;; esac; shift; done | گزینههای بلند با حلقهی دستی |
--output=*) output=${1#*=} ;; | گزینهی بلند با = |
local OPTIND=1 | getopts داخل تابع |