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

اولین اسکریپت

توی این درس یاد می‌گیری دستورهایی را که هر روز دستی تایپ می‌کنی در یک اسکریپت (script) بنویسی: یک فایل متنی ساده که شل (shell) آن را خط‌به‌خط اجرا می‌کند. می‌بینی خط اول، یعنی shebang (#!/usr/bin/env bash)، چه می‌کند، چرا اسکریپت به مجوز اجرا (chmod +x script.sh) نیاز دارد، چه روش‌های اجرایی هست (./script.sh، bash script.sh و فقط با اسم) و هر کدام چه فرقی دارند، و با کامنت (#) برای آدم‌ها یادداشت می‌نویسی. تمرین: اسکریپتی که اسم کاربر، تاریخ و فضای دیسک را چاپ کند.

مسئله: همان دستورها، هر روز

Section titled “مسئله: همان دستورها، هر روز”

تا اینجا هر دستور را یک بار توی ترمینال تایپ کرده‌ای و تمام. ولی کارهایی هست که هر روز یا هر هفته تکرار می‌شود: بکاپ گرفتن، چک‌کردن دیسک و حافظه، ساختن پوشه‌ی یک پروژه‌ی جدید با همان ساختار همیشگی. تایپ دستیِ این‌ها دو ایراد دارد: کند است و خطا می‌خورد (یک روز مرحله‌ی سوم یادت می‌رود). راه‌حل این است که دستورها را یک بار در یک فایل بنویسی و هر وقت خواستی همان فایل را اجرا کنی. به چنین فایلی اسکریپت می‌گویند.

زبانی که در این دوره می‌نویسی Bash است: همان شلی که در درس‌های لینوکس با آن کار می‌کردی. یعنی هر دستوری که توی ترمینال می‌زنی، عیناً داخل اسکریپت هم کار می‌کند. اسکریپت‌نویسی فقط «همان دستورها + ساختارهای برنامه‌نویسی» است (متغیر، شرط، حلقه، تابع)، که در درس‌های بعد می‌آیند. کار این درس فقط یک چیز است: اولین اسکریپت را از فایل خالی تا اجرا کامل کنی و بفهمی وسطش چه می‌گذرد.

اسکریپت مثل یک دستور پخت است. خود کاغذ غذا نمی‌پزد؛ یک آشپز باید آن را بردارد و خط‌به‌خط انجام بدهد. فایل اسکریپت همان کاغذ است، bash آشپز است، و خط اولِ کاغذ می‌گوید «این دستور را به کدام آشپز بدهید». آشپز دقیقاً همان چیزی را که نوشته‌ای انجام می‌دهد؛ نه کمتر، نه بیشتر، و بدون اینکه خودش چیزی حدس بزند.

اسکریپت چطور کار می‌کند؟

Section titled “اسکریپت چطور کار می‌کند؟”

چند اصطلاح که در این درس زیاد می‌شنوی:

اصطلاح یعنی چه
اسکریپت (script) فایل متنی که دستورها را پشت‌سرهم دارد
شل (shell) برنامه‌ای که دستورها را می‌خواند و اجرا می‌کند؛ رایج‌ترین‌شان Bash است
مفسر (interpreter) برنامه‌ای که متن یک برنامه را می‌خواند و بدون ترجمه‌ی قبلی، خط‌به‌خط اجرا می‌کند؛ مثل bash و python3
shebang خط اول اسکریپت: #! و بعدش مسیر مفسر
مجوز اجرا (execute permission) بیت x روی فایل؛ بدون آن نمی‌شود فایل را مثل یک برنامه اجرا کرد
کد خروج (exit status) عددی که هر برنامه و اسکریپت هنگام پایان به شل پدرش برمی‌گرداند؛ ۰ یعنی موفق
اسکریپت فقط یک فایل متنی است. مفسر (bash) آن را باز می‌کند، خط‌به‌خط اجرا می‌کند و در پایان یک کد خروج به شلی که صدایش زده برمی‌گرداند.

همه‌ی مثال‌ها داخل یک پوشه‌ی تمرین اتفاق می‌افتند. (دستورهای mkdir، ls -l، chmod و cd را از درس‌های لینوکس می‌شناسی.)

مثال ۱: ساده‌ترین اسکریپت، با bash hello.sh

Section titled “مثال ۱: ساده‌ترین اسکریپت، با bash hello.sh”

با ویرایشگر دلخواهت (مثلاً nano ~/bashlab/hello.sh) این فایل را بساز:

/home/ali/bashlab/hello.sh
#!/usr/bin/env bash
echo "Hello from my first script"
whoami
pwd

سه دستور پشت‌سرهم: یک پیام، اسم کاربر و پوشه‌ی فعلی. حالا اجرایش کن:

Terminal window
cd ~/bashlab
ls -l hello.sh
echo "--- اجرا با bash:"
bash hello.sh
خروجی
-rw-r--r-- 1 ali ali 65 Oct 4 09:31 hello.sh
--- اجرا با bash:
Hello from my first script
ali
/home/ali/bashlab

bash hello.sh یعنی «برنامه‌ی bash، این فایل را بخوان و دستورهایش را اجرا کن». دستورها به ترتیب اجرا شدند و خروجی هر کدام پشت سر هم آمد. دقت کن فایل مجوز اجرا ندارد (-rw-r--r--، هیچ x ای نیست) و باز هم کار کرد: چون برنامه‌ای که اجرا می‌شود bash است و hello.sh فقط یک متن است که bash آن را می‌خواند؛ برای خواندن فقط مجوز r لازم است.

خط اول (#!/usr/bin/env bash) را فعلاً همان‌طور بنویس؛ مثال ۳ می‌گوید چه می‌کند. برای bash hello.sh اثری ندارد، چون bash آن را یک کامنت می‌بیند (با # شروع می‌شود).

مثال ۲: مجوز اجرا و ./hello.sh

Section titled “مثال ۲: مجوز اجرا و ./hello.sh”

bash hello.sh کار می‌کند ولی هر بار باید اسم bash را هم بنویسی. دوست داری اسکریپت مثل یک دستور واقعی اجرا شود: فقط ./hello.sh. اول بدون مجوز امتحان کن:

Terminal window
cd ~/bashlab
./hello.sh; echo "کد خروج: $?"
echo "--- مجوزهای فعلی:"
ls -l hello.sh
chmod +x hello.sh
echo "--- بعد از chmod +x:"
ls -l hello.sh
./hello.sh; echo "کد خروج: $?"
خروجی
bash: ./hello.sh: Permission denied
کد خروج: 126
--- مجوزهای فعلی:
-rw-r--r-- 1 ali ali 65 Oct 4 09:31 hello.sh
--- بعد از chmod +x:
-rwxr-xr-x 1 ali ali 65 Oct 4 09:31 hello.sh
Hello from my first script
ali
/home/ali/bashlab
کد خروج: 0

بدون x، شل فایل را اجرا نمی‌کند: Permission denied و کد خروج ۱۲۶ («پیدا شد، ولی اجرا نشد»). بعد از chmod +x hello.sh سه x اضافه شد (برای مالک، گروه و بقیه) و حالا ./hello.sh اجرا می‌شود. ./ یعنی «همین پوشه»؛ در مثال ۵ می‌بینی چرا لازم است.

اگر نمی‌خواهی به همه مجوز بدهی، chmod راه‌های دیگری هم دارد (جزئیات در مجوزهای فایل):

Terminal window
cd ~/bashlab
cp hello.sh t.sh
for mode in u+x 755 700; do
chmod 644 t.sh
chmod $mode t.sh
stat -c "chmod $mode -> %a %A" t.sh
done
rm t.sh
خروجی
chmod u+x -> 744 -rwxr--r--
chmod 755 -> 755 -rwxr-xr-x
chmod 700 -> 700 -rwx------

u+x فقط برای مالک x اضافه می‌کند (744)، 755 یعنی مالک همه‌چیز و بقیه خواندن و اجرا (استاندارد برای اسکریپت‌های مشترک)، و 700 یعنی فقط مالک (برای اسکریپت‌های شخصی یا حساس).

مثال ۳: shebang، «این فایل را با چه برنامه‌ای اجرا کن»

Section titled “مثال ۳: shebang، «این فایل را با چه برنامه‌ای اجرا کن»”

وقتی ./hello.sh را می‌زنی، دیگر کسی نمی‌گوید فایل را با کدام برنامه باز کند؛ پس خود فایل باید بگوید. خط اولی که با #! شروع می‌شود همین کار را می‌کند و فقط مخصوص bash نیست. این هم یک اسکریپت Python:

/home/ali/bashlab/hi.py
#!/usr/bin/env python3
print("Hello from Python")
Terminal window
cd ~/bashlab
chmod +x hi.py
echo "--- ./hi.py (shebang: python3):"
./hi.py
echo "--- همان فایل را به‌زور با bash اجرا کنیم:"
bash hi.py
خروجی
--- ./hi.py (shebang: python3):
Hello from Python
--- همان فایل را به‌زور با bash اجرا کنیم:
hi.py: line 2: syntax error near unexpected token `"Hello from Python"'
hi.py: line 2: `print("Hello from Python")'

./hi.py درست کار کرد، چون خط اولش گفت «با python3 بخوانش». ولی bash hi.py یعنی «bash، این فایل را بخوان»، و bash با خط دومِ Python (print("...")) به مشکل خورد. shebang فقط وقتی کار می‌کند که فایل را مستقیم اجرا کنی (./hi.py)؛ اگر مفسر را خودت جلوی اسم فایل بنویسی، مفسرِ تو حکم است و shebang نادیده گرفته می‌شود.

حالا یک اشتباه معروف: shebang برابر /bin/sh با کدی که مخصوص bash است. روی Ubuntu و Debian، sh در واقع یک شل ساده‌تر و سریع‌تر به اسم dash است، نه bash:

/home/ali/bashlab/check-sh.sh
#!/bin/sh
name="ali"
if [[ $name == "ali" ]]; then
echo "hello ali"
fi
Terminal window
cd ~/bashlab
chmod +x check-sh.sh
echo "--- sh در این سیستم چیست؟"
ls -l /bin/sh
echo "--- اجرا با shebang برابر sh:"
./check-sh.sh; echo "کد خروج: $?"
echo "--- shebang را به bash عوض می‌کنیم:"
sed -i '1s|.*|#!/usr/bin/env bash|' check-sh.sh
head -n 1 check-sh.sh
./check-sh.sh; echo "کد خروج: $?"
خروجی
--- sh در این سیستم چیست؟
lrwxrwxrwx 1 root root 4 Mar 31 2024 /bin/sh -> dash
--- اجرا با shebang برابر sh:
./check-sh.sh: 3: [[: not found
کد خروج: 0
--- shebang را به bash عوض می‌کنیم:
#!/usr/bin/env bash
hello ali
کد خروج: 0

[[ ... ]] (که در درس شرط‌ها می‌خوانی) امکانی مخصوص bash است و dash آن را نمی‌شناسد: [[: not found. نکته‌ی ترسناک: کد خروج باز هم ۰ بود؛ یعنی اسکریپتِ خراب بی‌سروصدا «موفق» گزارش شد. (چرا؟ شرط if شکست خورد و دستور آخر اسکریپت، یعنی fi، موفق بود.) راه‌حل: برای کدی که امکانات bash دارد همیشه shebang بنویس bash.

شکل‌های رایج shebang را در بخش «جدول‌های مرجع» مقایسه کرده‌ام. فعلاً این را بدان: #!/usr/bin/env bash یعنی «برنامه‌ی env را اجرا کن و از او بخواه در PATH دنبال bash بگردد»، و برای همین روی هر سیستمی که bash در PATH باشد کار می‌کند، حتی اگر bash در /bin نباشد.

Terminal window
/bin/bash --version | head -n 1
خروجی
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin26)

هر چه بعد از # بیاید تا آخر همان خط کامنت است و bash نادیده‌اش می‌گیرد. کامنت برای آدم‌هاست: برای کسی که شش ماه دیگر (یا خودت) اسکریپت را باز می‌کند.

/home/ali/bashlab/comments.sh
#!/usr/bin/env bash
# ------------------------------------------------
# comments.sh: demo of the different comment forms
# ------------------------------------------------
echo "one" # a comment at the end of a line
# echo "disabled" <- a commented-out command does not run
echo "price: 5#" # '#' inside quotes is just text
echo color #ff0000 # here '#' starts a comment
echo a#b # '#' in the middle of a word is just text
echo "two"
: <<'COMMENT'
Bash has no real multi-line comment.
This here-document trick turns a whole block into a no-op.
COMMENT
echo "three"
Terminal window
cd ~/bashlab
bash comments.sh
خروجی
one
price: 5#
color
a#b
two
three

چند نکته از همین خروجی:

  • echo "disabled" که جلویش # بود اجرا نشد؛ برای غیرفعال‌کردن موقت یک دستور، # جلویش بگذار.
  • # فقط اول یک کلمه کامنت شروع می‌کند. a#b یک کلمه‌ی عادی است و داخل گیومه هم # متن معمولی است. به همین دلیل echo color #ff0000 فقط color را چاپ کرد: #ff0000 (یک رنگ CSS!) کامنت حساب شد. برای چاپ آن را در گیومه بگذار.
  • Bash کامنت چندخطی ندارد. معمول‌ترین کار # روی هر خط است. شکل : <<'COMMENT' ... COMMENT که آخر فایل دیدی ترفندی است که در درس here-doc می‌فهمی چرا کار می‌کند (: یک دستور بی‌کار است و here-doc متن را به آن می‌دهد).
  • shebang هم از دید bash یک کامنت است؛ برای همین bash hello.sh از خط اول نمی‌نالد.

چه چیزی کامنت کنیم؟ کامنتی که فقط تکرار خود کد است فایده ندارد (# چاپ می‌کند بالای echo). کامنت خوب چرا را می‌گوید: «این دستور دو بار تلاش می‌کند چون API بعد از بیکاری اولین درخواست را رد می‌کند»، یا بالای فایل می‌گوید اسکریپت چه می‌کند و چطور اجرا می‌شود.

مثال ۵: اجرا فقط با اسم، PATH

Section titled “مثال ۵: اجرا فقط با اسم، PATH”

./hello.sh راحت‌تر از bash hello.sh است ولی هنوز باید جایت را بدانی. ابزارهای واقعی (ls، grep) را از هر پوشه‌ای فقط با اسم اجرا می‌کنی. چطور اسکریپت خودت هم همین‌طور شود؟ اول ببین فقط اسم چه می‌کند:

Terminal window
cd ~/bashlab
hello.sh; echo "کد خروج: $?"
echo "--- پوشه‌هایی که شل برای پیداکردن دستور می‌گردد (PATH):"
echo "$PATH" | tr ':' '\n'
خروجی
bash: hello.sh: command not found
کد خروج: 127
--- پوشه‌هایی که شل برای پیداکردن دستور می‌گردد (PATH):
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin

command not found و کد خروج ۱۲۷. شل برای اسمی که / ندارد فقط پوشه‌های PATH را می‌گردد، و پوشه‌ی فعلی (.) در آن نیست. این عمدی است: اگر بود، یک فایل ls که کسی توی پوشه‌ی دانلودی‌ات گذاشته بود به‌جای ls واقعی اجرا می‌شد. (PATH را در درس متغیرهای محیطی کامل خوانده‌ای.) پس اسکریپت را به یکی از پوشه‌های PATH ببر. پوشه‌ی شخصی ~/bin برای همین است:

Terminal window
cd ~/bashlab
mkdir -p ~/bin
cp hello.sh ~/bin/hello
export PATH="$HOME/bin:$PATH"
echo "--- شل حالا دستور hello را کجا پیدا می‌کند؟"
type hello
echo "--- از هر پوشه‌ای:"
cd /tmp && hello
cd / && hello
خروجی
--- شل حالا دستور hello را کجا پیدا می‌کند؟
hello is /home/ali/bin/hello
--- از هر پوشه‌ای:
Hello from my first script
ali
/tmp
Hello from my first script
ali
/

سه کار کردیم: کپی کردن به ~/bin با اسم بدون .sh (دستورهای واقعی پسوند ندارند؛ .sh فقط قرارداد است و شل به آن کاری ندارد)، اضافه‌کردن ~/bin به اول PATH، و اجرا. دقت کن pwd داخل اسکریپت هر بار پوشه‌ای را نشان داد که از آن اجرایش کردیم (/tmp و /). برای دائمی‌شدن، خط export PATH=... را به ~/.bashrc اضافه کن.

مثال ۶: چرا cd داخل اسکریپت ترمینال مرا جابه‌جا نمی‌کند؟

Section titled “مثال ۶: چرا cd داخل اسکریپت ترمینال مرا جابه‌جا نمی‌کند؟”

یک سؤال همیشگی مبتدی‌ها: «اسکریپتی نوشتم که cd می‌کند، ولی بعد از اجرا هنوز همان‌جا هستم!». این یک ویژگی است، نه باگ:

/home/ali/bashlab/where.sh
#!/usr/bin/env bash
echo "script PID: $$"
cd /tmp
echo "script moved to:"
pwd
Terminal window
cd ~/bashlab
chmod +x where.sh
echo "shell PID: $$"
echo "--- اجرا مثل برنامه:"
./where.sh
echo "بعد از اجرا من اینجام:"; pwd
echo "--- با source:"
source ./where.sh
echo "حالا shell من جابه‌جا شد:"; pwd
خروجی
shell PID: 21041
--- اجرا مثل برنامه:
script PID: 21106
script moved to:
/tmp
بعد از اجرا من اینجام:
/home/ali/bashlab
--- با source:
script PID: 21041
script moved to:
/tmp
حالا shell من جابه‌جا شد:
/tmp

$$ شماره‌ی پروسه‌ی (PID) خود shell یا اسکریپت جاری است. با ./where.sh شماره‌ی پروسه‌ی اسکریپت با شماره‌ی shell من فرق دارد: شل برای اجرای اسکریپت یک پروسه‌ی فرزند می‌سازد که پوشه‌ی جاری و متغیرهای خودش را دارد؛ فرزند هر چه عوض کند، با تمام‌شدنش از بین می‌رود و به پدر نمی‌رسد. با source ./where.sh (یا . ./where.sh) فایل داخل همین shell اجرا می‌شود (شماره‌ی پروسه‌اش با shell برابر است) و برای همین cd اثر کرد. (همین فرق را در درس متغیرهای محیطی برای فایل‌های تنظیمات دیدی؛ برای همین source ~/.bashrc می‌زنی.)

مثال ۷: اسکریپت واقعی، گزارش صبحگاهی

Section titled “مثال ۷: اسکریپت واقعی، گزارش صبحگاهی”

حالا سناریوی ابتدای درس: اسکریپتی که هر صبح وضعیت یک سرور را چاپ می‌کند. همین کاری که ساختن فایل، shebang، کامنت و مجوز اجرا را کنار هم می‌گذارد:

/home/ali/bashlab/morning-check.sh
#!/usr/bin/env bash
# morning-check.sh: a quick health report for one server
# Usage: ./morning-check.sh
echo "=== Server report ==="
date
echo
echo "--- User running this script:"
whoami
echo
echo "--- Disk (root partition):"
df -h /
echo
echo "--- Memory:"
free -h
echo
echo "--- Uptime and load:"
uptime
Terminal window
cd ~/bashlab
chmod +x morning-check.sh
./morning-check.sh
خروجی
=== Server report ===
Sun Oct 4 09:31:59 UTC 2026
--- User running this script:
ali
--- Disk (root partition):
Filesystem Size Used Avail Use% Mounted on
overlay 453G 23G 408G 6% /
--- Memory:
total used free shared buff/cache available
Mem: 3.8Gi 1.4Gi 1.3Gi 13Mi 1.4Gi 2.5Gi
Swap: 1.0Gi 217Mi 806Mi
--- Uptime and load:
09:31:59 up 1 day, 12:54, 0 user, load average: 0.04, 0.08, 0.18

خطِ خالی بین بخش‌ها با echo تنها (بدون آرگومان) ساخته شده؛ یک echo بدون متن فقط یک خط خالی چاپ می‌کند. هر بخش یک دستور آشنا از درس‌های لینوکس است؛ ارزش اسکریپت در یک‌جا بودن و تکرارپذیری است. اگر دستوری را اشتباه نوشته باشی چه؟ پیش از اجرا می‌توانی فقط نحو فایل را بررسی کنی:

cd ~/bashlab
cat > broken.sh <<'EOF'
#!/usr/bin/env bash
if true; then
echo "inside if"
EOF
bash -n broken.sh; echo "کد خروج bash -n: $?"
echo "--- فایل سالم:"
bash -n morning-check.sh; echo "کد خروج bash -n: $?"
خروجی
broken.sh: line 4: syntax error: unexpected end of file
کد خروج bash -n: 2
--- فایل سالم:
کد خروج bash -n: 0

bash -n (از no execute) اسکریپت را اجرا نمی‌کند، فقط می‌خواند و ایراد نحوی را می‌گیرد: اینجا fi فراموش شده بود و خطا می‌گوید فایل بی‌موقع تمام شد. (برای ایرادهای منطقی ابزارهای قوی‌تری هست که در درس دیباگ می‌خوانی.)

هر اسکریپت وقتی تمام می‌شود، مثل هر دستور دیگری یک کد خروج دارد که با echo $? می‌خوانی؛ ۰ یعنی موفق و هر عدد دیگر یعنی خطا. اما کدام عدد؟

cd ~/bashlab
cat > exit-demo.sh <<'EOF'
#!/usr/bin/env bash
echo "step 1"
ls /no/such/dir
echo "step 2: still runs after the error"
EOF
chmod +x exit-demo.sh
./exit-demo.sh; echo "کد خروج اسکریپت: $?"
cat > exit-last.sh <<'EOF'
#!/usr/bin/env bash
echo "step 1"
ls /no/such/dir
EOF
chmod +x exit-last.sh
./exit-last.sh; echo "کد خروج اسکریپت: $?"
cat > exit-custom.sh <<'EOF'
#!/usr/bin/env bash
echo "disk is almost full!"
exit 3
EOF
chmod +x exit-custom.sh
./exit-custom.sh; echo "کد خروج اسکریپت: $?"
خروجی
step 1
ls: cannot access '/no/such/dir': No such file or directory
step 2: still runs after the error
کد خروج اسکریپت: 0
step 1
ls: cannot access '/no/such/dir': No such file or directory
کد خروج اسکریپت: 2
disk is almost full!
کد خروج اسکریپت: 3

دو چیز مهم:

  1. Bash به‌صورت پیش‌فرض بعد از خطا متوقف نمی‌شود. ls شکست خورد، ولی خط بعدی هم اجرا شد.
  2. کد خروج اسکریپت = کد خروج آخرین دستور. در اسکریپت اول آخرین دستور echo بود که موفق است، پس اسکریپت با ۰ تمام شد؛ با اینکه وسطش خطا داشت. در اسکریپت دوم آخرین دستور ls بود (کد ۲). و exit 3 به هر اسکریپتی اجازه می‌دهد خودش کد را مشخص کند.

برای چه مهم است؟ چون cron، سیستم‌های مانیتورینگ و اسکریپت‌های دیگر فقط به همین عدد نگاه می‌کنند. اسکریپتی که با خطا هم ۰ بدهد، خرابی را پنهان می‌کند. راه‌حلش (set -e و بقیه‌ی ابزارها) در درس «مدیریت خطا» است؛ این درس را همین‌قدر نگه دار: بعد از هر اجرا echo $? را نگاه کن.

پشت پرده: وقتی ./hello.sh را می‌زنی

Section titled “پشت پرده: وقتی ./hello.sh را می‌زنی”

مراحل دقیق چیست؟

  1. شل (bash‌ای که تایپ می‌کنی) یک پروسه‌ی فرزند می‌سازد (fork) و از هسته‌ی لینوکس (kernel) می‌خواهد فایل ./hello.sh را اجرا کند (execve).
  2. هسته دو بایت اول فایل را نگاه می‌کند. اگر #! باشد (در هگز 23 21؛ به آن «عدد جادویی» یا magic number می‌گویند)، بقیه‌ی خط اول را می‌خواند: مسیر مفسر و (حداکثر) یک آرگومان.
  3. هسته به‌جای خود فایل، مفسر را اجرا می‌کند و مسیر اسکریپت را به آن می‌دهد: /usr/bin/env bash ./hello.sh.
  4. env در PATH دنبال bash می‌گردد و خودش را با آن جایگزین می‌کند. حالا bash فایل را می‌خواند و خط‌به‌خط اجرا می‌کند.
  5. وقتی تمام شد، کد خروجش به شل پدر می‌رسد ($?).
مسیر اجرای ./hello.sh: شل فقط اسم فایل را به هسته می‌دهد؛ هسته با خواندن خط #! مفسر را پیدا می‌کند.

خودت ببین هسته چه می‌بیند:

Terminal window
cd ~/bashlab
echo "--- دو بایت اول فایل (هگز):"
head -c 2 hello.sh | od -An -tx1
echo "--- همان دو بایت به‌صورت متن:"
head -c 2 hello.sh; echo
echo "--- خط اول کامل:"
head -n 1 hello.sh
echo "--- ابزار file نوع فایل را از همین خط تشخیص می‌دهد:"
file hello.sh
خروجی
--- دو بایت اول فایل (هگز):
23 21
--- همان دو بایت به‌صورت متن:
#!
--- خط اول کامل:
#!/usr/bin/env bash
--- ابزار file نوع فایل را از همین خط تشخیص می‌دهد:
hello.sh: Bourne-Again shell script, ASCII text executable

فقط یک آرگومان. لینوکس هر چه بعد از مسیر مفسر بیاید را یک آرگومان می‌کند. پس #!/usr/bin/env bash -e به env می‌گوید دنبال برنامه‌ای به اسم bash -e (با فاصله!) بگردد:

Terminal window
cd ~/bashlab
printf '#!/usr/bin/env bash -e\necho "A: env + bash -e"\n' > opt-a.sh
printf '#!/usr/bin/env -S bash -e\necho "B: env -S"\n' > opt-b.sh
printf '#!/bin/bash -e\necho "C: direct path"\n' > opt-c.sh
chmod +x opt-a.sh opt-b.sh opt-c.sh
for f in opt-a.sh opt-b.sh opt-c.sh; do ./$f; echo "کد خروج: $?"; done
خروجی
/usr/bin/env: ‘bash -e’: No such file or directory
/usr/bin/env: use -[v]S to pass options in shebang lines
کد خروج: 127
B: env -S
کد خروج: 0
C: direct path
کد خروج: 0

حالت A خطا داد و env خودش پیشنهاد داد: -S رشته را به چند آرگومان می‌شکند (حالت B). در حالت C مسیر مستقیم به bash داده شده و -e تنها آرگومان اوست و کار می‌کند. بهترین راه: shebang را ساده نگه دار و گزینه‌ها را داخل اسکریپت با set بده (درس مدیریت خطا).

بدون shebang چه می‌شود؟ از داخل bash اتفاقی نمی‌افتد؛ چون وقتی هسته می‌گوید «این فایل قابل اجرا نیست» (ENOEXEC)، خود bash فایل را به‌عنوان اسکریپت bash اجرا می‌کند. ولی این رفتارِ bash است، نه قاعده‌ی سیستم:

Terminal window
cd ~/bashlab
printf 'echo "I have no shebang"\n' > noshe.sh
chmod +x noshe.sh
echo "--- از داخل bash:"
./noshe.sh; echo "کد خروج: $?"
echo "--- از داخل یک برنامه‌ی دیگر (Python):"
python3 -B -c 'import subprocess; subprocess.run(["./noshe.sh"])' 2>&1 | tail -n 1
خروجی
--- از داخل bash:
I have no shebang
کد خروج: 0
--- از داخل یک برنامه‌ی دیگر (Python):
OSError: [Errno 8] Exec format error: './noshe.sh'
Terminal window
mkdir -p /root/shebang-demo && cd /root/shebang-demo
printf 'echo "no shebang"\n' > no.sh
printf '#!/usr/bin/env bash\necho "with shebang"\n' > yes.sh
chmod +x no.sh yes.sh
echo "--- اجرا مثل یک سرویس systemd، بدون shebang:"
systemd-run --unit=lx-noshe --remain-after-exit /root/shebang-demo/no.sh > /dev/null 2>&1
sleep 1
systemctl status lx-noshe --no-pager | grep -E 'Main PID|Failed to execute' | sed -E 's/^[A-Z][a-z]{2} [0-9]+ [0-9:]+ //'
systemctl stop lx-noshe; systemctl reset-failed lx-noshe
echo "--- با shebang:"
systemd-run --wait --collect -P /root/shebang-demo/yes.sh 2>&1 | grep -E 'with shebang|status='
cd / && rm -rf /root/shebang-demo
خروجی
--- اجرا مثل یک سرویس systemd، بدون shebang:
Main PID: 18364 (code=exited, status=203/EXEC)
server (no.sh)[18364]: lx-noshe.service: Failed to execute /root/shebang-demo/no.sh: Exec format error
--- با shebang:
with shebang
Main processes terminated with: code=exited/status=0

systemd-run یک دستور را مثل یک سرویس systemd اجرا می‌کند. همان فایل no.sh که از bash کار می‌کرد، از Python با Exec format error و از systemd با کد ۲۰۳ (status=203/EXEC در systemctl status) شکست خورد؛ اگر یک سرویس واقعی را بدون shebang بنویسی همین را می‌بینی. نتیجه: همیشه shebang بنویس، حتی وقتی «بدون آن هم کار می‌کند».

چهار شکل رایج خط اول:

خط اول یعنی چه وقت
#!/usr/bin/env bash env در PATH دنبال bash می‌گردد پیشنهاد این دوره: قابل‌حمل (لینوکس، مک، هر جا bash در PATH باشد)
#!/bin/bash مسیر ثابت bash سرورهای لینوکس که مطمئنی bash آن‌جاست؛ ساده و کمی سریع‌تر
#!/bin/sh شل POSIX (روی Ubuntu و Debian: dash) فقط وقتی کد شما POSIX خالص است (بدون [[ ]]، آرایه و …)
#!/usr/bin/env python3 هر مفسر دیگر اسکریپت Python، Perl، Node و …

چهار راه اجرای اسکریپت:

روش مجوز shebang پروسه‌ی جدید کاربرد
bash script.sh فقط r نادیده بله آزمون سریع، یا فایلی که x ندارد
./script.sh r و x استفاده می‌شود بله روش معمول
فقط script (از PATH) r و x استفاده می‌شود بله ابزارهای شخصی و سیستمی
source script.sh یا . script.sh فقط r نادیده نه (همین shell) فایل‌های تنظیمات، تغییر دادن shell فعلی

برای ./script.sh هم r لازم است و هم x: مفسر فایل را با هویت تو می‌خواند، پس فقط‌اجرا (--x) کافی نیست.

کدهای خروج که در این درس دیدی:

کد یعنی
0 موفق
1 تا 255 خطا؛ معنی دقیقش را هر برنامه خودش مشخص می‌کند (exit 3 خودمان)
2 ایراد نحو در اسکریپت یا استفاده‌ی اشتباه از دستور (sh arr.sh، ls روی پوشه‌ی ناموجود)
126 فایل پیدا شد ولی قابل اجرا نیست (Permission denied)
127 دستور یا مفسر پیدا نشد (command not found)
130 با Ctrl+C قطع شد (۱۲۸ + شماره‌ی سیگنال ۲)

۱) Permission denied حتی بعد از chmod +x

Section titled “۱) Permission denied حتی بعد از chmod +x”

دو دلیل رایج دارد. اول: r را برداشته‌ای (فقط x). دوم: پارتیشنی که اسکریپت روی آن است با گزینه‌ی noexec mount شده؛ بعضی سرورها /tmp یا /home را برای امنیت این‌طور می‌کنند. در این حالت هیچ فایلی آن‌جا اجرا نمی‌شود، ولی bash script.sh هنوز کار می‌کند:

Terminal window
mkdir -p /mnt/nx
mount -t tmpfs -o noexec,size=1m tmpfs /mnt/nx
printf '#!/usr/bin/env bash\necho "ran fine"\n' > /mnt/nx/s.sh
chmod +x /mnt/nx/s.sh
ls -l /mnt/nx/s.sh
echo "--- مستقیم:"
/mnt/nx/s.sh; echo "کد خروج: $?"
echo "--- با bash:"
bash /mnt/nx/s.sh
umount /mnt/nx
خروجی
-rwxr-xr-x 1 root root 36 Oct 4 09:32 /mnt/nx/s.sh
--- مستقیم:
bash: /mnt/nx/s.sh: Permission denied
کد خروج: 126
--- با bash:
ran fine

فایل -rwxr-xr-x است ولی باز Permission denied. اگر فایل را روی پارتیشن دیگری کپی کنی یا با bash اجرا کنی کار می‌کند. (با mount | grep noexec پارتیشن‌های noexec را می‌یابی.)

۲) command not found برای اسکریپتی که توی همین پوشه است

Section titled “۲) command not found برای اسکریپتی که توی همین پوشه است”

hello.sh بدون ./ ← bash: hello.sh: command not found (مثال ۵). راه‌حل: ./hello.sh، یا اسکریپت را در پوشه‌ای از PATH بگذار.

۳) فایل از ویندوز آمده: bash\r

Section titled “۳) فایل از ویندوز آمده: bash\r”

