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

here-doc و پردازش متن در اسکریپت

توی این درس یاد می‌گیری متن چندخطی را داخل اسکریپت بنویسی و به یک دستور بدهی، با here-doc: cat <<EOF > file ... EOF. فرق سه شکل <<EOF (متغیرها باز می‌شوند)، <<'EOF' (همه‌چیز عین متن) و <<-EOF (تورفتگی با tab) را می‌دانی، می‌فهمی چرا sudo cat > file کار نمی‌کند، و با آن فایل تنظیمات واقعی می‌سازی. بعد grep، sed و awk را که از درس‌های لینوکس می‌شناسی داخل اسکریپت به کار می‌بری: شمردن، جایگزینی در قالب‌ها، و گزارش‌گیری از لاگ. تمرین: اسکریپتی که فایل تنظیمات nginx یک سایت را از روی اسم دامنه بسازد.

مسئله: فایل‌هایی که اسکریپت باید بنویسد

Section titled “مسئله: فایل‌هایی که اسکریپت باید بنویسد”

اسکریپت‌ها فقط فایل نمی‌خوانند؛ می‌سازند: فایل تنظیمات یک سرویس، یک README، یک اسکریپت دیگر، یک پیام ایمیل. اگر هر خطش را با echo "..." >> file بنویسی، ده خط یعنی ده بار echo، کوتیشن‌های تو در تو و \nهای گیج‌کننده. here-doc راه طبیعی است: متن را همان‌طور که هست، چندخطی، وسط اسکریپت می‌نویسی و بخشی از آن را (مثلاً اسم دامنه) با متغیر پر می‌کنی.

و بعد از ساختن، باید پردازش هم کرد: چند خط خطا داشت؟ کدام IP بیشترین درخواست را زد؟ «80» را در فایل به «8080» عوض کن. این کارها با grep، sed و awk است که دیگر می‌دانی چه‌اند؛ این درس نشان می‌دهد چطور در اسکریپت و با متغیرها کنار هم بنشینند.

تشبیه: فرم چاپی با جاهای خالی

Section titled “تشبیه: فرم چاپی با جاهای خالی”

here-doc مثل یک فرم چاپی است که متنش از قبل نوشته شده و چند جای خالی دارد (اسم، تاریخ). اسکریپت جاهای خالی را پر می‌کند و فرم را تحویل می‌دهد. یک نکته: بعضی فرم‌ها متنی دارند که خودش شبیه «جای خالی» است (مثلاً $uri در تنظیمات nginx، که متغیر خود nginx است نه شل). باید به شل بگویی «این‌ها را پر نکن»؛ و تفاوت <<EOF با <<'EOF' همین است.

ساختار
دستور <<DELIMITER
متن چندخطی
DELIMITER

شل متن بین خط <<DELIMITER و خطی که فقط DELIMITER دارد را به ورودی استاندارد دستور می‌دهد، مثل اینکه کسی تایپش کرده باشد. اسم جداکننده دلخواه است ولی معمولاً EOF می‌نویسند.

فرق بنیادی: اگر جداکننده بدون کوتیشن باشد، شل داخل متن متغیرها و دستورها را باز می‌کند؛ با کوتیشن، متن دقیقاً همان‌طور که نوشته‌ای به دستور می‌رسد.
شکل رفتار
<<EOF متغیر، $(...) و $((...)) باز می‌شوند؛ \$، \\ و \` برای نوشتن خود نویسه‌ها
<<'EOF' (یا <<"EOF") هیچ‌چیز باز نمی‌شود؛ متن خام
<<-EOF مثل <<EOF، ولی tabهای ابتدای هر خط (و جداکننده) حذف می‌شوند، تا بتوانی داخل if/تابع با تورفتگی بنویسی
<<<"متن" here-string: یک رشته‌ی تک‌خطی به‌عنوان ورودی (در درس ورودی و آرگومان‌ها دیدی)

مثال ۱: اولین here-doc، متغیر و دستور داخل متن

Section titled “مثال ۱: اولین here-doc، متغیر و دستور داخل متن”
Terminal window
name="Ali"
cat <<EOF
Hello, $name!
Today is $(date +%F).
Home is $HOME
A literal dollar sign: \$5, a backslash: \\ and a backtick: \`
EOF
echo "--- همان متن با جداکننده‌ی کوتیشن‌دار:"
cat <<'EOF'
Hello, $name!
Today is $(date +%F).
A literal dollar sign: $5
EOF
خروجی
Hello, Ali!
Today is 2026-10-04.
Home is /home/ali
A literal dollar sign: $5, a backslash: \ and a backtick: `
--- همان متن با جداکننده‌ی کوتیشن‌دار:
Hello, $name!
Today is $(date +%F).
A literal dollar sign: $5

در نسخه‌ی بدون کوتیشن، $name و $(date +%F) باز شدند و \$ یک $ معمولی نوشت. در نسخه‌ی کوتیشن‌دار هیچ‌چیز تغییر نکرد. نکته‌ی مهم: داخل here-doc کوتیشن‌های " و ' معنی خاص ندارند و همان‌طور چاپ می‌شوند؛ فقط $، ` و \ در نسخه‌ی بدون کوتیشن مهم‌اند.

مثال ۲: دامی که ساختن فایل تنظیمات را خراب می‌کند

Section titled “مثال ۲: دامی که ساختن فایل تنظیمات را خراب می‌کند”

فایل‌های تنظیمات پر از $ هستند که مال شل نیستند. یک بلوک ساده‌ی nginx را ببین ($uri متغیر خود nginx است):

Terminal window
domain="shop.example.ir"
echo "--- بدون محافظ: شل \$uri را متغیر خودش حساب می‌کند (خالی است)"
cat <<EOF
server_name $domain;
try_files $uri $uri/ =404;
EOF
echo "--- راه ۱: escape کردن فقط \$ های nginx"
cat <<EOF
server_name $domain;
try_files \$uri \$uri/ =404;
EOF
echo "--- راه ۲: جداکننده‌ی کوتیشن‌دار (ولی آن‌وقت $domain هم باز نمی‌شود!)"
cat <<'EOF'
server_name $domain;
try_files $uri $uri/ =404;
EOF
خروجی
--- بدون محافظ: شل $uri را متغیر خودش حساب می‌کند (خالی است)
server_name shop.example.ir;
try_files / =404;
--- راه ۱: escape کردن فقط $ های nginx
server_name shop.example.ir;
try_files $uri $uri/ =404;
--- راه ۲: جداکننده‌ی کوتیشن‌دار (ولی آن‌وقت shop.example.ir هم باز نمی‌شود!)
server_name $domain;
try_files $uri $uri/ =404;

