توی این درس یاد میگیری اسکریپتت را از یک کار ثابت به ابزاری تبدیل کنی که هر بار با ورودی دیگری کار کند. دو راه داری: از کاربر بپرسی (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) بدهد، کسی نیست جواب بدهد.
سه راه ورودی
Section titled “سه راه ورودی”| راه | شکل | مناسب |
|---|---|---|
| آرگومان | ./script.sh a b ← $1، $2 |
تنظیم هر اجرا (فایل، پوشه، گزینه)؛ همهی کارهای خودکار |
| ورودی استاندارد (stdin) | read name یا echo x | ./script.sh |
پرسیدن از کاربر؛ دادهای که از pipe میرسد |
| متغیر محیطی | BACKUP_DIR=/x ./script.sh |
تنظیمات ثابت و مشترک بین چند اسکریپت |
مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: read، پرسیدن از کاربر
Section titled “مثال ۱: read، پرسیدن از کاربر”read یک خط از ورودی میخواند (تا زدن Enter) و در متغیر میگذارد. با -p یک پرسش (prompt) هم قبلش چاپ میکند:
#!/usr/bin/env bash# ask.sh: ask two questions and use the answersread -p "Your name: " nameread -p "Your age: " ageecho "Hello, $name! In 10 years you will be $((age + 10))."برای اینکه صفحهی ترمینال را دقیقاً همانطور که تو میبینی نشانت بدهم، اسکریپتهای تعاملی را داخل یک ترمینال مجازی (tmux) اجرا میکنم و صفحهاش را با tmux capture-pane میگیرم (مثل درس ویرایشگرها). این دو تابع فقط برای ثبت صفحهی همین درساند و روی ترمینال خودت لازم نیستند:
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حالا اسکریپت را اجرا میکنیم و جوابها را «تایپ» میکنیم:
chmod +x ~/bashlab/ask.shstart_terminaltmux send-keys -t demo "./ask.sh" Entersleep 0.7echo "--- بعد از اجرا:"; screentmux send-keys -t demo "Ali" Entersleep 0.7echo "--- بعد از تایپ اسم:"; screentmux send-keys -t demo "30" Entersleep 0.7echo "--- بعد از تایپ سن:"; screen--- بعد از اجرا:$ ./ask.shYour name:--- بعد از تایپ اسم:$ ./ask.shYour name: AliYour age:--- بعد از تایپ سن:$ ./ask.shYour name: AliYour age: 30Hello, Ali! In 10 years you will be 40.$اسکریپت روی Your name: ایستاد و منتظر ماند؛ هر چه تایپ کردم و Enter زدم در name رفت. بعد همین کار را برای age کرد، و آخر از هر دو در جمله استفاده کرد (با محاسبهی $((age + 10)) درس قبل).
مثال ۲: گزینههای read
Section titled “مثال ۲: گزینههای read”read گزینههای زیادی دارد. چند تایش را با ورودی «از پیشنوشته» امتحان میکنیم. <<< (here-string) متن سمت راست را مثل متنی که کاربر تایپ کرده به دستور میدهد، تا بتوانیم بدون تایپ دستی نتیجه را ببینیم:
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) برای رمز. چیزی که تایپ میشود روی صفحه نمیآید:
#!/usr/bin/env bash# login.sh: read a username and a hidden passwordread -p "User: " userread -s -p "Password: " passechoecho "Logging in as $user (password has ${#pass} characters)"chmod +x ~/bashlab/login.shstart_terminaltmux send-keys -t demo "./login.sh" Entersleep 0.7tmux send-keys -t demo "ali" Entersleep 0.5tmux send-keys -t demo "s3cret-pass"sleep 0.5echo "--- وسط تایپ رمز (چیزی دیده نمیشود):"; screentmux send-keys -t demo Entersleep 0.7echo "--- بعد از Enter:"; screen--- وسط تایپ رمز (چیزی دیده نمیشود):$ ./login.shUser: aliPassword:--- بعد از Enter:$ ./login.shUser: aliPassword:Logging in as ali (password has 11 characters)$رمز تایپ شد، ولی روی صفحه هیچ نشانی ازش نیست. دقت کن بعد از read -s خودش خط جدید چاپ نمیکند (چون Enter هم نمایش داده نشده)؛ برای همین یک echo خالی گذاشتم. ${#pass} طول متغیر است (در درس کار با رشتهها میآید).
مثال ۳: آرگومانهای خط فرمان، $1 تا $9
Section titled “مثال ۳: آرگومانهای خط فرمان، $1 تا $9”هر کلمهای که بعد از اسم اسکریپت مینویسی، به ترتیب در $1، $2 و … میرود. $0 خود اسم اسکریپت و $# تعداد آرگومانهاست:
#!/usr/bin/env bash# args.sh: show the positional parametersecho "script name (\$0): $0"echo "first (\$1): $1"echo "second (\$2): $2"echo "third (\$3): $3"echo "count (\$#): $#"cd ~/bashlabchmod +x args.shecho "--- ./args.sh apple banana cherry"./args.sh apple banana cherryecho "--- آرگومان دارای فاصله باید کوتیشن داشته باشد:"./args.sh "big apple" bananaecho "--- بدون آرگومان:"./args.sh--- ./args.sh apple banana cherryscript name ($0): ./args.shfirst ($1): applesecond ($2): bananathird ($3): cherrycount ($#): 3--- آرگومان دارای فاصله باید کوتیشن داشته باشد:script name ($0): ./args.shfirst ($1): big applesecond ($2): bananathird ($3):count ($#): 2--- بدون آرگومان:script name ($0): ./args.shfirst ($1):second ($2):third ($3):count ($#): 0echo "$1" اولین آرگومان را چاپ میکند و echo "$#" تعدادشان را. کوتیشن را فراموش نکن: آنچه در کوتیشن است یک آرگومان میشود ("big apple" شد $1). آرگومانی که پاس ندادهای خالی است، نه خطا (مثل هر متغیر تعریفنشدهای). و از آرگومان دهم به بعد باید آکولاد بگذاری:
#!/usr/bin/env bashecho "\$10 -> $10"echo "\${10} -> ${10}"cd ~/bashlabchmod +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 “مثال ۴: $@ و $*، همهی آرگومانها”گاهی تعداد آرگومانها معلوم نیست (مثلاً «همهی این فایلها»). "$@" همهشان را میدهد، هر کدام جدا. این مهمترین چیزی است که از این درس باید یادت بماند:
#!/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"donecd ~/bashlabchmod +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 worldgot: bgot: c dسه آرگومان داریم (count: 3). "$@" سه تکهی سالم میدهد، "$*" همه را به یک رشته میچسباند، و $@ بدون کوتیشن دوباره در فاصلهها میشکند و پنج تکه میشود (همان مشکل درس قبل). قاعده: همیشه "$@". (حلقهی for در درس حلقهها کامل میآید؛ اینجا یعنی «برای هر آرگومان، این کار را بکن».)
کاربرد معمول "$@": ساختن اسکریپتی که همهی آرگومانهایش را به یک دستور دیگر پاس میدهد (یک wrapper):
#!/usr/bin/env bash# lsl.sh: ls -lh that passes every argument alongls -lh "$@"cd ~/bashlabchmod +x lsl.shmkdir -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 دو تا دور میاندازد:
#!/usr/bin/env bash# shift-demo.sh: shift drops $1 and moves the rest downecho "start: \$1=$1 \$#=$#"shiftecho "after shift: \$1=$1 \$#=$#"shift 2echo "after shift 2: \$1=$1 \$#=$#"cd ~/bashlabchmod +x shift-demo.sh./shift-demo.sh a b c d estart: $1=a $#=5after shift: $1=b $#=4after shift 2: $1=d $#=2کاربردش: وقتی آرگومان اول معنی ویژه دارد (مثلاً یک برچسب) و بقیه را میخواهی با "$@" به کار دیگری بدهی. این اسکریپت دستوری را اجرا میکند و قبلش یک برچسب لاگ میاندازد:
#!/usr/bin/env bash# with-label.sh: ./with-label.sh LABEL command [args...]label="$1"shiftecho "[$label] running: $*""$@"cd ~/bashlabchmod +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/hostnamelabel اولین آرگومان را گرفت، shift آن را دور انداخت، و حالا "$@" خودِ دستور (ls -l /etc/hostname) است که اجرا شد.
مثال ۶: پیام راهنما و اعتبارسنجی
Section titled “مثال ۶: پیام راهنما و اعتبارسنجی”اسکریپتی که آرگومان لازم دارد، باید وقتی نبود یا اشتباه بود راهنمایی بدهد و با کد خروج خطا تمام شود؛ نه اینکه با ورودی خالی کار خراب کند. این الگو را اکثر ابزارهای واقعی دارند:
#!/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 1fi
file="$1"
if [[ ! -f "$file" ]]; then echo "Error: '$file' is not a regular file" >&2 exit 2fi
size=$(stat -c %s "$file")lines=$(wc -l < "$file")echo "$file: $size bytes, $lines lines"cd ~/bashlabchmod +x file-info.shprintf 'one\ntwo\nthree\n' > sample.txtecho "--- درست:"./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>کد خروج: 1if را در درس بعد کامل میخوانی؛ فعلاً اینطور بخوانش: if [[ $# -ne 1 ]] یعنی «اگر تعداد آرگومانها یک نیست» (-ne = not equal)، و [[ ! -f "$file" ]] یعنی «اگر $file یک فایل معمولی نیست». نکتههای مهم:
>&2: پیام خطا را به خروجی خطا (stderr) میفرستد، نه خروجی عادی. اینطور اگر کسی خروجی اسکریپت را در pipe یا فایل بریزد، پیام خطا قاطی دادهها نمیشود (درس ورودی، خروجی و redirect).exit 1وexit 2: هر کد معنیای دارد که خودت تعیین میکنی (قرارداد رایج: ۱ استفادهی غلط یا خطای عمومی، ۲ ورودی نامعتبر). cron و اسکریپتهای دیگر فقط به همین عدد نگاه میکنند.$0در راهنما: اسم اسکریپت همانطور که صدایش زدهاند در راهنما میآید.
مثال ۷: آرگومان یا پرسش؟ هر دو
Section titled “مثال ۷: آرگومان یا پرسش؟ هر دو”بهترین اسکریپتها هر دو راه را بلدند: آرگومان اگر داده شد، و اگر نه، بپرسند (فقط وقتی کسی پای ترمینال است). این الگو را هم ببین:
#!/usr/bin/env bash# greet.sh: use the first argument as the name; ask only if it is missingif [[ $# -ge 1 ]]; then name="$1"else read -r -p "Your name: " namefiecho "Hello, $name!"cd ~/bashlabchmod +x greet.shecho "--- با آرگومان:"./greet.sh Aliecho "--- از pipe (ورودی استاندارد):"echo "Sara" | ./greet.shecho "--- مثل cron: ورودی تهی (هیچکس جواب نمیدهد):"./greet.sh < /dev/nullecho "--- تعاملی (با tmux):"start_terminaltmux send-keys -t demo "./greet.sh" Entersleep 0.7tmux send-keys -t demo "Reza" Entersleep 0.7screen--- با آرگومان:Hello, Ali!--- از pipe (ورودی استاندارد):Hello, Sara!--- مثل cron: ورودی تهی (هیچکس جواب نمیدهد):Hello, !--- تعاملی (با tmux):$ ./greet.shYour name: RezaHello, Reza!$یک نکتهی دقیق: وقتی ورودی از pipe میآید، read -p پرسش را چاپ نمیکند (پرسش فقط وقتی نشان داده میشود که ورودی یک ترمینال باشد؛ در ثبت بالا هم پرسش نیامد). و در حالت cron که ورودی تهی است، read بدون جواب برمیگردد و name خالی میماند (Hello, !). برای همین هر اسکریپتی که قرار است خودکار اجرا شود باید همهچیز را از آرگومان بگیرد و فقط اگر کسی پای ترمینال بود بپرسد. راه سادهی دادن مقدار پیشفرض، ${1:-World} است که در درس «کار با رشتهها» کامل میشود:
cd ~/bashlabcat > greet2.sh <<'EOF'#!/usr/bin/env bashname="${1:-World}"echo "Hello, $name!"EOFchmod +x greet2.sh./greet2.sh./greet2.sh AliHello, World!Hello, Ali!پشت پرده: آرگومانها چطور به اسکریپت میرسند؟
Section titled “پشت پرده: آرگومانها چطور به اسکریپت میرسند؟”وقتی ./args.sh apple "big banana" را میزنی:
- شل خط را به کلمهها میشکند، کوتیشنها و متغیرها را باز میکند (درس قبل)، و به هسته میگوید: برنامهی
./args.shرا با این فهرست آرگومانها (argv) اجرا کن. - هسته آنها را در حافظهی پروسهی جدید میگذارد.
- bash که اسکریپت را اجرا میکند، از روی همین فهرست
$0،$1،$2و … را میسازد.
یعنی آرگومانها بخشی از خود پروسهاند. و همینطور که در حافظهی پروسه هستند، دیگران هم میتوانند ببینندشان:
cat > ~/bashlab/slowtool.sh <<'EOF'#!/usr/bin/env bashsleep 20EOFchmod +x ~/bashlab/slowtool.sh~/bashlab/slowtool.sh --user ali --password "s3cret pass" &pid=$!disownecho "--- ps (هر کاربری روی سیستم این را میبیند):"ps -o args= -p $pidecho "--- /proc/PID/cmdline (آرگومانها با NUL جدا شدهاند):"tr '\0' '|' < /proc/$pid/cmdline; echoecho "--- مجوزها: 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) هم میمانند.
جدولهای مرجع
Section titled “جدولهای مرجع”پارامترهای ویژهی اسکریپت:
| پارامتر | یعنی |
|---|---|
$0 |
اسم اسکریپت (همانطور که اجرا شد) |
$1 … $9، ${10} … |
آرگومان اول، دوم، … (از دهم به بعد با آکولاد) |
$# |
تعداد آرگومانها (بدون $0) |
"$@" |
همهی آرگومانها، هر کدام جدا (این را به کار ببر) |
"$*" |
همهی آرگومانها در یک رشته |
shift / shift N |
دور انداختن اولین (یا N تای اول) آرگومان |
$? |
کد خروج آخرین دستور |
$$ |
شمارهی پروسهی خود اسکریپت |
$! |
شمارهی پروسهی آخرین کار پسزمینه (&) |
گزینههای read که در این درس دیدی:
| گزینه | کار |
|---|---|
-p "متن" |
قبل از خواندن، پرسش را نشان بده (فقط وقتی ورودی ترمینال است) |
-s |
تایپ را نشان نده (رمز) |
-r |
بکاسلش را معمولی بخوان (همیشه بنویس) |
-t N |
بعد از N ثانیه منصرف شو (کد خروج > ۱۲۸) |
-n N |
فقط N کاراکتر بخوان (بدون Enter) |
IFS=: read ... |
جداکنندهی دلخواه برای شکستن خط به متغیرها |
| بدون اسم متغیر | جواب در REPLY |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) خواندن با pipe، متغیر ناپدید میشود
Section titled “۱) خواندن با pipe، متغیر ناپدید میشود”echo "Ali 30" | read name ageecho "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 “۳) پیمایش آرگومانها با $@ بدون کوتیشن”#!/usr/bin/env bashecho "--- for arg in \$@ (wrong):"for arg in $@; do echo "[$arg]"; doneecho "--- for arg in \"\$@\" (right):"for arg in "$@"; do echo "[$arg]"; donecd ~/bashlabchmod +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 ~/bashlabcat > danger.sh <<'EOF'#!/usr/bin/env bashecho rm -rf $1/*EOFchmod +x danger.sh./danger.sh | cut -c1-60rm -rf /bin /bin.usr-is-merged /boot /dev /etc /home /lib /lراهحل: همیشه $# را بررسی کن (مثال ۶)، یا ${1:?usage: script <dir>} بنویس که در نبود آرگومان با پیام متوقف شود.
۵) shift وقتی آرگومانی نمانده
Section titled “۵) shift وقتی آرگومانی نمانده”cd ~/bashlabcat > shift2.sh <<'EOF'#!/usr/bin/env bashecho "args: $#"shiftecho "after 1st shift: status=$? args=$#"shiftecho "after 2nd shift: status=$? args=$#"echo "--- با shopt -s shift_verbose:"shopt -s shift_verboseshift 3echo "after shift 3: status=$? args=$#"EOFchmod +x shift2.sh./shift2.sh only-oneargs: 1after 1st shift: status=0 args=0after 2nd shift: status=1 args=0--- با shopt -s shift_verbose:./shift2.sh: line 9: shift: 3: shift count out of rangeafter shift 3: status=1 args=0اولین shift آرگومان را برداشت. دومی که چیزی برای برداشتن نداشت بیصدا با کد ۱ شکست خورد (پیام خطا ندارد!) و اسکریپت ادامه داد. فقط اگر shopt -s shift_verbose را روشن کنی، shift پیام shift count out of range میدهد. راهحل: اگر لازم است، قبل از shift ببین $# کافی است، و در اسکریپت جدی به کد خروج shift توجه کن.
۶) رمز در آرگومان
Section titled “۶) رمز در آرگومان”در بخش «پشت پرده» دیدی: رمز در ps پیداست. راهحل: read -s.
اسکریپت hello-name.sh را بنویس که اسم را بهعنوان آرگومان بگیرد و Hello, اسم! چاپ کند. سه بار اجرایش کن: با Ali، با "Ali Ahmadi" (در کوتیشن) و بدون آرگومان.
دیدن جواب
mkdir -p ~/bashlab && cd ~/bashlabcat > hello-name.sh <<'EOF'#!/usr/bin/env bash# hello-name.sh: greet the name given as the first argumentecho "Hello, $1!"EOFchmod +x hello-name.sh./hello-name.sh Ali./hello-name.sh "Ali Ahmadi"./hello-name.shHello, Ali!Hello, Ali Ahmadi!Hello, !بدون آرگومان $1 خالی است و Hello, ! میگیری. راه بهتر در مثال ۶ و ۷ بود: یا بررسی $#، یا مقدار پیشفرض ${1:-World}.
تمرین اصلی درس: اسکریپت count-files.sh که اسم یک پوشه را بگیرد و تعداد فایلهایش را بگوید (فقط فایلهای معمولی، نه زیرپوشهها). اگر آرگومان نداد یا پوشه وجود نداشت، پیام مناسب روی stderr و کد خروج ۱ و ۲.
دیدن جواب
mkdir -p ~/bashlab/photos/2026 && cd ~/bashlabtouch photos/a.jpg photos/b.jpg photos/notes.txt photos/2026/c.jpgcat > 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 1fi
dir="$1"
if [[ ! -d "$dir" ]]; then echo "Error: '$dir' is not a directory" >&2 exit 2fi
count=$(find "$dir" -maxdepth 1 -type f | wc -l)echo "$dir has $count files"EOFchmod +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کد خروج: 0Usage: ./count-files.sh <directory>کد خروج: 1Error: 'nowhere' is not a directoryکد خروج: 2photos/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 ~/bashlabcat > sum.sh <<'EOF'#!/usr/bin/env bash# sum.sh: add up all numeric argumentstotal=0for n in "$@"; do total=$((total + n))doneecho "numbers: $#, total: $total"EOFchmod +x sum.sh./sum.sh 10 20 30./sum.sh 5./sum.shnumbers: 3, total: 60numbers: 1, total: 5numbers: 0, total: 0بدون آرگومان، حلقه اصلاً اجرا نمیشود و جمع صفر است. (اگر آرگومان عدد نباشد چه میشود؟ در درس قبل دیدی: abc بیصدا صفر حساب میشود؛ در درس شرطها یاد میگیری ورودی را بررسی کنی.)
اسکریپت mkproject.sh: آرگومان اول اسم پروژه و بقیه اسم زیرپوشهها هستند (./mkproject.sh shop src docs tests). پوشهی پروژه و زیرپوشههایش را بسازد و ساختار را چاپ کند. اگر حتی اسم پروژه داده نشد، راهنما بدهد و با کد ۱ تمام شود. از shift و "$@" استفاده کن.
دیدن جواب
cd ~/bashlabcat > 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 1fi
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 | sortEOFchmod +x mkproject.sh./mkproject.sh shop src docs testsecho "--- بدون آرگومان:"./mkproject.sh; echo "کد خروج: $?"echo "--- فقط اسم پروژه:"./mkproject.sh empty-projectCreated shop with 3 sub-folders:shop/docsshop/srcshop/tests--- بدون آرگومان:Usage: ./mkproject.sh <project> [subfolder...]کد خروج: 1--- فقط اسم پروژه:Created empty-project with 0 sub-folders:دقت کن $# بعد از shift تعداد زیرپوشهها است (نه کل آرگومانها)، و هر mkdir -p با کوتیشن نوشته شده تا اسمهای دارای فاصله هم کار کنند.
آزمونک
Section titled “آزمونک”کدام یک برای پاسدادن همهی آرگومانهای اسکریپت به یک دستور دیگر، بدون خرابشدن آرگومانهای دارای فاصله، درست است؟
فقط «$@» با کوتیشن هر آرگومان را بهصورت یک تکهی جدا نگه میدارد؛ $@ بدون کوتیشن دوباره در فاصلهها میشکند و «$*» همه را یک رشته میکند.
در ./backup.sh /data "my dir" مقدار $# و $2 چیست؟
$# تعداد آرگومانهاست (بدون $0) و آنچه در کوتیشن است یک آرگومان میشود.
اسکریپتی با دستور read -p (پرسش Name:) را با echo Ali | ./script.sh اجرا میکنی. پرسش روی صفحه میآید؟
برای همین اسکریپتهای خودکار باید همهچیز را از آرگومان بگیرند.
در اسکریپتی که ۱۱ آرگومان گرفته، echo $10 چه چاپ میکند؟
برای آرگومان دهم باید ${10} بنویسی.
shift چه میکند؟
برای وقتی که آرگومان اول معنی ویژهای دارد و بقیه را با "$@" به کار دیگری میدهی.
echo "Ali 30" | read name age و بعد echo $name چه میدهد؟
راهحل: read name age <<< "Ali 30".
چرا رمز را نباید بهصورت آرگومان به اسکریپت داد (./login.sh --password secret)؟
با read -s بپرس، یا از فایل با مجوز 600 بخوان.
جمعبندی
Section titled “جمعبندی”- آرگومانها راه اصلی ورودیاند چون اسکریپت را خودکارپذیر میکنند:
$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' "$@" | دیدن اینکه هر آرگومان چطور رسید |