ویرایشگرهای ویندوز پایان هر خط را CRLF (دو بایت: \r\n) می‌گذارند، ولی لینوکس فقط \n را می‌شناسد. نتیجه: \r به آخر خط اول، یعنی اسم مفسر، می‌چسبد:

Terminal window
cd ~/bashlab
printf '#!/usr/bin/env bash\r\necho "hi from a Windows file"\r\n' > win.sh
chmod +x win.sh
file win.sh
./win.sh; echo "کد خروج: $?"
echo "--- درست‌کردن: حذف \\r از آخر خط‌ها"
sed -i 's/\r$//' win.sh
file win.sh
./win.sh
خروجی
win.sh: Bourne-Again shell script, ASCII text executable, with CRLF line terminators
/usr/bin/env: ‘bash\r’: No such file or directory
/usr/bin/env: use -[v]S to pass options in shebang lines
کد خروج: 127
--- درست‌کردن: حذف \r از آخر خط‌ها
win.sh: Bourne-Again shell script, ASCII text executable
hi from a Windows file

خطا می‌گوید env دنبال برنامه‌ای به اسم ‎bash\r‎ می‌گردد (نویسه‌ی \r با خود پیام نشان داده شده). file هم از اول گفته بود with CRLF line terminators. sed -i 's/\r$//' آخر هر خط \r را پاک می‌کند (ابزار dos2unix هم همین کار را می‌کند). در ویرایشگرت پایان خط را روی LF بگذار.

