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

آزمون بخش ۳: اسکریپت حرفه‌ای

این آزمون درس‌های بخش «اسکریپت حرفه‌ای» را می‌سنجد: مدیریت خطا، getopts، دیباگ و کیفیت کد و پروژه‌ی بکاپ و سلامت سرور. جواب هر سؤال همان لحظه با توضیح نشان داده می‌شود و نتیجه‌ات در همین مرورگر ذخیره می‌شود (پیشرفت من).

؟ آزمون بخش ۳
  1. اسکریپتی بدون set -e وسط کار یک cp ناموفق دارد و آخرش echo done است. cron چه کدی می‌بیند؟

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

  3. pg_dump db | gzip > db.gz با set -e اجرا شد و pg_dump شکست خورد ولی اسکریپت ادامه داد. چه چیزی جا افتاده؟

  4. اسکریپتی با set -u پیام «DEBUG: unbound variable» می‌دهد ولی DEBUG باید اختیاری باشد. راه‌حل؟

  5. count=0 و ((count++)) در اسکریپتی با set -e بی‌صدا آن را می‌بندد. چرا؟

  6. اولین خط تابع cleanup که با trap ... EXIT اجرا می‌شود، چه باید باشد و چرا؟

  7. اسکریپتی با Ctrl+C متوقف شد، trap EXIT اجرا شد ولی داخلش $? صفر بود. چطور کد درست (۱۳۰) را داخل trap ببینی؟

  8. trap "echo error at $LINENO" ERR همیشه خط ۱ را گزارش می‌دهد. چرا؟

  9. trap ERR خطای داخل تابع‌ها را گزارش نمی‌کند. کدام گزینه را فراموش کرده‌ای؟

  10. رشته‌ی گزینه‌های getopts برای «-h و -v فلگ، -o مقدار می‌خواهد، پیام خطا را خودم می‌نویسم» کدام است؟

  11. بعد از حلقه‌ی getopts، $1 هنوز -v است. چه چیزی جا افتاده؟

  12. کاربر ./tool.sh -o -v file زده (مقدار -o جا افتاده). با "vo:" نتیجه چیست؟

  13. در case یک حلقه‌ی getopts، شاخه‌ی ?) اول آمده و همه‌ی گزینه‌ها «ناشناخته» گزارش می‌شوند. اصلاح؟

  14. getopts کدام را پشتیبانی نمی‌کند؟

  15. پیام راهنمای -h باید کجا چاپ شود و با چه کدی تمام شود؟

  16. bash -n script.sh بدون خطا تمام شد. کدام نتیجه درست است؟

  17. در خروجی bash -x خط «+ rm -f reports/Monthly 'Reports/*.log'» را می‌بینی. مشکل چیست؟

  18. می‌خواهی در trace کنار هر خط، اسم فایل و شماره‌ی خط هم باشد. چه می‌کنی؟

  19. shellcheck برای cd $dir دو هشدار می‌دهد. کدام‌ها؟

  20. تابعی که خروجی‌اش با $(...) گرفته می‌شود، یک echo "DEBUG ..." دارد و داده خراب شده. راه‌حل؟

  21. چرا در پروژه، آرشیو را در یک پوشه‌ی موقت کنار مقصد می‌سازیم و نه در /tmp؟

  22. flock -n 9 بعد از exec 9> lockfile. چه برتری‌ای بر فایل قفل دستی (noclobber) دارد؟

  23. چرخش بکاپ با find "$dest/" -mtime +7 -delete. بزرگ‌ترین خطر چیست و چطور جلویش را می‌گیری؟

  24. lx-health -q در cron هر ۵ دقیقه اجرا می‌شود و MAILTO تنظیم شده. کی ایمیل می‌آید؟

🏁 چالش: ابزار چرخش لاگسخت

با همه‌ی آنچه در این بخش آموختی، ابزار logrot.sh را بنویس:

رابط ابزار
Usage: logrot.sh [-h] [-v] [-n] [-k DAYS] LOGDIR

۱. رابط: getopts با حالت بی‌صدا؛ -k (پیش‌فرض ۷، فقط عدد ۱ تا ۳۶۵)؛ -n اجرای آزمایشی؛ -v چاپ هر قدم؛ -h راهنما (stdout، کد ۰)؛ ورودی غلط: پیام روی stderr و کد ۲؛ پوشه‌ی ناموجود: کد ۳. ۲. کار: هر فایل *.log که بیش از یک روز از آخرین تغییرش گذشته را با gzip فشرده کند، و هر *.log.gz قدیمی‌تر از DAYS روز را پاک کند. با -n فقط بگوید چه می‌کرد. ۳. ایمنی: set -Eeuo pipefail؛ trap ERR که خط و دستور شکست‌خورده را روی stderr بگوید؛ قفل با flock (کد ۷۵ اگر نسخه‌ی دیگری در حال اجراست). ۴. کیفیت: shellcheck بدون هیچ هشداری. ۵. روی یک پوشه‌ی آزمایشی با فایل‌های قدیمی و جدید (با touch -d) اول با -n -v و بعد واقعی اجرایش کن.

راهنمایی

