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

مدیریت خطا

توی این درس یاد می‌گیری اسکریپتی بنویسی که بی‌صدا خراب نشود: وقتی دستوری شکست خورد، متوقف شود، بگوید کجا و چرا، و آشغال (فایل موقت، قفل، نیمه‌کاره‌ها) را پشت سرش جا نگذارد. سه کلید معروف set -euo pipefail را یکی‌یکی و با استثناهایشان می‌شناسی، با trap یک تابع پاک‌سازی (cleanup) می‌نویسی که در هر حالتی (موفق، خطا، Ctrl+C) اجرا شود، و کد خروج سفارشی می‌دهی تا اسکریپت‌ها و cron بفهمند دقیقاً چه شد. آخر درس، اسکریپت بکاپ را به مدیریت خطا و پاک‌سازی مجهز می‌کنی.

مسئله: اسکریپتی که بعد از خطا ادامه می‌دهد

Section titled “مسئله: اسکریپتی که بعد از خطا ادامه می‌دهد”

به‌صورت پیش‌فرض، Bash بعد از شکست یک دستور خیلی راحت می‌رود سراغ خط بعد. این برای ترمینال تعاملی خوب است، ولی در اسکریپت می‌تواند فاجعه باشد. این دو خط را ببین:

یک اسکریپت خطرناک
cd /var/www/myapp/cache
rm -rf *

اگر پوشه‌ی cache وجود نداشته باشد (اشتباه تایپی، سرور تازه، پوشه‌ای که کسی پاکش کرده)، cd شکست می‌خورد، اسکریپت در همان پوشه‌ی قبلی می‌ماند و rm -rf * همه‌چیزِ آنجا را پاک می‌کند. مشکل اصلی این نیست که cd خطا داد؛ مشکل این است که کسی به خطا گوش نداد.

تشبیه: خط تولید با دکمه‌ی توقف اضطراری

Section titled “تشبیه: خط تولید با دکمه‌ی توقف اضطراری”

اسکریپت مثل یک خط تولید است: هر ایستگاه (دستور) کارش را می‌کند و محصول را به ایستگاه بعد می‌دهد. خط تولید بدون مدیریت خطا، حتی وقتی یک ایستگاه قطعه‌ی خراب تحویل می‌دهد، کار را ادامه می‌دهد و آخرش جعبه‌ی خالی بسته‌بندی می‌کند. مدیریت خطا یعنی سه چیز:

  • حسگر روی هر ایستگاه که خرابی را ببیند (کد خروج، set -e و pipefail)،
  • دکمه‌ی توقف اضطراری که کل خط را نگه دارد (exit با کد معنادار)،
  • تیم نظافت که بعد از توقف، چه عادی چه اضطراری، میز کار را تمیز کند (trap ... EXIT).

کد خروج: زبان موفقیت و شکست

Section titled “کد خروج: زبان موفقیت و شکست”

هر دستور وقتی تمام می‌شود یک کد خروج (exit status) بین ۰ تا ۲۵۵ برمی‌گرداند: ۰ یعنی موفق و هر چیز دیگر یعنی شکست (درس شرط‌ها). کد آخرین دستور در $? است. کد خروج خود اسکریپت، اگر صریح exit N ننویسی، کد آخرین دستوری است که اجرا شد؛ این همان چیزی است که cron، systemd، CI و اسکریپت‌های دیگر می‌بینند.

مدیریت خطا در Bash از چند ابزار ساخته می‌شود:

ابزار کار
set -e (errexit) اگر دستوری شکست خورد (و «بررسی‌نشده» بود)، اسکریپت متوقف شود
set -u (nounset) استفاده از متغیر تعریف‌نشده خطا باشد، نه رشته‌ی خالی
set -o pipefail کد خروج pipe، کد اولین دستور شکست‌خورده باشد، نه فقط آخری
trap 'کد' EXIT کدی که موقع خروج اسکریپت (به هر دلیلی) اجرا شود: پاک‌سازی
trap 'کد' ERR کدی که موقع شکست یک دستور اجرا شود: گزارش خطا
exit N پایان با کد خروج مشخص و معنادار
مسیر یک خطا در اسکریپتی که set -euo pipefail و دو trap دارد: دستور شکست می‌خورد، trap ERR گزارش می‌دهد (کجا و چه دستوری)، اسکریپت متوقف می‌شود، trap EXIT پاک‌سازی می‌کند و همان کد خروج به صدازننده (cron، CI، اسکریپت دیگر) می‌رسد.

مثال ۱: اسکریپت بدون مدیریت خطا

Section titled “مثال ۱: اسکریپت بدون مدیریت خطا”

اول ببین پیش‌فرض Bash چطور رفتار می‌کند. این اسکریپت شبیه همان cd و rm خطرناک است، ولی به‌جای rm فقط چاپ می‌کند که کجا پاک می‌کرد:

/home/ali/bashlab/no-checks.sh
#!/usr/bin/env bash
# no-checks.sh: what happens when nobody listens to errors
cd /var/www/myapp/cache
echo "I would now run 'rm -rf *' in: $(pwd)"
cp /etc/no-such-config.conf ./backup.conf
echo "backup finished"
Terminal window
cd ~/bashlab
bash no-checks.sh
echo "کد خروج اسکریپت: $?"
خروجی
no-checks.sh: line 3: cd: /var/www/myapp/cache: No such file or directory
I would now run 'rm -rf *' in: /home/ali/bashlab
cp: cannot stat '/etc/no-such-config.conf': No such file or directory
backup finished
کد خروج اسکریپت: 0

هر سه مشکل را ببین:

  1. cd شکست خورد، ولی اسکریپت ادامه داد و حالا در پوشه‌ی خانه است. اگر آنجا rm -rf * بود، همه‌چیز پاک می‌شد.
  2. cp هم شکست خورد و باز ادامه داد.
  3. چون دستور آخر (echo) موفق بود، کد خروج کل اسکریپت ۰ است. cron یا CI فکر می‌کند همه‌چیز عالی بوده.

مثال ۲: set -e، توقف در اولین خطا

Section titled “مثال ۲: set -e، توقف در اولین خطا”

با set -e (اسم بلندش set -o errexit)، Bash بعد از اولین دستور شکست‌خورده متوقف می‌شود و کد خروج همان دستور را برمی‌گرداند:

/home/ali/bashlab/with-e.sh
#!/usr/bin/env bash
# with-e.sh: stop at the first failing command
set -e
cd /var/www/myapp/cache
echo "I would now run 'rm -rf *' in: $(pwd)"
echo "backup finished"
Terminal window
cd ~/bashlab
bash with-e.sh
echo "کد خروج اسکریپت: $?"
خروجی
with-e.sh: line 4: cd: /var/www/myapp/cache: No such file or directory
کد خروج اسکریپت: 1