۴) sh script.sh به‌جای bash script.sh

Section titled “۴) sh script.sh به‌جای bash script.sh”

روی سرور، اسکریپتی که با bash نوشته‌ای را با sh اجرا کنی، مفسر dash است، نه bash. امکاناتی مثل آرایه‌ها از کار می‌افتند:

Terminal window
cd ~/bashlab
cat > arr.sh <<'EOF'
arr=(one two three)
echo "${arr[1]}"
EOF
echo "--- sh arr.sh:"
sh arr.sh; echo "کد خروج: $?"
echo "--- bash arr.sh:"
bash arr.sh
خروجی
--- sh arr.sh:
arr.sh: 1: Syntax error: "(" unexpected
کد خروج: 2
--- bash arr.sh:
two

راه‌حل: با bash یا ./script.sh (با shebang درست) اجرا کن.

۵) shebang جای درست نیست یا مفسر اشتباه است

Section titled “۵) shebang جای درست نیست یا مفسر اشتباه است”

شرط کار کردن این است که #! دو بایت اولِ فایل باشد؛ حتی یک خط خالی یا فاصله قبلش، آن را از کار می‌اندازد. و اگر مسیر مفسر غلط باشد، پیام گمراه‌کننده‌ای می‌گیری:

Terminal window
cd ~/bashlab
printf '\n#!/usr/bin/env python3\nprint("hi from python")\n' > lead.py
chmod +x lead.py
echo "--- یک خط خالی قبل از shebang:"
./lead.py; echo "کد خروج: $?"
printf '#!/usr/bin/pythonn\nprint("hi")\n' > typo.py
chmod +x typo.py
echo "--- مسیر مفسر غلط (pythonn):"
./typo.py; echo "کد خروج: $?"
خروجی
--- یک خط خالی قبل از shebang:
./lead.py: line 3: syntax error near unexpected token `"hi from python"'
./lead.py: line 3: `print("hi from python")'
کد خروج: 2
--- مسیر مفسر غلط (pythonn):
bash: ./typo.py: cannot execute: required file not found
کد خروج: 127