بدون محافظ، $uri خالی شد و try_files بی‌معنی؛ فایل نوشته می‌شود ولی nginx آن را رد می‌کند (یا بدتر، کار اشتباه می‌کند). دو راه داری: (۱) بدون کوتیشن، و جلوی $های nginx بک‌اسلش بگذاری (این‌طور $domain پر می‌شود)؛ (۲) با کوتیشن، که هیچ‌چیز پر نمی‌شود و بعداً جاهای خالی را خودت با sed پر می‌کنی (مثال ۶). قاعده‌ی کلی: اگر متن زیاد $ ی غیرشلی دارد، <<'EOF' و جایگزینی با sed؛ اگر فقط چند جای خالی دارد، <<EOF و escape.

مثال ۳: نوشتن در فایل، افزودن، تورفتگی و sudo

Section titled “مثال ۳: نوشتن در فایل، افزودن، تورفتگی و sudo”

here-doc ورودی یک دستور است؛ هر جا بخواهی خروجی‌اش را هم با > و >> به فایل می‌فرستی:

Terminal window
cd ~/bashlab
cat > notes.txt <<EOF
first line
EOF
cat >> notes.txt <<EOF
second line (appended)
EOF
cat notes.txt
خروجی
first line
second line (appended)

cat > فایل <<EOF فایل را از اول می‌نویسد (قبلی پاک می‌شود)، cat >> فایل <<EOF به انتهایش اضافه می‌کند. یک مشکل ظاهری: داخل if یا تابع، جداکننده باید در ابتدای خط باشد، پس متن بی‌تورفتگی و زشت می‌شود. راه‌حل <<-: tabهای ابتدای خط‌ها (و جداکننده) را برمی‌دارد:

/home/ali/bashlab/indent.sh
#!/usr/bin/env bash
# indent.sh: <<- strips leading TABs, so the here-doc can be indented
name="Ali"
if true; then
cat <<-EOF
Hello, $name!
This block is indented with tabs inside the script,
but printed without them.
EOF
fi
Terminal window
cd ~/bashlab
bash indent.sh
خروجی
Hello, Ali!
This block is indented with tabs inside the script,
but printed without them.

<<- فقط tab را حذف می‌کند، نه فاصله. اگر ویرایشگرت tab را به فاصله تبدیل کند، یا خطاهای عجیب می‌گیری یا تورفتگی چاپ می‌شود (در اشتباهات رایج). یک دام معروف دیگر، نوشتن در مسیری که دسترسی root می‌خواهد:

Terminal window
echo "--- sudo cat > ... (تغییر جهت را خود شل تو انجام می‌دهد، نه sudo):"
sudo cat > /etc/lx-demo.conf <<EOF
setting=1
EOF
echo "کد خروج: $?"
echo "--- sudo tee (خود tee با دسترسی root فایل را می‌نویسد):"
sudo tee /etc/lx-demo.conf > /dev/null <<EOF
setting=1
EOF
echo "کد خروج: $?"
cat /etc/lx-demo.conf
sudo rm -f /etc/lx-demo.conf
خروجی
--- sudo cat > ... (تغییر جهت را خود شل تو انجام می‌دهد، نه sudo):
bash: /etc/lx-demo.conf: Permission denied
کد خروج: 1
--- sudo tee (خود tee با دسترسی root فایل را می‌نویسد):
کد خروج: 0
setting=1

در sudo cat > فایل، علامت > را شل تو (با دسترسی خودت) اجرا می‌کند، قبل از اینکه sudo شروع شود؛ پس Permission denied. راه‌حل sudo tee فایل است: خود tee با دسترسی root می‌نویسد (> /dev/null برای اینکه محتوا دوباره روی صفحه چاپ نشود؛ tee -a برای افزودن). (درس sudo و root را به یاد بیاور.)

مثال ۴: here-doc برای دستورهای دیگر و برای متغیرها

Section titled “مثال ۴: here-doc برای دستورهای دیگر و برای متغیرها”

اسم here-doc «سند» است، ولی به هر دستوری که از ورودی بخواند می‌شود داد:

Terminal window
echo "--- sort:"
sort <<EOF
pear
apple
fig
EOF
echo "--- یک اسکریپت پایتون کوتاه بدون ساختن فایل:"
python3 - <<'PY'
total = sum(range(5))
print("python says", total)
PY
echo "--- یک bash فرزند:"
bash -s <<'EOF'
echo "hello from a child bash"
EOF
echo "--- در یک متغیر:"
message=$(cat <<EOF
Dear Ali,
your backup finished.
EOF
)
echo "$message"
echo "--- متغیر چندخطی با read -d (بدون پروسه‌ی جدید):"
read -r -d '' usage <<'EOF'
Usage: tool [options]
-h show help
EOF
echo "$usage"
خروجی
--- sort:
apple
fig
pear
--- یک اسکریپت پایتون کوتاه بدون ساختن فایل:
python says 10
--- یک bash فرزند:
hello from a child bash
--- در یک متغیر:
Dear Ali,
your backup finished.
--- متغیر چندخطی با read -d (بدون پروسه‌ی جدید):
Usage: tool [options]
-h show help

python3 - یعنی «برنامه را از ورودی استاندارد بخوان» و bash -s همین‌طور؛ برای اجرای یک تکه کد بدون فایل موقت عالی است (و ssh میزبان 'bash -s' <<'EOF' برای اجرای یک بلوک روی سرور دیگر). برای ریختن در متغیر دو راه داری: $(cat <<EOF ... EOF) یا read -r -d '' var <<'EOF'. (read -d '' همیشه با کد ۱ تمام می‌شود چون به پایان ورودی می‌رسد؛ در اسکریپتی با set -e این را در نظر بگیر.)