دو echo بعد از cd هیچ‌وقت اجرا نشدند و کد خروج ۱ است (کد خروج cd). حالا صدازننده می‌فهمد چیزی خراب شده.

ولی set -e همه‌جا اسکریپت را نمی‌بندد. دستوری که شکستش «بررسی» می‌شود، اسکریپت را متوقف نمی‌کند، چون Bash فرض می‌کند خودت حواست به آن هست:

cd ~/bashlab
cat > e-exceptions.sh <<'EOF'
#!/usr/bin/env bash
set -e
if grep -q root /etc/no-such-file 2>/dev/null; then echo "found"; else echo "1) inside if: not stopped"; fi
false || echo "2) after ||: not stopped"
false && echo "never printed"
echo "3) after &&: not stopped (only the last command of an && list counts)"
! false
echo "4) after !: not stopped"
while false; do :; done
echo "5) loop condition: not stopped"
false
echo "never printed: plain false stops the script"
EOF
bash e-exceptions.sh
echo "کد خروج: $?"
خروجی
1) inside if: not stopped
2) after ||: not stopped
3) after &&: not stopped (only the last command of an && list counts)
4) after !: not stopped
5) loop condition: not stopped
کد خروج: 1

قاعده: set -e روی دستوری که شرطِ if/while/until است، دستورهای قبل از آخرین عضو یک زنجیره‌ی &&/||، و دستوری که با ! برعکس شده اثر ندارد. فقط false تنها، یعنی شکستی که «کسی بررسی‌اش نکرد»، اسکریپت را بست. این رفتار درست است: اگر شکست را عمداً با if یا || بررسی کنی، یعنی برایش برنامه داری.

مثال ۳: set -u، متغیر تعریف‌نشده خطاست

Section titled “مثال ۳: set -u، متغیر تعریف‌نشده خطاست”

بدون set -u (اسم بلند: nounset)، متغیر تعریف‌نشده بی‌صدا رشته‌ی خالی می‌شود. یک اشتباه تایپی در اسم متغیر می‌تواند مسیر را عوض کند:

cd ~/bashlab
cat > typo.sh <<'EOF'
#!/usr/bin/env bash
backup_dir="/home/ali/bashlab/old-backups"
mkdir -p "$backup_dir"
touch "$backup_dir/old.tar.gz"
# typo: backup_dri instead of backup_dir
echo "would run: rm -rf \"$backup_dri/\"*"
EOF
echo "--- بدون set -u:"
bash typo.sh
echo "--- با set -u:"
bash -u typo.sh
echo "کد خروج: $?"
خروجی
--- بدون set -u:
would run: rm -rf "/"*
--- با set -u:
typo.sh: line 6: backup_dri: unbound variable
کد خروج: 1

بدون set -u مسیر نهایی "/"* شد، یعنی کل ریشه‌ی سیستم! (فقط چاپش کردیم.) با -u (اینجا با bash -u، همان اثر set -u در اول فایل) Bash همان‌جا با unbound variable متوقف شد.

گاهی واقعاً می‌خواهی ببینی متغیری تعریف شده یا نه (مثلاً آرگومان اختیاری). با set -u از مقدار پیش‌فرض استفاده کن (درس کار با رشته‌ها):

cd ~/bashlab
cat > optional.sh <<'EOF'
#!/usr/bin/env bash
set -u
name="${1:-World}"
echo "Hello, $name"
echo "DEBUG is: [${DEBUG:-}]"
if [[ -n ${DEBUG:-} ]]; then echo "debug mode on"; fi
echo "number of args: $#"
echo "first arg without default: $1"
EOF
bash optional.sh
echo "کد خروج: $?"
echo "---"
DEBUG=1 bash optional.sh Ali
خروجی
Hello, World
DEBUG is: []
number of args: 0
optional.sh: line 8: $1: unbound variable
کد خروج: 1
---
Hello, Ali
DEBUG is: [1]
debug mode on
number of args: 1
first arg without default: Ali

${1:-World} و ${DEBUG:-} با set -u امن‌اند (اگر تعریف نشده بود، پیش‌فرض یا رشته‌ی خالی). $# همیشه تعریف شده است. ولی $1 خالی بدون پیش‌فرض، خطا داد. نکته: در Bash ۴.۴ به بعد، "${arr[@]}" برای آرایه‌ی خالی با set -u خطا نمی‌دهد.

مثال ۴: set -o pipefail و PIPESTATUS

Section titled “مثال ۴: set -o pipefail و PIPESTATUS”

کد خروج یک pipe به‌صورت پیش‌فرض فقط کد آخرین دستور است. پس شکست وسط pipe دیده نمی‌شود، حتی با set -e:

cd ~/bashlab
cat > pipe.sh <<'EOF'
#!/usr/bin/env bash
set -e
mysqldump_fake() { echo "connection refused" >&2; return 2; }
mysqldump_fake | gzip > db.sql.gz
echo "pipeline status was 0, so set -e did not stop; db.sql.gz size: $(stat -c %s db.sql.gz) bytes"
EOF
bash pipe.sh
echo "کد خروج: $?"
خروجی
connection refused
pipeline status was 0, so set -e did not stop; db.sql.gz size: 20 bytes
کد خروج: 0

این دقیقاً سناریوی اول درس است: dump شکست خورد، gzip با موفقیت یک فایل تقریباً خالی ساخت و اسکریپت با ۰ تمام شد. با pipefail، کد pipe کد آخرین دستور شکست‌خورده (از راست) می‌شود:

Terminal window
cd ~/bashlab
sed -i 's/^set -e$/set -eo pipefail/' pipe.sh
bash pipe.sh
echo "کد خروج: $?"
echo "--- PIPESTATUS: کد تک‌تک عضوهای آخرین pipe"
true | false | true
echo "${PIPESTATUS[@]}"
set -o pipefail
true | false | true
echo "با pipefail، \$? = $?"
خروجی
connection refused
کد خروج: 2
--- PIPESTATUS: کد تک‌تک عضوهای آخرین pipe
0 1 0
با pipefail، $? = 1

حالا اسکریپت با کد ۲ (کد mysqldump_fake) متوقف شد. آرایه‌ی PIPESTATUS کد همه‌ی عضوهای آخرین pipe را نگه می‌دارد (0 1 0)؛ باید بلافاصله بعد از pipe خوانده شود، چون دستور بعدی آن را بازنویسی می‌کند.