در حالت اول shebang نادیده گرفته شد و bash کد Python را اجرا کرد. در حالت دوم پیام required file not found با اینکه خودِ typo.py وجود دارد، منظورش مفسر است. راه‌حل: head -n 1 script را نگاه کن و مسیر را با command -v python3 یا command -v bash بسنج.

وقتی چیزی کار نمی‌کند، وسوسه می‌شوی chmod 777 بزنی. اسکریپت را نه برای گروه و نه برای بقیه قابل‌نوشتن نکن: اگر کسی بتواند آن را عوض کند و تو (یا cron، یا root) آن را اجرا کنی، او هر کاری با دسترسی تو می‌کند. برای اسکریپت معمولاً 755 (یا 700 اگر فقط مال خودت است) کافی است.

✎ تمرینآسان

یک اسکریپت intro.sh بساز که سه خط چاپ کند: اسم تو، اسم دوره (LoopX) و تاریخ امروز (با دستور date). اول با bash intro.sh اجرایش کن، بعد chmod +x بزن و با ./intro.sh اجرا کن.

دیدن جواب
mkdir -p ~/bashlab && cd ~/bashlab
cat > intro.sh <<'EOF'
#!/usr/bin/env bash
# intro.sh: my first practice script
echo "Name: Ali"
echo "Course: LoopX"
date
EOF
echo "--- bash intro.sh:"
bash intro.sh
chmod +x intro.sh
echo "--- ./intro.sh:"
./intro.sh
خروجی
--- bash intro.sh:
Name: Ali
Course: LoopX
Sun Oct 4 09:31:59 UTC 2026
--- ./intro.sh:
Name: Ali
Course: LoopX
Sun Oct 4 09:31:59 UTC 2026
✎ تمرینمتوسط