درس grep و regex را داری؛ حالا در اسکریپت، با کد خروج و آرگومان‌ها. یک لاگ دسترسی نمونه (قالب استاندارد nginx) می‌سازیم تا روی آن کار کنیم:

Terminal window
cd ~/bashlab
cat > access.log <<'EOF'
192.168.1.10 - - [04/Oct/2026:09:00:01 +0000] "GET / HTTP/1.1" 200 1534 "-" "Mozilla/5.0"
192.168.1.11 - - [04/Oct/2026:09:00:03 +0000] "GET /about HTTP/1.1" 200 842 "-" "Mozilla/5.0"
192.168.1.10 - - [04/Oct/2026:09:00:07 +0000] "GET /style.css HTTP/1.1" 200 5120 "http://example.ir/" "Mozilla/5.0"
10.0.0.5 - - [04/Oct/2026:09:01:12 +0000] "POST /login HTTP/1.1" 302 0 "-" "curl/8.5.0"
192.168.1.12 - - [04/Oct/2026:09:01:30 +0000] "GET /missing HTTP/1.1" 404 153 "-" "Mozilla/5.0"
10.0.0.5 - - [04/Oct/2026:09:02:01 +0000] "GET /admin HTTP/1.1" 403 153 "-" "curl/8.5.0"
192.168.1.10 - - [04/Oct/2026:09:02:15 +0000] "GET /api/items HTTP/1.1" 500 62 "-" "Mozilla/5.0"
192.168.1.11 - - [04/Oct/2026:09:03:00 +0000] "GET /api/items HTTP/1.1" 200 2048 "-" "Mozilla/5.0"
10.0.0.5 - - [04/Oct/2026:09:03:30 +0000] "GET /admin HTTP/1.1" 403 153 "-" "curl/8.5.0"
192.168.1.12 - - [04/Oct/2026:09:04:10 +0000] "GET / HTTP/1.1" 200 1534 "-" "Mozilla/5.0"
192.168.1.10 - - [04/Oct/2026:09:04:44 +0000] "GET /missing HTTP/1.1" 404 153 "-" "Mozilla/5.0"
10.0.0.5 - - [04/Oct/2026:09:05:01 +0000] "GET /admin HTTP/1.1" 403 153 "-" "curl/8.5.0"
EOF
echo "lines: $(wc -l < access.log)"
echo "404 responses: $(grep -c ' 404 ' access.log)"
echo "not from browsers: $(grep -vc 'Mozilla' access.log)"
echo "client errors (4xx/5xx): $(grep -cE '" [45][0-9]{2} ' access.log)"
echo "--- if grep -q:"
if grep -q ' 500 ' access.log; then
echo "ALERT: the log contains server errors (500)"
fi
echo "--- IP addresses ordered by number of requests:"
grep -oE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' access.log | sort | uniq -c | sort -rn
خروجی
lines: 12
404 responses: 2
not from browsers: 4
client errors (4xx/5xx): 6
--- if grep -q:
ALERT: the log contains server errors (500)
--- IP addresses ordered by number of requests:
4 192.168.1.10
4 10.0.0.5
2 192.168.1.12
2 192.168.1.11
  • grep -c تعداد خط‌های منطبق را می‌دهد (به‌جای خود خط‌ها)؛ -v برعکس (غیرمنطبق‌ها)؛ -E regex گسترده؛ -o فقط بخش منطبق هر خط؛ -q بی‌صدا، فقط کد خروج، برای if.
  • $(...) در echo خروجی grep -c را در جمله می‌گذارد؛ برای شمارش‌های داخل اسکریپت همین‌طور در متغیر می‌ریزی (errors=$(grep -c ...)).
  • grep -o ... | sort | uniq -c | sort -rn الگوی معروف «شمردن و رتبه‌بندی» است: uniq -c فقط خط‌های متوالی یکسان را می‌شمارد، برای همین قبلش sort.

مثال ۶: sed در اسکریپت: قالب و ویرایش

Section titled “مثال ۶: sed در اسکریپت: قالب و ویرایش”

sed برای جایگزینی در متن است. در اسکریپت دو کاربرد اصلی دارد: پرکردن قالب (جایگزینی جاهای خالی) و ویرایش فایل:

Terminal window
cd ~/bashlab
cat > template.conf <<'EOF'
server {
listen 80;
server_name __DOMAIN__ www.__DOMAIN__;
root __ROOT__;
location / {
try_files $uri $uri/ =404;
}
}
EOF
domain="shop.example.ir"
root_dir="/srv/sites/shop"
echo "--- جایگزینی __DOMAIN__ (قالب پر می‌شود و \$uri دست‌نخورده می‌ماند):"
sed "s/__DOMAIN__/$domain/g" template.conf | head -n 3
echo "--- مقدار دارای / با جداکننده‌ی / خراب می‌شود:"
sed "s/__ROOT__/$root_dir/g" template.conf | head -n 4
echo "--- راه‌حل: جداکننده‌ی دیگر (|)، و چند جایگزینی با چند -e:"
sed -e "s/__DOMAIN__/$domain/g" -e "s|__ROOT__|$root_dir|g" template.conf
خروجی
--- جایگزینی __DOMAIN__ (قالب پر می‌شود و $uri دست‌نخورده می‌ماند):
server {
listen 80;
server_name shop.example.ir www.shop.example.ir;
--- مقدار دارای / با جداکننده‌ی / خراب می‌شود:
sed: -e expression #1, char 13: unknown option to `s'
--- راه‌حل: جداکننده‌ی دیگر (|)، و چند جایگزینی با چند -e:
server {
listen 80;
server_name shop.example.ir www.shop.example.ir;
root /srv/sites/shop;
location / {
try_files $uri $uri/ =404;
}
}

الگوی s/قدیم/جدید/g را با کوتیشن دوتایی نوشتیم تا شل $domain را داخلش باز کند (با کوتیشن تکی باز نمی‌شد). مشکل: اگر مقدار جایگزین خودش / داشته باشد، sed جداکننده را با آن اشتباه می‌گیرد و unknown option to s می‌دهد. جداکننده‌ی s هر نویسه می‌تواند باشد (|، #، ,)؛ نویسه‌ای را بگذار که در مقدار نیست.

یک کاربرد دیگر، حذف کامنت و خط خالی از یک فایل تنظیمات و ویرایش درجا:

Terminal window
cd ~/bashlab
cat > app.conf <<'EOF'
# main settings
port = 80
# database
db_host = localhost
EOF
echo "--- بدون کامنت و خط خالی:"
sed -e '/^[[:space:]]*#/d' -e '/^[[:space:]]*$/d' app.conf
echo "--- ویرایش درجا و با نسخه‌ی پشتیبان (-i.bak، سازگار با GNU و macOS):"
sed -i.bak 's/^port = 80$/port = 8080/' app.conf
grep '^port' app.conf app.conf.bak
echo "--- فقط خط‌های ۲ تا ۴ (-n و p):"
sed -n '2,4p' app.conf
خروجی
--- بدون کامنت و خط خالی:
port = 80
db_host = localhost
--- ویرایش درجا و با نسخه‌ی پشتیبان (-i.bak، سازگار با GNU و macOS):
app.conf:port = 8080
app.conf.bak:port = 80
--- فقط خط‌های ۲ تا ۴ (-n و p):
port = 8080
# database
  • /الگو/d خط‌های منطبق را حذف می‌کند؛ -e چند دستور پشت‌سرهم؛ -n '2,4p' فقط خط‌های ۲ تا ۴ را چاپ می‌کند.
  • sed -i فایل را همان‌جا عوض می‌کند. -i.bak یک نسخه‌ی پشتیبان هم نگه می‌دارد، و مهم‌تر، روی GNU sed (لینوکس) و BSD sed (macOS) هر دو کار می‌کند. sed -i 's/a/b/' file (بدون پسوند) فقط روی لینوکس کار می‌کند:
Terminal window
printf 'a\n' > sedtest.txt
sed -i 's/a/b/' sedtest.txt 2>&1
echo "--- با -i.bak:"
sed -i.bak 's/a/b/' sedtest.txt && cat sedtest.txt
خروجی
sed: 1: "sedtest.txt
": unescaped newline inside substitute pattern
--- با -i.bak:
b

روی macOS، sed -i یک آرگومان پسوند می‌خواهد و 's/a/b/' را به‌عنوان پسوند می‌گیرد (خطای گیج‌کننده‌ی بالا). -i.bak در هر دو سیستم یکسان است؛ بعدش فایل .bak را پاک کن اگر لازم نیست.

مثال ۷: awk در اسکریپت: ستون‌ها، شرط و جمع

Section titled “مثال ۷: awk در اسکریپت: ستون‌ها، شرط و جمع”

awk برای ستون‌ها و حساب است (درس sed و awk). در اسکریپت، مهم‌ترین نکته این است که مقدار متغیر شل را با -v به آن بدهی:

Terminal window
cd ~/bashlab
echo "--- IP یکتا:"
awk '{print $1}' access.log | sort -u
echo "--- خط‌های خطا (وضعیت ≥ 400): IP، وضعیت، مسیر"
awk '$9 >= 400 {print $1, $9, $7}' access.log
echo "--- مجموع بایت‌های فرستاده‌شده:"
awk '{sum += $10} END {print sum, "bytes"}' access.log
echo "--- پاس‌دادن متغیر شل با -v (فقط پاسخ‌های بزرگ‌تر از limit):"
limit=1000
awk -v min="$limit" '$10 > min {print $7, $10}' access.log
echo "--- شمارش بر اساس کد وضعیت با آرایه‌ی awk:"
awk '{count[$9]++} END {for (s in count) print s, count[s]}' access.log | sort
echo "--- کاربران عادی سیستم (فیلدها با : جدا می‌شوند):"
awk -F: '$3 >= 1000 && $3 < 60000 {print $1, $3}' /etc/passwd
خروجی
--- IP یکتا:
10.0.0.5
192.168.1.10
192.168.1.11
192.168.1.12
--- خط‌های خطا (وضعیت ≥ 400): IP، وضعیت، مسیر
192.168.1.12 404 /missing
10.0.0.5 403 /admin
192.168.1.10 500 /api/items
10.0.0.5 403 /admin
192.168.1.10 404 /missing
10.0.0.5 403 /admin
--- مجموع بایت‌های فرستاده‌شده:
11905 bytes
--- پاس‌دادن متغیر شل با -v (فقط پاسخ‌های بزرگ‌تر از limit):
/ 1534
/style.css 5120
/api/items 2048
/ 1534
--- شمارش بر اساس کد وضعیت با آرایه‌ی awk:
200 5
302 1
403 3
404 2
500 1
--- کاربران عادی سیستم (فیلدها با : جدا می‌شوند):
ubuntu 1000
ali 1001

هر خط ورودی به فیلدها ($1، $2 و…) شکسته می‌شود؛ در لاگ nginx، $1 آی‌پی، $7 مسیر، $9 کد وضعیت و $10 تعداد بایت است. شرط {عمل} عمل را فقط برای خط‌های منطبق اجرا می‌کند، و END {...} بعد از آخرین خط. آرایه‌ی awk (count[$9]++) همان آرایه‌ی انجمنی درس قبل است. -F: جداکننده‌ی فیلد را عوض می‌کند. و -v min="$limit" مقدار متغیر شل را امن به متغیر awk می‌دهد (کوتیشن دوتایی دور $limit را فراموش نکن).

مثال ۸: ترکیب: گزارش‌گیر لاگ

Section titled “مثال ۸: ترکیب: گزارش‌گیر لاگ”

حالا همه‌ی ابزارها در یک اسکریپت واقعی: خلاصه‌ی لاگ دسترسی با شمارش، رتبه‌بندی و مجموع:

/home/ali/bashlab/log-report.sh
#!/usr/bin/env bash
# log-report.sh: summarize an nginx access log
# Usage: ./log-report.sh <access.log> [top-n]
log="$1"
top_n="${2:-3}"
if [[ $# -lt 1 || ! -f $log ]]; then
echo "Usage: $0 <access.log> [top-n]" >&2
exit 1
fi
total=$(wc -l < "$log")
errors=$(awk '$9 >= 400' "$log" | wc -l)
percent=$(awk -v e="$errors" -v t="$total" 'BEGIN {printf "%.1f", e * 100 / t}')
echo "=== Log report: $log ==="
echo "Requests: $total"
echo "Errors: $errors ($percent%)"
echo
echo "Top $top_n clients:"
awk '{print $1}' "$log" | sort | uniq -c | sort -rn | head -n "$top_n"
echo
echo "Status codes:"
awk '{count[$9]++} END {for (s in count) printf " %s %d\n", s, count[s]}' "$log" | sort
echo
echo "Most requested paths:"
awk '{print $7}' "$log" | sort | uniq -c | sort -rn | head -n "$top_n"
echo
awk '{sum += $10} END {printf "Traffic: %.1f KB\n", sum / 1024}' "$log"
Terminal window
cd ~/bashlab
chmod +x log-report.sh
./log-report.sh access.log
echo "--- بدون آرگومان:"
./log-report.sh; echo "کد خروج: $?"
خروجی
=== Log report: access.log ===
Requests: 12
Errors: 6 (50.0%)
Top 3 clients:
4 192.168.1.10
4 10.0.0.5
2 192.168.1.12
Status codes:
200 5
302 1
403 3
404 2
500 1
Most requested paths:
3 /admin
2 /missing
2 /api/items
Traffic: 11.6 KB
--- بدون آرگومان:
Usage: ./log-report.sh <access.log> [top-n]
کد خروج: 1

دقت کن هر ابزار کار خودش را می‌کند: wc -l شمارش خط‌ها، awk شرط روی کد وضعیت و حساب درصد (BEGIN یعنی قبل از خواندن ورودی؛ برای محاسبه‌ی اعشاری که Bash ندارد)، sort | uniq -c | sort -rn | head رتبه‌بندی. و هر مقدار شل ($errors، $log) با کوتیشن یا -v پاس شده است. چنین اسکریپتی را با cron اجرا می‌کنی و خروجی‌اش را ایمیل می‌کنی.

here-doc فقط ورودی استاندارد است. شل متن را در یک pipe یا فایل موقت می‌گذارد و آن را به‌عنوان ورودی استاندارد (توصیف‌گر ۰) به دستور وصل می‌کند. می‌توانی ببینی دستور چه چیزی را به‌عنوان ورودی دارد:

Terminal window
readlink /proc/self/fd/0 <<EOF
some text
EOF
echo "--- در برابر ورودی از یک فایل:"
echo "x" > /tmp/lx-input.txt
readlink /proc/self/fd/0 < /tmp/lx-input.txt
rm -f /tmp/lx-input.txt
خروجی
pipe:[5086732]
--- در برابر ورودی از یک فایل:
/tmp/lx-input.txt

برای readlink، توصیف‌گر ۰ در حالت اول یک pipe (و در نسخه‌های قدیمی‌تر یک فایل موقت پاک‌شده) و در حالت دوم خود فایل است. دستور فرقی بین «کسی تایپ کرد»، «فایل» و «here-doc» نمی‌بیند؛ همه ورودی استاندارد است. چند نتیجه:

  • شل قبل از اجرای دستور متن here-doc را آماده می‌کند: اگر بدون کوتیشن باشد، گسترش‌ها (متغیر و $(...)) همان لحظه انجام می‌شوند.
  • جداکننده باید تنها روی یک خط و از ابتدای خط باشد (مگر با <<- و tab). حتی یک فاصله‌ی ابتدا یا انتها آن را از کار می‌اندازد.
  • چون here-doc ورودی دستور است، cat <<EOF > فایل به معنی «ورودی cat این متن است و خروجی‌اش به فایل می‌رود». و sudo cat > فایل <<EOF مشکل‌ساز است چون > قبل از sudo توسط شل خودت اجرا می‌شود.

here-doc:

شکل معنی
cat <<EOF متن با گسترش متغیر و دستور
cat <<'EOF' متن خام
cat <<-EOF حذف tabهای ابتدای خط‌ها
cat > f <<EOF / cat >> f <<EOF نوشتن / افزودن به فایل
sudo tee f > /dev/null <<EOF نوشتن در فایل نیازمند root
var=$(cat <<EOF ... EOF) ریختن در متغیر
read -r -d '' var <<'EOF' ریختن در متغیر (کد خروج ۱)
python3 - <<'PY'، bash -s <<'EOF' اجرای کد از ورودی

چه ابزاری برای چه کاری:

کار ابزار
بریدن و جایگزینی ساده روی یک متغیر گسترش پارامتر ${...} (درس قبل)
انتخاب خط‌ها، شمردن، آیا هست؟ grep (-c، -q، -v، -o، -E، -F)
جایگزینی متن، حذف خط، ویرایش فایل sed (s///، /الگو/d، -n 'p'، -i.bak)
ستون‌ها، شرط روی ستون، جمع و میانگین، گزارش awk ($1، -F، -v، END، آرایه)
ترتیب، یکتا، شمارش sort، uniq -c، wc -l، head، cut

۱) متغیرهای غیرشلی در here-doc بدون کوتیشن

Section titled “۱) متغیرهای غیرشلی در here-doc بدون کوتیشن”

مثال ۲: $uri، $host و هر چیز شبیه متغیر که مال برنامه‌ی دیگری است. نتیجه‌ی اجرای بدون محافظ:

Terminal window
cd ~/bashlab
cat > broken.conf <<EOF
location / {
try_files $uri $uri/ =404;
proxy_set_header Host $host;
}
EOF
cat broken.conf
خروجی
location / {
try_files / =404;
proxy_set_header Host ;
}

راه‌حل: \$uri و \$host، یا <<'EOF'.

۲) جداکننده‌ای که جور نیست

Section titled “۲) جداکننده‌ای که جور نیست”
Terminal window
cd ~/bashlab
printf 'cat <<EOF\nhello\n EOF\n' > space-before.sh
echo "--- فاصله قبل از EOF:"
bash space-before.sh 2>&1
printf 'cat <<-EOF\n hello\n EOF\n' > spaces-instead-of-tabs.sh
echo "--- <<- با فاصله به‌جای tab:"
bash spaces-instead-of-tabs.sh 2>&1
خروجی
--- فاصله قبل از EOF:
space-before.sh: line 3: warning: here-document at line 1 delimited by end-of-file (wanted `EOF')
hello
EOF
--- <<- با فاصله به‌جای tab:
spaces-instead-of-tabs.sh: line 3: warning: here-document at line 1 delimited by end-of-file (wanted `EOF')
hello
EOF

شل جداکننده را پیدا نمی‌کند، تا انتهای فایل می‌خواند، و هشدار «delimited by end-of-file» می‌دهد. نتیجه: فایل ناقص یا متن اضافه در خروجی. <<- فقط tab را برمی‌دارد؛ ویرایشگرت را روی tab تنظیم کن یا tab را با cat -A بررسی کن (^I).

مثال ۳. راه‌حل: sudo tee فایل.

۴) متغیر شل داخل regex یا الگوی awk

Section titled “۴) متغیر شل داخل regex یا الگوی awk”
Terminal window
cd ~/bashlab
path="/admin"
echo "--- متغیر مستقیم داخل الگوی awk (/ جداکننده را می‌شکند):"
awk "/$path/ {print \$7}" access.log 2>&1 | head -n 3
echo "--- راه‌حل: -v و عملگر ~:"
awk -v p="$path" '$7 ~ p {print $7}' access.log | sort | uniq -c
خروجی
--- متغیر مستقیم داخل الگوی awk (/ جداکننده را می‌شکند):
awk: cmd. line:1: //admin/ {print $7}
awk: cmd. line:1: ^ syntax error
--- راه‌حل: -v و عملگر ~:
3 /admin