مثال ۵: trap ... EXIT، پاک‌سازی در هر حالت

Section titled “مثال ۵: trap ... EXIT، پاک‌سازی در هر حالت”

اسکریپت‌ها اغلب فایل یا پوشه‌ی موقت می‌سازند. اگر وسط کار متوقف شوند (با set -e یا Ctrl+C)، این فایل‌ها می‌مانند و دیسک را پر می‌کنند. trap 'دستور' EXIT به Bash می‌گوید «هر وقت این اسکریپت تمام شد، به هر دلیلی، این را اجرا کن». پوشه‌ی موقت را هم با mktemp بساز تا اسمش یکتا و امن باشد:

/home/ali/bashlab/tempwork.sh
#!/usr/bin/env bash
# tempwork.sh: always remove the temp directory, whatever happens
set -euo pipefail
workdir="$(mktemp -d)"
echo "created $workdir"
cleanup() {
local status=$?
rm -rf "$workdir"
echo "cleanup: removed $workdir (exit status $status)"
}
trap cleanup EXIT
echo "data" > "$workdir/part1"
if [[ ${FAIL:-no} == yes ]]; then
echo "simulating a failure..."
false
fi
echo "all steps done"
Terminal window
cd ~/bashlab
chmod +x tempwork.sh
echo "=== اجرای موفق:"
./tempwork.sh; echo "کد خروج: $?"
echo "=== اجرای ناموفق:"
FAIL=yes ./tempwork.sh; echo "کد خروج: $?"
echo "=== پوشه‌ی موقتی از این اسکریپت در /tmp مانده؟"
ls -d /tmp/tmp.* 2>/dev/null | wc -l
خروجی
=== اجرای موفق:
created /tmp/tmp.ZQjtZgQg4q
all steps done
cleanup: removed /tmp/tmp.ZQjtZgQg4q (exit status 0)
کد خروج: 0
=== اجرای ناموفق:
created /tmp/tmp.sVCiNYloY0
simulating a failure...
cleanup: removed /tmp/tmp.sVCiNYloY0 (exit status 1)
کد خروج: 1
=== پوشه‌ی موقتی از این اسکریپت در /tmp مانده؟
0

هر دو بار تابع cleanup اجرا شد و پوشه را پاک کرد. چند نکته‌ی مهم:

  • local status=$? اولین خط cleanup است. وقتی trap EXIT اجرا می‌شود، $? کد خروجی است که اسکریپت با آن تمام می‌شود. اگر اول rm را بزنی، $? مال rm می‌شود.
  • trap EXIT کد خروج را عوض نمی‌کند: اسکریپت ناموفق، با پاک‌سازی هم ناموفق (کد ۱) ماند.
  • trap را بلافاصله بعد از ساختن منبع بگذار. اگر بین mktemp و trap خطایی رخ دهد، پوشه می‌ماند.
  • mktemp -d یک پوشه با اسم تصادفی (مثل /tmp/tmp.XXXXXXXXXX) و دسترسی فقط برای خودت می‌سازد. اسم ثابت مثل /tmp/mywork با اجرای همزمان دو نسخه یا کاربر دیگر تداخل دارد.

وقتی کاربر Ctrl+C می‌زند، پروسه سیگنال SIGINT می‌گیرد؛ kill به‌صورت پیش‌فرض SIGTERM می‌فرستد. trap EXIT در Bash در این حالت‌ها هم اجرا می‌شود. این را در یک ترمینال واقعی (داخل tmux) امتحان می‌کنیم: اسکریپت را اجرا می‌کنیم، وسط کار Ctrl+C می‌زنیم و بعد کد خروج را می‌پرسیم:

cd ~/bashlab
cat > slow.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
workdir="$(mktemp -d)"
cleanup() {
local status=$?
rm -rf "$workdir"
echo "cleanup ran (status inside trap: $status); $workdir removed"
}
trap cleanup EXIT
echo "working in $workdir ... (press Ctrl+C)"
sleep 3
echo "finished normally"
EOF
chmod +x slow.sh
tmux new-session -d -s lx -x 100 -y 12 "bash --norc"
tmux send-keys -t lx 'cd ~/bashlab && ./slow.sh' Enter
sleep 1.5
tmux send-keys -t lx C-c
sleep 0.5
tmux send-keys -t lx 'echo "exit status: $?"' Enter
sleep 0.5
tmux capture-pane -t lx -p | grep -v '^$'
tmux kill-session -t lx
خروجی
bash-5.2$ cd ~/bashlab && ./slow.sh
working in /tmp/tmp.otIRJBICzd ... (press Ctrl+C)
^Ccleanup ran (status inside trap: 0); /tmp/tmp.otIRJBICzd removed
bash-5.2$ echo "exit status: $?"
exit status: 130
bash-5.2$

^C همان Ctrl+C است. دو نکته در این خروجی هست:

  • پاک‌سازی اجرا شد و کد خروج واقعی اسکریپت ۱۳۰ است. قاعده: پروسه‌ای که با سیگنال شماره‌ی N بسته شود، کد ۱۲۸+N می‌گیرد؛ SIGINT شماره‌ی ۲ است، پس ۱۳۰.
  • ولی داخل trap، $? عدد ۰ را نشان داد، نه ۱۳۰. وقتی اسکریپت با سیگنال کشته می‌شود، $? داخل trap EXIT کد آخرین دستور قبلی است و قابل اعتماد نیست.

راه‌حل: برای سیگنال‌ها trap جدا بگذار که خودش با کد درست exit کند؛ آن‌وقت trap EXIT کد درست را می‌بیند:

Terminal window
cd ~/bashlab
sed -i 's/^trap cleanup EXIT$/trap cleanup EXIT\ntrap "exit 130" INT\ntrap "exit 143" TERM/' slow.sh
grep -n '^trap' slow.sh
tmux new-session -d -s lx -x 100 -y 12 "bash --norc"
tmux send-keys -t lx 'cd ~/bashlab && ./slow.sh' Enter
sleep 1.5
tmux send-keys -t lx C-c
sleep 0.5
tmux send-keys -t lx 'echo "exit status: $?"' Enter
sleep 0.5
tmux capture-pane -t lx -p | grep -v '^$'
tmux kill-session -t lx
echo "=== با kill (SIGTERM) از بیرون:"
start=$SECONDS
./slow.sh &
pid=$!
sleep 1
kill "$pid"
wait "$pid"
echo "exit status after kill: $? (after $((SECONDS - start)) seconds)"
خروجی
9:trap cleanup EXIT
10:trap "exit 130" INT
11:trap "exit 143" TERM
bash-5.2$ cd ~/bashlab && ./slow.sh
working in /tmp/tmp.5DAtoyypsK ... (press Ctrl+C)
^Ccleanup ran (status inside trap: 130); /tmp/tmp.5DAtoyypsK removed
bash-5.2$ echo "exit status: $?"
exit status: 130
bash-5.2$
=== با kill (SIGTERM) از بیرون:
working in /tmp/tmp.hv4gleTo3C ... (press Ctrl+C)
cleanup ran (status inside trap: 143); /tmp/tmp.hv4gleTo3C removed
exit status after kill: 143 (after 3 seconds)

