توی این درس یاد میگیری رشتهها را بدون صدا زدن ابزار بیرونی (sed، cut، basename) و فقط با خود Bash پردازش کنی، با ابزاری به اسم گسترش پارامتر (parameter expansion): ${...}. طول (${#var}) و برش (${var:2:3})، حذف پیشوند و پسوند (${file%.txt}، ${path##*/})، جایگزینی (${var/old/new})، تبدیل حروف (${var^^}) و مقدار پیشفرض (${var:-default}) را میبینی، و آخرش چند کاربرد واقعی مثل trim، ساخت slug و تجزیهی URL. تمرین: پسوند همهی فایلهای txt را به md تغییر بده.
مسئله: اسم را عوض کن، ولی بدون ابزار
Section titled “مسئله: اسم را عوض کن، ولی بدون ابزار”اسم یک فایل را داری (/var/log/app.log.gz) و فقط خود اسم (app.log.gz) را میخواهی. یا فقط پسوند را. یا اسم بدون پسوند را. یا یک آرگومان را که شاید خالی باشد با مقدار پیشفرض عوض کنی. راههای بیرونی هست (basename $path، echo $f | sed ...)، ولی هر کدام یک پروسهی جدید میسازند و برای هر فایل یک بار اجرا میشوند. خود Bash برای همینها عملگرهای داخلی دارد که سریعاند (بدون پروسه) و دقیقاً همانجا کار میکنند، در خود اسکریپت. اینها همه از یک شکل میآیند: ${متغیر عملگر ...}.
تشبیه: قیچی و چسب روی نوار کاغذ
Section titled “تشبیه: قیچی و چسب روی نوار کاغذ”رشته مثل یک نوار کاغذ است. ${var#...} از سمت چپ میبرد (پیشوند)، ${var%...} از سمت راست (پسوند)، ${var/...} یک تکه را پیدا میکند و با تکهی دیگری عوض میکند، و ${var:-...} یک نوار یدکی میچسباند اگر نوار اصلی خالی بود. نوار اصلی دستنخورده میماند؛ هر عملگر یک نوار تازه میدهد که میتوانی در متغیر دیگری بریزی.
یک راه یادسپاری: روی صفحهکلید، # (شماره ۳) سمت چپِ $ (شماره ۴) است و % (شماره ۵) سمت راستآن: # از چپ میبرد، % از راست.
عملگرهای گسترش پارامتر
Section titled “عملگرهای گسترش پارامتر”| عملگر | کار | نتیجه برای s="hello world" |
|---|---|---|
${#s} |
طول رشته | 11 |
${s:6} / ${s:0:5} |
برش: از خانهی ۶ تا آخر / ۵ کاراکتر از خانهی ۰ | world / hello |
${s#pat} / ${s##pat} |
حذف پیشوند (کوتاهترین / بلندترین) | |
${s%pat} / ${s%%pat} |
حذف پسوند (کوتاهترین / بلندترین) | |
${s/old/new} / ${s//old/new} |
جایگزینی اولین / همه | |
${s^} ${s^^} ${s,} ${s,,} |
حرف اول بزرگ / همه بزرگ / حرف اول کوچک / همه کوچک | Hello world … |
${s:-d} |
اگر s خالی یا تعریفنشده بود، d |
|
${s:=d} |
مثل بالا، و d را در s هم میریزد |
|
${s:?msg} |
اگر خالی بود، پیام خطا و توقف | |
${s:+alt} |
اگر s مقدار داشت، alt (وگرنه خالی) |
مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: طول و برش
Section titled “مثال ۱: طول و برش”s="hello world"echo "length: ${#s}"echo "from 6: ${s:6}"echo "5 chars from 0: ${s:0:5}"echo "last 5 chars: ${s: -5}"echo "3 chars from -5: ${s:(-5):3}"echo "--- دام: فاصله قبل از منفی!"echo "\${s: -5} = ${s: -5}"echo "\${s:-5} = ${s:-5} (این «مقدار پیشفرض» است، نه برش)"echo "--- فارسی: کاراکتر یا بایت؟"fa="سلام"echo "characters: ${#fa}"LC_ALL=C bash -c 'fa="سلام"; echo "bytes (C locale): ${#fa}"'length: 11from 6: world5 chars from 0: hellolast 5 chars: world3 chars from -5: wor--- دام: فاصله قبل از منفی!${s: -5} = world${s:-5} = hello world (این «مقدار پیشفرض» است، نه برش)--- فارسی: کاراکتر یا بایت؟characters: 4bytes (C locale): 8${s:offset:length}: خانهها از ۰ شمارهگذاری میشوند. بدونlengthتا آخر رشته.- برای شمارش از انتها، قبل از عدد منفی فاصله بگذار:
${s: -5}(یا${s:(-5)}). اگر بنویسی${s:-5}، Bash آن را «مقدار پیشفرض» میفهمد (:-) و کل رشته را برمیگرداند. یکی از رایجترین دامها. ${#s}تعداد کاراکتر را میشمارد، نه بایت: «سلام» چهار کاراکتر است ولی در UTF-8 هشت بایت (هر حرف فارسی دو بایت). باLC_ALL=Clocale ساده میشود و به بایت میشمارد.wc -cهمیشه بایت میشمارد؛ پس این دو ممکن است فرق کنند.
مثال ۲: حذف پیشوند و پسوند
Section titled “مثال ۲: حذف پیشوند و پسوند”مهمترین عملگرها در کار روزمره. الگو همان wildcard است (درس case):
p="/var/log/app.log.gz"echo "path: $p"echo "--- از چپ (پیشوند):"echo "\${p#*/} (کوتاه): ${p#*/}"echo "\${p##*/} (بلند): ${p##*/}"echo "--- از راست (پسوند):"echo "\${p%.*} (کوتاه): ${p%.*}"echo "\${p%%.*} (بلند): ${p%%.*}"echo "--- کاربردهای معروف:"echo "basename: ${p##*/}"echo "dirname: ${p%/*}"echo "extension (last): ${p##*.}"echo "name without last ext: ${p%.*}"echo "--- حذف یک پسوند مشخص:"f="report.txt"echo "${f%.txt}.md"path: /var/log/app.log.gz--- از چپ (پیشوند):${p#*/} (کوتاه): var/log/app.log.gz${p##*/} (بلند): app.log.gz--- از راست (پسوند):${p%.*} (کوتاه): /var/log/app.log${p%%.*} (بلند): /var/log/app--- کاربردهای معروف:basename: app.log.gzdirname: /var/logextension (last): gzname without last ext: /var/log/app.log--- حذف یک پسوند مشخص:report.md- کوتاه یا بلند؟
${p#*/}کوتاهترین تکهای را که با*/جور باشد از چپ حذف میکند (فقط اولین/)، ولی${p##*/}بلندترین (تا آخرین/) و فقط اسم فایل میماند. برای پسوند هم همین:%.*فقط.gzرا برمیدارد و%%.*همهی نقطهها را (.log.gz). - چهار الگوی طلایی که در همهی اسکریپتها میبینی:
${p##*/}(اسم فایل، مثلbasename)،${p%/*}(پوشه، مثلdirname)،${f##*.}(پسوند)،${f%.*}(اسم بدون پسوند). ${f%.txt}.mdپسوند.txtرا برمیدارد و.mdمیچسباند؛ همان کاری که فایلها را عوض میکند (تمرین اصلی).
مثال ۳: جایگزینی و تبدیل حروف
Section titled “مثال ۳: جایگزینی و تبدیل حروف”t="a-b-c-d"echo "first only: ${t/-/+}"echo "all: ${t//-/+}"echo "at start: ${t/#a/X}"echo "at end: ${t/%d/Z}"echo "delete all: ${t//-/}"echo "--- الگو (wildcard) هم میپذیرد:"u="foo.bar.baz"echo "dots to spaces: ${u//./ }"echo "everything up to the last dot removed: ${u/*./}"echo "--- جایگزینی اسلش (باید escape شود):"dir="/var/log/nginx"echo "${dir//\//-}"echo "--- تبدیل حروف (Bash 4+):"n="hello World"echo "first upper: ${n^}"echo "all upper: ${n^^}"echo "all lower: ${n,,}"echo "first lower: ${n,}"first only: a+b-c-dall: a+b+c+dat start: X-b-c-dat end: a-b-c-Zdelete all: abcd--- الگو (wildcard) هم میپذیرد:dots to spaces: foo bar bazeverything up to the last dot removed: baz--- جایگزینی اسلش (باید escape شود):-var-log-nginx--- تبدیل حروف (Bash 4+):first upper: Hello Worldall upper: HELLO WORLDall lower: hello worldfirst lower: hello World/یک بار و//همهی تطابقها./#فقط اگر تطابق اول رشته باشد و/%فقط آخر. جایگزین خالی یعنی حذف (${t//-/}).- الگو wildcard است نه regex:
${u/*./}از ابتدا تا آخرین نقطه را (حریصانه) برمیدارد وbazمیماند. ${n^^}و${n,,}حروف بزرگ/کوچک (Bash ۴ به بعد) را عوض میکنند؛ برای مقایسهی بیتوجه به حروف خیلی مفید است:[[ ${answer,,} == y* ]].
یک نکتهی نسخهبهنسخه: در Bash ۵.۲ نویسهی & در متن جایگزین، «همان چیزی که پیدا شد» است (قابلیت patsub_replacement که پیشفرض روشن است)؛ در نسخههای قبلی & معمولی بود:
w="a and b"echo "[${w//and/&}] (& = متن پیداشده)"echo "[${w//and/\&}] (با \\ یک & معمولی)"echo "[${w//and/[&]}] (& داخل کروشه)"shopt -s | grep patsub[a and b] (& = متن پیداشده)[a & b] (با \ یک & معمولی)[a [and] b] (& داخل کروشه)patsub_replacement on/bin/bash -c 'w="a and b"; echo "[${w//and/&}]"'/bin/bash -c 's=hello; echo "${s^^}"' 2>&1[a & b]/bin/bash: ${s^^}: bad substitutionدو خط آخر از bash ۳.۲ مک گرفته شده: & را همانطور چاپ کرد (a & b) و ${s^^} را اصلاً نمیشناسد (bad substitution). اگر اسکریپتت باید روی مک و لینوکس هر دو بدون تغییر کار کند، به این تفاوتها دقت کن: برای & از \& استفاده کن و برای تبدیل حروف از tr.
مثال ۴: مقدار پیشفرض و بررسی
Section titled “مثال ۴: مقدار پیشفرض و بررسی”وقتی متغیر ممکن است تعریفنشده یا خالی باشد، پنج عملگر داری که فرقشان را باید بدانی:
unset unem=""se="set"echo "--- \${v:-d} (خالی یا تعریفنشده ← d):"echo "unset=[${un:-d}] empty=[${em:-d}] set=[${se:-d}]"echo "--- \${v-d} (فقط تعریفنشده ← d؛ خالی خالی میماند):"echo "unset=[${un-d}] empty=[${em-d}] set=[${se-d}]"echo "--- \${v:+alt} (اگر مقدار داشت ← alt):"echo "unset=[${un:+alt}] empty=[${em:+alt}] set=[${se:+alt}]"echo "--- \${v:=d} (پیشفرض را در متغیر هم میریزد):"echo "[${cfg:=assigned}] cfg is now: $cfg"--- ${v:-d} (خالی یا تعریفنشده ← d):unset=[d] empty=[d] set=[set]--- ${v-d} (فقط تعریفنشده ← d؛ خالی خالی میماند):unset=[d] empty=[] set=[set]--- ${v:+alt} (اگر مقدار داشت ← alt):unset=[] empty=[] set=[alt]--- ${v:=d} (پیشفرض را در متغیر هم میریزد):[assigned] cfg is now: assigned${v:-d}: مقدار پیشفرض (بدون دستزدن بهv). پرکاربردترین:name="${1:-World}"(درس ورودی و آرگومانها).${v-d}(بدون دونقطه): فقط وقتیvتعریفنشده باشد. با:یعنی «تعریفنشده یا خالی».${v:=d}: مثل:-ولیdرا درvهم میگذارد.${v:+alt}: برعکس: اگرvمقدار دارد،alt(مثلاً${verbose:+--verbose}برای افزودن یک گزینه فقط وقتی روشن است).${v:?پیام}: اگرvخالی/تعریفنشده بود، پیام خطا و اسکریپت متوقف میشود. محافظ عالی برای متغیرهای ضروری:
#!/usr/bin/env bash# must-have.sh: refuses to run without a backup directorybackup_dir="${1:?usage: must-have.sh <backup-dir>}"echo "backing up to $backup_dir"cd ~/bashlabchmod +x must-have.sh./must-have.sh; echo "کد خروج: $?"./must-have.sh /backups; echo "کد خروج: $?"./must-have.sh: line 3: 1: usage: must-have.sh <backup-dir>کد خروج: 1backing up to /backupsکد خروج: 0بدون آرگومان، اسکریپت با پیام توقف کرد و خط دوم (echo) هرگز اجرا نشد؛ با آرگومان بهخوبی ادامه داد. این همان محافظی است که در درس متغیرها برای rm -rf "${dir:?}"/* معرفی کردم.
مثال ۵: تجزیهی رشتهها: URL، تنظیمات و trim
Section titled “مثال ۵: تجزیهی رشتهها: URL، تنظیمات و trim”با همین عملگرها میشود متن را تجزیه کرد. اول یک URL، تکه به تکه:
url="https://example.com:8443/path/to/page?x=1"scheme="${url%%://*}"rest="${url#*://}"hostport="${rest%%/*}"path="/${rest#*/}"host="${hostport%%:*}"port="${hostport#*:}"echo "scheme: $scheme"echo "host: $host"echo "port: $port"echo "path: ${path%%\?*}"echo "query: ${path#*\?}"scheme: httpshost: example.comport: 8443path: /path/to/pagequery: x=1هر خط یک برش است: %%://* همهچیز از :// به بعد را میبرد (میماند https)، #*:// همهچیز تا :// را (میماند example.com:8443/path...) و همینطور ادامه. دقت کن ? در الگو یک wildcard (یک کاراکتر) است، پس برای خود علامت سؤال \? نوشتیم.
حالا یک خط تنظیمات KEY = VALUE با فاصلههای اضافه؛ جدا کردن آسان است ولی فاصلهها میمانند:
line=" PORT = 8080 "key="${line%%=*}"value="${line#*=}"echo "raw: [$key] [$value]"
trim() { local s="$1" s="${s#"${s%%[![:space:]]*}"}" s="${s%"${s##*[![:space:]]}"}" printf '%s' "$s"}
echo "trimmed: [$(trim "$key")] [$(trim "$value")]"raw: [ PORT ] [ 8080 ]trimmed: [PORT] [8080]تابع trim فاصلههای ابتدا و انتها را برمیدارد، فقط با گسترش پارامتر. بخوانش از داخل به بیرون: ${s%%[![:space:]]*} همهی فاصلههای ابتدای رشته را (از اولین غیرفاصله به بعد را میبرد) بهعنوان یک تکه میدهد و ${s#"..."} همان تکه را از سر s حذف میکند (کوتیشن داخلی، یعنی «متن دقیق» نه الگو). نیمهی دوم قرینهی آن برای انتهاست. معروفترین روش trim در Bash است؛ اگر خواستی بدون این پیچیدگی، sed 's/^[[:space:]]*//; s/[[:space:]]*$//' هم هست.
مثال ۶: تغییر اسم فایلها
Section titled “مثال ۶: تغییر اسم فایلها”حالا ترکیب با حلقه، برای چند کار واقعی. اول حروف بزرگ و کوچک:
mkdir -p ~/bashlab/docs && cd ~/bashlab/docstouch README.TXT "Meeting Notes.TXT" Budget.Final.TXTecho "--- قبل:"lsfor f in *; do mv -n -- "$f" "${f,,}"doneecho "--- بعد از حروف کوچک:"ls--- قبل:Budget.Final.TXTMeeting Notes.TXTREADME.TXT--- بعد از حروف کوچک:budget.final.txtmeeting notes.txtreadme.txtو برای کل آرایه یکجا (بدون حلقه): عملگرها روی هر عضو آرایه اعمال میشوند:
files=("a.txt" "b c.txt" "d.txt")echo "names: ${files[@]}"echo "no extension: ${files[@]%.txt}"echo "as .bak: ${files[@]/%.txt/.bak}"for f in "${files[@]%.txt}"; do echo "[$f]"donenames: a.txt b c.txt d.txtno extension: a b c das .bak: a.bak b c.bak d.bak[a][b c][d]"${files[@]%.txt}" پسوند را از همهی عضوها برمیدارد و (با کوتیشن) هر عضو را سالم نگه میدارد؛ حتی b c. این میانبر در اسکریپتهای آرایهمحور خیلی به کار میآید.
مثال ۷: چرا سریعتر است؟ بدون پروسهی جدید
Section titled “مثال ۷: چرا سریعتر است؟ بدون پروسهی جدید”basename یک برنامهی جداست؛ برای هر صدازدن، شل باید یک پروسهی فرزند بسازد (fork) و آن را اجرا کند (exec). گسترش پارامتر داخل خود شل است. تفاوت را اندازه بگیریم، برای ۳۰۰۰ مسیر:
TIMEFORMAT='%R seconds'paths=(/var/log/app.log /home/ali/docs/report.txt /etc/hosts)
echo "basename (3000 processes):"time { for ((i = 0; i < 1000; i++)); do for p in "${paths[@]}"; do x=$(basename "$p") done done}
echo "\${p##*/} (no process):"time { for ((i = 0; i < 1000; i++)); do for p in "${paths[@]}"; do x=${p##*/} done done}basename (3000 processes):1.370 seconds${p##*/} (no process):0.013 secondsنتیجه برای هر کدام یک عدد ثانیه است؛ نسخهی ${p##*/} حدود صد برابر سریعتر است (اعداد دقیق به ماشین بستگی دارند، ولی نسبت همین حدود است). برای یک فایل فرقی حس نمیشود؛ برای حلقه روی هزار فایل (یا اسکریپتی که هر ثانیه چندین بار اجرا میشود) فرق بزرگ است.
پشت پرده: کار عملگرها
Section titled “پشت پرده: کار عملگرها”گسترش پارامتر یکی از مرحلههای بازنویسی خط فرمان است (درس متغیرها): بعد از آکولاد و ~، شل ${...} را با نتیجه جایگزین میکند. چند نکته:
- الگوها همان wildcard نام فایلاند (
*،?،[...])، نه regex. برای همین${s/[0-9]+/N}«یک یا چند رقم» نیست:+یک کاراکتر معمولی است. (باshopt -s extglobالگوی+([0-9])هم داری، درسcase.) - نتیجهی عملگر یک رشتهی جدید است؛ متغیر اصلی عوض نمیشود، مگر خودت دوباره در آن بریزی (
s="${s%.txt}") یا از:=استفاده کنی. - عملگرها روی هر عضو آرایه (مثال ۶) و روی آرگومانها (
"${@%.txt}") هم اثر میکنند. ${!name}(گسترش غیرمستقیم): مقدار متغیری را میدهد که اسمش درnameاست. و${var@Q}رشته را طوری برمیگرداند که بتوانی مستقیم در شل به کار ببری (با کوتیشن امن). این دو برای دیباگ و کدهای پیشرفته به کار میآیند:
name=HOMEecho "indirect: \${!name} = ${!name}"q="it's a b"echo "quoted for reuse: ${q@Q}"indirect: ${!name} = /home/aliquoted for reuse: 'it'\''s a b'جدولهای مرجع
Section titled “جدولهای مرجع”حذف و جایگزینی (برای p=/var/log/app.log.gz):
| عبارت | نتیجه | کاربرد |
|---|---|---|
${p##*/} |
app.log.gz |
اسم فایل (basename) |
${p%/*} |
/var/log |
پوشه (dirname) |
${p##*.} |
gz |
پسوند |
${p%.*} |
/var/log/app.log |
بدون آخرین پسوند |
${p%%.*} |
/var/log/app |
بدون همهی پسوندها |
${p//\//-} |
-var-log-app.log.gz |
همهی / ها را - کن |
${p/log/LOG} |
/var/LOG/app.log.gz |
اولین log را عوض کن |
مقدار پیشفرض:
| عبارت | اگر تعریفنشده | اگر خالی | اگر مقدار دارد |
|---|---|---|---|
${v:-d} |
d |
d |
مقدار |
${v-d} |
d |
خالی | مقدار |
${v:=d} |
d (و در v میریزد) |
d (و میریزد) |
مقدار |
${v:+alt} |
خالی | خالی | alt |
${v:?msg} |
خطا و توقف | خطا و توقف | مقدار |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) ${s:-5} بهجای ${s: -5}
Section titled “۱) ${s:-5} بهجای ${s: -5}”مثال ۱: بدون فاصله، «مقدار پیشفرض» میشود و کل رشته برمیگردد.
۲) پسوندی که وجود ندارد
Section titled “۲) پسوندی که وجود ندارد”for f in archive notes.txt .bashrc; do echo "$f -> ext: [${f##*.}] no-ext: [${f%.*}]"donearchive -> ext: [archive] no-ext: [archive]notes.txt -> ext: [txt] no-ext: [notes].bashrc -> ext: [bashrc] no-ext: []برای فایل بیپسوند (archive)، ${f##*.} کل اسم را برمیگرداند (چون الگو جور نشد و چیزی حذف نشد) و ${f%.*} همان اسم. برای .bashrc هم نقطهی اول پسوند حساب میشود (پس bashrc را بهعنوان پسوند میدهد و نام خالی میماند). راهحل: اگر ممکن است پسوند نباشد، اول بررسی کن: [[ $f == *.* ]].
۳) اسمهای دارای فاصله و نبود کوتیشن
Section titled “۳) اسمهای دارای فاصله و نبود کوتیشن”cd ~/bashlab/docstouch "my notes.txt"f="my notes.txt"echo "--- بدون کوتیشن:"mv $f ${f%.txt}.mdecho "--- با کوتیشن:"mv -- "$f" "${f%.txt}.md"ls--- بدون کوتیشن:mv: target 'notes.md': No such file or directory--- با کوتیشن:budget.final.txtmeeting notes.txtmy notes.mdreadme.txtبدون کوتیشن mv my notes.txt my notes.md شد (چهار آرگومان) و خطا داد. راهحل: همیشه "$f" و "${f%.txt}.md".
۴) الگو را regex فرض کردن
Section titled “۴) الگو را regex فرض کردن”v="order 12345 shipped"echo "wildcard +: ${v/[0-9]+/N}"echo "one digit: ${v/[0-9]/N}"echo "all digits: ${v//[0-9]/N}"shopt -s extglobecho "extglob: ${v/+([0-9])/N}"wildcard +: order 12345 shippedone digit: order N2345 shippedall digits: order NNNNN shippedextglob: order N shipped+ در wildcard معمولی است و [0-9]+ یعنی «یک رقم و بعدش +» که اینجا جور نشد و رشته عوض نشد. [0-9] یک رقم، // همهی رقمها، و با extglob الگوی «یک یا چند رقم» (+([0-9])) کار میکند.
۵) فراموشکردن اینکه متغیر عوض نمیشود
Section titled “۵) فراموشکردن اینکه متغیر عوض نمیشود”${s%.txt} فقط نتیجه را میدهد؛ اگر لازم داری در s بماند، s="${s%.txt}". (مگر اینکه فقط میخواهی چاپش کنی.)
۶) ${var^^} در macOS قدیمی
Section titled “۶) ${var^^} در macOS قدیمی”مثال ۳: bad substitution در bash ۳.۲؛ tr '[:lower:]' '[:upper:]' راه سازگار است.
برای مسیر /home/ali/docs/report.final.tar.gz با گسترش پارامتر (نه basename/dirname) اینها را چاپ کن: پوشه، اسم فایل، آخرین پسوند، اسم بدون آخرین پسوند، و اسم بدون هیچ پسوند.
دیدن جواب
path="/home/ali/docs/report.final.tar.gz"name="${path##*/}"echo "directory: ${path%/*}"echo "file name: $name"echo "last extension: ${name##*.}"echo "without last extension: ${name%.*}"echo "without any extension: ${name%%.*}"directory: /home/ali/docsfile name: report.final.tar.gzlast extension: gzwithout last extension: report.final.tarwithout any extension: reportتمرین اصلی درس: پسوند همهی فایلهای txt یک پوشه را به md تغییر بده. اسکریپت txt2md.sh پوشه را از آرگومان بگیرد، اسمهای دارای فاصله را درست مدیریت کند، روی فایل md موجود رونویسی نکند (آن را رد کند و بگوید) و در پایان تعداد تغییرها را چاپ کند. قبل و بعد را نشان بده.
دیدن جواب
mkdir -p ~/bashlab/notes && cd ~/bashlabtouch notes/todo.txt "notes/meeting notes.txt" notes/ideas.txt notes/ideas.md notes/readme.mdcat > txt2md.sh <<'EOF'#!/usr/bin/env bash# txt2md.sh: change the extension of every .txt file in a folder to .md# Usage: ./txt2md.sh <folder>if [[ $# -ne 1 || ! -d $1 ]]; then echo "Usage: $0 <folder>" >&2 exit 1fi
cd "$1" || exit 1shopt -s nullglobrenamed=0
for f in *.txt; do new="${f%.txt}.md" if [[ -e $new ]]; then echo "skip (exists): $f -> $new" continue fi mv -- "$f" "$new" echo "renamed: $f -> $new" ((renamed++))done
echo "done: $renamed file(s) renamed"EOFchmod +x txt2md.shecho "--- قبل:"; ls notesecho "--- اجرا:"; ./txt2md.sh notesecho "--- بعد:"; ls notes--- قبل:ideas.mdideas.txtmeeting notes.txtreadme.mdtodo.txt--- اجرا:skip (exists): ideas.txt -> ideas.mdrenamed: meeting notes.txt -> meeting notes.mdrenamed: todo.txt -> todo.mddone: 2 file(s) renamed--- بعد:ideas.mdideas.txtmeeting notes.mdreadme.mdtodo.md${f%.txt}.md همان الگوی طلایی است. ideas.txt رد شد چون ideas.md از قبل بود (جلوگیری از گمشدن محتوا). nullglob پوشهی بدون txt را بیخطا رد میکند و کوتیشنها از اسم meeting notes.txt محافظت میکنند.
فایل تنظیماتی با خطهایی مثل PORT = 8080 داری، همراه با کامنت (# ...) و خط خالی. اسکریپتی بنویس که هر خط تنظیم را بخواند (کامنت و خط خالی را رد کند)، کلید و مقدار را trim کند و به شکل PORT -> 8080 چاپ کند.
دیدن جواب
cd ~/bashlabcat > app.conf <<'EOF'# application settings PORT = 8080HOST=localhost
# debug modeDEBUG = trueEOFcat > read-conf.sh <<'EOF'#!/usr/bin/env bash# read-conf.sh: print "key -> value" for every setting in a config file
trim() { local s="$1" s="${s#"${s%%[![:space:]]*}"}" s="${s%"${s##*[![:space:]]}"}" printf '%s' "$s"}
while IFS= read -r line; do line="$(trim "$line")" [[ -z $line || $line == \#* ]] && continue key="$(trim "${line%%=*}")" value="$(trim "${line#*=}")" echo "$key -> $value"done < "$1"EOFchmod +x read-conf.sh./read-conf.sh app.confPORT -> 8080HOST -> localhostDEBUG -> trueترکیب چند درس: while IFS= read -r (حلقهها)، تابع trim (مثال ۵)، [[ ... ]] و continue برای رد کردن، و ${line%%=*} / ${line#*=} برای جدا کردن کلید و مقدار. \#* در [[ ]] یعنی «با # شروع شود» (علامت # باید escape شود تا کامنت حساب نشود).
تابع slugify بنویس که یک عنوان را به slug مناسب URL تبدیل کند: فقط حروف کوچک انگلیسی، عدد و -؛ هر فاصله یک -؛ علامتهای نگارشی حذف؛ چند - پشتسرهم یکی؛ بدون - در ابتدا و انتها. فقط با گسترش پارامتر (بدون sed/tr).
دیدن جواب
cd ~/bashlabcat > slug.sh <<'EOF'#!/usr/bin/env bash# slug.sh: turn a title into a URL slug using only parameter expansion
slugify() { local s="${1,,}" # lowercase s="${s//[^a-z0-9 -]/}" # drop everything except a-z 0-9 space - s="${s// /-}" # spaces -> hyphens while [[ $s == *--* ]]; do # collapse repeated hyphens s="${s//--/-}" done s="${s#-}" # trim leading hyphen s="${s%-}" # trim trailing hyphen printf '%s\n' "$s"}
slugify " Hello, World! Bash & Linux --- 2026 "slugify "Learn BASH in 10 Minutes"echo "[$(slugify '***')]"EOFbash slug.shhello-world-bash-linux-2026learn-bash-in-10-minutes[]مرحلهها پشتسرهم همان عملگرهای درساند: ${1,,} حروف کوچک، ${s//[^a-z0-9 -]/} حذف همهچیزهای ناخواسته (کلاس منفی wildcard)، ${s// /-} فاصله به خطتیره، حلقهی while برای فشردهکردن -- (یک جایگزینی ساده فقط جفتهای غیرهمپوشان را میگیرد، پس تا وقتی -- هست تکرار میکنیم)، و #-/%- برای حذف خطتیرهی ابتدا و انتها. برای ورودی *** (که چیزی از آن نمیماند) رشتهی خالی برگشت ([] در خروجی).
آزمونک
Section titled “آزمونک”اگر f=archive.tar.gz باشد، echo ${f%%.*} چه چاپ میکند؟
%% بلندترین تطابق از راست را حذف میکند: .tar.gz (از اولین نقطه). با تک % فقط .gz حذف میشد و archive.tar میماند.
p=/var/log/app.log.gz و echo ${p##*/} چه چاپ میکند؟
## بلندترین پیشوند تا آخرین / را برمیدارد (معادل basename).
تفاوت ${v:-d} و ${v-d}؟
${v:=d} است که مقدار را در v هم میریزد.
s="hello world" و echo ${s:-5} چه میدهد؟ و برای پنج کاراکتر آخر چه مینویسی؟
فاصله قبل از عدد منفی، برش را از مقدار پیشفرض جدا میکند.
for f in *.txt; do mv $f ${f%.txt}.md; done با فایل «my notes.txt» چه میشود؟
همیشه کوتیشن دور گسترشها.
چرا ${p##*/} از $(basename "$p") سریعتر است؟
در حلقههای بزرگ فرق صد برابری میشود.
echo ${#s} برای s="سلام" در locale UTF-8 چه میدهد؟
با LC_ALL=C بایت میشمارد (۸). wc -c همیشه بایت میشمارد.
جمعبندی
Section titled “جمعبندی”- گسترش پارامتر
${...}رشته را داخل خود Bash و بدون پروسهی جدید پردازش میکند؛ نتیجه یک رشتهی تازه است و متغیر عوض نمیشود (مگر دوباره بریزی یا:=). - طول و برش:
${#s}(کاراکتر)،${s:2:3}، از انتها${s: -3}(با فاصله؛${s:-3}پیشفرض است). - حذف:
#و##از چپ،%و%%از راست (تکی کوتاهترین، دوتایی بلندترین). الگوهای طلایی:${p##*/}اسم،${p%/*}پوشه،${f##*.}پسوند،${f%.*}بیپسوند،${f%.txt}.mdتغییر پسوند. - جایگزینی:
${s/old/new}(اولی)،${s//old/new}(همه)،/#و/%(ابتدا/انتها)؛ الگو wildcard است نه regex؛&در Bash ۵.۲ یعنی متن پیداشده. - حروف:
${s^^}${s,,}${s^}${s,}(Bash ۴+؛ در مک ۳.۲ نیست). - پیشفرض:
${v:-d}،${v:=d}،${v:+alt}،${v:?msg}(توقف با پیام) و نسخهی بدون دونقطه که فقط «تعریفنشده» را میسنجد. - روی هر عضو آرایه (
"${files[@]%.txt}") و آرگومان اعمال میشود. همیشه کوتیشن:"${f%.txt}.md".
| دستور | کاری که میکند |
|---|---|
${#s} ${s:2:3} ${s: -3} | طول / برش / سه کاراکتر آخر |
${p##*/} ${p%/*} | اسم فایل / پوشه (basename و dirname) |
${f##*.} ${f%.*} ${f%%.*} | پسوند / بیآخرینپسوند / بیهمهیپسوندها |
${f%.txt}.md | تغییر پسوند |
${s#pat} ${s##pat} ${s%pat} ${s%%pat} | حذف پیشوند/پسوند (کوتاه و بلند) |
${s/old/new} ${s//old/new} | جایگزینی اولی / همه |
${s//[^a-z0-9]/} | حذف هر چیز جز حرف و عدد |
${s^^} ${s,,} ${s^} | بزرگ / کوچک / حرف اول بزرگ (Bash 4+) |
${v:-d} ${v:=d} ${v:+alt} | پیشفرض / پیشفرض و ذخیره / فقط اگر مقدار دارد |
${v:?پیام} | اگر خالی بود، پیام و توقف |
"${files[@]%.txt}" | حذف پسوند از همهی عضوهای آرایه |
${!name} ${s@Q} | گسترش غیرمستقیم / رشتهی امن برای استفادهی مجدد |