تمرین اصلی درس: اسکریپت sysinfo.sh بنویس که اسم کاربر، تاریخ و فضای دیسک را چاپ کند؛ هر بخش با یک عنوان. بالای فایل یک کامنت بگذار که بگوید اسکریپت چه می‌کند. با bash -n نحوش را بررسی کن و بعد اجرایش کن و کد خروجش را نشان بده.

دیدن جواب
mkdir -p ~/bashlab && cd ~/bashlab
cat > sysinfo.sh <<'EOF'
#!/usr/bin/env bash
# sysinfo.sh: prints the current user, the date and the disk usage
echo "--- User:"
whoami
echo "--- Date:"
date
echo "--- Disk:"
df -h /
EOF
chmod +x sysinfo.sh
bash -n sysinfo.sh && echo "syntax OK"
./sysinfo.sh
echo "کد خروج: $?"
خروجی
syntax OK
--- User:
ali
--- Date:
Sun Oct 4 09:31:59 UTC 2026
--- Disk:
Filesystem Size Used Avail Use% Mounted on
overlay 453G 23G 408G 6% /
کد خروج: 0

در درس بعد یاد می‌گیری خروجی whoami و date را وسط یک جمله بگذاری و اسکریپت کوتاه‌تر شود.

✎ تمرینمتوسط