حالا داخل trap هم ۱۳۰ دیده شد. با kill کد ۱۴۳ است (۱۲۸ + ۱۵ که شماره‌ی SIGTERM است). ولی به زمان دقت کن: اسکریپت تا تمام شدن sleep 3 صبر کرد و بعد trap را اجرا کرد. قاعده‌ی Bash این است: وقتی منتظر یک دستور پیش‌زمینه است و سیگنالی می‌رسد که برایش trap داری، trap بعد از پایان آن دستور اجرا می‌شود. با Ctrl+C این مشکل پیش نمی‌آید، چون ترمینال SIGINT را به همه‌ی پروسه‌های پیش‌زمینه (از جمله sleep) می‌فرستد.

(فقط kill -9 یعنی SIGKILL را هیچ برنامه‌ای نمی‌تواند بگیرد؛ با آن trap اجرا نمی‌شود و فایل‌های موقت می‌مانند. برای همین همیشه اول kill معمولی بزن.)

مثال ۷: trap ... ERR، گزارش «کجا» و «چه»

Section titled “مثال ۷: trap ... ERR، گزارش «کجا» و «چه»”

set -e اسکریپت را می‌بندد ولی نمی‌گوید کدام خط. با trap ERR (که بعد از هر دستور شکست‌خورده اجرا می‌شود) شماره‌ی خط و خودِ دستور را چاپ کن:

/home/ali/bashlab/deploy.sh
#!/usr/bin/env bash
# deploy.sh: report the failing line and command
set -Eeuo pipefail
on_error() {
local status=$?
echo "ERROR: '$BASH_COMMAND' failed with status $status at line $1" >&2
}
trap 'on_error $LINENO' ERR
prepare() {
echo "preparing release..."
cp /etc/hostname /tmp/release-info
}
copy_assets() {
echo "copying assets..."
cp -r ./assets /tmp/release-assets
}
prepare
copy_assets
echo "deployed"
Terminal window
cd ~/bashlab
chmod +x deploy.sh
./deploy.sh
echo "کد خروج: $?"
rm -f /tmp/release-info
خروجی
preparing release...
copying assets...
cp: cannot stat './assets': No such file or directory
ERROR: 'cp -r ./assets /tmp/release-assets' failed with status 1 at line 18
کد خروج: 1
  • $LINENO شماره‌ی خطی است که خطا در آن رخ داد و $BASH_COMMAND متن دستوری که در حال اجرا بود. چون trap با کوتیشن تکی نوشته شده، $LINENO در لحظه‌ی خطا باز می‌شود (نه موقع تعریف trap).
  • -E (اسم بلند errtrace) لازم است تا trap ERR داخل تابع‌ها هم اجرا شود. بدون آن، خطای داخل copy_assets گزارش نمی‌شود و اسکریپت بدون گزارش خط و دستور بسته می‌شود؛ امتحانش کن:
Terminal window
cd ~/bashlab
sed 's/^set -Eeuo pipefail$/set -euo pipefail/' deploy.sh > deploy-no-E.sh
bash deploy-no-E.sh
echo "کد خروج: $?"
rm -f /tmp/release-info
خروجی
preparing release...
copying assets...
cp: cannot stat './assets': No such file or directory
کد خروج: 1
  • trap ERR فقط گزارش می‌دهد؛ توقف کار set -e است. اگر trap EXIT هم داشته باشی، بعد از ERR آن هم اجرا می‌شود.

مثال ۸: کد خروج سفارشی و مدیریت خطای قابل‌انتظار

Section titled “مثال ۸: کد خروج سفارشی و مدیریت خطای قابل‌انتظار”

همه‌ی شکست‌ها یکی نیستند. «آرگومان غلط دادی» با «دیسک پر است» فرق دارد و اسکریپت صدازننده شاید بخواهد برای هر کدام کار متفاوتی بکند. کدهای خروج ثابت و مستند تعریف کن و با exit N برگردان:

/home/ali/bashlab/check-site.sh
#!/usr/bin/env bash
# check-site.sh: check a directory before publishing it
# Exit codes:
# 0 ok
# 1 generic error
# 2 usage error (wrong arguments)
# 3 directory not found
# 4 index.html missing
set -euo pipefail
readonly E_USAGE=2 E_NODIR=3 E_NOINDEX=4
die() {
local code=$1
shift
echo "check-site: $*" >&2
exit "$code"
}
[[ $# -eq 1 ]] || die "$E_USAGE" "usage: check-site.sh <directory>"
dir=$1
[[ -d $dir ]] || die "$E_NODIR" "'$dir' is not a directory"
[[ -f $dir/index.html ]] || die "$E_NOINDEX" "'$dir/index.html' is missing"
# grep exits 1 when nothing matches: an expected result, not an error
if ! grep -qi '<title>' "$dir/index.html"; then
echo "warning: index.html has no <title>"
fi
count=$(find "$dir" -type f | wc -l)
echo "ok: $count files ready in $dir"
Terminal window
cd ~/bashlab
chmod +x check-site.sh
mkdir -p site-a site-b
echo '<h1>hi</h1>' > site-a/index.html
for args in "" "nope" "site-b" "site-a"; do
./check-site.sh $args
status=$?
case $status in
0) echo " -> [$args] published" ;;
2) echo " -> [$args] fix the command line" ;;
3|4) echo " -> [$args] fix the site files (status $status)" ;;
*) echo " -> [$args] unknown error $status" ;;
esac
done
خروجی
check-site: usage: check-site.sh <directory>
-> [] fix the command line
check-site: 'nope' is not a directory
-> [nope] fix the site files (status 3)
check-site: 'site-b/index.html' is missing
-> [site-b] fix the site files (status 4)
warning: index.html has no <title>
ok: 1 files ready in site-a
-> [site-a] published

(اینجا $args را عمداً بدون کوتیشن نوشتیم تا رشته‌ی خالی هیچ آرگومانی نشود؛ استثنای آگاهانه.)