awk "/$path/" می‌شود awk "//admin/" که برای awk خطای نحوی است. با -v p="$path" مقدار به‌صورت داده می‌رسد، نه کد. (همین مشکل تزریق، برای مقدارهایی که از کاربر می‌آیند خطرناک هم هست.)

۵) grep با متغیر به‌عنوان regex

Section titled “۵) grep با متغیر به‌عنوان regex”
Terminal window
cd ~/bashlab
printf 'host a.b\nhost axb\n' > hosts.txt
name="a.b"
echo "--- grep \$name (نقطه = هر کاراکتر):"
grep -c "$name" hosts.txt
echo "--- grep -F (متن دقیق):"
grep -cF "$name" hosts.txt
خروجی
--- grep $name (نقطه = هر کاراکتر):
2
--- grep -F (متن دقیق):
1

در regex نقطه هر نویسه‌ای است؛ پس a.b با axb هم جور شد (۲ خط). وقتی مقدار یک متن ساده است (نام دامنه، نسخه) از -F (fixed string) استفاده کن.

۶) sed -i بدون پسوند (macOS) و / در مقدار

Section titled “۶) sed -i بدون پسوند (macOS) و / در مقدار”

مثال ۶: sed -i.bak سازگار است؛ و جداکننده‌ی دیگر (|) برای مقدارهای دارای /.

