این آزمون درسهای بخش «اسکریپت حرفهای» را میسنجد: مدیریت خطا، getopts، دیباگ و کیفیت کد و پروژهی بکاپ و سلامت سرور. جواب هر سؤال همان لحظه با توضیح نشان داده میشود و نتیجهات در همین مرورگر ذخیره میشود (پیشرفت من).
اسکریپتی بدون set -e وسط کار یک cp ناموفق دارد و آخرش echo done است. cron چه کدی میبیند؟
خطاهای وسط اسکریپت بدون set -e یا exit صریح پنهان میمانند.
با set -e، کدام خط اسکریپت را متوقف میکند؟
شکست در شرط if/while و قبل از || «بررسیشده» است و اسکریپت را نمیبندد.
pg_dump db | gzip > db.gz با set -e اجرا شد و pg_dump شکست خورد ولی اسکریپت ادامه داد. چه چیزی جا افتاده؟
بدون pipefail کد pipe کد آخرین عضو (gzip) است.
اسکریپتی با set -u پیام «DEBUG: unbound variable» میدهد ولی DEBUG باید اختیاری باشد. راهحل؟
${var:-} مقدار خالی میدهد، بدون خطا.
count=0 و ((count++)) در اسکریپتی با set -e بیصدا آن را میبندد. چرا؟
((++count)) یا count=$((count + 1)) یا ((count++)) || true.
اولین خط تابع cleanup که با trap ... EXIT اجرا میشود، چه باید باشد و چرا؟
هر دستور بعدی $? را بازنویسی میکند.
اسکریپتی با Ctrl+C متوقف شد، trap EXIT اجرا شد ولی داخلش $? صفر بود. چطور کد درست (۱۳۰) را داخل trap ببینی؟
trap سیگنال با exit صریح، کد درست را به trap EXIT میدهد.
trap "echo error at $LINENO" ERR همیشه خط ۱ را گزارش میدهد. چرا؟
متن trap را در کوتیشن تکی بنویس تا موقع اجرا باز شود.
trap ERR خطای داخل تابعها را گزارش نمیکند. کدام گزینه را فراموش کردهای؟
set -Eeuo pipefail.
رشتهی گزینههای getopts برای «-h و -v فلگ، -o مقدار میخواهد، پیام خطا را خودم مینویسم» کدام است؟
: اول یعنی حالت بیصدا و : بعد از o یعنی مقدار.
بعد از حلقهی getopts، $1 هنوز -v است. چه چیزی جا افتاده؟
گزینههای خواندهشده را باید کنار گذاشت تا عملوندها در $@ بمانند.
کاربر ./tool.sh -o -v file زده (مقدار -o جا افتاده). با "vo:" نتیجه چیست؟
مقدارهایی را که با - شروع میشوند در اعتبارسنجی رد کن.
در case یک حلقهی getopts، شاخهی ?) اول آمده و همهی گزینهها «ناشناخته» گزارش میشوند. اصلاح؟
\?) فقط با خود علامت ? جور میشود.
getopts کدام را پشتیبانی نمیکند؟
گزینهی بلند را با حلقهی دستی while/case یا getopt (util-linux) بخوان.
پیام راهنمای -h باید کجا چاپ شود و با چه کدی تمام شود؟
با ورودی غلط: پیام روی stderr و کد ۲.
bash -n script.sh بدون خطا تمام شد. کدام نتیجه درست است؟
بعد از bash -n، shellcheck بزن.
در خروجی bash -x خط «+ rm -f reports/Monthly 'Reports/*.log'» را میبینی. مشکل چیست؟
"$dir"/*.log درست است.
میخواهی در trace کنار هر خط، اسم فایل و شمارهی خط هم باشد. چه میکنی؟
PS4 پیشوند خطهای trace است و موقع چاپ هر خط باز میشود.
shellcheck برای cd $dir دو هشدار میدهد. کدامها؟
cd "$dir" || exit.
تابعی که خروجیاش با $(...) گرفته میشود، یک echo "DEBUG ..." دارد و داده خراب شده. راهحل؟
$(...) فقط stdout را میگیرد.
چرا در پروژه، آرشیو را در یک پوشهی موقت کنار مقصد میسازیم و نه در /tmp؟
mktemp -d -p "$dest".
flock -n 9 بعد از exec 9> lockfile. چه برتریای بر فایل قفل دستی (noclobber) دارد؟
فایل قفل دستی بعد از kill -9 میماند و باید دستی پاکش کرد.
چرخش بکاپ با find "$dest/" -mtime +7 -delete. بزرگترین خطر چیست و چطور جلویش را میگیری؟
همیشه اول با dry run (-print بدون -delete) ببین.
lx-health -q در cron هر ۵ دقیقه اجرا میشود و MAILTO تنظیم شده. کی ایمیل میآید؟
cron فقط وقتی خروجی غیرخالی باشد ایمیل میفرستد.
با همهی آنچه در این بخش آموختی، ابزار 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/quiz3cat > 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 runningset -Eeuo pipefail
readonly PROG=logrot.shkeep=7verbose=falsedry_run=false
usage() { cat <<USAGEUsage: $PROG [-h] [-v] [-n] [-k DAYS] LOGDIR
Compress *.log files older than one day and delete *.log.gzfiles 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 helpUSAGE}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" ;; esacdoneshift $((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=$1if [[ ! -d $dir ]]; then echo "$PROG: '$dir' is not a directory" >&2 exit 3fi
exec 9> "$dir/.logrot.lock"if ! flock -n 9; then echo "$PROG: another instance is running" >&2 exit 75fi
compressed=0while 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=0while 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)"EOFchmod +x logrot.shshellcheck logrot.sh && echo "shellcheck: clean"mkdir -p logsecho "today" > logs/app.logecho "yesterday" > logs/app-1.log && touch -d "2 days ago" logs/app-1.logecho "old" > logs/app-20.log && gzip logs/app-20.log && touch -d "20 days ago" logs/app-20.log.gzecho "recent" > logs/app-3.log && gzip logs/app-3.log && touch -d "3 days ago" logs/app-3.log.gzecho "=== -n -v:"./logrot.sh -n -v logsecho "=== اجرای واقعی:"./logrot.sh -v logsls logsecho "=== ورودیهای غلط:"./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.logdry run: would delete logs/app-20.log.gzlogrot.sh: 1 compressed, 1 deleted (dry run: true)=== اجرای واقعی:logrot.sh: compressed logs/app-1.loglogrot.sh: deleted logs/app-20.log.gzlogrot.sh: 1 compressed, 1 deleted (dry run: false)app-1.log.gzapp-3.log.gzapp.log=== ورودیهای غلط:logrot.sh: -k must be 1-365, got 0try 'logrot.sh -h'کد خروج: 2logrot.sh: 'nothing' is not a directoryکد خروج: 3logrot.sh: unknown option -xtry 'logrot.sh -h'کد خروج: 2چرا say با if نوشته شد و نه [[ ... ]] && echo؟ چون وقتی verbose خاموش است، شکل کوتاه با کد ۱ تمام میشود و چون آخرین دستور تابع است، کد تابع ۱ میشود و set -e اسکریپت را میبندد (درس مدیریت خطا). فایل app.log امروز تغییر کرده، پس فشرده نشد؛ app-3.log.gz سه روزه است و با پیشفرض ۷ روز ماند.