نکته‌ی مهم این مثال grep: کد ۱ grep یعنی «پیدا نشد»، که خطا نیست. اگر آن را تنها می‌نوشتی، set -e اسکریپت را می‌بست. وقتی شکستی قابل‌انتظار است، صریح بررسی‌اش کن: با if ! ...، یا cmd || true اگر واقعاً نتیجه برایت مهم نیست، یا cmd || die ... اگر می‌خواهی پیام بهتری بدهی.

پشت پرده: این گزینه‌ها کجا نگه داشته می‌شوند؟

Section titled “پشت پرده: این گزینه‌ها کجا نگه داشته می‌شوند؟”

set فقط چند «پرچم» در خود شل را روشن و خاموش می‌کند. متغیر ویژه‌ی $- پرچم‌های تک‌حرفی فعلی را نشان می‌دهد و set -o همه را با اسم بلند:

Terminal window
echo "قبل: $-"
set -euo pipefail
echo "بعد: $-"
set -o | grep -E '^(errexit|nounset|pipefail|errtrace)'
set +euo pipefail
echo "خاموش با +: $-"
خروجی
قبل: hB
بعد: ehuB
errexit on
errtrace off
nounset on
pipefail on
خاموش با +: hB

e و u به $- اضافه شدند (pipefail حرف کوتاه ندارد). با + به‌جای - همان گزینه خاموش می‌شود؛ گاهی برای یک تکه‌ی کوچک لازم است: set +e; دستور; status=$?; set -e.

trap‌ها هم در حافظه‌ی شل‌اند. trap -p همه‌ی trap‌های فعلی را نشان می‌دهد و kill -l فهرست سیگنال‌ها و شماره‌شان را:

Terminal window
trap 'echo bye' EXIT
trap 'echo "error!"' ERR
trap -p
trap - ERR
echo "--- بعد از trap - ERR:"
trap -p
trap - EXIT
echo "--- چند سیگنال مهم:"
kill -l 2 15 9 1
خروجی
trap -- 'echo bye' EXIT
trap -- 'echo "error!"' ERR
--- بعد از trap - ERR:
trap -- 'echo bye' EXIT
--- چند سیگنال مهم:
INT
TERM
KILL
HUP

kill -l با شماره، اسم سیگنال را می‌دهد: ۲ ← INT، ۱۵ ← TERM، ۹ ← KILL، ۱ ← HUP. trap - SIGNAL یک trap را برمی‌دارد. EXIT و ERR سیگنال واقعی سیستم‌عامل نیستند؛ رویدادهای «شبه‌سیگنال» خود Bash‌اند.

$(...) گزینه‌ی -e را به ارث نمی‌برد. جایگزینی دستور در یک پروسه‌ی فرزند اجرا می‌شود و Bash به‌صورت پیش‌فرض errexit را داخل آن خاموش می‌کند:

Terminal window
bash -c 'set -e; x=$(false; echo "still running inside"); echo "x=[$x]"'
echo "--- با shopt -s inherit_errexit:"
bash -c 'set -e; shopt -s inherit_errexit; x=$(false; echo "still running inside"); echo "x=[$x]"'
echo "کد خروج: $?"
خروجی
x=[still running inside]
--- با shopt -s inherit_errexit:
کد خروج: 1

در حالت اول false داخل $(...) نادیده گرفته شد. با shopt -s inherit_errexit (Bash ۴.۴+)، فرزند هم در اولین خطا می‌ایستد و چون انتساب x=$(...) کد خروج فرزند را می‌گیرد، کل اسکریپت متوقف شد. خیلی‌ها این خط را هم کنار set -euo pipefail می‌گذارند.

گزینه‌های set برای اسکریپت‌نویسی امن:

گزینه اسم بلند اثر خاموش‌کردن
-e errexit توقف بعد از دستور شکست‌خورده‌ی بررسی‌نشده set +e
-u nounset متغیر تعریف‌نشده خطاست set +u
-o pipefail pipefail کد pipe = کد آخرین عضو شکست‌خورده set +o pipefail
-E errtrace trap ERR داخل تابع‌ها و subshell‌ها هم اجرا شود set +E
-x xtrace چاپ هر دستور قبل از اجرا (درس دیباگ) set +x
shopt -s inherit_errexit -e داخل $(...) هم فعال باشد shopt -u inherit_errexit

trap‌های پرکاربرد:

trap کی اجرا می‌شود کاربرد
trap cleanup EXIT هر نوع پایان اسکریپت (عادی، exit، set -e، SIGINT/SIGTERM) پاک‌کردن فایل موقت، آزادکردن قفل
trap 'on_error $LINENO' ERR بعد از هر دستور شکست‌خورده (همان‌هایی که set -e می‌بیند) گزارش خط و دستور
trap 'echo stop; exit 130' INT Ctrl+C رفتار سفارشی موقع لغو
trap '' HUP (رشته‌ی خالی یعنی نادیده‌گرفتن) ادامه بعد از بسته‌شدن ترمینال
trap - EXIT برداشتن trap
trap -p دیدن trap‌های فعلی

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

کد معنی
0 موفق
1 خطای عمومی
2 استفاده‌ی غلط (آرگومان نادرست)؛ همچنین خطای نحو در دستورهای داخلی bash
126 فایل پیدا شد ولی قابل اجرا نیست (مجوز)
127 دستور پیدا نشد
128+N بسته‌شده با سیگنال N: ‏130 = Ctrl+C، 143 = kill، 137 = kill -9
3 تا 125 آزاد برای کدهای سفارشی خودت (مستندشان کن)

۱) ((i++)) اسکریپت را می‌بندد

Section titled “۱) ((i++)) اسکریپت را می‌بندد”
cd ~/bashlab
cat > counter.sh <<'EOF'
#!/usr/bin/env bash
set -e
count=0
for f in a b c; do
((count++))
echo "processed $f (count=$count)"
done
echo "total: $count"
EOF
bash counter.sh
echo "کد خروج: $?"
خروجی
کد خروج: 1

هیچ چیزی چاپ نشد! ((count++)) مقدار قبلی (۰) را برمی‌گرداند و (( 0 )) یعنی «غلط»، کد خروج ۱. set -e آن را شکست می‌بیند. راه‌حل: ((++count)) (مقدار جدید، ۱، درست است)، یا count=$((count + 1))، یا ((count++)) || true.

۲) local x=$(cmd) خطا را پنهان می‌کند

Section titled “۲) local x=$(cmd) خطا را پنهان می‌کند”
Terminal window
bash -c 'set -e
f() { local out=$(false); echo "masked: still running after a failed command"; }
f'
echo "--- راه‌حل:"
bash -c 'set -e
g() { local out; out=$(false); echo "never printed"; }
g'
echo "کد خروج: $?"
خروجی
masked: still running after a failed command
--- راه‌حل:
کد خروج: 1