۷) فراموش‌کردن sort قبل از uniq -c

Section titled “۷) فراموش‌کردن sort قبل از uniq -c”

uniq فقط خط‌های متوالی را یکی می‌کند. بدون sort، نتیجه‌ی uniq -c خرد و غلط است.

✎ تمرینآسان

با یک here-doc فایل README.md بساز که اسم پروژه (متغیر project)، تاریخ امروز و اسم کاربر فعلی را داشته باشد، و یک خط هم که عیناً Price: $5 باشد. (این خط را بدون باز شدن بنویس.) محتوا را نشان بده.

دیدن جواب
Terminal window
mkdir -p ~/bashlab && cd ~/bashlab
project="LoopX Demo"
cat > README.md <<EOF
# $project
Created on $(date +%F) by $(whoami).
Price: \$5
EOF
cat README.md
خروجی
# LoopX Demo
Created on 2026-10-04 by ali.
Price: $5

در here-doc بدون کوتیشن، $project و $(...) باز می‌شوند و \$5 یک $ معمولی نوشت. اگر می‌خواستی همه‌چیز عیناً بماند، <<'EOF'.

✎ تمرینمتوسط

تمرین اصلی درس: اسکریپتی که فایل تنظیمات nginx یک سایت را از روی اسم دامنه بسازد. اسکریپت make-site.sh <دامنه> [پوشه‌ی خروجی] فایل دامنه.conf را بسازد که server_name (با و بدون www.)، مسیر root و مسیر لاگ‌ها را از روی دامنه پر کند و try_files $uri $uri/ =404 را سالم نگه دارد. دامنه را اعتبارسنجی کند (و قبل از نوشتن جلوی تزریق چیزهایی مثل ; را بگیرد)، روی فایل موجود رونویسی نکند، و در پایان با خود nginx (nginx -t) ثابت کنی فایل ساخته‌شده سالم است.

دیدن جواب
mkdir -p ~/bashlab/logs ~/bashlab/prefix ~/bashlab/tmp && cd ~/bashlab
cat > make-site.sh <<'EOF'
#!/usr/bin/env bash
# make-site.sh: generate an nginx server block for a domain
# Usage: ./make-site.sh <domain> [output-dir]
# Environment: WEB_ROOT (default /var/www), LOG_DIR (default /var/log/nginx)
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <domain> [output-dir]" >&2
exit 1
fi
domain="$1"
out_dir="${2:-$HOME/bashlab/sites}"
web_root="${WEB_ROOT:-/var/www}"
log_dir="${LOG_DIR:-/var/log/nginx}"
if [[ ! $domain =~ ^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$ ]]; then
echo "Error: '$domain' does not look like a domain name" >&2
exit 2
fi
conf="$out_dir/$domain.conf"
if [[ -e $conf ]]; then
echo "Error: $conf already exists" >&2
exit 3
fi
mkdir -p "$out_dir"
cat > "$conf" <<NGINX
server {
listen 80;
server_name $domain www.$domain;
root $web_root/$domain;
index index.html;
access_log $log_dir/$domain.access.log;
error_log $log_dir/$domain.error.log;
location / {
try_files \$uri \$uri/ =404;
}
}
NGINX
echo "Created $conf"
EOF
chmod +x make-site.sh
export LOG_DIR="$HOME/bashlab/logs"
echo "--- ساختن:"
./make-site.sh example.ir; echo "کد خروج: $?"
cat sites/example.ir.conf
echo "--- دوباره (رونویسی نکند):"
./make-site.sh example.ir; echo "کد خروج: $?"
echo "--- ورودی بد:"
./make-site.sh 'bad;domain'; echo "کد خروج: $?"
./make-site.sh localhost; echo "کد خروج: $?"
./make-site.sh; echo "کد خروج: $?"
خروجی
--- ساختن:
Created /home/ali/bashlab/sites/example.ir.conf
کد خروج: 0
server {
listen 80;
server_name example.ir www.example.ir;
root /var/www/example.ir;
index index.html;
access_log /home/ali/bashlab/logs/example.ir.access.log;
error_log /home/ali/bashlab/logs/example.ir.error.log;
location / {
try_files $uri $uri/ =404;
}
}
--- دوباره (رونویسی نکند):
Error: /home/ali/bashlab/sites/example.ir.conf already exists
کد خروج: 3
--- ورودی بد:
Error: 'bad;domain' does not look like a domain name
کد خروج: 2
Error: 'localhost' does not look like a domain name
کد خروج: 2
Usage: ./make-site.sh <domain> [output-dir]
کد خروج: 1

