رفتن به محتوا
LoopX

گزینه‌های خط فرمان با getopts

توی این درس یاد می‌گیری اسکریپتی بسازی که مثل ابزارهای واقعی لینوکس فلگ (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 no

7 چیست؟ روز نگهداری؟ سطح فشرده‌سازی؟ yes و no کدام‌اند؟ ترتیب را هم نمی‌شود عوض کرد و اگر فقط بخواهی گزینه‌ی چهارم را بدهی، باید سه تای قبلی را هم بنویسی. ابزارهای واقعی این مشکل را با گزینه‌های اسم‌دار حل کرده‌اند:

خواناتر
./backup.sh -v -o /backups -k 7 /data

هر گزینه اسم دارد، ترتیبشان مهم نیست، هر کدام را که لازم نداری نمی‌نویسی و -h می‌گوید اسکریپت چه می‌پذیرد.

تشبیه: فرم سفارش رستوران

Section titled “تشبیه: فرم سفارش رستوران”

آرگومان‌های جایگاهی مثل این است که سفارش را بدون توضیح پشت هم بگویی: «دو، بزرگ، بله، نه». گزینه‌ها مثل فرم سفارش است: چند تیک (فلگ‌هایی مثل -v که فقط روشن یا خاموش‌اند) و چند فیلد که باید پر شوند (گزینه‌هایی مثل -o /backups که مقدار می‌خواهند). فرم را به هر ترتیبی پر کنی، آشپز می‌فهمد. getopts همان کسی است که فرم را می‌خواند و به آشپز (اسکریپت تو) تحویل می‌دهد.

قراردادهای ابزارهای لینوکس (POSIX) برای خط فرمان این‌ها هستند:

قسمت نمونه توضیح
فلگ (گزینه‌ی بدون مقدار) -v فقط روشن یا خاموش
گزینه با مقدار -o /backups یا -o/backups مقدار بعد از گزینه، با فاصله یا چسبیده
ترکیب فلگ‌ها -vn یعنی -v -n؛ آخرین حرف می‌تواند مقدار بگیرد: -vo /backups
پایان گزینه‌ها -- هر چه بعد از آن بیاید، گزینه حساب نمی‌شود
عملوند (operand) /data آرگومان‌های عادی، بعد از گزینه‌ها

getopts یک دستور داخلی Bash (و همه‌ی shellهای POSIX) است که دقیقاً همین قواعد را پیاده می‌کند. شکل استفاده:

الگوی getopts
while getopts "رشته‌ی-گزینه‌ها" opt; do
case $opt in
...) ... ;;
esac
done
shift $((OPTIND - 1))

رشته‌ی گزینه‌ها (optstring) فهرست حرف‌های مجاز است؛ هر حرفی که بعدش : بیاید، مقدار می‌خواهد. مثلاً "hvo:k:" یعنی -h و -v فلگ‌اند و -o و -k مقدار می‌گیرند. هر بار که حلقه می‌چرخد، getopts گزینه‌ی بعدی را در متغیر opt می‌گذارد، مقدارش را (اگر داشت) در OPTARG، و شماره‌ی آرگومان بعدی را در OPTIND. وقتی گزینه‌ها تمام شوند، با کد غیرصفر برمی‌گردد و حلقه تمام می‌شود.

getopts در هر دور حلقه یک گزینه را می‌خواند (حرفش در opt و مقدارش در OPTARG)، case آن را پردازش می‌کند و OPTIND جلو می‌رود. وقتی به اولین آرگومانی رسید که گزینه نیست (یا به --)، حلقه تمام می‌شود و shift گزینه‌های خوانده‌شده را کنار می‌گذارد تا فقط عملوندها در $@ بمانند.

ساده‌ترین حالت: دو فلگ بدون مقدار، -h برای راهنما و -v برای «پرحرف (verbose)»:

/home/ali/bashlab/hello.sh
#!/usr/bin/env bash
# hello.sh: two simple flags with getopts
verbose=false
while getopts "hv" opt; do
case $opt in
h) echo "usage: hello.sh [-h] [-v]"; exit 0 ;;
v) verbose=true ;;
*) exit 2 ;;
esac
done
echo "Hello!"
if [[ $verbose == true ]]; then
echo "(verbose mode: running as $USER on $(hostname))"
fi
Terminal window
cd ~/bashlab
chmod +x hello.sh
./hello.sh
echo "---"
./hello.sh -v
echo "---"
./hello.sh -h
echo "---"
./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 است:

/home/ali/bashlab/opts.sh
#!/usr/bin/env bash
# opts.sh: show what getopts sees for each option
while getopts "vo:k:" opt; do
echo "opt=$opt OPTARG=${OPTARG:-(none)} OPTIND=$OPTIND"
done
Terminal window
cd ~/bashlab
chmod +x opts.sh
echo '=== -v -o /backups -k 7'
./opts.sh -v -o /backups -k 7
echo '=== -o/backups (چسبیده)'
./opts.sh -o/backups
echo '=== -vk 7 (ترکیب فلگ و گزینه‌ی مقداردار)'
./opts.sh -vk 7
echo '=== -o بدون مقدار'
./opts.sh -o
خروجی
=== -v -o /backups -k 7
opt=v OPTARG=(none) OPTIND=2
opt=o OPTARG=/backups OPTIND=4
opt=k OPTARG=7 OPTIND=6
=== -o/backups (چسبیده)
opt=o OPTARG=/backups OPTIND=2
=== -vk 7 (ترکیب فلگ و گزینه‌ی مقداردار)
opt=v OPTARG=(none) OPTIND=1
opt=k OPTARG=7 OPTIND=3
=== -o بدون مقدار
./opts.sh: option requires an argument -- o
opt=? 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)) همه‌ی گزینه‌های خوانده‌شده را کنار می‌گذارد و فقط عملوندها در $@ می‌مانند:

/home/ali/bashlab/operands.sh
#!/usr/bin/env bash
# operands.sh: options first, then operands in "$@"
verbose=false
output=.
while getopts "vo:" opt; do
case $opt in
v) verbose=true ;;
o) output=$OPTARG ;;
*) exit 2 ;;
esac
done
shift $((OPTIND - 1))
echo "verbose=$verbose output=$output"
echo "operands ($#):"
for arg in "$@"; do
echo " [$arg]"
done
Terminal window
cd ~/bashlab
chmod +x operands.sh
echo '=== گزینه‌ها و بعد دو عملوند'
./operands.sh -v -o /backups /data "/home/ali/my docs"
echo '=== گزینه بعد از عملوند: دیگر گزینه نیست'
./operands.sh /data -v
echo '=== -- پایان گزینه‌ها: فایلی که اسمش با - شروع می‌شود'
./operands.sh -v -- -report.txt
خروجی
=== گزینه‌ها و بعد دو عملوند
verbose=true output=/backups
operands (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 است.
/home/ali/bashlab/quiet.sh
#!/usr/bin/env bash
# quiet.sh: custom error messages with a leading ':' in the optstring
while 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 ;;
esac
done
Terminal window
cd ~/bashlab
chmod +x quiet.sh
./quiet.sh -v -o /tmp
./quiet.sh -x; echo "کد خروج: $?"
./quiet.sh -v -o; echo "کد خروج: $?"
خروجی
verbose on
output: /tmp
quiet.sh: unknown option '-x' (try -h)
کد خروج: 2
verbose on
quiet.sh: option '-o' needs a value, e.g. -o /backups
کد خروج: 2

دقت کن در case، علامت ? را باید \? بنویسی؛ ? تنها در الگوی case یعنی «هر یک کاراکتر» و با همه‌ی گزینه‌ها جور می‌شود.

مثال ۵: تابع usage و پیام راهنمای استاندارد

Section titled “مثال ۵: تابع usage و پیام راهنمای استاندارد”

هر ابزار خوب با -h توضیح کوتاهی از خودش می‌دهد. قرارداد رایج:

  • خط Usage با شکل کلی (گزینه‌های اختیاری داخل [ ]، عملوندها با حروف بزرگ).
  • فهرست گزینه‌ها با توضیح و مقدار پیش‌فرض.
  • با -h: راهنما روی stdout و کد ۰ (درخواست کاربر بوده). با ورودی غلط: راهنما (یا یک خط از آن) روی stderr و کد ۲.
/home/ali/bashlab/usage-demo.sh
#!/usr/bin/env bash
# usage-demo.sh: a standard -h help message with a here-doc
set -euo pipefail
prog=$(basename "$0")
usage() {
cat <<EOF
Usage: $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 error
EOF
}
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 ;;
esac
done
shift $((OPTIND - 1))
if [[ $# -eq 0 ]]; then
echo "$prog: at least one SOURCE is required (see -h)" >&2
exit 2
fi
echo "would back up: $*"
Terminal window
cd ~/bashlab
chmod +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 0
error went to stderr (hidden), exit 2

$prog اسم واقعی اسکریپت است (حتی اگر تغییر نامش بدهی). here-doc بدون کوتیشن، $prog را باز کرد. آخرین دو خط ثابت می‌کنند راهنمای -h روی stdout است (با > /dev/null پنهان شد) و پیام خطا روی stderr (با 2> /dev/null پنهان شد)؛ پس کسی که خروجی را در فایل می‌ریزد، پیام خطا را قاطی داده‌اش نمی‌کند.

مثال ۶: اعتبارسنجی ورودی

Section titled “مثال ۶: اعتبارسنجی ورودی”

getopts فقط شکل را بررسی می‌کند، نه معنی را: -k abc را با کمال میل قبول می‌کند. اعتبارسنجی کار توست و بهترین جایش بلافاصله بعد از حلقه، قبل از هر کار واقعی است:

/home/ali/bashlab/validate-opts.sh
#!/usr/bin/env bash
# validate-opts.sh: validate option values before doing any work
set -euo pipefail
prog=$(basename "$0")
die_usage() {
echo "$prog: $*" >&2
echo "try '$prog -h' for help" >&2
exit 2
}
output=./backups
keep=7
mode=""
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" ;;
esac
done
shift $((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"
Terminal window
cd ~/bashlab
chmod +x validate-opts.sh
for 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 /etc
validate-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 /etc
validate-opts.sh: -k must be between 1 and 365, got 900
try 'validate-opts.sh -h' for help
(کد خروج: 2)
=== ./validate-opts.sh -m weekly /etc
validate-opts.sh: -m must be 'full' or 'incr', got 'weekly'
try 'validate-opts.sh -h' for help
(کد خروج: 2)
=== ./validate-opts.sh /etc
validate-opts.sh: -m is required
try 'validate-opts.sh -h' for help
(کد خروج: 2)
=== ./validate-opts.sh -m full
validate-opts.sh: expected exactly one SOURCE, got 0
try 'validate-opts.sh -h' for help
(کد خروج: 2)
=== ./validate-opts.sh -m full /nope
validate-opts.sh: SOURCE '/nope' is not a directory
try 'validate-opts.sh -h' for help
(کد خروج: 2)
=== ./validate-opts.sh -m full -k 14 /etc
OK: mode=full source=/etc output=./backups keep=14 days
(کد خروج: 0)

ترتیب بررسی‌ها: اول نوع (فقط رقم؟)، بعد بازه، بعد گزینه‌های اجباری و مقدارهای مجاز (با case)، و آخر عملوندها. همه قبل از اینکه حتی یک فایل ساخته شود. ($args را باز هم عمداً بدون کوتیشن نوشتیم تا رشته به چند آرگومان بشکند؛ فقط در این حلقه‌ی آزمایشی.)

مثال ۷: گزینه‌های بلند (--help) با حلقه‌ی دستی

Section titled “مثال ۷: گزینه‌های بلند (--help) با حلقه‌ی دستی”

getopts فقط گزینه‌های یک‌حرفی را می‌فهمد. برای --help یا --output=DIR، رایج‌ترین راه یک حلقه‌ی ساده‌ی while و case روی $1 است:

/home/ali/bashlab/long.sh
#!/usr/bin/env bash
# long.sh: short and long options with a manual loop
set -euo pipefail
verbose=false
output=./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
shift
done
echo "verbose=$verbose output=$output operands: $*"
Terminal window
cd ~/bashlab
chmod +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-name
خروجی
verbose=true output=/mnt/backup operands: /data
verbose=true output=/mnt/backup operands: /data /etc
long.sh: -o needs a value
کد خروج: 2
long.sh: unknown option --colour
کد خروج: 2
verbose=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 ادامه می‌دهد:

Terminal window
type getopts
echo "OPTIND at start: $OPTIND"
set -- -a -b file
getopts "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 builtin
OPTIND at start: 1
1st call: o=a OPTIND=2
2nd call: o=b OPTIND=3
3rd call: status=1 o=? OPTIND=3 (stopped at 'file')

set -- آرگومان‌های جایگاهی شل فعلی را عوض می‌کند (اینجا برای شبیه‌سازی -a -b file). در فراخوانی سوم، getopts به file رسید، کد ۱ برگرداند و opt را ? گذاشت؛ همین کد ۱ است که حلقه‌ی while را تمام می‌کند. OPTIND=3 یعنی «اولین عملوند، آرگومان سوم است»، برای همین shift $((OPTIND - 1)) دو تا را کنار می‌گذارد.

این حافظه‌ی سراسری یک دام دارد: اگر getopts را داخل تابع به کار ببری و تابع را دو بار صدا بزنی، بار دوم از جایی که بار اول تمام شد ادامه می‌دهد:

Terminal window
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 -v
parse -v
echo "parse_fixed (local OPTIND=1):"
parse_fixed -v
parse_fixed -v
خروجی
parse (بدون reset):
parse_fixed (local OPTIND=1):
got -v
got -v

بار دوم parse هیچ گزینه‌ای ندید، چون OPTIND از بار قبل ۲ مانده بود. راه‌حل: داخل تابع local OPTIND=1 (یا قبل از هر بار، OPTIND=1).

نحو رشته‌ی گزینه‌ها:

رشته معنی
"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) ✅ ✅ ✅ ❌ فقط لینوکس

۱) فراموش کردن shift $((OPTIND - 1))

Section titled “۱) فراموش کردن shift $((OPTIND - 1))”
cd ~/bashlab
cat > noshift.sh <<'EOF'
#!/usr/bin/env bash
while getopts "v" opt; do :; done
echo "first operand is: $1"
EOF
bash noshift.sh -v /data
خروجی
first operand is: -v

بدون shift، $1 هنوز -v است. راه‌حل: بلافاصله بعد از حلقه shift $((OPTIND - 1)).

۲) جاانداختن : برای گزینه‌ی مقداردار

Section titled “۲) جاانداختن : برای گزینه‌ی مقداردار”
cd ~/bashlab
cat > nocolon.sh <<'EOF'
#!/usr/bin/env bash
out=default
while getopts "o" opt; do
case $opt in o) out=$OPTARG ;; esac
done
shift $((OPTIND - 1))
echo "out=[$out] operands=[$*]"
EOF
bash nocolon.sh -o /backups /data
خروجی
out=[] operands=[/backups /data]