local کد خروج خودش (۰) را برمی‌گرداند (درس تابع‌ها). راه‌حل: local out در یک خط و out=$(cmd) در خط بعد. همین برای export x=$(cmd) و readonly x=$(cmd) هم درست است.

۳) تابعی که در if صدا زده می‌شود، از set -e بیرون است

Section titled “۳) تابعی که در if صدا زده می‌شود، از set -e بیرون است”
Terminal window
bash -c 'set -e
setup() { mkdir /root/forbidden 2>/dev/null; echo "setup continued after mkdir failed!"; }
if setup; then echo "setup reported success"; fi'
خروجی
setup continued after mkdir failed!
setup reported success

چون setup شرطِ if است، set -e داخل کل تابع خاموش است؛ mkdir شکست خورد و تابع ادامه داد و موفق گزارش شد. راه‌حل: داخل تابع‌های مهم شکست را صریح برگردان: mkdir ... || return 1.

Terminal window
bash -c 'trap "echo error on line $LINENO" ERR
echo line 2
false'
echo "--- با کوتیشن تکی:"
bash -c "trap 'echo error on line \$LINENO' ERR
echo line 2
false"
خروجی
line 2
error on line 1
--- با کوتیشن تکی:
line 2
error on line 3

با کوتیشن دوتایی، $LINENO همان لحظه‌ی تعریف trap باز شد (خط ۱). با کوتیشن تکی، موقع اجرای trap (خط ۳). راه‌حل: متن trap را همیشه در کوتیشن تکی بنویس، یا آن را در یک تابع بگذار.

۵) پاک‌سازی بدون محافظت از متغیر خالی

Section titled “۵) پاک‌سازی بدون محافظت از متغیر خالی”