بعد با خود nginx بررسی می‌کنیم فایل ساخته‌شده سالم است. فایل nginx-test.conf یک پوشش حداقلی است که فقط فایل‌های sites/*.conf را می‌خواند (و مسیرهای موقت را داخل پوشه‌ی خودمان می‌گذارد تا بدون root اجرا شود):

Terminal window
cd ~/bashlab
cat > nginx-test.conf <<EOF
pid $HOME/bashlab/nginx.pid;
error_log $HOME/bashlab/logs/error.log;
events {}
http {
client_body_temp_path $HOME/bashlab/tmp/body;
proxy_temp_path $HOME/bashlab/tmp/proxy;
fastcgi_temp_path $HOME/bashlab/tmp/fastcgi;
uwsgi_temp_path $HOME/bashlab/tmp/uwsgi;
scgi_temp_path $HOME/bashlab/tmp/scgi;
include $HOME/bashlab/sites/*.conf;
}
EOF
echo "--- nginx -t روی فایل ساخته‌شده:"
nginx -t -c "$HOME/bashlab/nginx-test.conf" -p "$HOME/bashlab/prefix"; echo "کد خروج: $?"
echo "--- یک غلط تایپی در فایل را خود nginx این‌طور می‌گیرد:"
sed 's/try_files/try_filez/' sites/example.ir.conf > sites/typo.example.ir.conf
nginx -t -c "$HOME/bashlab/nginx-test.conf" -p "$HOME/bashlab/prefix" 2>&1 | head -n 2; echo "کد خروج: ${PIPESTATUS[0]}"
rm -f sites/typo.example.ir.conf
خروجی
--- nginx -t روی فایل ساخته‌شده:
nginx: the configuration file /home/ali/bashlab/nginx-test.conf syntax is ok
nginx: configuration file /home/ali/bashlab/nginx-test.conf test is successful
کد خروج: 0
--- یک غلط تایپی در فایل را خود nginx این‌طور می‌گیرد:
2026/10/04 10:48:52 [emerg] 32558#32558: unknown directive "try_filez" in /home/ali/bashlab/sites/typo.example.ir.conf:12
nginx: configuration file /home/ali/bashlab/nginx-test.conf test failed
کد خروج: 1

نکته‌های پاسخ: ۱) جداکننده‌ی NGINX (نه EOF) تا با EOF اسکریپت بیرونی قاطی نشود، و بدون کوتیشن تا $domain پر شود؛ \$uri برای نگه‌داشتن متغیر nginx. ۲) regex دامنه فقط حروف کوچک، عدد و - و نقطه می‌پذیرد، پس ;، فاصله، خط جدید و هر چیزی که بتواند فایل را خراب کند رد می‌شود؛ اعتبارسنجی ورودی همیشه قبل از ساختن فایل از ورودی لازم است. ۳) nginx -t تنها راه واقعی اطمینان است؛ و در آخر نشان دادیم یک غلط تایپی چطور گرفته می‌شود (PIPESTATUS آرایه‌ای از کد خروج همه‌ی دستورهای یک pipe است؛ ${PIPESTATUS[0]} کد خروج nginx است، نه head).

✎ تمرینمتوسط

فایل CSV فروش (product,qty,price) را با here-doc بساز و با awk برای هر محصول مجموع درآمد (qty × price) و در پایان جمع کل را چاپ کن، مرتب‌شده از بیشترین درآمد.

دیدن جواب
Terminal window
cd ~/bashlab
cat > sales.csv <<'EOF'
product,qty,price
apple,10,1.5
banana,5,0.5
apple,5,1.5
cherry,2,8
banana,10,0.5
EOF
awk -F, 'NR > 1 {
revenue[$1] += $2 * $3
total += $2 * $3
}
END {
for (p in revenue) printf "%-8s %8.2f\n", p, revenue[p]
printf "%-8s %8.2f\n", "TOTAL", total
}' sales.csv | sort -k2,2 -nr
خروجی
TOTAL 46.00
apple 22.50
cherry 16.00
banana 7.50

-F, جداکننده را ویرگول می‌کند، NR > 1 سطر عنوان را رد می‌کند، آرایه‌ی revenue برای هر محصول جمع می‌زند و END گزارش را چاپ می‌کند. sort -k2,2 -nr ستون دوم را عددی و نزولی مرتب می‌کند (جمع کل بالاتر از همه می‌افتد).

✎ تمرینسخت

یک موتور قالب ساده بنویس: render.sh قالب KEY=value ... هر @@KEY@@ در قالب را با مقدار داده‌شده عوض کند. اگر بعد از جایگزینی هنوز @@چیزی@@ مانده بود، با کد ۲ و فهرست آن‌ها خطا بدهد. فقط با Bash (گسترش پارامتر) و grep؛ مقدارهایی که / یا & دارند هم باید درست جایگزین شوند.

دیدن جواب
Terminal window
cd ~/bashlab
cat > greeting.tpl <<'EOF'
Dear @@NAME@@,
Your site @@SITE@@ is ready (backup folder: @@DIR@@).
Plan: @@PLAN@@
EOF
cat > render.sh <<'EOF'
#!/usr/bin/env bash
# render.sh: fill @@KEY@@ placeholders in a template
# Usage: ./render.sh <template> KEY=value ...
if [[ $# -lt 1 || ! -f $1 ]]; then
echo "Usage: $0 <template> KEY=value ..." >&2
exit 1
fi
template="$1"
shift
content=$(cat "$template")
for pair in "$@"; do
key="${pair%%=*}"
value="${pair#*=}"
content="${content//@@$key@@/"$value"}"
done
left=$(grep -o '@@[A-Z_]*@@' <<< "$content" | sort -u | paste -sd' ')
if [[ -n $left ]]; then
echo "Error: unresolved placeholders: $left" >&2
exit 2
fi
printf '%s\n' "$content"
EOF
chmod +x render.sh
echo "--- همه‌ی مقدارها (با / و & هم):"
./render.sh greeting.tpl NAME="R&D team" SITE=shop.example.ir DIR=/srv/backups/shop PLAN=pro; echo "کد خروج: $?"
echo "--- یک مقدار کم:"
./render.sh greeting.tpl NAME=Ali SITE=a.ir; echo "کد خروج: $?"
echo "--- چرا \"\$value\" با کوتیشن؟ (Bash 5.2: & یعنی متن پیداشده)"
v='R&D'; c='@@N@@'
echo "unquoted: ${c//@@N@@/$v}"
echo "quoted: ${c//@@N@@/"$v"}"
خروجی
--- همه‌ی مقدارها (با / و & هم):
Dear R&D team,
Your site shop.example.ir is ready (backup folder: /srv/backups/shop).
Plan: pro
کد خروج: 0
--- یک مقدار کم:
Error: unresolved placeholders: @@DIR@@ @@PLAN@@
کد خروج: 2
--- چرا "$value" با کوتیشن؟ (Bash 5.2: & یعنی متن پیداشده)
unquoted: R@@N@@D
quoted: R&D

نکته‌ها: ۱) "$value" داخل کوتیشن در جایگزینی، یعنی مقدار «متن دقیق» است؛ در Bash ۵.۲ بدون آن، & در مقدار (R&D) به‌عنوان «متن پیداشده» تفسیر می‌شد (درس قبل). ۲) چون هیچ sed ای در کار نیست، / در مقدار (/srv/backups/shop) مشکلی نمی‌سازد. ۳) <<< (here-string) به grep متن می‌دهد؛ paste -sd' ' خط‌ها را با فاصله به هم می‌چسباند. ۴) آرگومان‌ها با "$@" پیمایش می‌شوند و هر KEY=value با %%=* و #*= شکسته می‌شود (حتی اگر مقدار خودش = داشته باشد).

⚡ بررسی سریع

کدام یک $uri را در متن here-doc عیناً نگه می‌دارد؟

؟ آزمونک
  1. چرا sudo cat > /etc/app.conf <<EOF ... EOF با Permission denied شکست می‌خورد؟

  2. <<-EOF دقیقاً چه کاری می‌کند؟

  3. در sed "s/__DOMAIN__/$root/g" وقتی root=/srv/sites چه می‌شود و راه‌حل؟

  4. چرا باید مقدار متغیر شل را با awk -v var="$x" بدهی و نه مستقیم داخل برنامه‌ی awk؟

  5. چرا قبل از uniq -c معمولاً sort می‌گذاریم؟

  6. برای جست‌وجوی متن دقیق example.ir (نقطه واقعی) در یک فایل از متغیر name، کدام درست‌تر است؟

  • here-doc (دستور <<EOF ... EOF) متن چندخطی را به ورودی استاندارد یک دستور می‌دهد. <<EOF متغیر و $(...) را باز می‌کند (\$ برای $ واقعی)؛ <<'EOF' همه‌چیز را خام می‌گذارد؛ <<-EOF tabهای ابتدای خط را برمی‌دارد.
  • جداکننده باید تنها و از ابتدای خط باشد؛ فاصله‌ی اضافه هشدار «delimited by end-of-file» می‌دهد.
  • نوشتن در فایل: cat > f <<EOF (بازنویسی)، >> (افزودن)؛ برای فایل نیازمند root sudo tee f > /dev/null <<EOF، نه sudo cat >.
  • برای فایل‌های تنظیمات پر از $ی غیرشلی: \$uri یا <<'EOF' و پرکردن جاهای خالی با sed "s|__KEY__|$value|g". ورودی کاربر را قبل از نوشتن در فایل اعتبارسنجی کن.
  • grep در اسکریپت: -c (شمارش)، -q (فقط کد خروج برای if)، -v، -o، -E، و -F برای متن دقیق. sed: s/// (با جداکننده‌ی مناسب)، /الگو/d، -n 'p'، -i.bak (سازگار با GNU و macOS). awk: فیلدها، -F، شرط، END، آرایه، و -v برای دادن متغیر شل.
  • الگوی «رتبه‌بندی»: ... | sort | uniq -c | sort -rn | head. برای حساب اعشاری از awk استفاده کن.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
cat <<EOF > file ... EOFنوشتن متن چندخطی با گسترش متغیر
cat <<'EOF'متن خام (بدون گسترش)
cat <<-EOFتورفتگی با tab مجاز
cat >> file <<EOFافزودن به انتهای فایل
sudo tee file > /dev/null <<EOFنوشتن در فایل نیازمند root
var=$(cat <<EOF ... EOF)ریختن متن چندخطی در متغیر
grep -c pat f grep -q pat f grep -F "$x" fشمارش / فقط کد خروج / متن دقیق
grep -oE "[0-9.]+" f | sort | uniq -c | sort -rnشمردن و رتبه‌بندی
sed "s|__KEY__|$value|g" tplپرکردن قالب (جداکننده‌ی |)
sed -i.bak "s/old/new/" fویرایش درجا (GNU و macOS)
awk '$9 >= 400 {print $1}' fشرط روی ستون
awk -v min="$x" '$10 > min' f awk '{c[$9]++} END {...}'متغیر شل با -v / شمارش با آرایه