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

ورودی و آرگومان‌ها

توی این درس یاد می‌گیری اسکریپتت را از یک کار ثابت به ابزاری تبدیل کنی که هر بار با ورودی دیگری کار کند. دو راه داری: از کاربر بپرسی (read -p "Name: " name، با گزینه‌هایی مثل -s برای رمز، -r، -t و -n)، و آرگومان‌های خط فرمان را بخوانی ($1 تا $9، $0 اسم اسکریپت، $# تعدادشان، "$@" همه‌شان، و shift برای جلورفتن). یاد می‌گیری پیام راهنما بدهی و ورودی را اعتبارسنجی کنی. تمرین: اسکریپتی که اسم پوشه را بگیرد و تعداد فایل‌هایش را بگوید.

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

Section titled “مسئله: اسکریپتی که هر بار باید ویرایشش کنی”

اسکریپت‌های درس‌های قبل هر بار دقیقاً یک کار می‌کردند. ولی ابزارهای واقعی این‌طور نیستند: cp اسم مبدأ و مقصد را می‌گیرد، grep الگو و فایل را، ls پوشه را. اسکریپت هم باید همین‌طور باشد؛ وگرنه برای هر پوشه یا هر فایل جدید مجبوری آن را ویرایش یا کپی کنی. دو راه برای دادن ورودی به اسکریپت هست:

  • آرگومان‌های خط فرمان: ./backup.sh /var/www/blog. ورودی را همان لحظه‌ی اجرا می‌دهی. این راه اصلی است، چون اسکریپتی که آرگومان می‌گیرد را می‌شود خودکار اجرا کرد (cron، اسکریپت دیگر، pipeline).
  • پرسیدن از کاربر: اسکریپت وسط کار می‌ایستد، سؤال می‌پرسد و منتظر جواب می‌ماند (read). فقط وقتی مناسب است که آدمی پای ترمینال باشد.

تشبیه: دستور پخت و مواد اولیه

Section titled “تشبیه: دستور پخت و مواد اولیه”

اگر اسکریپت یک دستور پخت است، آرگومان‌ها مواد اولیه‌ای هستند که موقع اجرا به آن می‌دهی: همان دستور پخت با مواد دیگر، غذای دیگری می‌پزد. و read یعنی آشپز وسط کار از تو سؤال می‌پرسد («چند نفر هستید؟») و منتظر جواب می‌ماند. برای وقتی که کسی پای اجاق هست خوب است، ولی اگر سفارش را شب‌هنگام یک ماشین (cron) بدهد، کسی نیست جواب بدهد.

راه شکل مناسب
آرگومان ./script.sh a b ← $1، $2 تنظیم هر اجرا (فایل، پوشه، گزینه)؛ همه‌ی کارهای خودکار
ورودی استاندارد (stdin) read name یا echo x | ./script.sh پرسیدن از کاربر؛ داده‌ای که از pipe می‌رسد
متغیر محیطی BACKUP_DIR=/x ./script.sh تنظیمات ثابت و مشترک بین چند اسکریپت
شل خط فرمان را به کلمه‌ها می‌شکند و کلمه‌ی اول را (اسکریپت) اجرا می‌کند؛ بقیه‌ی کلمه‌ها با ترتیبشان به متغیرهای $1، $2 و ... داخل اسکریپت می‌رسند. آنچه در کوتیشن باشد یک کلمه است.

مثال ۱: read، پرسیدن از کاربر

Section titled “مثال ۱: read، پرسیدن از کاربر”

read یک خط از ورودی می‌خواند (تا زدن Enter) و در متغیر می‌گذارد. با -p یک پرسش (prompt) هم قبلش چاپ می‌کند:

/home/ali/bashlab/ask.sh
#!/usr/bin/env bash
# ask.sh: ask two questions and use the answers
read -p "Your name: " name
read -p "Your age: " age
echo "Hello, $name! In 10 years you will be $((age + 10))."

برای اینکه صفحه‌ی ترمینال را دقیقاً همان‌طور که تو می‌بینی نشانت بدهم، اسکریپت‌های تعاملی را داخل یک ترمینال مجازی (tmux) اجرا می‌کنم و صفحه‌اش را با tmux capture-pane می‌گیرم (مثل درس ویرایشگرها). این دو تابع فقط برای ثبت صفحه‌ی همین درس‌اند و روی ترمینال خودت لازم نیستند:

Terminal window
start_terminal() {
tmux -u kill-server 2>/dev/null
tmux -u new-session -d -s demo -x 60 -y 12 "env PS1='\$ ' bash --norc"
sleep 1
tmux send-keys -t demo "cd ~/bashlab" Enter "clear" Enter
}
screen() { tmux capture-pane -t demo -p | sed -e :a -e '/^[[:space:]]*$/{$d;N;ba' -e '}'; }
echo "ready: start_terminal and screen"
خروجی
ready: start_terminal and screen

حالا اسکریپت را اجرا می‌کنیم و جواب‌ها را «تایپ» می‌کنیم:

Terminal window
chmod +x ~/bashlab/ask.sh
start_terminal
tmux send-keys -t demo "./ask.sh" Enter
sleep 0.7
echo "--- بعد از اجرا:"; screen
tmux send-keys -t demo "Ali" Enter
sleep 0.7
echo "--- بعد از تایپ اسم:"; screen
tmux send-keys -t demo "30" Enter
sleep 0.7
echo "--- بعد از تایپ سن:"; screen
خروجی
--- بعد از اجرا:
$ ./ask.sh
Your name:
--- بعد از تایپ اسم:
$ ./ask.sh
Your name: Ali
Your age:
--- بعد از تایپ سن:
$ ./ask.sh
Your name: Ali
Your age: 30
Hello, Ali! In 10 years you will be 40.
$

اسکریپت روی Your name: ایستاد و منتظر ماند؛ هر چه تایپ کردم و Enter زدم در name رفت. بعد همین کار را برای age کرد، و آخر از هر دو در جمله استفاده کرد (با محاسبه‌ی $((age + 10)) درس قبل).

read گزینه‌های زیادی دارد. چند تایش را با ورودی «از پیش‌نوشته» امتحان می‌کنیم. <<< (here-string) متن سمت راست را مثل متنی که کاربر تایپ کرده به دستور می‌دهد، تا بتوانیم بدون تایپ دستی نتیجه را ببینیم:

Terminal window
echo "--- چند متغیر از یک خط (آخری هر چه مانده را می‌گیرد):"
read first last <<< "Ali Ahmadi Khorasani"
echo "first=[$first] last=[$last]"
echo "--- بدون اسم متغیر، جواب در REPLY می‌ماند:"
read <<< "hello there"
echo "REPLY=[$REPLY]"
echo "--- فقط یک کاراکتر (read -n 1):"
read -n 1 answer <<< "yes"
echo "answer=[$answer]"
echo "--- بدون -r بک‌اسلش خورده می‌شود:"
read path <<< 'C:\temp\new'
echo "without -r: [$path]"
read -r path <<< 'C:\temp\new'
echo "with -r: [$path]"
echo "--- جداکننده‌ی دلخواه با IFS (یک خط از /etc/passwd):"
IFS=: read -r user x uid gid comment home shell <<< "$(grep '^ali:' /etc/passwd)"
echo "user=$user uid=$uid home=$home shell=$shell"
echo "--- زمان‌سنج (read -t 1) وقتی ورودی نمی‌رسد:"
sleep 3 | { read -t 1 late; echo "status=$?"; }
خروجی
--- چند متغیر از یک خط (آخری هر چه مانده را می‌گیرد):
first=[Ali] last=[Ahmadi Khorasani]
--- بدون اسم متغیر، جواب در REPLY می‌ماند:
REPLY=[hello there]
--- فقط یک کاراکتر (read -n 1):
answer=[y]
--- بدون -r بک‌اسلش خورده می‌شود:
without -r: [C:tempnew]
with -r: [C:\temp\new]
--- جداکننده‌ی دلخواه با IFS (یک خط از /etc/passwd):
user=ali uid=1001 home=/home/ali shell=/bin/bash
--- زمان‌سنج (read -t 1) وقتی ورودی نمی‌رسد:
status=142
  • با چند اسم متغیر، read خط را در فاصله‌ها می‌شکند و به ترتیب می‌دهد؛ آخرین متغیر هر چه مانده را می‌گیرد (last شد Ahmadi Khorasani).
  • read -r (raw) همیشه بنویس. بدون آن، بک‌اسلش معنی خاص دارد و از متن حذف می‌شود (C:\temp\new شد C:tempnew). کمتر کسی این را می‌خواهد.
  • IFS (جداکننده‌ی فیلد) مشخص می‌کند خط را کجا بشکند. با IFS=: read ... فقط برای همین یک دستور جداکننده : می‌شود؛ مناسب فایل‌هایی مثل /etc/passwd.
  • read -t 1 فقط یک ثانیه صبر می‌کند و اگر ورودی نیامد با کد خروج بزرگ‌تر از ۱۲۸ (اینجا ۱۴۲) برمی‌گردد.
  • read -n 1 به‌جای یک خط، یک کاراکتر می‌خواند (برای «ادامه بدهم؟ y/n» که لازم نیست Enter بزنند).

حالا read -s (silent) برای رمز. چیزی که تایپ می‌شود روی صفحه نمی‌آید:

/home/ali/bashlab/login.sh
#!/usr/bin/env bash
# login.sh: read a username and a hidden password
read -p "User: " user
read -s -p "Password: " pass
echo
echo "Logging in as $user (password has ${#pass} characters)"
Terminal window
chmod +x ~/bashlab/login.sh
start_terminal
tmux send-keys -t demo "./login.sh" Enter
sleep 0.7
tmux send-keys -t demo "ali" Enter
sleep 0.5
tmux send-keys -t demo "s3cret-pass"
sleep 0.5
echo "--- وسط تایپ رمز (چیزی دیده نمی‌شود):"; screen
tmux send-keys -t demo Enter
sleep 0.7
echo "--- بعد از Enter:"; screen
خروجی
--- وسط تایپ رمز (چیزی دیده نمی‌شود):
$ ./login.sh
User: ali
Password:
--- بعد از Enter:
$ ./login.sh
User: ali
Password:
Logging in as ali (password has 11 characters)
$

رمز تایپ شد، ولی روی صفحه هیچ نشانی ازش نیست. دقت کن بعد از read -s خودش خط جدید چاپ نمی‌کند (چون Enter هم نمایش داده نشده)؛ برای همین یک echo خالی گذاشتم. ${#pass} طول متغیر است (در درس کار با رشته‌ها می‌آید).

مثال ۳: آرگومان‌های خط فرمان، $1 تا $9

Section titled “مثال ۳: آرگومان‌های خط فرمان، $1 تا $9”

هر کلمه‌ای که بعد از اسم اسکریپت می‌نویسی، به ترتیب در $1، $2 و … می‌رود. $0 خود اسم اسکریپت و $# تعداد آرگومان‌هاست:

/home/ali/bashlab/args.sh
#!/usr/bin/env bash
# args.sh: show the positional parameters
echo "script name (\$0): $0"
echo "first (\$1): $1"
echo "second (\$2): $2"
echo "third (\$3): $3"
echo "count (\$#): $#"
Terminal window
cd ~/bashlab
chmod +x args.sh
echo "--- ./args.sh apple banana cherry"
./args.sh apple banana cherry
echo "--- آرگومان دارای فاصله باید کوتیشن داشته باشد:"
./args.sh "big apple" banana
echo "--- بدون آرگومان:"
./args.sh
خروجی
--- ./args.sh apple banana cherry
script name ($0): ./args.sh
first ($1): apple
second ($2): banana
third ($3): cherry
count ($#): 3
--- آرگومان دارای فاصله باید کوتیشن داشته باشد:
script name ($0): ./args.sh
first ($1): big apple
second ($2): banana
third ($3):
count ($#): 2
--- بدون آرگومان:
script name ($0): ./args.sh
first ($1):
second ($2):
third ($3):
count ($#): 0

echo "$1" اولین آرگومان را چاپ می‌کند و echo "$#" تعدادشان را. کوتیشن را فراموش نکن: آنچه در کوتیشن است یک آرگومان می‌شود ("big apple" شد $1). آرگومانی که پاس نداده‌ای خالی است، نه خطا (مثل هر متغیر تعریف‌نشده‌ای). و از آرگومان دهم به بعد باید آکولاد بگذاری:

/home/ali/bashlab/ten.sh
#!/usr/bin/env bash
echo "\$10 -> $10"
echo "\${10} -> ${10}"
Terminal window
cd ~/bashlab
chmod +x ten.sh
./ten.sh a b c d e f g h i j k
خروجی
$10 -> a0
${10} -> j

$10 را شل می‌خواند «$1 و بعد کاراکتر 0»، پس a0 شد؛ ${10} آرگومان دهم است (j). (در عمل اسکریپتی که ده آرگومان موقعیتی دارد، بهتر است از getopts استفاده کند.)

مثال ۴: $@ و $*، همه‌ی آرگومان‌ها

Section titled “مثال ۴: $@ و $*، همه‌ی آرگومان‌ها”

گاهی تعداد آرگومان‌ها معلوم نیست (مثلاً «همه‌ی این فایل‌ها»). "$@" همه‌شان را می‌دهد، هر کدام جدا. این مهم‌ترین چیزی است که از این درس باید یادت بماند:

/home/ali/bashlab/show.sh
#!/usr/bin/env bash
# show.sh: the different ways to expand "all arguments"
echo "count: $#"
echo '--- "$@" (each argument stays separate):'
printf '[%s]\n' "$@"
echo '--- "$*" (one single string):'
printf '[%s]\n' "$*"
echo '--- $@ without quotes (split again at spaces):'
printf '[%s]\n' $@
echo '--- for arg in "$@":'
for arg in "$@"; do
echo "got: $arg"
done
Terminal window
cd ~/bashlab
chmod +x show.sh
./show.sh "hello world" b "c d"
خروجی
count: 3
--- "$@" (each argument stays separate):
[hello world]
[b]
[c d]
--- "$*" (one single string):
[hello world b c d]
--- $@ without quotes (split again at spaces):
[hello]
[world]
[b]
[c]
[d]
--- for arg in "$@":
got: hello world
got: b
got: c d

سه آرگومان داریم (count: 3). "$@" سه تکه‌ی سالم می‌دهد، "$*" همه را به یک رشته می‌چسباند، و $@ بدون کوتیشن دوباره در فاصله‌ها می‌شکند و پنج تکه می‌شود (همان مشکل درس قبل). قاعده: همیشه "$@". (حلقه‌ی for در درس حلقه‌ها کامل می‌آید؛ اینجا یعنی «برای هر آرگومان، این کار را بکن».)

کاربرد معمول "$@": ساختن اسکریپتی که همه‌ی آرگومان‌هایش را به یک دستور دیگر پاس می‌دهد (یک wrapper):

/home/ali/bashlab/lsl.sh
#!/usr/bin/env bash
# lsl.sh: ls -lh that passes every argument along
ls -lh "$@"
Terminal window
cd ~/bashlab
chmod +x lsl.sh
mkdir -p "my dir" && echo "hello" > "my dir/note.txt" && echo "x" > a.txt
./lsl.sh a.txt "my dir"
خروجی
-rw-r--r-- 1 ali ali 2 Oct 4 09:44 a.txt
my dir:
total 4.0K
-rw-r--r-- 1 ali ali 6 Oct 4 09:44 note.txt

نام «my dir» با فاصله بدون هیچ دردسری رسید؛ چون "$@" حدود آرگومان‌ها را حفظ می‌کند.

مثال ۵: shift، آرگومان اول را کنار بگذار

Section titled “مثال ۵: shift، آرگومان اول را کنار بگذار”

shift آرگومان اول را دور می‌اندازد و بقیه یکی جلو می‌آیند ($2 می‌شود $1 و…). $# هم یکی کم می‌شود. shift 2 دو تا دور می‌اندازد:

shift: آرگومان‌ها یک جا به چپ می‌روند و اولی از بین می‌رود.
/home/ali/bashlab/shift-demo.sh
#!/usr/bin/env bash
# shift-demo.sh: shift drops $1 and moves the rest down
echo "start: \$1=$1 \$#=$#"
shift
echo "after shift: \$1=$1 \$#=$#"
shift 2
echo "after shift 2: \$1=$1 \$#=$#"
Terminal window
cd ~/bashlab
chmod +x shift-demo.sh
./shift-demo.sh a b c d e
خروجی
start: $1=a $#=5
after shift: $1=b $#=4
after shift 2: $1=d $#=2

کاربردش: وقتی آرگومان اول معنی ویژه دارد (مثلاً یک برچسب) و بقیه را می‌خواهی با "$@" به کار دیگری بدهی. این اسکریپت دستوری را اجرا می‌کند و قبلش یک برچسب لاگ می‌اندازد:

/home/ali/bashlab/with-label.sh
#!/usr/bin/env bash
# with-label.sh: ./with-label.sh LABEL command [args...]
label="$1"
shift
echo "[$label] running: $*"
"$@"
Terminal window
cd ~/bashlab
chmod +x with-label.sh
./with-label.sh build ls -l /etc/hostname
خروجی
[build] running: ls -l /etc/hostname
-rw-r--r-- 1 root root 3 Oct 3 21:00 /etc/hostname

label اولین آرگومان را گرفت، shift آن را دور انداخت، و حالا "$@" خودِ دستور (ls -l /etc/hostname) است که اجرا شد.

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

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

اسکریپتی که آرگومان لازم دارد، باید وقتی نبود یا اشتباه بود راهنمایی بدهد و با کد خروج خطا تمام شود؛ نه اینکه با ورودی خالی کار خراب کند. این الگو را اکثر ابزارهای واقعی دارند:

/home/ali/bashlab/file-info.sh
#!/usr/bin/env bash
# file-info.sh: print the size and the number of lines of a file
# Usage: ./file-info.sh <file>
if [[ $# -ne 1 ]]; then
echo "Usage: $0 <file>" >&2
exit 1
fi
file="$1"
if [[ ! -f "$file" ]]; then
echo "Error: '$file' is not a regular file" >&2
exit 2
fi
size=$(stat -c %s "$file")
lines=$(wc -l < "$file")
echo "$file: $size bytes, $lines lines"
Terminal window
cd ~/bashlab
chmod +x file-info.sh
printf 'one\ntwo\nthree\n' > sample.txt
echo "--- درست:"
./file-info.sh sample.txt; echo "کد خروج: $?"
echo "--- بدون آرگومان:"
./file-info.sh; echo "کد خروج: $?"
echo "--- فایل ناموجود:"
./file-info.sh missing.txt; echo "کد خروج: $?"
echo "--- دو آرگومان:"
./file-info.sh a b; echo "کد خروج: $?"
خروجی
--- درست:
sample.txt: 14 bytes, 3 lines
کد خروج: 0
--- بدون آرگومان:
Usage: ./file-info.sh <file>
کد خروج: 1
--- فایل ناموجود:
Error: 'missing.txt' is not a regular file
کد خروج: 2
--- دو آرگومان:
Usage: ./file-info.sh <file>
کد خروج: 1

if را در درس بعد کامل می‌خوانی؛ فعلاً این‌طور بخوانش: if [[ $# -ne 1 ]] یعنی «اگر تعداد آرگومان‌ها یک نیست» (-ne = not equal)، و [[ ! -f "$file" ]] یعنی «اگر $file یک فایل معمولی نیست». نکته‌های مهم:

  • >&2: پیام خطا را به خروجی خطا (stderr) می‌فرستد، نه خروجی عادی. این‌طور اگر کسی خروجی اسکریپت را در pipe یا فایل بریزد، پیام خطا قاطی داده‌ها نمی‌شود (درس ورودی، خروجی و redirect).
  • exit 1 و exit 2: هر کد معنی‌ای دارد که خودت تعیین می‌کنی (قرارداد رایج: ۱ استفاده‌ی غلط یا خطای عمومی، ۲ ورودی نامعتبر). cron و اسکریپت‌های دیگر فقط به همین عدد نگاه می‌کنند.
  • $0 در راهنما: اسم اسکریپت همان‌طور که صدایش زده‌اند در راهنما می‌آید.

مثال ۷: آرگومان یا پرسش؟ هر دو

Section titled “مثال ۷: آرگومان یا پرسش؟ هر دو”

بهترین اسکریپت‌ها هر دو راه را بلدند: آرگومان اگر داده شد، و اگر نه، بپرسند (فقط وقتی کسی پای ترمینال است). این الگو را هم ببین:

/home/ali/bashlab/greet.sh
#!/usr/bin/env bash
# greet.sh: use the first argument as the name; ask only if it is missing
if [[ $# -ge 1 ]]; then
name="$1"
else
read -r -p "Your name: " name
fi
echo "Hello, $name!"
Terminal window
cd ~/bashlab
chmod +x greet.sh
echo "--- با آرگومان:"
./greet.sh Ali
echo "--- از pipe (ورودی استاندارد):"
echo "Sara" | ./greet.sh
echo "--- مثل cron: ورودی تهی (هیچ‌کس جواب نمی‌دهد):"
./greet.sh < /dev/null
echo "--- تعاملی (با tmux):"
start_terminal
tmux send-keys -t demo "./greet.sh" Enter
sleep 0.7
tmux send-keys -t demo "Reza" Enter
sleep 0.7
screen
خروجی
--- با آرگومان:
Hello, Ali!
--- از pipe (ورودی استاندارد):
Hello, Sara!
--- مثل cron: ورودی تهی (هیچ‌کس جواب نمی‌دهد):
Hello, !
--- تعاملی (با tmux):
$ ./greet.sh
Your name: Reza
Hello, Reza!
$

یک نکته‌ی دقیق: وقتی ورودی از pipe می‌آید، read -p پرسش را چاپ نمی‌کند (پرسش فقط وقتی نشان داده می‌شود که ورودی یک ترمینال باشد؛ در ثبت بالا هم پرسش نیامد). و در حالت cron که ورودی تهی است، read بدون جواب برمی‌گردد و name خالی می‌ماند (Hello, !). برای همین هر اسکریپتی که قرار است خودکار اجرا شود باید همه‌چیز را از آرگومان بگیرد و فقط اگر کسی پای ترمینال بود بپرسد. راه ساده‌ی دادن مقدار پیش‌فرض، ${1:-World} است که در درس «کار با رشته‌ها» کامل می‌شود:

cd ~/bashlab
cat > greet2.sh <<'EOF'
#!/usr/bin/env bash
name="${1:-World}"
echo "Hello, $name!"
EOF
chmod +x greet2.sh
./greet2.sh
./greet2.sh Ali
خروجی
Hello, World!
Hello, Ali!

پشت پرده: آرگومان‌ها چطور به اسکریپت می‌رسند؟

Section titled “پشت پرده: آرگومان‌ها چطور به اسکریپت می‌رسند؟”

وقتی ./args.sh apple "big banana" را می‌زنی:

  1. شل خط را به کلمه‌ها می‌شکند، کوتیشن‌ها و متغیرها را باز می‌کند (درس قبل)، و به هسته می‌گوید: برنامه‌ی ./args.sh را با این فهرست آرگومان‌ها (argv) اجرا کن.
  2. هسته آن‌ها را در حافظه‌ی پروسه‌ی جدید می‌گذارد.
  3. bash که اسکریپت را اجرا می‌کند، از روی همین فهرست $0، $1، $2 و … را می‌سازد.

یعنی آرگومان‌ها بخشی از خود پروسه‌اند. و همین‌طور که در حافظه‌ی پروسه هستند، دیگران هم می‌توانند ببینندشان:

cat > ~/bashlab/slowtool.sh <<'EOF'
#!/usr/bin/env bash
sleep 20
EOF
chmod +x ~/bashlab/slowtool.sh
~/bashlab/slowtool.sh --user ali --password "s3cret pass" &
pid=$!
disown
echo "--- ps (هر کاربری روی سیستم این را می‌بیند):"
ps -o args= -p $pid
echo "--- /proc/PID/cmdline (آرگومان‌ها با NUL جدا شده‌اند):"
tr '\0' '|' < /proc/$pid/cmdline; echo
echo "--- مجوزها: cmdline را همه می‌خوانند، environ فقط صاحب پروسه:"
ls -l /proc/$pid/cmdline /proc/$pid/environ
خروجی
--- ps (هر کاربری روی سیستم این را می‌بیند):
bash /home/ali/bashlab/slowtool.sh --user ali --password s3cret pass
--- /proc/PID/cmdline (آرگومان‌ها با NUL جدا شده‌اند):
bash|/home/ali/bashlab/slowtool.sh|--user|ali|--password|s3cret pass|
--- مجوزها: cmdline را همه می‌خوانند، environ فقط صاحب پروسه:
-r--r--r-- 1 ali ali 0 Oct 4 09:44 /proc/21881/cmdline
-r-------- 1 ali ali 0 Oct 4 09:44 /proc/21881/environ

رمز در ps و در /proc/PID/cmdline (که برای همه خواناست) دیده می‌شود. نتیجه‌ی امنیتی: رمز، توکن و هر راز دیگری را هرگز به‌صورت آرگومان نده. بهتر است با read -s بپرسی، از یک فایل با مجوز 600 بخوانی، یا از متغیر محیطی بدهی (فایل environ فقط برای صاحب پروسه خواناست). نکته‌ی هم‌خانواده: آرگومان‌ها در تاریخچه‌ی shell (history) هم می‌مانند.

پارامترهای ویژه‌ی اسکریپت:

پارامتر یعنی
$0 اسم اسکریپت (همان‌طور که اجرا شد)
$1 … $9، ${10} … آرگومان اول، دوم، … (از دهم به بعد با آکولاد)
$# تعداد آرگومان‌ها (بدون $0)
"$@" همه‌ی آرگومان‌ها، هر کدام جدا (این را به کار ببر)
"$*" همه‌ی آرگومان‌ها در یک رشته
shift / shift N دور انداختن اولین (یا N تای اول) آرگومان
$? کد خروج آخرین دستور
$$ شماره‌ی پروسه‌ی خود اسکریپت
$! شماره‌ی پروسه‌ی آخرین کار پس‌زمینه (&)

گزینه‌های read که در این درس دیدی:

گزینه کار
-p "متن" قبل از خواندن، پرسش را نشان بده (فقط وقتی ورودی ترمینال است)
-s تایپ را نشان نده (رمز)
-r بک‌اسلش را معمولی بخوان (همیشه بنویس)
-t N بعد از N ثانیه منصرف شو (کد خروج > ۱۲۸)
-n N فقط N کاراکتر بخوان (بدون Enter)
IFS=: read ... جداکننده‌ی دلخواه برای شکستن خط به متغیرها
بدون اسم متغیر جواب در REPLY

۱) خواندن با pipe، متغیر ناپدید می‌شود

Section titled “۱) خواندن با pipe، متغیر ناپدید می‌شود”
Terminal window
echo "Ali 30" | read name age
echo "after pipe: [$name] [$age]"
read name age <<< "Ali 30"
echo "with here-string: [$name] [$age]"
خروجی
after pipe: [] []
with here-string: [Ali] [30]

هر دستور سمت یک pipe در پروسه‌ی جداگانه اجرا می‌شود؛ read متغیرها را در آن پروسه ساخت و با پایانش از بین رفتند. راه‌حل: <<< (یا read ... < فایل). این مشکل در حلقه‌های while read هم می‌آید؛ در درس حلقه‌ها می‌بینی.

۲) read بدون -r، و $10 بدون آکولاد

Section titled “۲) read بدون -r، و $10 بدون آکولاد”

مثال‌های ۲ و ۳: read بدون -r بک‌اسلش‌ها را می‌خورد (C:tempnew)؛ $10 یعنی $1 به‌اضافه‌ی 0. راه‌حل: read -r، و ${10}.

۳) پیمایش آرگومان‌ها با $@ بدون کوتیشن

Section titled “۳) پیمایش آرگومان‌ها با $@ بدون کوتیشن”
/home/ali/bashlab/loop-bad.sh
#!/usr/bin/env bash
echo "--- for arg in \$@ (wrong):"
for arg in $@; do echo "[$arg]"; done
echo "--- for arg in \"\$@\" (right):"
for arg in "$@"; do echo "[$arg]"; done
Terminal window
cd ~/bashlab
chmod +x loop-bad.sh
./loop-bad.sh "my file.txt" other.txt
خروجی
--- for arg in $@ (wrong):
[my]
[file.txt]
[other.txt]
--- for arg in "$@" (right):
[my file.txt]
[other.txt]

با $@ بدون کوتیشن، my file.txt دو تکه شد. راه‌حل: "$@". (ابزار ShellCheck در درس دیباگ این را خودکار می‌گیرد.)

۴) آرگومان خالی در دستور مخرب

Section titled “۴) آرگومان خالی در دستور مخرب”

اگر اسکریپت rm -rf $1/* داشته باشد و کاربر آرگومان را فراموش کند، $1 خالی است و دستور rm -rf /* می‌شود. باز هم با echo نشان می‌دهیم:

cd ~/bashlab
cat > danger.sh <<'EOF'
#!/usr/bin/env bash
echo rm -rf $1/*
EOF
chmod +x danger.sh
./danger.sh | cut -c1-60
خروجی
rm -rf /bin /bin.usr-is-merged /boot /dev /etc /home /lib /l

راه‌حل: همیشه $# را بررسی کن (مثال ۶)، یا ${1:?usage: script <dir>} بنویس که در نبود آرگومان با پیام متوقف شود.

۵) shift وقتی آرگومانی نمانده

Section titled “۵) shift وقتی آرگومانی نمانده”
cd ~/bashlab
cat > shift2.sh <<'EOF'
#!/usr/bin/env bash
echo "args: $#"
shift
echo "after 1st shift: status=$? args=$#"
shift
echo "after 2nd shift: status=$? args=$#"
echo "--- با shopt -s shift_verbose:"
shopt -s shift_verbose
shift 3
echo "after shift 3: status=$? args=$#"
EOF
chmod +x shift2.sh
./shift2.sh only-one
خروجی
args: 1
after 1st shift: status=0 args=0
after 2nd shift: status=1 args=0
--- با shopt -s shift_verbose:
./shift2.sh: line 9: shift: 3: shift count out of range
after shift 3: status=1 args=0

اولین shift آرگومان را برداشت. دومی که چیزی برای برداشتن نداشت بی‌صدا با کد ۱ شکست خورد (پیام خطا ندارد!) و اسکریپت ادامه داد. فقط اگر shopt -s shift_verbose را روشن کنی، shift پیام shift count out of range می‌دهد. راه‌حل: اگر لازم است، قبل از shift ببین $# کافی است، و در اسکریپت جدی به کد خروج shift توجه کن.

در بخش «پشت پرده» دیدی: رمز در ps پیداست. راه‌حل: read -s.

✎ تمرینآسان

اسکریپت hello-name.sh را بنویس که اسم را به‌عنوان آرگومان بگیرد و Hello, اسم! چاپ کند. سه بار اجرایش کن: با Ali، با "Ali Ahmadi" (در کوتیشن) و بدون آرگومان.

دیدن جواب
mkdir -p ~/bashlab && cd ~/bashlab
cat > hello-name.sh <<'EOF'
#!/usr/bin/env bash
# hello-name.sh: greet the name given as the first argument
echo "Hello, $1!"
EOF
chmod +x hello-name.sh
./hello-name.sh Ali
./hello-name.sh "Ali Ahmadi"
./hello-name.sh
خروجی
Hello, Ali!
Hello, Ali Ahmadi!
Hello, !

بدون آرگومان $1 خالی است و Hello, ! می‌گیری. راه بهتر در مثال ۶ و ۷ بود: یا بررسی $#، یا مقدار پیش‌فرض ${1:-World}.

✎ تمرینمتوسط

تمرین اصلی درس: اسکریپت count-files.sh که اسم یک پوشه را بگیرد و تعداد فایل‌هایش را بگوید (فقط فایل‌های معمولی، نه زیرپوشه‌ها). اگر آرگومان نداد یا پوشه وجود نداشت، پیام مناسب روی stderr و کد خروج ۱ و ۲.

دیدن جواب
mkdir -p ~/bashlab/photos/2026 && cd ~/bashlab
touch photos/a.jpg photos/b.jpg photos/notes.txt photos/2026/c.jpg
cat > count-files.sh <<'EOF'
#!/usr/bin/env bash
# count-files.sh: tell how many files a directory contains
# Usage: ./count-files.sh <directory>
if [[ $# -ne 1 ]]; then
echo "Usage: $0 <directory>" >&2
exit 1
fi
dir="$1"
if [[ ! -d "$dir" ]]; then
echo "Error: '$dir' is not a directory" >&2
exit 2
fi
count=$(find "$dir" -maxdepth 1 -type f | wc -l)
echo "$dir has $count files"
EOF
chmod +x count-files.sh
./count-files.sh photos; echo "کد خروج: $?"
./count-files.sh; echo "کد خروج: $?"
./count-files.sh nowhere; echo "کد خروج: $?"
./count-files.sh photos/2026; echo "کد خروج: $?"
خروجی
photos has 3 files
کد خروج: 0
Usage: ./count-files.sh <directory>
کد خروج: 1
Error: 'nowhere' is not a directory
کد خروج: 2
photos/2026 has 1 files
کد خروج: 0

-maxdepth 1 یعنی فقط خودِ پوشه، نه زیرپوشه‌ها؛ -type f یعنی فقط فایل. برای photos سه فایل (a.jpg، b.jpg، notes.txt) شمرده شد و 2026/c.jpg نه.

✎ تمرینمتوسط

اسکریپت sum.sh که هر تعداد عدد را به‌عنوان آرگومان بگیرد و جمعشان را چاپ کند (مثلاً ./sum.sh 10 20 30 ← numbers: 3, total: 60). از "$@"، $# و محاسبه‌ی $(( )) استفاده کن.

دیدن جواب
cd ~/bashlab
cat > sum.sh <<'EOF'
#!/usr/bin/env bash
# sum.sh: add up all numeric arguments
total=0
for n in "$@"; do
total=$((total + n))
done
echo "numbers: $#, total: $total"
EOF
chmod +x sum.sh
./sum.sh 10 20 30
./sum.sh 5
./sum.sh
خروجی
numbers: 3, total: 60
numbers: 1, total: 5
numbers: 0, total: 0

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

✎ تمرینسخت

اسکریپت mkproject.sh: آرگومان اول اسم پروژه و بقیه اسم زیرپوشه‌ها هستند (./mkproject.sh shop src docs tests). پوشه‌ی پروژه و زیرپوشه‌هایش را بسازد و ساختار را چاپ کند. اگر حتی اسم پروژه داده نشد، راهنما بدهد و با کد ۱ تمام شود. از shift و "$@" استفاده کن.

دیدن جواب
cd ~/bashlab
cat > mkproject.sh <<'EOF'
#!/usr/bin/env bash
# mkproject.sh: create a project folder with the given sub-folders
# Usage: ./mkproject.sh <project> [subfolder...]
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <project> [subfolder...]" >&2
exit 1
fi
project="$1"
shift # now "$@" holds only the sub-folder names
mkdir -p "$project"
for sub in "$@"; do
mkdir -p "$project/$sub"
done
echo "Created $project with $# sub-folders:"
find "$project" -mindepth 1 | sort
EOF
chmod +x mkproject.sh
./mkproject.sh shop src docs tests
echo "--- بدون آرگومان:"
./mkproject.sh; echo "کد خروج: $?"
echo "--- فقط اسم پروژه:"
./mkproject.sh empty-project
خروجی
Created shop with 3 sub-folders:
shop/docs
shop/src
shop/tests
--- بدون آرگومان:
Usage: ./mkproject.sh <project> [subfolder...]
کد خروج: 1
--- فقط اسم پروژه:
Created empty-project with 0 sub-folders:

دقت کن $# بعد از shift تعداد زیرپوشه‌ها است (نه کل آرگومان‌ها)، و هر mkdir -p با کوتیشن نوشته شده تا اسم‌های دارای فاصله هم کار کنند.

⚡ بررسی سریع

کدام یک برای پاس‌دادن همه‌ی آرگومان‌های اسکریپت به یک دستور دیگر، بدون خراب‌شدن آرگومان‌های دارای فاصله، درست است؟

؟ آزمونک
  1. در ./backup.sh /data "my dir" مقدار $# و $2 چیست؟

  2. اسکریپتی با دستور read -p (پرسش Name:) را با echo Ali | ./script.sh اجرا می‌کنی. پرسش روی صفحه می‌آید؟

  3. در اسکریپتی که ۱۱ آرگومان گرفته، echo $10 چه چاپ می‌کند؟

  4. shift چه می‌کند؟

  5. echo "Ali 30" | read name age و بعد echo $name چه می‌دهد؟

  6. چرا رمز را نباید به‌صورت آرگومان به اسکریپت داد (./login.sh --password secret)؟

  • آرگومان‌ها راه اصلی ورودی‌اند چون اسکریپت را خودکارپذیر می‌کنند: $1..$9، ${10}، $0 (اسم اسکریپت)، $# (تعداد). آنچه در کوتیشن باشد یک آرگومان است.
  • "$@" همه‌ی آرگومان‌ها را هر کدام جدا می‌دهد (همیشه این را بنویس)؛ "$*" همه را یک رشته می‌کند؛ بدون کوتیشن دوباره در فاصله‌ها می‌شکند.
  • shift آرگومان اول را کنار می‌گذارد. با label="$1"; shift; "$@" الگوی معروف «اولی ویژه است، بقیه برای دستور دیگر» را می‌سازی.
  • read -r -p "پرسش: " var از کاربر می‌پرسد؛ -s برای رمز، -t برای زمان‌سنج، -n برای یک کاراکتر. -p فقط وقتی ورودی ترمینال است پرسش را نشان می‌دهد؛ <<< برای ورودی «از پیش‌نوشته».
  • اسکریپتی که آرگومان لازم دارد: بررسی $#، پیام راهنما روی stderr (>&2) و exit با کد غیرصفر.
  • آرگومان‌ها برای همه‌ی کاربران سیستم (ps، /proc) دیدنی‌اند؛ راز را آرگومان نده.
  • مشکل pipe: echo x | read v متغیر را از دست می‌دهد؛ read v <<< "x".
برگه‌ی تقلب این درس
دستورکاری که می‌کند
read -r -p "Name: " nameپرسیدن از کاربر
read -s -p "Password: " passخواندن رمز بدون نمایش
read -t 5 var / read -n 1 keyبا زمان‌سنج / فقط یک کاراکتر
read a b rest <<< "x y z w"شکستن یک خط به چند متغیر
echo "$1" ${10}آرگومان اول / آرگومان دهم
echo "$#"تعداد آرگومان‌ها
"$@"همه‌ی آرگومان‌ها، هر کدام جدا
shift / shift 2دور انداختن اولین آرگومان (یا دو تای اول)
if [[ $# -ne 1 ]]; then ... exit 1; fiبررسی تعداد آرگومان (مثال ۶)
echo "Usage: $0 <file>" >&2راهنما روی stderr
name="${1:-World}"مقدار پیش‌فرض برای آرگومان
printf '[%s]\n' "$@"دیدن اینکه هر آرگومان چطور رسید