بدون :، -o فلگ حساب شد، OPTARG خالی ماند و /backups به‌عنوان عملوند رفت. راه‌حل: "o:".

۳) گزینه‌ی مقدارداری که گزینه‌ی بعدی را می‌بلعد

Section titled “۳) گزینه‌ی مقدارداری که گزینه‌ی بعدی را می‌بلعد”
Terminal window
cd ~/bashlab
./operands.sh -o -v /data
خروجی
verbose=false output=-v
operands (1):
[/data]

کاربر مقدار -o را فراموش کرد و getopts با خیال راحت -v را مقدار -o گرفت. راه‌حل: در اعتبارسنجی، مقداری که با - شروع می‌شود را مشکوک بدان: [[ $output == -* ]] && die_usage "-o needs a directory".

cd ~/bashlab
cat > noescape.sh <<'EOF'
#!/usr/bin/env bash
while getopts ":hv" opt; do
case $opt in
?) echo "unknown option" ;;
h) echo "help" ;;
v) echo "verbose" ;;
esac
done
EOF
bash noescape.sh -h -v
خروجی
unknown option
unknown option

-h و -v معتبر بودند ولی هر دو «unknown» گزارش شدند، چون الگوی ?) با هر حرفی جور می‌شود و اول آمده است. راه‌حل: \?).