ساختار درس پروژه را دنبال کن: پیش‌فرض‌ها، تابع usage و die_usage، حلقه‌ی getopts، shift $((OPTIND - 1))، اعتبارسنجی، قفل (exec 9> "$lock"; flock -n 9 || ...)، و بعد دو حلقه‌ی while IFS= read -r f; do ...; done < <(find ...). برای «بیش از یک روز»: -mtime +0. برای -v یک تابع say بنویس که فقط در حالت verbose چاپ کند؛ و دقت کن [[ $verbose == true ]] && echo ... به‌عنوان آخرین دستور تابع، با set -e مشکل‌ساز است (از if استفاده کن).

راه‌حل
mkdir -p ~/bashlab/quiz3 && cd ~/bashlab/quiz3
cat > logrot.sh <<'EOF'
#!/usr/bin/env bash
# logrot.sh: compress old logs and delete very old compressed logs
# Usage: logrot.sh [-h] [-v] [-n] [-k DAYS] LOGDIR
# Exit codes: 0 ok, 1 error, 2 usage error, 3 LOGDIR not found, 75 already running
set -Eeuo pipefail
readonly PROG=logrot.sh
keep=7
verbose=false
dry_run=false
usage() {
cat <<USAGE
Usage: $PROG [-h] [-v] [-n] [-k DAYS] LOGDIR
Compress *.log files older than one day and delete *.log.gz
files older than DAYS days (default: $keep).
-k DAYS keep compressed logs for DAYS days (1-365)
-n dry run: only show what would happen
-v print every step
-h show this help
USAGE
}
die_usage() { echo "$PROG: $*" >&2; echo "try '$PROG -h'" >&2; exit 2; }
say() {
if [[ $verbose == true ]]; then
echo "$PROG: $*"
fi
}
trap 'echo "$PROG: line $LINENO: \"$BASH_COMMAND\" failed" >&2' ERR
while getopts ":hvnk:" opt; do
case $opt in
h) usage; exit 0 ;;
v) verbose=true ;;
n) dry_run=true ;;
k) keep=$OPTARG ;;
\?) die_usage "unknown option -$OPTARG" ;;
:) die_usage "-$OPTARG needs a value" ;;
esac
done
shift $((OPTIND - 1))
(( $# == 1 )) || die_usage "expected exactly one LOGDIR"
[[ $keep =~ ^[0-9]+$ ]] || die_usage "-k must be a number, got '$keep'"
(( keep >= 1 && keep <= 365 )) || die_usage "-k must be 1-365, got $keep"
dir=$1
if [[ ! -d $dir ]]; then
echo "$PROG: '$dir' is not a directory" >&2
exit 3
fi
exec 9> "$dir/.logrot.lock"
if ! flock -n 9; then
echo "$PROG: another instance is running" >&2
exit 75
fi
compressed=0
while IFS= read -r f; do
if [[ $dry_run == true ]]; then
echo "dry run: would compress $f"
else
gzip -- "$f"
say "compressed $f"
fi
compressed=$((compressed + 1))
done < <(find "$dir" -maxdepth 1 -type f -name '*.log' -mtime +0 | sort)
deleted=0
while IFS= read -r f; do
if [[ $dry_run == true ]]; then
echo "dry run: would delete $f"
else
rm -f -- "$f"
say "deleted $f"
fi
deleted=$((deleted + 1))
done < <(find "$dir" -maxdepth 1 -type f -name '*.log.gz' -mtime +"$keep" | sort)
echo "$PROG: $compressed compressed, $deleted deleted (dry run: $dry_run)"
EOF
chmod +x logrot.sh
shellcheck logrot.sh && echo "shellcheck: clean"
mkdir -p logs
echo "today" > logs/app.log
echo "yesterday" > logs/app-1.log && touch -d "2 days ago" logs/app-1.log
echo "old" > logs/app-20.log && gzip logs/app-20.log && touch -d "20 days ago" logs/app-20.log.gz
echo "recent" > logs/app-3.log && gzip logs/app-3.log && touch -d "3 days ago" logs/app-3.log.gz
echo "=== -n -v:"
./logrot.sh -n -v logs
echo "=== اجرای واقعی:"
./logrot.sh -v logs
ls logs
echo "=== ورودی‌های غلط:"
./logrot.sh -k 0 logs; echo "کد خروج: $?"
./logrot.sh nothing; echo "کد خروج: $?"
./logrot.sh -x logs; echo "کد خروج: $?"
خروجی
shellcheck: clean
=== -n -v:
dry run: would compress logs/app-1.log
dry run: would delete logs/app-20.log.gz
logrot.sh: 1 compressed, 1 deleted (dry run: true)
=== اجرای واقعی:
logrot.sh: compressed logs/app-1.log
logrot.sh: deleted logs/app-20.log.gz
logrot.sh: 1 compressed, 1 deleted (dry run: false)
app-1.log.gz
app-3.log.gz
app.log
=== ورودی‌های غلط:
logrot.sh: -k must be 1-365, got 0
try 'logrot.sh -h'
کد خروج: 2
logrot.sh: 'nothing' is not a directory
کد خروج: 3
logrot.sh: unknown option -x
try 'logrot.sh -h'
کد خروج: 2

چرا say با if نوشته شد و نه [[ ... ]] && echo؟ چون وقتی verbose خاموش است، شکل کوتاه با کد ۱ تمام می‌شود و چون آخرین دستور تابع است، کد تابع ۱ می‌شود و set -e اسکریپت را می‌بندد (درس مدیریت خطا). فایل app.log امروز تغییر کرده، پس فشرده نشد؛ app-3.log.gz سه روزه است و با پیش‌فرض ۷ روز ماند.