- LoopX
- اسکریپتنویسی Bash
- ساختارمند نوشتن
- here-doc و پردازش متن در اسکریپت
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' همین است.
here-doc: سه شکل
Section titled “here-doc: سه شکل”دستور <<DELIMITERمتن چندخطیDELIMITERشل متن بین خط <<DELIMITER و خطی که فقط DELIMITER دارد را به ورودی استاندارد دستور میدهد، مثل اینکه کسی تایپش کرده باشد. اسم جداکننده دلخواه است ولی معمولاً EOF مینویسند.
| شکل | رفتار |
|---|---|
<<EOF |
متغیر، $(...) و $((...)) باز میشوند؛ \$، \\ و \` برای نوشتن خود نویسهها |
<<'EOF' (یا <<"EOF") |
هیچچیز باز نمیشود؛ متن خام |
<<-EOF |
مثل <<EOF، ولی tabهای ابتدای هر خط (و جداکننده) حذف میشوند، تا بتوانی داخل if/تابع با تورفتگی بنویسی |
<<<"متن" |
here-string: یک رشتهی تکخطی بهعنوان ورودی (در درس ورودی و آرگومانها دیدی) |
مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: اولین here-doc، متغیر و دستور داخل متن
Section titled “مثال ۱: اولین here-doc، متغیر و دستور داخل متن”name="Ali"cat <<EOFHello, $name!Today is $(date +%F).Home is $HOMEA literal dollar sign: \$5, a backslash: \\ and a backtick: \`EOFecho "--- همان متن با جداکنندهی کوتیشندار:"cat <<'EOF'Hello, $name!Today is $(date +%F).A literal dollar sign: $5EOFHello, Ali!Today is 2026-10-04.Home is /home/aliA 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 است):
domain="shop.example.ir"echo "--- بدون محافظ: شل \$uri را متغیر خودش حساب میکند (خالی است)"cat <<EOFserver_name $domain;try_files $uri $uri/ =404;EOFecho "--- راه ۱: escape کردن فقط \$ های nginx"cat <<EOFserver_name $domain;try_files \$uri \$uri/ =404;EOFecho "--- راه ۲: جداکنندهی کوتیشندار (ولی آنوقت $domain هم باز نمیشود!)"cat <<'EOF'server_name $domain;try_files $uri $uri/ =404;EOF--- بدون محافظ: شل $uri را متغیر خودش حساب میکند (خالی است)server_name shop.example.ir;try_files / =404;--- راه ۱: escape کردن فقط $ های nginxserver_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 ورودی یک دستور است؛ هر جا بخواهی خروجیاش را هم با > و >> به فایل میفرستی:
cd ~/bashlabcat > notes.txt <<EOFfirst lineEOFcat >> notes.txt <<EOFsecond line (appended)EOFcat notes.txtfirst linesecond line (appended)cat > فایل <<EOF فایل را از اول مینویسد (قبلی پاک میشود)، cat >> فایل <<EOF به انتهایش اضافه میکند. یک مشکل ظاهری: داخل if یا تابع، جداکننده باید در ابتدای خط باشد، پس متن بیتورفتگی و زشت میشود. راهحل <<-: tabهای ابتدای خطها (و جداکننده) را برمیدارد:
#!/usr/bin/env bash# indent.sh: <<- strips leading TABs, so the here-doc can be indentedname="Ali"if true; then cat <<-EOF Hello, $name! This block is indented with tabs inside the script, but printed without them. EOFficd ~/bashlabbash indent.shHello, Ali!This block is indented with tabs inside the script,but printed without them.<<- فقط tab را حذف میکند، نه فاصله. اگر ویرایشگرت tab را به فاصله تبدیل کند، یا خطاهای عجیب میگیری یا تورفتگی چاپ میشود (در اشتباهات رایج). یک دام معروف دیگر، نوشتن در مسیری که دسترسی root میخواهد:
echo "--- sudo cat > ... (تغییر جهت را خود شل تو انجام میدهد، نه sudo):"sudo cat > /etc/lx-demo.conf <<EOFsetting=1EOFecho "کد خروج: $?"echo "--- sudo tee (خود tee با دسترسی root فایل را مینویسد):"sudo tee /etc/lx-demo.conf > /dev/null <<EOFsetting=1EOFecho "کد خروج: $?"cat /etc/lx-demo.confsudo rm -f /etc/lx-demo.conf--- sudo cat > ... (تغییر جهت را خود شل تو انجام میدهد، نه sudo):bash: /etc/lx-demo.conf: Permission deniedکد خروج: 1--- sudo tee (خود tee با دسترسی root فایل را مینویسد):کد خروج: 0setting=1در sudo cat > فایل، علامت > را شل تو (با دسترسی خودت) اجرا میکند، قبل از اینکه sudo شروع شود؛ پس Permission denied. راهحل sudo tee فایل است: خود tee با دسترسی root مینویسد (> /dev/null برای اینکه محتوا دوباره روی صفحه چاپ نشود؛ tee -a برای افزودن). (درس sudo و root را به یاد بیاور.)
مثال ۴: here-doc برای دستورهای دیگر و برای متغیرها
Section titled “مثال ۴: here-doc برای دستورهای دیگر و برای متغیرها”اسم here-doc «سند» است، ولی به هر دستوری که از ورودی بخواند میشود داد:
echo "--- sort:"sort <<EOFpearapplefigEOFecho "--- یک اسکریپت پایتون کوتاه بدون ساختن فایل:"python3 - <<'PY'total = sum(range(5))print("python says", total)PYecho "--- یک bash فرزند:"bash -s <<'EOF'echo "hello from a child bash"EOFecho "--- در یک متغیر:"message=$(cat <<EOFDear Ali,your backup finished.EOF)echo "$message"echo "--- متغیر چندخطی با read -d (بدون پروسهی جدید):"read -r -d '' usage <<'EOF'Usage: tool [options] -h show helpEOFecho "$usage"--- sort:applefigpear--- یک اسکریپت پایتون کوتاه بدون ساختن فایل:python says 10--- یک bash فرزند:hello from a child bash--- در یک متغیر:Dear Ali,your backup finished.--- متغیر چندخطی با read -d (بدون پروسهی جدید):Usage: tool [options] -h show helppython3 - یعنی «برنامه را از ورودی استاندارد بخوان» و bash -s همینطور؛ برای اجرای یک تکه کد بدون فایل موقت عالی است (و ssh میزبان 'bash -s' <<'EOF' برای اجرای یک بلوک روی سرور دیگر). برای ریختن در متغیر دو راه داری: $(cat <<EOF ... EOF) یا read -r -d '' var <<'EOF'. (read -d '' همیشه با کد ۱ تمام میشود چون به پایان ورودی میرسد؛ در اسکریپتی با set -e این را در نظر بگیر.)
مثال ۵: grep در اسکریپت
Section titled “مثال ۵: grep در اسکریپت”درس grep و regex را داری؛ حالا در اسکریپت، با کد خروج و آرگومانها. یک لاگ دسترسی نمونه (قالب استاندارد nginx) میسازیم تا روی آن کار کنیم:
cd ~/bashlabcat > 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"EOFecho "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)"fiecho "--- IP addresses ordered by number of requests:"grep -oE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' access.log | sort | uniq -c | sort -rnlines: 12404 responses: 2not from browsers: 4client 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.11grep -cتعداد خطهای منطبق را میدهد (بهجای خود خطها)؛-vبرعکس (غیرمنطبقها)؛-Eregex گسترده؛-oفقط بخش منطبق هر خط؛-qبیصدا، فقط کد خروج، برایif.$(...)درechoخروجیgrep -cرا در جمله میگذارد؛ برای شمارشهای داخل اسکریپت همینطور در متغیر میریزی (errors=$(grep -c ...)).grep -o ... | sort | uniq -c | sort -rnالگوی معروف «شمردن و رتبهبندی» است:uniq -cفقط خطهای متوالی یکسان را میشمارد، برای همین قبلشsort.
مثال ۶: sed در اسکریپت: قالب و ویرایش
Section titled “مثال ۶: sed در اسکریپت: قالب و ویرایش”sed برای جایگزینی در متن است. در اسکریپت دو کاربرد اصلی دارد: پرکردن قالب (جایگزینی جاهای خالی) و ویرایش فایل:
cd ~/bashlabcat > template.conf <<'EOF'server { listen 80; server_name __DOMAIN__ www.__DOMAIN__; root __ROOT__; location / { try_files $uri $uri/ =404; }}EOFdomain="shop.example.ir"root_dir="/srv/sites/shop"echo "--- جایگزینی __DOMAIN__ (قالب پر میشود و \$uri دستنخورده میماند):"sed "s/__DOMAIN__/$domain/g" template.conf | head -n 3echo "--- مقدار دارای / با جداکنندهی / خراب میشود:"sed "s/__ROOT__/$root_dir/g" template.conf | head -n 4echo "--- راهحل: جداکنندهی دیگر (|)، و چند جایگزینی با چند -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 هر نویسه میتواند باشد (|، #، ,)؛ نویسهای را بگذار که در مقدار نیست.
یک کاربرد دیگر، حذف کامنت و خط خالی از یک فایل تنظیمات و ویرایش درجا:
cd ~/bashlabcat > app.conf <<'EOF'# main settingsport = 80
# databasedb_host = localhostEOFecho "--- بدون کامنت و خط خالی:"sed -e '/^[[:space:]]*#/d' -e '/^[[:space:]]*$/d' app.confecho "--- ویرایش درجا و با نسخهی پشتیبان (-i.bak، سازگار با GNU و macOS):"sed -i.bak 's/^port = 80$/port = 8080/' app.confgrep '^port' app.conf app.conf.bakecho "--- فقط خطهای ۲ تا ۴ (-n و p):"sed -n '2,4p' app.conf--- بدون کامنت و خط خالی:port = 80db_host = localhost--- ویرایش درجا و با نسخهی پشتیبان (-i.bak، سازگار با GNU و macOS):app.conf:port = 8080app.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(بدون پسوند) فقط روی لینوکس کار میکند:
printf 'a\n' > sedtest.txtsed -i 's/a/b/' sedtest.txt 2>&1echo "--- با -i.bak:"sed -i.bak 's/a/b/' sedtest.txt && cat sedtest.txtsed: 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 به آن بدهی:
cd ~/bashlabecho "--- IP یکتا:"awk '{print $1}' access.log | sort -uecho "--- خطهای خطا (وضعیت ≥ 400): IP، وضعیت، مسیر"awk '$9 >= 400 {print $1, $9, $7}' access.logecho "--- مجموع بایتهای فرستادهشده:"awk '{sum += $10} END {print sum, "bytes"}' access.logecho "--- پاسدادن متغیر شل با -v (فقط پاسخهای بزرگتر از limit):"limit=1000awk -v min="$limit" '$10 > min {print $7, $10}' access.logecho "--- شمارش بر اساس کد وضعیت با آرایهی awk:"awk '{count[$9]++} END {for (s in count) print s, count[s]}' access.log | sortecho "--- کاربران عادی سیستم (فیلدها با : جدا میشوند):"awk -F: '$3 >= 1000 && $3 < 60000 {print $1, $3}' /etc/passwd--- IP یکتا:10.0.0.5192.168.1.10192.168.1.11192.168.1.12--- خطهای خطا (وضعیت ≥ 400): IP، وضعیت، مسیر192.168.1.12 404 /missing10.0.0.5 403 /admin192.168.1.10 500 /api/items10.0.0.5 403 /admin192.168.1.10 404 /missing10.0.0.5 403 /admin--- مجموع بایتهای فرستادهشده:11905 bytes--- پاسدادن متغیر شل با -v (فقط پاسخهای بزرگتر از limit):/ 1534/style.css 5120/api/items 2048/ 1534--- شمارش بر اساس کد وضعیت با آرایهی awk:200 5302 1403 3404 2500 1--- کاربران عادی سیستم (فیلدها با : جدا میشوند):ubuntu 1000ali 1001هر خط ورودی به فیلدها ($1، $2 و…) شکسته میشود؛ در لاگ nginx، $1 آیپی، $7 مسیر، $9 کد وضعیت و $10 تعداد بایت است. شرط {عمل} عمل را فقط برای خطهای منطبق اجرا میکند، و END {...} بعد از آخرین خط. آرایهی awk (count[$9]++) همان آرایهی انجمنی درس قبل است. -F: جداکنندهی فیلد را عوض میکند. و -v min="$limit" مقدار متغیر شل را امن به متغیر awk میدهد (کوتیشن دوتایی دور $limit را فراموش نکن).
مثال ۸: ترکیب: گزارشگیر لاگ
Section titled “مثال ۸: ترکیب: گزارشگیر لاگ”حالا همهی ابزارها در یک اسکریپت واقعی: خلاصهی لاگ دسترسی با شمارش، رتبهبندی و مجموع:
#!/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 1fi
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%)"echoecho "Top $top_n clients:"awk '{print $1}' "$log" | sort | uniq -c | sort -rn | head -n "$top_n"echoecho "Status codes:"awk '{count[$9]++} END {for (s in count) printf " %s %d\n", s, count[s]}' "$log" | sortechoecho "Most requested paths:"awk '{print $7}' "$log" | sort | uniq -c | sort -rn | head -n "$top_n"echoawk '{sum += $10} END {printf "Traffic: %.1f KB\n", sum / 1024}' "$log"cd ~/bashlabchmod +x log-report.sh./log-report.sh access.logecho "--- بدون آرگومان:"./log-report.sh; echo "کد خروج: $?"=== Log report: access.log ===Requests: 12Errors: 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 چیست؟
Section titled “پشت پرده: here-doc چیست؟”here-doc فقط ورودی استاندارد است. شل متن را در یک pipe یا فایل موقت میگذارد و آن را بهعنوان ورودی استاندارد (توصیفگر ۰) به دستور وصل میکند. میتوانی ببینی دستور چه چیزی را بهعنوان ورودی دارد:
readlink /proc/self/fd/0 <<EOFsome textEOFecho "--- در برابر ورودی از یک فایل:"echo "x" > /tmp/lx-input.txtreadlink /proc/self/fd/0 < /tmp/lx-input.txtrm -f /tmp/lx-input.txtpipe:[5086732]--- در برابر ورودی از یک فایل:/tmp/lx-input.txtبرای readlink، توصیفگر ۰ در حالت اول یک pipe (و در نسخههای قدیمیتر یک فایل موقت پاکشده) و در حالت دوم خود فایل است. دستور فرقی بین «کسی تایپ کرد»، «فایل» و «here-doc» نمیبیند؛ همه ورودی استاندارد است. چند نتیجه:
- شل قبل از اجرای دستور متن here-doc را آماده میکند: اگر بدون کوتیشن باشد، گسترشها (متغیر و
$(...)) همان لحظه انجام میشوند. - جداکننده باید تنها روی یک خط و از ابتدای خط باشد (مگر با
<<-و tab). حتی یک فاصلهی ابتدا یا انتها آن را از کار میاندازد. - چون here-doc ورودی دستور است،
cat <<EOF > فایلبه معنی «ورودیcatاین متن است و خروجیاش به فایل میرود». وsudo cat > فایل <<EOFمشکلساز است چون>قبل ازsudoتوسط شل خودت اجرا میشود.
جدولهای مرجع
Section titled “جدولهای مرجع”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 |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) متغیرهای غیرشلی در here-doc بدون کوتیشن
Section titled “۱) متغیرهای غیرشلی در here-doc بدون کوتیشن”مثال ۲: $uri، $host و هر چیز شبیه متغیر که مال برنامهی دیگری است. نتیجهی اجرای بدون محافظ:
cd ~/bashlabcat > broken.conf <<EOFlocation / { try_files $uri $uri/ =404; proxy_set_header Host $host;}EOFcat broken.conflocation / { try_files / =404; proxy_set_header Host ;}راهحل: \$uri و \$host، یا <<'EOF'.
۲) جداکنندهای که جور نیست
Section titled “۲) جداکنندهای که جور نیست”cd ~/bashlabprintf 'cat <<EOF\nhello\n EOF\n' > space-before.shecho "--- فاصله قبل از EOF:"bash space-before.sh 2>&1printf 'cat <<-EOF\n hello\n EOF\n' > spaces-instead-of-tabs.shecho "--- <<- با فاصله بهجای 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 cat > فایل
Section titled “۳) sudo cat > فایل”مثال ۳. راهحل: sudo tee فایل.
۴) متغیر شل داخل regex یا الگوی awk
Section titled “۴) متغیر شل داخل regex یا الگوی awk”cd ~/bashlabpath="/admin"echo "--- متغیر مستقیم داخل الگوی awk (/ جداکننده را میشکند):"awk "/$path/ {print \$7}" access.log 2>&1 | head -n 3echo "--- راهحل: -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 /adminawk "/$path/" میشود awk "//admin/" که برای awk خطای نحوی است. با -v p="$path" مقدار بهصورت داده میرسد، نه کد. (همین مشکل تزریق، برای مقدارهایی که از کاربر میآیند خطرناک هم هست.)
۵) grep با متغیر بهعنوان regex
Section titled “۵) grep با متغیر بهعنوان regex”cd ~/bashlabprintf 'host a.b\nhost axb\n' > hosts.txtname="a.b"echo "--- grep \$name (نقطه = هر کاراکتر):"grep -c "$name" hosts.txtecho "--- 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 باشد. (این خط را بدون باز شدن بنویس.) محتوا را نشان بده.
دیدن جواب
mkdir -p ~/bashlab && cd ~/bashlabproject="LoopX Demo"cat > README.md <<EOF# $project
Created on $(date +%F) by $(whoami).
Price: \$5EOFcat 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 ~/bashlabcat > 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 1fi
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 2fi
conf="$out_dir/$domain.conf"if [[ -e $conf ]]; then echo "Error: $conf already exists" >&2 exit 3fi
mkdir -p "$out_dir"cat > "$conf" <<NGINXserver { 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; }}NGINXecho "Created $conf"EOFchmod +x make-site.shexport LOG_DIR="$HOME/bashlab/logs"echo "--- ساختن:"./make-site.sh example.ir; echo "کد خروج: $?"cat sites/example.ir.confecho "--- دوباره (رونویسی نکند):"./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کد خروج: 0server { 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کد خروج: 2Error: 'localhost' does not look like a domain nameکد خروج: 2Usage: ./make-site.sh <domain> [output-dir]کد خروج: 1بعد با خود nginx بررسی میکنیم فایل ساختهشده سالم است. فایل nginx-test.conf یک پوشش حداقلی است که فقط فایلهای sites/*.conf را میخواند (و مسیرهای موقت را داخل پوشهی خودمان میگذارد تا بدون root اجرا شود):
cd ~/bashlabcat > nginx-test.conf <<EOFpid $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;}EOFecho "--- 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.confnginx -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 oknginx: 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:12nginx: 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) و در پایان جمع کل را چاپ کن، مرتبشده از بیشترین درآمد.
دیدن جواب
cd ~/bashlabcat > sales.csv <<'EOF'product,qty,priceapple,10,1.5banana,5,0.5apple,5,1.5cherry,2,8banana,10,0.5EOFawk -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 -nrTOTAL 46.00apple 22.50cherry 16.00banana 7.50-F, جداکننده را ویرگول میکند، NR > 1 سطر عنوان را رد میکند، آرایهی revenue برای هر محصول جمع میزند و END گزارش را چاپ میکند. sort -k2,2 -nr ستون دوم را عددی و نزولی مرتب میکند (جمع کل بالاتر از همه میافتد).
یک موتور قالب ساده بنویس: render.sh قالب KEY=value ... هر @@KEY@@ در قالب را با مقدار دادهشده عوض کند. اگر بعد از جایگزینی هنوز @@چیزی@@ مانده بود، با کد ۲ و فهرست آنها خطا بدهد. فقط با Bash (گسترش پارامتر) و grep؛ مقدارهایی که / یا & دارند هم باید درست جایگزین شوند.
دیدن جواب
cd ~/bashlabcat > greeting.tpl <<'EOF'Dear @@NAME@@,
Your site @@SITE@@ is ready (backup folder: @@DIR@@).Plan: @@PLAN@@EOFcat > 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 1fi
template="$1"shiftcontent=$(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 2fi
printf '%s\n' "$content"EOFchmod +x render.shecho "--- همهی مقدارها (با / و & هم):"./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@@Dquoted: R&Dنکتهها: ۱) "$value" داخل کوتیشن در جایگزینی، یعنی مقدار «متن دقیق» است؛ در Bash ۵.۲ بدون آن، & در مقدار (R&D) بهعنوان «متن پیداشده» تفسیر میشد (درس قبل). ۲) چون هیچ sed ای در کار نیست، / در مقدار (/srv/backups/shop) مشکلی نمیسازد. ۳) <<< (here-string) به grep متن میدهد؛ paste -sd' ' خطها را با فاصله به هم میچسباند. ۴) آرگومانها با "$@" پیمایش میشوند و هر KEY=value با %%=* و #*= شکسته میشود (حتی اگر مقدار خودش = داشته باشد).
آزمونک
Section titled “آزمونک”کدام یک $uri را در متن here-doc عیناً نگه میدارد؟
جداکنندهی کوتیشندار (<<'EOF') هیچ گسترشی انجام نمیدهد. روش دیگر: بدون کوتیشن و نوشتن \$uri با بکاسلش.
چرا sudo cat > /etc/app.conf <<EOF ... EOF با Permission denied شکست میخورد؟
sudo tee /etc/app.conf > /dev/null <<EOF.
<<-EOF دقیقاً چه کاری میکند؟
اگر ویرایشگر tab را به فاصله تبدیل کند، <<- کار نمیکند.
در sed "s/__DOMAIN__/$root/g" وقتی root=/srv/sites چه میشود و راهحل؟
s|__DOMAIN__|$root|g.
چرا باید مقدار متغیر شل را با awk -v var="$x" بدهی و نه مستقیم داخل برنامهی awk؟
مثلاً awk "/$path/" با path=/admin خطای نحوی میدهد.
چرا قبل از uniq -c معمولاً sort میگذاریم؟
sort خطهای یکسان را کنار هم میآورد.
برای جستوجوی متن دقیق example.ir (نقطه واقعی) در یک فایل از متغیر name، کدام درستتر است؟
بدون -F نقطه «هر کاراکتر» است و exampleXir هم جور میشود.
جمعبندی
Section titled “جمعبندی”- here-doc (
دستور <<EOF ... EOF) متن چندخطی را به ورودی استاندارد یک دستور میدهد.<<EOFمتغیر و$(...)را باز میکند (\$برای$واقعی)؛<<'EOF'همهچیز را خام میگذارد؛<<-EOFtabهای ابتدای خط را برمیدارد. - جداکننده باید تنها و از ابتدای خط باشد؛ فاصلهی اضافه هشدار «delimited by end-of-file» میدهد.
- نوشتن در فایل:
cat > f <<EOF(بازنویسی)،>>(افزودن)؛ برای فایل نیازمند rootsudo 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 / شمارش با آرایه |