مثال ۳: ./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 ~/bashlab
cat > greet.sh <<'EOF'
#!/usr/bin/env bash
# greet.sh: greet.sh [-h] [-u] [-n NAME]
set -euo pipefail
name=World
upper=false
while 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 ;;
esac
done
msg="Hello, $name!"
if [[ $upper == true ]]; then
msg=${msg^^}
fi
echo "$msg"
EOF
chmod +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 ~/bashlab
cat > backup.sh <<'EOF'
#!/usr/bin/env bash
# backup.sh: archive directories, with -h, -o and -v
set -euo pipefail
prog=$(basename "$0")
usage() {
cat <<USAGE
Usage: $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 exit
USAGE
}
die_usage() { echo "$prog: $*" >&2; echo "try '$prog -h'" >&2; exit 2; }
output=./backups
verbose=false
log() { 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" ;;
esac
done
shift $((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")"
done
echo "$prog: $# archive(s) written to $output"
EOF
chmod +x backup.sh
mkdir -p data/site data/db
echo "<h1>hi</h1>" > data/site/index.html
echo "dump" > data/db/dump.sql
./backup.sh -h
echo "==="
./backup.sh -v -o /tmp/lx-bk data/site data/db
ls /tmp/lx-bk
echo "==="
./backup.sh -o -v data/site; echo "کد خروج: $?"
./backup.sh -o /tmp/lx-bk data/nothing; echo "کد خروج: $?"
./backup.sh data/site
rm -rf /tmp/lx-bk backups
خروجی
Usage: 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.gz
backup.sh: 2 archive(s) written to /tmp/lx-bk
db-2026-10-04.tar.gz
site-2026-10-04.tar.gz
===
backup.sh: -o needs a directory, got '-v'
try 'backup.sh -h'
کد خروج: 2
backup.sh: SOURCE 'data/nothing' is not a directory
try 'backup.sh -h'
کد خروج: 2
backup.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 ~/bashlab
cat > clean-logs.sh <<'EOF'
#!/usr/bin/env bash
# clean-logs.sh: delete *.log files older than N days
set -euo pipefail
prog=$(basename "$0")
die_usage() { echo "$prog: $*" >&2; exit 2; }
days=7
dry_run=false
while (( $# > 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
shift
done
[[ $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 | sort
else
count=$(find "$1" -name '*.log' -type f -mtime +"$days" -print -delete | wc -l)
echo "deleted $count file(s) older than $days days"
fi
EOF
chmod +x clean-logs.sh
mkdir -p logs
for d in 1 5 10 30; do
touch -d "$d days ago" "logs/app-$d.log"
done
touch -d "30 days ago" logs/keep.txt
ls logs
echo "=== --dry-run با پیش‌فرض ۷ روز:"
./clean-logs.sh --dry-run logs
echo "=== --days=3 -n:"
./clean-logs.sh --days=3 -n logs
echo "=== اجرای واقعی با -d 7:"
./clean-logs.sh -d 7 logs
ls logs
./clean-logs.sh --days abc logs; echo "کد خروج: $?"
خروجی
app-1.log
app-10.log
app-30.log
app-5.log
keep.txt
=== --dry-run با پیش‌فرض ۷ روز:
dry run: these files would be deleted:
logs/app-10.log
logs/app-30.log
=== --days=3 -n:
dry run: these files would be deleted:
logs/app-10.log
logs/app-30.log
logs/app-5.log
=== اجرای واقعی با -d 7:
deleted 2 file(s) older than 7 days
app-1.log
app-5.log
keep.txt
clean-logs.sh: DAYS must be a number, got 'abc'
کد خروج: 2

-mtime +7 یعنی «آخرین تغییر بیش از ۷ روز پیش» (درس پیدا کردن فایل‌ها). --dry-run همان find را بدون -delete اجرا می‌کند؛ عادت خوبی است که هر اسکریپت پاک‌کننده چنین حالتی داشته باشد. فایل keep.txt با اینکه قدیمی است، چون .log نیست، دست‌نخورده ماند.

⚡ بررسی سریع

در رشته‌ی گزینه‌های getopts "vo:k" کدام گزینه مقدار می‌خواهد؟

؟ آزمونک
  1. بعد از حلقه‌ی getopts، چرا shift $((OPTIND - 1)) می‌زنیم؟

  2. رشته‌ی گزینه‌ها با : شروع شده (":ho:"). کاربر -x داده است. opt و OPTARG چه هستند؟

  3. ./script.sh /data -v با getopts چه می‌کند؟

  4. چرا در case برای گزینه‌ی ناشناخته باید \?) نوشت و نه ?)؟

  5. کاربر ./backup.sh -o -v /data نوشته (مقدار -o را فراموش کرده). با "vo:" چه می‌شود؟

  6. تابعی که داخلش getopts دارد، بار دوم که صدا زده می‌شود هیچ گزینه‌ای نمی‌خواند. راه‌حل؟

  • گزینه‌های اسم‌دار (-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=1getopts داخل تابع