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

متغیرها و نقل‌قول

توی این درس یاد می‌گیری داده‌ی متغیر را در اسکریپت نگه داری: متغیر تعریف کنی (name="value") و بخوانی (echo "$name")؛ بفهمی چرا name = "value" با فاصله خطا می‌دهد؛ فرق کوتیشن دوتایی، تکی و بدون کوتیشن را بدانی و چرا تقریباً همیشه باید "$name" بنویسی؛ خروجی یک دستور را در متغیر بگذاری (command substitution، مثل $(date +%F)) و محاسبه کنی ($((a + b))). تمرین: اسکریپتی که اسم فایل بکاپ را با تاریخ امروز بسازد.

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

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

اسکریپتی که فقط دستورهای ثابت دارد، فقط یک کار ثابت می‌کند. کار واقعی با داده‌ی متغیر است: اسم یک فایل، تاریخ امروز، یک عدد، مسیر یک پوشه. و اگر مسیر /var/www/shop را در ده خط اسکریپت نوشته باشی و روزی عوض شود، باید ده جا را عوض کنی و حتماً یکی را جا می‌اندازی. متغیر (variable) یک اسم است که به یک مقدار وصل می‌شود: یک بار تعریفش می‌کنی و هر جا لازم شد با اسم صدایش می‌زنی.

تشبیه: جعبه‌ی برچسب‌دار

Section titled “تشبیه: جعبه‌ی برچسب‌دار”

متغیر مثل یک جعبه‌ی برچسب‌دار است: روی جعبه نوشته‌ای name و داخلش Ali گذاشته‌ای. هر جا بنویسی $name یعنی «محتوای جعبه‌ی name را همین‌جا بریز». نکته‌ی مهم: شل این جایگزینی را قبل از اجرای دستور انجام می‌دهد؛ دستوری که اجرا می‌شود اصلاً نمی‌داند متغیری در کار بوده. این یک جمله، نصف مشکلات کوتیشن را توضیح می‌دهد، و در بخش «پشت پرده» می‌بینی.

متغیرها و کوتیشن: قاعده‌ها

Section titled “متغیرها و کوتیشن: قاعده‌ها”
موضوع قاعده
تعریف name="value" (بدون فاصله دور =)
خواندن $name یا ${name}
نام حرف، عدد و _؛ با عدد شروع نمی‌شود؛ حروف کوچک و بزرگ فرق دارند
قرارداد متغیرهای اسکریپت با حروف کوچک (backup_dir)، ثابت‌ها و متغیرهای محیطی با حروف بزرگ (BACKUP_DIR)
نوع همه‌چیز متن است؛ عدد را وقتی محاسبه می‌کنی به‌صورت عدد می‌خوانند
متغیر تعریف‌نشده خطا نمی‌دهد؛ خالی است

سه نوع کوتیشن:

شکل $name و $(...) فاصله و * نمونه
بدون کوتیشن جایگزین می‌شود شکسته می‌شود (جدا جدا) echo $name
دوتایی "..." جایگزین می‌شود حفظ می‌شود (یک تکه) echo "$name"
تکی '...' جایگزین نمی‌شود؛ همان متن حفظ می‌شود echo '$name'
مهم‌ترین تفاوت: بدون کوتیشن، شل مقدار متغیر را در فاصله‌ها می‌شکند و دستور چند آرگومان می‌گیرد؛ با کوتیشن مقدار یک آرگومان می‌ماند.

همه‌ی کدهای این درس را می‌توانی هم مستقیم توی ترمینال بزنی، هم داخل یک اسکریپت بگذاری؛ فرقی ندارد.

مثال ۱: تعریف و خواندن متغیر

Section titled “مثال ۱: تعریف و خواندن متغیر”
Terminal window
name="Ali"
age=30
echo "Hello, $name. You are $age."
name="Sara"
echo "Now the name is $name"
echo "A variable that was never set: [$nothing]"
خروجی
Hello, Ali. You are 30.
Now the name is Sara
A variable that was never set: []

name="Ali" مقدار Ali را در متغیر name می‌گذارد؛ با $name می‌خوانیش. دوباره انتساب بدهی، مقدار قبلی عوض می‌شود. متغیری که هرگز تعریف نشده خطا نمی‌دهد؛ خالی است (آخرین خط). این راحت است ولی خطرناک هم هست (بخش اشتباهات رایج)، و در درس مدیریت خطا با set -u یاد می‌گیری از آن جلوگیری کنی.

بزرگ‌ترین دام تعریف متغیر: فاصله دور =. در بیشتر زبان‌ها می‌شود name = "Ali" نوشت، ولی در Bash نه:

Terminal window
name = "Ali"
name= "Ali"
خروجی
bash: name: command not found
bash: Ali: command not found

در حالت اول شل خط را این‌طور می‌بیند: «دستوری به اسم name را با دو آرگومان = و Ali اجرا کن» و چنین دستوری نیست. در حالت دوم name= یک انتساب موقت (برای همان یک دستور) است و دستوری که می‌خواهد اجرا کند Ali است که باز هم وجود ندارد. قاعده: name="value"، همه چسبیده.

Terminal window
name="Ali"
echo "double: Hello, $name"
echo 'single: Hello, $name'
echo unquoted: Hello, $name
echo "--- کوتیشن داخل کوتیشن:"
echo "He said: \"hi\""
echo "It's fine inside double quotes"
echo 'He said: "hi" (inside single quotes)'
echo "price: \$5"
echo 'It'\''s tricky inside single quotes'
خروجی
double: Hello, Ali
single: Hello, $name
unquoted: Hello, Ali
--- کوتیشن داخل کوتیشن:
He said: "hi"
It's fine inside double quotes
He said: "hi" (inside single quotes)
price: $5
It's tricky inside single quotes
  • در دوتایی فقط چند کاراکتر خاص معنی دارند: $ (متغیر و جایگزینی)، ` (روش قدیمی جایگزینی دستور)، \ و خود ". برای چاپ خود " یا $ جلویشان \ می‌گذاری.
  • در تکی هیچ‌چیز معنی خاصی ندارد: متن دقیقاً همان است که نوشته‌ای. هیچ راهی برای گذاشتن ' داخل کوتیشن تکی نیست؛ باید کوتیشن را ببندی، \' بگذاری و دوباره باز کنی ('It'\''s')، یا از دوتایی استفاده کنی.
  • انتخاب: وقتی می‌خواهی متغیر یا دستور جایگزین شود، دوتایی؛ وقتی متن باید دقیقاً همان‌طور بماند (مثلاً یک الگوی grep یا یک دستور awk که $ دارد)، تکی.

مثال ۳: فاصله و *، شل مقدار را «تکه‌تکه» می‌کند

Section titled “مثال ۳: فاصله و *، شل مقدار را «تکه‌تکه» می‌کند”

وقتی متغیر را بدون کوتیشن می‌خوانی، شل بعد از جایگزینی دو کار دیگر هم می‌کند: متن را در فاصله‌ها می‌شکند (word splitting) و اگر * یا ? باشد با نام فایل‌ها عوضش می‌کند (globbing):

Terminal window
msg="a b"
echo $msg
echo "$msg"
echo "--- هر آرگومان را داخل [ ] چاپ می‌کنیم تا شمارش آرگومان‌ها دیده شود:"
printf '[%s]\n' $msg
printf '[%s]\n' "$msg"
echo "--- و با *:"
mkdir -p ~/bashlab/glob && cd ~/bashlab/glob && touch one.txt two.txt
pattern="*.txt"
echo $pattern
echo "$pattern"
خروجی
a b
a b
--- هر آرگومان را داخل [ ] چاپ می‌کنیم تا شمارش آرگومان‌ها دیده شود:
[a]
[b]
[a b]
--- و با *:
one.txt two.txt
*.txt

echo $msg فاصله‌های اضافی را حذف کرد (a b) چون به echo دو آرگومان a و b داد. printf '[%s]\n' ابزار خوبی برای دیدن این است: هر آرگومان را در یک خط و داخل [ ] چاپ می‌کند (دو آرگومان در برابر یک آرگومان). و *.txt بدون کوتیشن به نام فایل‌های واقعی باز شد. این «دوره‌ی بدون کوتیشن» تقریباً هیچ‌وقت چیزی نیست که بخواهی.

مثال ۴: کابوس واقعی، نام فایل با فاصله

Section titled “مثال ۴: کابوس واقعی، نام فایل با فاصله”

فایلی به اسم my report.txt (با فاصله) داری و اسمش را در یک متغیر گذاشته‌ای:

Terminal window
mkdir -p ~/bashlab/spaces && cd ~/bashlab/spaces
file="my report.txt"
echo "important data" > "$file"
echo "--- cat بدون کوتیشن:"
cat $file
echo "--- cat با کوتیشن:"
cat "$file"
خروجی
--- cat بدون کوتیشن:
cat: my: No such file or directory
cat: report.txt: No such file or directory
--- cat با کوتیشن:
important data

بدون کوتیشن، cat دو آرگومان my و report.txt گرفت و دنبال دو فایل ناموجود گشت. حالا بدتر: همین پوشه دو فایل دیگر هم دارد که اتفاقاً اسمشان my و report.txt است، و می‌خواهی my report.txt را پاک کنی:

Terminal window
cd ~/bashlab/spaces
touch my report.txt
echo "--- قبل از rm:"
ls
file="my report.txt"
rm $file
echo "--- بعد از rm \$file (بدون کوتیشن):"
ls
خروجی
--- قبل از rm:
my
my report.txt
report.txt
--- بعد از rm $file (بدون کوتیشن):
my report.txt

rm دو فایل دیگر را پاک کرد (my و report.txt، که شاید مهم بودند) و فایلی که می‌خواستی هنوز هست. این فقط یک نمونه‌ی کوچک است. قاعده‌ی طلایی: هر جا متغیر را می‌خوانی کوتیشن دوتایی بگذار: "$file". استثنای واقعی کم است (مثلاً وقتی عمداً می‌خواهی متن شکسته شود).

اسم متغیر کجا تمام می‌شود؟ شل تا جایی که حرف، عدد و _ ببیند ادامه می‌دهد:

Terminal window
name="report"
echo "[$name_final]"
echo "[${name}_final]"
echo "[${name}2026]"
echo "[$name-final]"
خروجی
[]
[report_final]
[report2026]
[report-final]

$name_final یعنی متغیری به اسم name_final (که وجود ندارد، پس خالی). _ جزو اسم است. با ${name} مرز را مشخص می‌کنی: ${name}_final. در $name-final مشکلی نیست چون - جزو اسم متغیر نیست. قاعده‌ی عملی: وقتی بلافاصله بعد از متغیر حرف، عدد یا _ می‌آید، آکولاد بگذار؛ بقیه‌ی وقت‌ها هر دو شکل یکی است. (آکولاد امکانات بیشتری هم دارد، مثل ${#name} و ${name:-default} که در درس «کار با رشته‌ها» می‌آید.)

مثال ۶: جایگزینی دستور، $(...)

Section titled “مثال ۶: جایگزینی دستور، $(...)”

می‌خواهی خروجی یک دستور را در متغیر بگذاری؟ دستور را داخل $(...) بنویس؛ شل آن را اجرا می‌کند و خروجی‌اش را جای خودش می‌گذارد (command substitution):

Terminal window
today=$(date +%F)
echo "Today is $today"
count=$(ls /etc | wc -l)
echo "/etc has $count entries"
echo "Current folder name: $(basename "$(pwd)")"
echo "Old style (backticks): `date +%F`"
echo "--- خروجی چندخطی و کوتیشن:"
lines=$(printf 'one\ntwo\nthree\n')
echo "$lines"
echo $lines
echo "--- خط‌های خالی انتهای خروجی حذف می‌شود:"
x=$(printf 'a\n\n\n')
echo "[$x]"
خروجی
Today is 2026-10-04
/etc has 139 entries
Current folder name: ali
Old style (backticks): 2026-10-04
--- خروجی چندخطی و کوتیشن:
one
two
three
one two three
--- خط‌های خالی انتهای خروجی حذف می‌شود:
[a]
  • $(date +%F) تاریخ امروز را به شکل سال-ماه-روز می‌دهد (جدول date پایین‌تر آمده).
  • می‌توان $(...) را تو در تو نوشت ($(basename "$(pwd)")) و هر کدام کوتیشن خودش را دارد.
  • شکل قدیمی با بک‌تیک (`date +%F`) هم کار می‌کند، ولی تو در تو شدنش بد است؛ در کد جدید $(...) بنویس.
  • اگر خروجی چند خط باشد و کوتیشن نگذاری (echo $lines)، خط‌ها به هم می‌چسبند (همان شکسته‌شدن مثال ۳).
  • خط‌های خالی آخر خروجی حذف می‌شوند.

Bash می‌تواند محاسبه‌ی عدد صحیح هم بکند. داخل $(( )) می‌توانی اسم متغیرها را بدون $ هم بنویسی:

Terminal window
a=17
b=5
echo "a + b = $((a + b))"
echo "a - b = $((a - b))"
echo "a * b = $((a * b))"
echo "a / b = $((a / b)) (integer division)"
echo "a % b = $((a % b)) (remainder)"
echo "2 ** 10 = $((2 ** 10))"
echo "max = $(( a > b ? a : b ))"
echo "a > b is $(( a > b )) (1 = true, 0 = false)"
echo "--- متغیرها متن‌اند، نه عدد:"
echo $a+$b
echo "--- افزایش و کاهش:"
count=0
((count++))
((count += 5))
echo "count = $count"
echo "--- اعشار؟ Bash بلد نیست؛ از awk استفاده کن:"
echo "17 / 5 = $(awk 'BEGIN { printf "%.2f", 17 / 5 }')"
خروجی
a + b = 22
a - b = 12
a * b = 85
a / b = 3 (integer division)
a % b = 2 (remainder)
2 ** 10 = 1024
max = 17
a > b is 1 (1 = true, 0 = false)
--- متغیرها متن‌اند، نه عدد:
17+5
--- افزایش و کاهش:
count = 6
--- اعشار؟ Bash بلد نیست؛ از awk استفاده کن:
17 / 5 = 3.40
  • عملگرها: + - * / % ** (توان). تقسیم همیشه صحیح است و عدد اعشاری را دور می‌اندازد: 17 / 5 برابر ۳.
  • echo $a+$b فقط متن 17+5 را چاپ کرد، چون + در متن عادی عملگر نیست. محاسبه فقط داخل $(( )) اتفاق می‌افتد.
  • ((count++)) و ((count += 5)) (بدون $) خودِ محاسبه را به‌عنوان یک دستور اجرا می‌کنند و متغیر را عوض می‌کنند؛ برای شمارنده‌ها در حلقه‌ها کاربردی است.
  • مقایسه‌ها (>، ==) داخل $(( )) نتیجه‌ی 1 (درست) یا 0 (غلط) می‌دهند، و شرط ? الف : ب مثل زبان‌های دیگر است.
  • اعشار نیاز به ابزار دیگری دارد؛ awk در همه‌ی سیستم‌ها هست (در درس sed و awk با آن آشنا شدی).

مثال ۸: ساختن اسم با تاریخ، سناریوی بکاپ

Section titled “مثال ۸: ساختن اسم با تاریخ، سناریوی بکاپ”

حالا مسئله‌ی ابتدای درس: اسم فایلی که تاریخ امروز را دارد. دستور date با + یک الگو می‌گیرد و هر %x را با بخشی از تاریخ عوض می‌کند:

Terminal window
for code in %F %T %Y %m %d %H %M %S %A %j %s; do
printf '%-3s -> %s\n' "$code" "$(date "+$code")"
done
echo "--- ساختن اسم:"
today=$(date +%F)
now=$(date +%F_%H-%M-%S)
echo "backup-$today.tar.gz"
echo "backup-$now.tar.gz"
echo "backup-$(date +%Y%m%d).tar.gz"
خروجی
%F -> 2026-10-04
%T -> 09:38:33
%Y -> 2026
%m -> 10
%d -> 04
%H -> 09
%M -> 38
%S -> 33
%A -> Sunday
%j -> 277
%s -> 1791106713
--- ساختن اسم:
backup-2026-10-04.tar.gz
backup-2026-10-04_09-38-33.tar.gz
backup-20261004.tar.gz

%F همان %Y-%m-%d است. اسمِ مرتب‌شونده‌ی سال-ماه-روز مهم است: وقتی ls اسم‌ها را الفبایی مرتب می‌کند، ترتیب زمانی هم درست می‌شود (برعکس 04-10-2026). اگر در یک روز چند بار بکاپ می‌گیری، ساعت را هم اضافه کن ($now). مشکل : در اسم فایل (%T) این است که بعضی ابزارها و ویندوز آن را نمی‌پذیرند؛ برای اسم فایل از - استفاده کن.

مثال ۹: متغیرهای آماده، readonly و unset

Section titled “مثال ۹: متغیرهای آماده، readonly و unset”

Bash چند متغیر آماده دارد که لازم نیست تعریفشان کنی:

Terminal window
echo "HOME=$HOME USER=$USER PWD=$PWD UID=$UID"
echo "BASH_VERSION=$BASH_VERSION"
echo "HOSTNAME=$HOSTNAME"
echo "RANDOM: $RANDOM $RANDOM (هر بار عدد دیگری)"
echo "SECONDS=$SECONDS (ثانیه از شروع این shell)"
echo "--- readonly: ثابت واقعی"
readonly PI=3.14
echo "PI=$PI"
PI=3
echo "--- unset: پاک‌کردن متغیر"
tmp="temporary"
echo "[$tmp]"
unset tmp
echo "[$tmp]"
خروجی
HOME=/home/ali USER=ali PWD=/home/ali UID=1001
BASH_VERSION=5.2.21(1)-release
HOSTNAME=pc
RANDOM: 8340 3700 (هر بار عدد دیگری)
SECONDS=0 (ثانیه از شروع این shell)
--- readonly: ثابت واقعی
PI=3.14
bash: PI: readonly variable
--- unset: پاک‌کردن متغیر
[temporary]
[]

readonly متغیری می‌سازد که عوض‌شدنش خطا می‌دهد؛ برای ثابت‌های اسکریپت (مثل مسیر بکاپ) خوب است. unset متغیر را کاملاً پاک می‌کند. و یادآوری از درس متغیرهای محیطی: متغیری که export نشده فقط در همین shell است و پروسه‌های فرزند (مثل bash script.sh) آن را نمی‌بینند.

پشت پرده: شل قبل از اجرا چه می‌کند؟

Section titled “پشت پرده: شل قبل از اجرا چه می‌کند؟”

وقتی خطی مثل cat $file را می‌زنی، شل اول خط را بازنویسی می‌کند و بعد دستور را اجرا می‌کند. مراحل بازنویسی به این ترتیب‌اند:

ترتیب بازنویسی خط فرمان توسط Bash (خلاصه). کوتیشن دوتایی جلوی دو مرحله‌ی شکستن و گسترش با * را می‌گیرد؛ کوتیشن تکی جلوی همه‌ی مرحله‌های جایگزینی را.

خودت ببین شل چه چیزی را واقعاً اجرا می‌کند. set -x (در درس دیباگ کامل می‌خوانی) هر دستور را بعد از بازنویسی چاپ می‌کند، با + در اول خط:

Terminal window
cd ~/bashlab/spaces
file="my report.txt"
set -x
cat $file
cat "$file"
set +x
خروجی
+ cat my report.txt
cat: my: No such file or directory
cat: report.txt: No such file or directory
+ cat 'my report.txt'
important data
+ set +x

خط اول (+ cat my report.txt): دستور با دو آرگومان رفت (دو خطای بعدی از cat است). خط دوم (+ cat 'my report.txt'): set -x آرگومانی را که فاصله دارد داخل '...' نشان می‌دهد تا معلوم شود یک آرگومان است. این ابزار قوی‌ترین راه فهمیدن «شل چه دید» است. (set +x آن را خاموش می‌کند.) چند نکته‌ی دیگر که از همین ترتیب می‌آید:

۱) $(...) در یک پروسه‌ی فرزند اجرا می‌شود. چیزی که داخلش به متغیرها می‌دهی بیرون نمی‌آید:

Terminal window
x=1
y=$(x=2; echo "inside: x=$x")
echo "$y"
echo "outside: x=$x"
خروجی
inside: x=2
outside: x=1

۲) محاسبه با عدد ۶۴ بیتی علامت‌دار است. از حدش بگذری، بی‌سروصدا دور می‌زند:

Terminal window
echo $((2 ** 62))
echo $((2 ** 63))
echo $((2 ** 64))
echo $((9223372036854775807 + 1))
خروجی
4611686018427387904
-9223372036854775808
0
-9223372036854775808

بزرگ‌ترین عدد قابل‌نمایش ۹٬۲۲۳٬۳۷۲٬۰۳۶٬۸۵۴٬۷۷۵٬۸۰۷ است؛ یکی بیشتر شود، منفی می‌شود. برای عددهای بزرگ یا اعشاری از ابزار دیگری (awk، bc) استفاده کن.

۳) چرا به‌صورت پیش‌فرض متن؟ چون شل برای اجرای برنامه‌هاست و برنامه‌ها آرگومانِ متنی می‌گیرند. عدد فقط وقتی عدد است که داخل $(( ))، (( )) یا declare -i باشد:

Terminal window
declare -i num=5
num=num+3
echo "declare -i: $num"
str=5
str=str+3
echo "بدون declare -i: $str"
expr 7 + 3
خروجی
declare -i: 8
بدون declare -i: str+3
10

با declare -i انتسابِ num=num+3 محاسبه می‌شود، بدون آن فقط متن است. expr (آخرین خط) روش قدیمی محاسبه است که در اسکریپت‌های قدیمی می‌بینی؛ برای کد جدید از $(( )) استفاده کن.

عملگر معنی مثال نتیجه
+ - * جمع، تفریق، ضرب $((17 * 5)) 85
/ تقسیم صحیح $((17 / 5)) 3
% باقیمانده $((17 % 5)) 2
** توان $((2 ** 10)) 1024
++ -- += -= افزایش، کاهش، انتساب (در (( ))) ((count += 5)) count پنج بیشتر
< > == != مقایسه $((a > b)) 1 یا 0
? : شرط $((a > b ? a : b)) بزرگ‌تر

کدهای رایج date (ساختن اسم و برچسب):

کد یعنی مثال
%F تاریخ استاندارد (%Y-%m-%d) 2026-10-04
%T ساعت (%H:%M:%S) 09:37:54
%Y %m %d سال، ماه، روز 2026 10 04
%H %M %S ساعت، دقیقه، ثانیه 09 37 54
%A اسم روز هفته Sunday
%j روز از سال 277
%s ثانیه از ۱۹۷۰ (برای محاسبه‌ی اختلاف زمان) 1791106674

قواعد کوتیشن به‌صورت خلاصه: داخل دوتایی جایگزینی ($) هست؛ داخل تکی نیست؛ بدون کوتیشن جایگزینی هست و شکستن و گسترش * هم هست.

۱) فاصله دور =، یا $ در انتساب

Section titled “۱) فاصله دور =، یا $ در انتساب”

مثال ۱: name = "Ali" ← bash: name: command not found. و $ فقط برای خواندن است؛ در انتساب نمی‌گذاری:

Terminal window
unset name
$name="x"
echo "کد خروج: $?"
خروجی
bash: =x: command not found
کد خروج: 127

شل اول $name را باز می‌کند و چون name خالی است، آنچه می‌ماند ="x" است و می‌خواهد آن را مثل یک دستور اجرا کند. راه‌حل: name="x".

۲) فراموش‌کردن کوتیشن دور متغیر

Section titled “۲) فراموش‌کردن کوتیشن دور متغیر”

مثال ۴: cat $file و rm $file با اسم فایل دارای فاصله. راه‌حل: همیشه "$file". ابزار ShellCheck (در درس دیباگ) این را خودکار پیدا می‌کند.

۳) کوتیشن تکی وقتی جایگزینی می‌خواستی

Section titled “۳) کوتیشن تکی وقتی جایگزینی می‌خواستی”
Terminal window
today=$(date +%F)
echo 'Backup of $today'
echo "Backup of $today"
echo 'Today: $(date +%F)'
خروجی
Backup of $today
Backup of 2026-10-04
Today: $(date +%F)

در کوتیشن تکی متن همان‌طور می‌ماند و هیچ جایگزینی نمی‌شود. راه‌حل: دوتایی.

۴) نام متغیر بلند و آکولاد فراموش‌شده

Section titled “۴) نام متغیر بلند و آکولاد فراموش‌شده”

مثال ۵: "$name_final" یعنی متغیر name_final، نه $name به‌اضافه‌ی _final. راه‌حل: "${name}_final".

عددی که با 0 شروع شود در محاسبه هشتایی حساب می‌شود. وقتی ماه یا روز را از date یا از اسم فایل می‌گیری، 08 و 09 دچار مشکل می‌شوند:

Terminal window
month=08
echo $((month + 1))
echo $((10#$month + 1))
خروجی
bash: 08: value too great for base (error token is "08")
9

خطای اول می‌گوید 08 برای پایه‌ی هشت بیش از حد بزرگ است. 10# به bash می‌گوید عدد را در مبنای ۱۰ بخوان. (توجه: Bash بعد از چنین خطایی بقیه‌ی همان خط را اجرا نمی‌کند؛ برای همین هر echo در یک خط جداست.) یک دام مشابه: متن غیرعددی در محاسبه بی‌صدا صفر می‌شود:

Terminal window
a=abc
echo $((a + 1))
خروجی
1

نتیجه 1 شد، بدون هیچ خطایی؛ چون abc به‌عنوان اسم یک متغیر خالی خوانده شد. راه‌حل: ورودی را قبل از محاسبه بررسی کن (درس شرط‌ها).

۶) متغیر خالی در rm -rf $dir/*

Section titled “۶) متغیر خالی در rm -rf $dir/*”

یکی از مشهورترین فاجعه‌های اسکریپت‌نویسی. وقتی dir به هر دلیل خالی باشد، $dir/* می‌شود /*، یعنی همه‌چیز. برای اینکه خودمان آسیب نبینیم، به‌جای rm فقط echo می‌کنیم:

Terminal window
dir=""
echo "--- بدون کوتیشن:"
echo rm -rf $dir/* | cut -c1-72
echo "--- با کوتیشن (هنوز خطرناک است!):"
echo rm -rf "$dir"/* | cut -c1-72
echo "--- با محافظ :? (داخل یک اسکریپت؛ متوقف می‌شود):"
cat > ~/bashlab/guard.sh <<'EOF'
dir=""
echo rm -rf "${dir:?dir is not set}"/*
echo "never reached"
EOF
bash ~/bashlab/guard.sh
echo "کد خروج: $?"
خروجی
--- بدون کوتیشن:
rm -rf /bin /bin.usr-is-merged /boot /dev /etc /home /lib /lib.usr-is-me
--- با کوتیشن (هنوز خطرناک است!):
rm -rf /bin /bin.usr-is-merged /boot /dev /etc /home /lib /lib.usr-is-me
--- با محافظ :? (داخل یک اسکریپت؛ متوقف می‌شود):
/home/ali/bashlab/guard.sh: line 2: dir: dir is not set
کد خروج: 1

دقت کن که کوتیشن اینجا نجات نمی‌دهد. ${dir:?پیام} (در درس کار با رشته‌ها می‌آید) اگر dir خالی باشد، پیام می‌دهد و اسکریپت را متوقف می‌کند. راه کلی‌تر set -u (متغیر تعریف‌نشده خطا باشد) و یک بررسی صریح ([[ -n "$dir" ]]) قبل از هر دستور مخرب است.

✎ تمرینآسان

اسکریپت greet.sh را بنویس: دو متغیر name و course (مقدار LoopX) تعریف کن و جمله‌ی Hello, Ali! Welcome to LoopX. را با کوتیشن دوتایی چاپ کن. بعد همان جمله را با کوتیشن تکی چاپ کن تا ببینی چه می‌شود.

دیدن جواب
mkdir -p ~/bashlab && cd ~/bashlab
cat > greet.sh <<'EOF'
#!/usr/bin/env bash
# greet.sh: variables and the two kinds of quotes
name="Ali"
course="LoopX"
echo "Hello, $name! Welcome to $course."
echo 'Hello, $name! Welcome to $course.'
EOF
chmod +x greet.sh
./greet.sh
خروجی
Hello, Ali! Welcome to LoopX.
Hello, $name! Welcome to $course.
✎ تمرینمتوسط

تمرین اصلی درس: اسکریپت backup.sh که اسم فایل بکاپ را با تاریخ امروز بسازد. پوشه‌ی ~/bashlab/data را (با چند فایل نمونه) با tar در فایل data-backup-تاریخ.tar.gz فشرده کند، اسم و اندازه‌ی فایل را چاپ کند و محتوای آرشیو را نشان بدهد. مسیرها و اسم‌ها را در متغیر بگذار.

دیدن جواب
Terminal window
mkdir -p ~/bashlab/data && cd ~/bashlab
echo "alpha" > data/a.txt
echo "beta" > data/b.txt
cat > backup.sh <<'EOF'
#!/usr/bin/env bash
# backup.sh: archive a folder into a file named with today's date
source_dir="$HOME/bashlab/data"
backup_dir="$HOME/bashlab"
today=$(date +%F)
backup_file="$backup_dir/data-backup-$today.tar.gz"
echo "Backing up: $source_dir"
tar -czf "$backup_file" -C "$(dirname "$source_dir")" "$(basename "$source_dir")"
echo "Created: $backup_file"
ls -lh "$backup_file"
echo "Contents:"
tar -tzf "$backup_file"
EOF
chmod +x backup.sh
./backup.sh
خروجی
Backing up: /home/ali/bashlab/data
Created: /home/ali/bashlab/data-backup-2026-10-04.tar.gz
-rw-r--r-- 1 ali ali 170 Oct 4 09:38 /home/ali/bashlab/data-backup-2026-10-04.tar.gz
Contents:
data/
data/b.txt
data/a.txt

دقت کن همه‌ی متغیرها توی کوتیشن‌اند و اسم فایل از $today ساخته شده است. اگر فردا دوباره اجرا کنی، فایل جدیدی با اسم دیگر می‌ماند.

✎ تمرینمتوسط

داخل یک پوشه سه فایل notes.txt، my notes.txt و old notes.txt بساز. با یک متغیر و بدون کوتیشن my notes.txt را به my notes.bak کپی کن و خطا را ببین. بعد درستش کن.

دیدن جواب
Terminal window
mkdir -p ~/bashlab/fix && cd ~/bashlab/fix
echo "n" > notes.txt; echo "m" > "my notes.txt"; echo "o" > "old notes.txt"
src="my notes.txt"
echo "--- بدون کوتیشن:"
cp $src "my notes.bak"
echo "کد خروج: $?"
echo "--- با کوتیشن:"
cp "$src" "my notes.bak"
echo "کد خروج: $?"
cat "my notes.bak"
خروجی
--- بدون کوتیشن:
cp: target 'my notes.bak': No such file or directory
کد خروج: 1
--- با کوتیشن:
کد خروج: 0
m

cp $src "my notes.bak" برای شل می‌شود cp my notes.txt "my notes.bak": سه آرگومان. وقتی cp بیش از دو آرگومان بگیرد، آخری را «پوشه‌ی مقصد» حساب می‌کند و چون چنین پوشه‌ای نیست خطا می‌دهد؛ هیچ‌چیز کپی نشد.

✎ تمرینسخت

اسکریپتی بنویس که بگوید از شروع امروز چند ثانیه گذشته و آن را به شکل HH:MM چاپ کند (از date +%s، محاسبه‌ی $(( )) و جایگزینی دستور تو در تو استفاده کن). ثابت کن جوابت با date +%H:%M برابر است. (این تمرین date -d دارد که فقط در لینوکس هست؛ روی مک کار نمی‌کند.)

دیدن جواب
cat > ~/bashlab/since-midnight.sh <<'EOF'
#!/usr/bin/env bash
# since-midnight.sh: seconds since 00:00 today, shown as HH:MM
now=$(date +%s)
midnight=$(date -d 00:00 +%s)
seconds=$(( now - midnight ))
hours=$(( seconds / 3600 ))
minutes=$(( (seconds % 3600) / 60 ))
printf 'Seconds since midnight: %d\n' "$seconds"
printf 'As HH:MM: %02d:%02d\n' "$hours" "$minutes"
printf 'date says: %s\n' "$(date +%H:%M)"
EOF
bash ~/bashlab/since-midnight.sh
خروجی
Seconds since midnight: 34713
As HH:MM: 09:38
date says: 09:38

$(date -d 00:00 +%s) ثانیه‌ی نیمه‌شب امروز را می‌دهد و تفاضلش با حالا همان جواب است. printf با %02d عدد را با صفر اول (دو رقمی) چاپ می‌کند.

⚡ بررسی سریع

کدام یک خروجی دستور date +%F را در متغیر today می‌گذارد؟

؟ آزمونک
  1. کدام انتساب در Bash درست است؟

  2. name="Ali" و بعد echo 'Hello, $name' چه چاپ می‌کند؟

  3. file="my report.txt" و دستور cat $file چرا خطا می‌دهد؟

  4. name="report" و echo "$name_final" چه چاپ می‌کند و چرا؟

  5. echo $((7 / 2)) چه چاپ می‌کند؟

  6. month=08 و echo $((month + 1)) چه می‌شود؟

  • متغیر: name="value" (همه چسبیده، بدون فاصله دور =) و خواندن با "$name". همه‌چیز متن است؛ متغیر تعریف‌نشده خالی است (نه خطا).
  • کوتیشن دوتایی جایگزینی می‌کند ولی مقدار را یک تکه نگه می‌دارد؛ تکی هیچ جایگزینی نمی‌کند؛ بدون کوتیشن مقدار را در فاصله می‌شکند و * را به نام فایل‌ها باز می‌کند. قاعده‌ی عملی: همیشه "$var".
  • ${name} وقتی لازم است که بلافاصله بعد از متغیر حرف، عدد یا _ بیاید ("${name}_backup").
  • $(command) خروجی دستور را جایگزین می‌کند (در یک پروسه‌ی فرزند؛ خط‌های خالی انتها حذف می‌شود). $(date +%F) تاریخ امروز برای اسم فایل است.
  • $((expr)) محاسبه‌ی عدد صحیح است (+ - * / % **)؛ تقسیم صحیح؛ صفر اول یعنی هشتایی (10#$n راه‌حل است)؛ اعشار را به awk بسپار.
  • شل خط را قبل از اجرا بازنویسی می‌کند (آکولاد، ~، متغیر و دستور، شکستن، *، حذف کوتیشن)؛ با set -x می‌بینی چه اجرا شد. کوتیشن دور متغیر خالی در rm -rf "$dir"/* نجاتت نمی‌دهد؛ ${dir:?}، set -u و بررسی صریح لازم است.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
name="value"تعریف متغیر (بدون فاصله دور =)
echo "$name"خواندن متغیر، همیشه با کوتیشن دوتایی
echo '$name'متن دقیق، بدون جایگزینی
"${name}_backup"آکولاد برای مشخص‌کردن مرز اسم
today=$(date +%F)خروجی دستور در متغیر
$((a + b)) $((a * b))محاسبه‌ی عدد صحیح
((count++)) ((count += 5))تغییر شمارنده
$((10#$month + 1))محاسبه با عدد دارای صفر اول
date +%F_%H-%M-%Sتاریخ و ساعت برای اسم فایل
readonly PI=3.14 unset nameثابت واقعی / پاک‌کردن متغیر
printf '[%s]\n' $varدیدن اینکه شل متغیر را چند تکه کرد
${dir:?پیام}اگر خالی بود، پیام بده و متوقف شو