سه اسکریپت خراب داریم. بدون عوض‌کردن محتوای اصلی‌شان، هر سه را درست کن تا هر کدام OK چاپ کنند.

Terminal window
mkdir -p ~/bashlab/fix && cd ~/bashlab/fix
printf '#!/usr/bin/env bash\necho "a: OK"\n' > a.sh
printf '#!/usr/bin/env bash\r\necho "b: OK"\r\n' > b.sh && chmod +x b.sh
printf '#!/bin/sh\nx=1\nif [[ $x == 1 ]]; then echo "c: OK"; fi\n' > c.sh && chmod +x c.sh
for f in a b c; do echo "--- $f.sh:"; ./$f.sh; done
خروجی
--- a.sh:
bash: ./a.sh: Permission denied
--- b.sh:
/usr/bin/env: ‘bash\r’: No such file or directory
/usr/bin/env: use -[v]S to pass options in shebang lines
--- c.sh:
./c.sh: 3: [[: not found
دیدن جواب

سه ایراد مختلف: a.sh مجوز اجرا ندارد، b.sh پایان خط‌های ویندوزی دارد و c.sh shebang اش sh است در حالی که کدش bash می‌خواهد.

Terminal window
cd ~/bashlab/fix
chmod +x a.sh
sed -i 's/\r$//' b.sh
sed -i '1s|.*|#!/usr/bin/env bash|' c.sh
for f in a b c; do ./$f.sh; done
خروجی
a: OK
b: OK
c: OK
✎ تمرینسخت

یک اسکریپت Bash به اسم hello-sh و یک اسکریپت Python به اسم hello-py در ~/bin بساز، طوری که هر دو فقط با اسم و از هر پوشه‌ای (مثلاً /) اجرا شوند. با file نوع هر دو را نشان بده و ثابت کن اگر bash را روی فایل Python بزنی خطا می‌گیری. چرا؟

دیدن جواب
mkdir -p ~/bin
cat > ~/bin/hello-sh <<'EOF'
#!/usr/bin/env bash
echo "Hello from bash"
EOF
cat > ~/bin/hello-py <<'EOF'
#!/usr/bin/env python3
print("Hello from Python")
EOF
chmod +x ~/bin/hello-sh ~/bin/hello-py
export PATH="$HOME/bin:$PATH"
cd /
hello-sh
hello-py
echo "--- نوع فایل‌ها:"
file ~/bin/hello-sh ~/bin/hello-py
echo "--- bash روی فایل Python:"
bash ~/bin/hello-py; echo "کد خروج: $?"
خروجی
Hello from bash
Hello from Python
--- نوع فایل‌ها:
/home/ali/bin/hello-sh: Bourne-Again shell script, ASCII text executable
/home/ali/bin/hello-py: Python script, ASCII text executable
--- bash روی فایل Python:
/home/ali/bin/hello-py: line 2: syntax error near unexpected token `"Hello from Python"'
/home/ali/bin/hello-py: line 2: `print("Hello from Python")'
کد خروج: 2

file از روی shebang نوع را تشخیص داد و هر دو با اسم اجرا شدند. ولی bash ~/bin/hello-py یعنی «bash، این را بخوان» و shebang نادیده گرفته می‌شود؛ پس bash سعی می‌کند کد Python را مثل Bash بخواند و به ایراد نحو می‌خورد.

⚡ بررسی سریع

چرا برای اجرای «bash hello.sh» مجوز اجرا (x) لازم نیست؟

؟ آزمونک
  1. خط #!/usr/bin/env bash در اول اسکریپت چه می‌کند؟

  2. اسکریپتی ساخته‌ای ولی chmod +x نزده‌ای. کدام دستور آن را اجرا می‌کند؟

  3. چرا اسکریپت داخل پوشه‌ی فعلی را باید با ./ اجرا کنی؟

  4. اسکریپتی cd /tmp دارد. بعد از ./script.sh پوشه‌ی ترمینال تو چه می‌شود؟

  5. اسکریپتی سه دستور دارد: echo، ls /no/such/dir (که خطا می‌دهد) و echo done. کد خروج اسکریپت چند است؟

  6. اسکریپتی از ویندوز آمده و ./script.sh خطای «env: bash\r: No such file or directory» می‌دهد. علت و راه‌حل؟

  • اسکریپت یک فایل متنی از دستورهاست که bash خط‌به‌خط اجرا می‌کند. هر دستوری که در ترمینال کار می‌کند در اسکریپت هم کار می‌کند.
  • خط اول shebang است، #!/usr/bin/env bash: وقتی فایل مستقیم اجرا شود، هسته از آن می‌فهمد با چه مفسری اجرا کند. باید دو بایت اول فایل باشد. همیشه بنویسش؛ بدون آن فقط در bash کار می‌کند.
  • برای ./script.sh هم r و هم x لازم است (chmod +x script.sh)؛ bash script.sh فقط r می‌خواهد و shebang را نادیده می‌گیرد. اسکریپت داخل پوشه‌ای از PATH را بدون ./ هم می‌شود اجرا کرد.
  • اسکریپت در پروسه‌ی فرزند اجرا می‌شود: cd و متغیرهایش به shell تو نمی‌رسد؛ با source script.sh در همان shell اجرا می‌شود.
  • کامنت با # شروع می‌شود (فقط اول یک کلمه)؛ چرا را بنویس، نه چه.
  • کد خروج اسکریپت = کد آخرین دستور (یا exit N)؛ بعد از هر اجرا echo $?. bash -n script.sh فقط نحو را بدون اجرا بررسی می‌کند.
  • خطاهای رایج: Permission denied (۱۲۶: نبودن x یا noexec)، command not found (۱۲۷: فراموشیِ ./)، bash\r (فایل ویندوزی)، [[: not found (shebang برابر sh).
برگه‌ی تقلب این درس
دستورکاری که می‌کند
#!/usr/bin/env bashخط اول: اجرا با bash (قابل‌حمل)
chmod +x script.shدادن مجوز اجرا
./script.shاجرای مستقیم (نیاز به x و shebang)
bash script.shاجرا بدون نیاز به x؛ shebang نادیده
source script.sh / . script.shاجرا در همین shell (cd و متغیرها می‌مانند)
# توضیحکامنت؛ تا آخر خط
bash -n script.shبررسی نحو بدون اجرا
exit 3پایان اسکریپت با کد ۳
echo $?کد خروج آخرین دستور
file script.shتشخیص نوع فایل (از روی shebang)
sed -i 's/\r$//' fileتبدیل CRLF ویندوز به LF