اگر trap قبل از ساختن پوشه‌ی موقت ثبت شود و mktemp شکست بخورد، rm -rf "$workdir" با متغیر خالی اجرا می‌شود. با rm -rf "$workdir"/* این یعنی rm -rf /*! راه‌حل: اول mktemp و بعد trap؛ از set -u استفاده کن؛ و در cleanup بنویس [[ -n ${workdir:-} ]] && rm -rf -- "$workdir".

مثال ۴: cmd | gzip > file حتی با set -e، شکست cmd را نمی‌بیند. راه‌حل: همیشه set -o pipefail (یا در یک خط: set -euo pipefail).

۷) خطایی که «انتظارش را داری» اسکریپت را می‌بندد

Section titled “۷) خطایی که «انتظارش را داری» اسکریپت را می‌بندد”
Terminal window
bash -c 'set -eo pipefail
errors=$(grep -c ERROR /etc/hostname)
echo "errors: $errors"'
echo "کد خروج: $?"
echo "--- راه‌حل:"
bash -c 'set -eo pipefail
errors=$(grep -c ERROR /etc/hostname || true)
echo "errors: $errors"'
خروجی
کد خروج: 1
--- راه‌حل:
errors: 0

grep -c وقتی چیزی پیدا نکند 0 چاپ می‌کند ولی کد خروج ۱ می‌دهد و set -e اسکریپت را بی‌پیام می‌بندد. راه‌حل: || true برای شکستی که معنی‌اش «نتیجه‌ی خالی» است.

✎ تمرینآسان

این اسکریپت سه ایراد امنیتی دارد که با set -euo pipefail پیدا می‌شوند. اول آن را بدون تغییر اجرا کن و ببین با کد ۰ تمام می‌شود؛ بعد سطر set -euo pipefail را اضافه کن و بگو کجا متوقف شد و چرا. در پایان ایرادها را درست کن.

report.sh
#!/usr/bin/env bash
logdir=/var/log/myapp
lines=$(cat $logdir/app.log | wc -l)
echo "log has $lines lines"
echo "report for $USERNAME"
دیدن جواب
cd ~/bashlab
cat > report.sh <<'EOF'
#!/usr/bin/env bash
logdir=/var/log/myapp
lines=$(cat $logdir/app.log | wc -l)
echo "log has $lines lines"
echo "report for $USERNAME"
EOF
echo "--- بدون set:"
bash report.sh; echo "کد خروج: $?"
sed -i '1a set -euo pipefail' report.sh
echo "--- با set -euo pipefail:"
bash report.sh; echo "کد خروج: $?"
echo "--- نسخه‌ی درست:"
cat > report.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
logdir=/var/log/myapp
logfile="$logdir/app.log"
if [[ ! -f $logfile ]]; then
echo "report: $logfile not found" >&2
exit 3
fi
lines=$(wc -l < "$logfile")
echo "log has $lines lines"
echo "report for ${USERNAME:-$USER}"
EOF
bash report.sh; echo "کد خروج: $?"
خروجی
--- بدون set:
cat: /var/log/myapp/app.log: No such file or directory
log has 0 lines
report for
کد خروج: 0
--- با set -euo pipefail:
cat: /var/log/myapp/app.log: No such file or directory
کد خروج: 1
--- نسخه‌ی درست:
report: /var/log/myapp/app.log not found
کد خروج: 3

۱) بدون گزینه‌ها، cat شکست خورد ولی wc «۰ خط» گزارش کرد و $USERNAME (که در این سیستم تعریف نشده؛ اسم درست USER است) خالی چاپ شد؛ کد ۰. ۲) با pipefail، شکست cat به lines=$(...) رسید و set -e اسکریپت را با کد ۱ بست (اگر از آن رد می‌شد، set -u روی USERNAME می‌ایستاد). ۳) نسخه‌ی درست وجود فایل را صریح بررسی می‌کند، کد خروج معنادار (۳) می‌دهد، cat | بی‌دلیل را حذف کرده و برای متغیر اختیاری مقدار پیش‌فرض دارد.

✎ تمرینمتوسط

تمرین اصلی درس: به اسکریپت بکاپ مدیریت خطا و پاک‌سازی اضافه کن. اسکریپت backup.sh <پوشه‌ی-مبدأ> <پوشه‌ی-مقصد>:

۱. set -euo pipefail داشته باشد و کدهای خروج مستند: ۲ برای استفاده‌ی غلط، ۳ برای نبودن مبدأ. ۲. آرشیو را اول در یک پوشه‌ی موقت (mktemp -d) بسازد و فقط اگر کامل و سالم بود (tar -tzf بدون خطا) به مقصد منتقل کند؛ تا هیچ‌وقت آرشیو نیمه‌کاره در مقصد نماند. ۳. با trap روی EXIT پوشه‌ی موقت را در هر حالتی پاک کند و با trap ERR خط و دستور شکست‌خورده را گزارش کند.

ثابت کن در حالت موفق یک آرشیو سالم در مقصد هست، و در حالتی که ساخت آرشیو شکست می‌خورد (مثلاً یک فایل غیرقابل‌خواندن در مبدأ) هیچ آرشیو و هیچ پوشه‌ی موقتی نمی‌ماند.

دیدن جواب
cd ~/bashlab
cat > backup.sh <<'EOF'
#!/usr/bin/env bash
# backup.sh: ./backup.sh <source-dir> <dest-dir>
# Exit codes: 0 ok, 1 error, 2 usage, 3 source not found
set -Eeuo pipefail
readonly E_USAGE=2 E_NOSRC=3
die() {
local code=$1
shift
echo "backup: $*" >&2
exit "$code"
}
[[ $# -eq 2 ]] || die "$E_USAGE" "usage: backup.sh <source-dir> <dest-dir>"
src=$1
dest=$2
[[ -d $src ]] || die "$E_NOSRC" "source '$src' not found"
mkdir -p "$dest"
tmpdir="$(mktemp -d)"
cleanup() {
local status=$?
[[ -n ${tmpdir:-} ]] && rm -rf -- "$tmpdir"
if (( status != 0 )); then
echo "backup: FAILED (status $status); nothing was written to $dest" >&2
fi
}
trap cleanup EXIT
trap 'echo "backup: error at line $LINENO: $BASH_COMMAND" >&2' ERR
name="$(basename "$src")-$(date +%F_%H%M%S).tar.gz"
tar -czf "$tmpdir/$name" -C "$(dirname "$src")" "$(basename "$src")"
tar -tzf "$tmpdir/$name" > /dev/null
mv "$tmpdir/$name" "$dest/"
echo "backup: created $dest/$name"
EOF
chmod +x backup.sh
mkdir -p data/docs
echo "report" > data/docs/report.txt
echo "notes" > data/notes.txt
echo "=== حالت موفق:"
./backup.sh data backups; echo "کد خروج: $?"
ls backups
tar -tzf backups/*.tar.gz
echo "=== استفاده‌ی غلط و مبدأ ناموجود:"
./backup.sh; echo "کد خروج: $?"
./backup.sh nothing backups; echo "کد خروج: $?"
echo "=== حالت ناموفق (فایل غیرقابل‌خواندن):"
rm -rf backups
echo "secret" > data/locked.txt
chmod 000 data/locked.txt
./backup.sh data backups; echo "کد خروج: $?"
echo "آرشیو در مقصد: $(ls backups | wc -l)"
echo "پوشه‌ی موقت باقی‌مانده: $(find /tmp -maxdepth 1 -name 'tmp.*' -user ali | wc -l)"
chmod 644 data/locked.txt
خروجی
=== حالت موفق:
backup: created backups/data-2026-10-04_114245.tar.gz
کد خروج: 0
data-2026-10-04_114245.tar.gz
data/
data/notes.txt
data/docs/
data/docs/report.txt
=== استفاده‌ی غلط و مبدأ ناموجود:
backup: usage: backup.sh <source-dir> <dest-dir>
کد خروج: 2
backup: source 'nothing' not found
کد خروج: 3
=== حالت ناموفق (فایل غیرقابل‌خواندن):
tar: data/locked.txt: Cannot open: Permission denied
tar: Exiting with failure status due to previous errors
backup: error at line 33: tar -czf "$tmpdir/$name" -C "$(dirname "$src")" "$(basename "$src")"
backup: FAILED (status 2); nothing was written to backups
کد خروج: 2
آرشیو در مقصد: 0
پوشه‌ی موقت باقی‌مانده: 0

در حالت ناموفق: tar نتوانست locked.txt را بخواند و کد ۲ داد؛ trap ERR خط و دستور را گفت؛ set -e اسکریپت را بست؛ cleanup پوشه‌ی موقت را پاک کرد و پیام FAILED داد؛ و چون آرشیو فقط بعد از موفقیت به مقصد mv می‌شود، پوشه‌ی backups خالی ماند. این الگوی «اول در موقت، بعد انتقال» برای هر فایل مهمی که می‌سازی (تنظیمات، گزارش، آرشیو) به کار می‌آید؛ چون mv در یک فایل‌سیستم تقریباً آنی است، کسی هرگز فایل نیمه‌کاره نمی‌بیند. (اگر /tmp و مقصد روی دو دیسک جدا باشند، mv در واقع کپی است؛ آن‌وقت پوشه‌ی موقت را کنار مقصد بساز: mktemp -d -p "$dest".)

✎ تمرینمتوسط

اسکریپتی بنویس که فقط یک نسخه از آن همزمان اجرا شود: اول کار یک «فایل قفل» (/tmp/myjob.lock) بسازد و اگر قفل از قبل بود، با پیام و کد ۷۵ بیرون برود؛ و قفل را با trap EXIT در هر حالتی بردارد. برای ساختن قفل از set -o noclobber استفاده کن (که > روی فایل موجود را خطا می‌کند) تا بررسی و ساختن در یک قدم انجام شود. دو نسخه را همزمان اجرا کن.

دیدن جواب
cd ~/bashlab
cat > single.sh <<'EOF'
#!/usr/bin/env bash
# single.sh: only one instance may run at a time
set -euo pipefail
lock=/tmp/myjob.lock
if ! (set -o noclobber; echo "$$" > "$lock") 2>/dev/null; then
echo "[$$] another instance is running (pid $(cat "$lock")); exiting" >&2
exit 75
fi
trap 'rm -f "$lock"' EXIT
echo "[$$] got the lock, working..."
sleep 2
echo "[$$] done"
EOF
chmod +x single.sh
rm -f /tmp/myjob.lock
./single.sh &
sleep 0.5
./single.sh; echo "نسخه‌ی دوم، کد خروج: $?"
wait
echo "قفل بعد از پایان مانده؟ $(ls /tmp/myjob.lock 2>/dev/null || echo 'نه')"
خروجی
[4637] got the lock, working...
[4641] another instance is running (pid 4637); exiting
نسخه‌ی دوم، کد خروج: 75
[4637] done
قفل بعد از پایان مانده؟ نه

نکته‌ها: ۱) set -o noclobber باعث می‌شود > روی فایلی که هست شکست بخورد؛ پس «ببین قفل هست؟» و «بساز» یک عمل واحد است و دو نسخه نمی‌توانند هر دو موفق شوند (اگر اول با [[ -f ]] بررسی کنی و بعد بسازی، بین این دو قدم نسخه‌ی دیگری می‌تواند برسد). ۲) داخل پرانتز نوشتیم تا noclobber فقط همان‌جا روشن باشد. ۳) trap فقط بعد از گرفتن قفل ثبت شد؛ وگرنه نسخه‌ی دوم موقع خروج، قفل نسخه‌ی اول را پاک می‌کرد. ۴) کد ۷۵ در قرارداد sysexits یعنی «موقتاً ممکن نیست، بعداً دوباره امتحان کن». (ابزار آماده‌ی flock هم همین کار را می‌کند و قفل را حتی بعد از kill -9 آزاد می‌کند.)

✎ تمرینسخت

تابع run_step بنویس که یک «قدم» از یک فرایند را اجرا کند: run_step "اسم قدم" دستور آرگومان‌ها.... اگر دستور موفق بود [ OK ] اسم قدم چاپ کند؛ اگر شکست خورد [FAIL] اسم قدم (status N) را روی stderr بنویسد، خروجی خطای دستور را (که در یک فایل موقت نگه داشته) نشان دهد، و اسکریپت را با همان کد خروج دستور ببندد. فایل موقت در هر حالتی پاک شود. با سه قدم امتحانش کن که قدم دوم شکست می‌خورد.

دیدن جواب
cd ~/bashlab
cat > steps.sh <<'EOF'
#!/usr/bin/env bash
# steps.sh: run named steps, stop at the first failure with its exit status
set -euo pipefail
errfile="$(mktemp)"
trap 'rm -f -- "$errfile"' EXIT
run_step() {
local name=$1
shift
local status=0
"$@" > /dev/null 2> "$errfile" || status=$?
if (( status == 0 )); then
echo "[ OK ] $name"
else
echo "[FAIL] $name (status $status)" >&2
sed 's/^/ | /' "$errfile" >&2
exit "$status"
fi
}
run_step "check disk space" df -h /
run_step "read the config" cat /etc/myapp/config.yml
run_step "say hello" echo hello
EOF
chmod +x steps.sh
./steps.sh; echo "کد خروج: $?"
echo "فایل موقت باقی‌مانده: $(find /tmp -maxdepth 1 -name 'tmp.*' -user ali -type f | wc -l)"
خروجی
[ OK ] check disk space
[FAIL] read the config (status 1)
| cat: /etc/myapp/config.yml: No such file or directory
کد خروج: 1
فایل موقت باقی‌مانده: 0

نکته‌ها: ۱) "$@" ... || status=$? شکست را بررسی‌شده می‌کند، پس set -e اسکریپت را وسط تابع نمی‌بندد و ما فرصت داریم پیام بدهیم؛ $? بعد از || هنوز کد دستور است. ۲) exit "$status" همان کد واقعی (cat: ۱) را به صدازننده می‌دهد. ۳) خروجی خطا را با sed تورفتگی دادیم تا از پیام خودمان جدا دیده شود. ۴) trap EXIT فایل موقت را حتی بعد از exit داخل تابع پاک کرد. قدم سوم اجرا نشد.

⚡ بررسی سریع

اسکریپتی بدون set -e دستور cp را با شکست اجرا می‌کند و دستور آخرش echo done است. کد خروج اسکریپت چیست؟

؟ آزمونک
  1. با set -e، کدام خط اسکریپت را متوقف می‌کند؟

  2. mysqldump db | gzip > db.gz با set -e؛ اگر mysqldump شکست بخورد چه می‌شود؟

  3. در تابع cleanup که با trap ... EXIT اجرا می‌شود، چرا local status=$? باید خط اول باشد؟

  4. اسکریپتی با set -e حلقه‌ای با count=0 و ((count++)) دارد و بی‌صدا در اولین دور بسته می‌شود. چرا؟

  5. اسکریپتی با Ctrl+C متوقف شده و کد خروجش ۱۳۰ است. ۱۳۰ از کجا آمده؟

  6. trap "echo error on line $LINENO" ERR همیشه خط ۱ را گزارش می‌کند. چرا؟

  • بدون مدیریت خطا، Bash بعد از شکست ادامه می‌دهد و کد خروج اسکریپت کد آخرین دستور است؛ خطاهای وسط پنهان می‌مانند.
  • set -euo pipefail را اول هر اسکریپت جدی بگذار: -e توقف در دستور شکست‌خورده‌ی بررسی‌نشده، -u خطا برای متغیر تعریف‌نشده (${var:-} برای اختیاری‌ها)، pipefail تا شکست وسط pipe دیده شود. -E تا trap ERR در تابع‌ها هم اجرا شود.
  • set -e در شرط if/while، قبل از ||/&&، با !، داخل تابعی که در if صدا زده شده و داخل $(...) (مگر inherit_errexit) اثر ندارد. دستورهای حساس را صریح بررسی کن: cmd || die ....
  • شکست قابل‌انتظار را صریح مدیریت کن: if ! cmd، cmd || true. دام‌ها: ((i++)) با مقدار ۰، local x=$(cmd).
  • trap cleanup EXIT بلافاصله بعد از ساختن منبع موقت (mktemp -d)؛ در هر پایانی اجرا می‌شود (جز kill -9). اولین خط cleanup: local status=$?.
  • trap '... $LINENO ... $BASH_COMMAND' ERR با کوتیشن تکی برای گزارش «کجا و چه».
  • کدهای خروج سفارشی را ثابت و مستند کن (readonly E_USAGE=2)؛ ۱۲۶، ۱۲۷ و ۱۲۸+N معنی خاص دارند.
  • الگوی «اول در موقت، بعد mv» تا هیچ‌کس فایل نیمه‌کاره نبیند.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
set -euo pipefailتوقف در خطا، متغیر تعریف‌نشده خطا، شکست وسط pipe
set -Eeuo pipefailهمان، به‌علاوه‌ی اجرای trap ERR داخل تابع‌ها
shopt -s inherit_errexitاثر set -e داخل $(...)
trap cleanup EXITپاک‌سازی در هر نوع خروج
trap 'echo "line $LINENO: $BASH_COMMAND"' ERRگزارش خط و دستور شکست‌خورده
tmp="$(mktemp -d)"پوشه‌ی موقت امن و یکتا
exit 2 die() { echo "$*" >&2; exit 1; }پایان با کد خروج معنادار
cmd || true if ! cmd; thenشکست قابل‌انتظار، بدون توقف
echo "${PIPESTATUS[@]}"کد خروج همه‌ی عضوهای آخرین pipe
${var:-}متغیر اختیاری با set -u
trap -p trap - EXITدیدن و برداشتن trap‌ها
echo $- set -oگزینه‌های فعلی شل