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

آرایه‌ها

توی این درس یاد می‌گیری چند مقدار را در یک متغیر نگه داری. آرایه‌ی ساده (arr=(a b c)) را می‌سازی و با ${arr[0]}، همه را با "${arr[@]}" و تعدادشان را با ${#arr[@]} می‌خوانی؛ عضو اضافه و حذف و برش می‌دهی؛ می‌فهمی چرا "${arr[@]}" باید کوتیشن داشته باشد و $arr فقط عضو اول است. سپس آرایه‌ی انجمنی (declare -A map) را می‌بینی، یعنی جدول «کلید ← مقدار» برای شمارش و جست‌وجو. تمرین: لیست سرورها را در آرایه بگذار و به هر کدام ping بزن.

مسئله: متغیرِ «یک چیز» کافی نیست

Section titled “مسئله: متغیرِ «یک چیز» کافی نیست”

تا اینجا هر متغیر یک مقدار داشت. ولی کار واقعی پر از فهرست است: سرورهایی که باید چک شوند، فایل‌هایی که باید بایگانی شوند، آرگومان‌هایی که باید به یک دستور برسند. راه‌حل بی‌نظم: server1="..."، server2="..."، server3="..." و کدی که هر سه را جدا می‌نویسد. یا یک رشته‌ی «a b c» که در فاصله‌ها می‌شکند و با اولین اسم دارای فاصله خراب می‌شود (درس متغیرها). آرایه راه درست است: یک متغیر، چند عضو، هر عضو با اندیسش.

تشبیه: قفسه‌ی شماره‌دار و دفترچه‌ی تلفن

Section titled “تشبیه: قفسه‌ی شماره‌دار و دفترچه‌ی تلفن”

آرایه‌ی ساده مثل یک قفسه‌ی شماره‌دار است: خانه‌ی ۰، خانه‌ی ۱، خانه‌ی ۲ و… در هر خانه یک چیز. آرایه‌ی انجمنی مثل دفترچه‌ی تلفن است: به‌جای شماره‌ی خانه، با اسم (کلید) پیدایش می‌کنی («سارا» ← ۰۹۱۲…). اولی برای فهرست‌های مرتب، دومی وقتی می‌خواهی چیزی را با اسمش پیدا کنی.

آرایه‌ی ساده (indexed) آرایه‌ی انجمنی (associative)
تعریف arr=(a b c) declare -A map (حتماً)
کلید عدد (از ۰) هر متن
یک عضو ${arr[1]} ${map[key]}
همه‌ی مقدارها "${arr[@]}" "${map[@]}"
همه‌ی کلیدها "${!arr[@]}" (اندیس‌ها) "${!map[@]}"
تعداد ${#arr[@]} ${#map[@]}
ترتیب ترتیب اندیس بدون ترتیب مشخص
نسخه‌ی Bash همه ۴ به بعد
آرایه‌ی ساده با شماره‌ی خانه (اندیس) پیدا می‌شود و آرایه‌ی انجمنی با کلید متنی. دومی ترتیب ثابتی ندارد.
Terminal window
fruits=(apple banana "cherry pie")
echo "first: ${fruits[0]}"
echo "third: ${fruits[2]}"
echo "last: ${fruits[-1]}"
echo "count: ${#fruits[@]}"
echo "length of the third element: ${#fruits[2]}"
echo "all: ${fruits[@]}"
echo "--- declare -p ساختار واقعی را نشان می‌دهد:"
declare -p fruits
echo "--- \$fruits بدون اندیس فقط عضو اول است:"
echo $fruits
echo ${#fruits}
خروجی
first: apple
third: cherry pie
last: cherry pie
count: 3
length of the third element: 10
all: apple banana cherry pie
--- declare -p ساختار واقعی را نشان می‌دهد:
declare -a fruits=([0]="apple" [1]="banana" [2]="cherry pie")
--- $fruits بدون اندیس فقط عضو اول است:
apple
5
  • اعضا را با فاصله داخل پرانتز می‌نویسی؛ عضو دارای فاصله باید در کوتیشن باشد ("cherry pie").
  • اندیس از ۰ شروع می‌شود؛ ${fruits[-1]} آخرین عضو است (Bash ۴.۳ به بعد).
  • ${fruits[@]} همه‌ی عضوها؛ ${#fruits[@]} تعداد عضوها؛ ${#fruits[2]} طول متن عضو سوم (درس «کار با رشته‌ها»).
  • declare -p بهترین ابزار برای دیدن آرایه است: اندیس‌ها و مقدارها را دقیقاً نشان می‌دهد.
  • $fruits بدون اندیس یعنی ${fruits[0]}: فقط عضو اول. و ${#fruits} طول همان عضو اول (۵ = apple) است، نه تعداد عضوها. این دو اشتباه رایج‌اند.

مثال ۲: افزودن، تغییر، حذف و برش

Section titled “مثال ۲: افزودن، تغییر، حذف و برش”
Terminal window
fruits=(apple banana "cherry pie")
fruits+=(date elderberry)
echo "after +=: ${fruits[@]} (count ${#fruits[@]})"
fruits[1]=blueberry
echo "after fruits[1]=..: ${fruits[@]}"
echo "slice (from 1, two items): ${fruits[@]:1:2}"
unset 'fruits[1]'
echo "--- بعد از unset 'fruits[1]' (یک «سوراخ» می‌ماند):"
declare -p fruits
echo "indices: ${!fruits[@]} count: ${#fruits[@]}"
fruits=("${fruits[@]}")
echo "--- بعد از فشرده‌کردن مجدد:"
declare -p fruits
خروجی
after +=: apple banana cherry pie date elderberry (count 5)
after fruits[1]=..: apple blueberry cherry pie date elderberry
slice (from 1, two items): blueberry cherry pie
--- بعد از unset 'fruits[1]' (یک «سوراخ» می‌ماند):
declare -a fruits=([0]="apple" [2]="cherry pie" [3]="date" [4]="elderberry")
indices: 0 2 3 4 count: 4
--- بعد از فشرده‌کردن مجدد:
declare -a fruits=([0]="apple" [1]="cherry pie" [2]="date" [3]="elderberry")
  • +=(...) عضو به انتها اضافه می‌کند.
  • ${fruits[@]:1:2} یک برش است: از اندیس ۱، دو عضو.
  • unset 'arr[1]' عضو را حذف می‌کند ولی اندیس‌ها جابه‌جا نمی‌شوند: آرایه‌ی «سوراخ‌دار» می‌شود (اندیس‌ها 0 2 3 4). ${#fruits[@]} تعداد عضوهای موجود را می‌دهد (۴) و ${!fruits[@]} اندیس‌های موجود را. اگر اندیس‌های پیوسته می‌خواهی، arr=("${arr[@]}") دوباره بسته‌بندی‌شان می‌کند.
  • دور unset کوتیشن تکی گذاشتیم تا شل [1] را مثل الگوی نام فایل تفسیر نکند (در «اشتباهات رایج» می‌بینی چرا مهم است).

مثال ۳: کوتیشن، مهم‌ترین قاعده‌ی آرایه‌ها

Section titled “مثال ۳: کوتیشن، مهم‌ترین قاعده‌ی آرایه‌ها”

مثل "$@" در درس آرگومان‌ها: "${arr[@]}" هر عضو را یک آرگومان جدا می‌دهد، حتی اگر فاصله داشته باشد:

Terminal window
files=("my report.txt" "notes.txt" "old data.csv")
show() {
local i=0
for a in "$@"; do
echo " arg $((++i)): [$a]"
done
}
echo '--- show "${files[@]}" (درست):'
show "${files[@]}"
echo '--- show ${files[@]} (بدون کوتیشن: در فاصله‌ها می‌شکند):'
show ${files[@]}
echo '--- show "${files[*]}" (همه در یک رشته):'
show "${files[*]}"
echo '--- show $files (فقط عضو اول):'
show $files
echo "--- چسباندن عضوها با یک جداکننده (IFS داخل پرانتز، تا IFS اصلی دست نخورد):"
( IFS=,; echo "${files[*]}" )
خروجی
--- show "${files[@]}" (درست):
arg 1: [my report.txt]
arg 2: [notes.txt]
arg 3: [old data.csv]
--- show ${files[@]} (بدون کوتیشن: در فاصله‌ها می‌شکند):
arg 1: [my]
arg 2: [report.txt]
arg 3: [notes.txt]
arg 4: [old]
arg 5: [data.csv]
--- show "${files[*]}" (همه در یک رشته):
arg 1: [my report.txt notes.txt old data.csv]
--- show $files (فقط عضو اول):
arg 1: [my]
arg 2: [report.txt]
--- چسباندن عضوها با یک جداکننده (IFS داخل پرانتز، تا IFS اصلی دست نخورد):
my report.txt,notes.txt,old data.csv

سه شکل و سه نتیجه: "${files[@]}" سه آرگومان سالم؛ بدون کوتیشن پنج تکه (دو تا از اسم‌ها شکسته شدند)؛ "${files[*]}" همه یک رشته‌ی واحد. برای چسباندن اعضا با ویرگول (یا هر جداکننده) از "${arr[*]}" و IFS استفاده می‌شود: اولین کاراکتر IFS بین اعضا می‌نشیند. قاعده: همیشه "${arr[@]}".

مثال ۴: پیمایش و ساختن آرایه از فایل و دستور

Section titled “مثال ۴: پیمایش و ساختن آرایه از فایل و دستور”
Terminal window
mkdir -p ~/bashlab/ar && cd ~/bashlab/ar
touch "my report.txt" notes.txt data.csv
files=(*.txt)
echo "txt files: ${#files[@]}"
echo "--- پیمایش مقدارها:"
for f in "${files[@]}"; do
echo "file: [$f]"
done
echo "--- پیمایش با اندیس:"
for i in "${!files[@]}"; do
echo "$i: ${files[$i]}"
done
echo "--- C-style:"
for ((i = 0; i < ${#files[@]}; i++)); do
echo -n "${files[i]} | "
done
echo
خروجی
txt files: 2
--- پیمایش مقدارها:
file: [my report.txt]
file: [notes.txt]
--- پیمایش با اندیس:
0: my report.txt
1: notes.txt
--- C-style:
my report.txt | notes.txt |

files=(*.txt) الگو را خود شل باز می‌کند و نام‌های دارای فاصله سالم می‌مانند. این روش درست ساختن آرایه‌ی فایل‌هاست (برخلاف files=($(ls))). حالا از خروجی یک دستور و از متن:

Terminal window
cd ~/bashlab/ar
printf 'one\ntwo words\nthree\n' > lines.txt
echo "--- mapfile: هر خط یک عضو (-t یعنی newline آخر را بردار):"
mapfile -t lines < lines.txt
declare -p lines
echo "--- روش اشتباه: arr=(\$(cat file)) در فاصله‌ها می‌شکند:"
words=($(cat lines.txt))
declare -p words
echo "--- شکستن یک رشته با read -ra:"
IFS=, read -ra parts <<< "10.0.0.1,10.0.0.2,10.0.0.3"
declare -p parts
echo "--- مرتب‌کردن (sort) و برگرداندن به آرایه:"
mapfile -t sorted < <(printf '%s\n' pear apple fig | sort)
declare -p sorted
خروجی
--- mapfile: هر خط یک عضو (-t یعنی newline آخر را بردار):
declare -a lines=([0]="one" [1]="two words" [2]="three")
--- روش اشتباه: arr=($(cat file)) در فاصله‌ها می‌شکند:
declare -a words=([0]="one" [1]="two" [2]="words" [3]="three")
--- شکستن یک رشته با read -ra:
declare -a parts=([0]="10.0.0.1" [1]="10.0.0.2" [2]="10.0.0.3")
--- مرتب‌کردن (sort) و برگرداندن به آرایه:
declare -a sorted=([0]="apple" [1]="fig" [2]="pear")
  • mapfile -t arr < فایل (هم‌معنی: readarray) هر خط فایل را یک عضو می‌کند. برای خروجی یک دستور: mapfile -t arr < <(دستور) (process substitution، درس حلقه‌ها).
  • arr=($(دستور)) خروجی را در فاصله‌ها می‌شکند و (با الگوهای *) باز هم می‌کند؛ two words شد دو عضو. فقط وقتی که مطمئنی هر عضو یک کلمه‌ی ساده است.
  • read -ra arr <<< "a,b,c" با IFS=, رشته را به اعضا می‌شکند (-a یعنی در آرایه بریز).
  • مرتب‌سازی: sort روی printf '%s\n' "${arr[@]}" و بعد mapfile.

وقتی می‌خواهی با اسم به چیزی برسی، به‌جای شماره، declare -A (حتماً با -A) می‌نویسی:

Terminal window
declare -A ports
ports[http]=80
ports[https]=443
ports[ssh]=22
ports[postgres]=5432
echo "ssh -> ${ports[ssh]}"
echo "count: ${#ports[@]}"
echo "--- کلیدها و مقدارها (ترتیب دلخواهِ Bash، نه ترتیب افزودن):"
for k in "${!ports[@]}"; do
echo "$k=${ports[$k]}"
done
echo "--- مرتب‌شده بر اساس کلید:"
for k in $(printf '%s\n' "${!ports[@]}" | sort); do
echo "$k=${ports[$k]}"
done
echo "--- وجود کلید (-v) و حذف:"
[[ -v ports[ssh] ]] && echo "ssh is defined"
[[ -v ports[ftp] ]] || echo "ftp is not defined"
unset 'ports[ssh]'
echo "after unset: ${!ports[@]}"
echo "--- ساخت با مقدار اولیه:"
declare -A colors=([red]="#ff0000" [green]="#00ff00")
declare -p colors
خروجی
ssh -> 22
count: 4
--- کلیدها و مقدارها (ترتیب دلخواهِ Bash، نه ترتیب افزودن):
postgres=5432
ssh=22
https=443
http=80
--- مرتب‌شده بر اساس کلید:
http=80
https=443
postgres=5432
ssh=22
--- وجود کلید (-v) و حذف:
ssh is defined
ftp is not defined
after unset: postgres https http
--- ساخت با مقدار اولیه:
declare -A colors=([red]="#ff0000" [green]="#00ff00" )

"${!ports[@]}" با علامت ! همه‌ی کلیدها را می‌دهد. ترتیب کلیدها همان ترتیب افزودن نیست؛ اگر ترتیب مشخص می‌خواهی کلیدها را sort کن. با [[ -v map[key] ]] وجود کلید را می‌سنجی (برخلاف [[ -n ${map[key]} ]] که کلیدی با مقدار خالی را «وجود ندارد» می‌بیند).

کاربرد اصلی: شمارش. هر بار که چیزی دیده شد، شمارنده‌ی کلیدش را زیاد کن. این‌جا درخواست‌های یک لاگ دسترسی را بر اساس IP و کد وضعیت می‌شماریم:

Terminal window
cat > ~/bashlab/access.log <<'EOF'
10.0.0.1 200
10.0.0.2 404
10.0.0.1 200
10.0.0.3 500
10.0.0.2 200
10.0.0.1 404
EOF
declare -A hits
declare -A statuses
while read -r ip status; do
((hits[$ip]++))
((statuses[$status]++))
done < ~/bashlab/access.log
echo "--- requests per IP:"
for ip in $(printf '%s\n' "${!hits[@]}" | sort); do
echo "$ip ${hits[$ip]}"
done
echo "--- requests per status code:"
for s in $(printf '%s\n' "${!statuses[@]}" | sort); do
echo "$s ${statuses[$s]}"
done
خروجی
--- requests per IP:
10.0.0.1 3
10.0.0.2 2
10.0.0.3 1
--- requests per status code:
200 3
404 2
500 1

((hits[$ip]++)) اگر کلید هنوز نبود، آن را از صفر شروع می‌کند و یکی زیاد می‌کند؛ همین یک خط کل کار شمارش است. (همین کار با sort | uniq -c هم می‌شد؛ ولی وقتی چند چیز را هم‌زمان می‌شماری یا نتیجه را در ادامه‌ی اسکریپت به کار می‌بری، آرایه‌ی انجمنی بهتر است.)

Terminal window
/bin/bash -c 'declare -A m' 2>&1
/bin/bash -c 'a=(x y); echo "${a[-1]}"' 2>&1
خروجی
/bin/bash: line 0: declare: -A: invalid option
declare: usage: declare [-afFirtx] [-p] [name[=value] ...]
/bin/bash: a: bad array subscript

آرایه‌ی انجمنی در Bash ۴ به بعد هست. خروجی بالا از یک مک گرفته شده: bash ۳.۲ آن را نمی‌شناسد (declare: -A: invalid option) و اندیس منفی (${a[-1]}) هم ندارد. روی لینوکس مشکلی نیست؛ روی مک brew install bash بزن.

آرایه را به تابع مثل آرگومان‌های معمولی می‌دهی ("${arr[@]}") و تابع آن‌ها را در "$@" می‌بیند. برگرداندن آرایه از تابع سخت‌تر است؛ دو راه:

Terminal window
show() {
echo "got $# arguments:"
for a in "$@"; do
echo " [$a]"
done
}
names=("Ali Ahmadi" "Sara" "Reza Karimi")
show "${names[@]}"
echo "--- راه ۱: تابع چند خط چاپ کند، بیرون با mapfile بگیری:"
list_dirs() {
printf '%s\n' /etc /usr /var
}
mapfile -t dirs < <(list_dirs)
declare -p dirs
echo "--- راه ۲: نام آرایه‌ی بیرونی را بدهی (nameref، Bash 4.3+):"
fill() {
local -n out=$1
out=(x y z)
}
fill result
declare -p result
خروجی
got 3 arguments:
[Ali Ahmadi]
[Sara]
[Reza Karimi]
--- راه ۱: تابع چند خط چاپ کند، بیرون با mapfile بگیری:
declare -a dirs=([0]="/etc" [1]="/usr" [2]="/var")
--- راه ۲: نام آرایه‌ی بیرونی را بدهی (nameref، Bash 4.3+):
declare -a result=([0]="x" [1]="y" [2]="z")

در راه اول داده با printf '%s\n' (هر عضو در یک خط) از تابع بیرون می‌آید و mapfile آن را به آرایه تبدیل می‌کند (همان مشکل «عضو با خط جدید» را دارد، ولی برای اکثر داده‌ها خوب است). در راه دوم local -n out=$1 یک اشاره‌گر به متغیری می‌سازد که اسمش را آرگومان داده‌اند؛ هر چه به out بدهی، به result می‌رود. چون fill مستقیم (بدون $(...)) صدا زده شد، تغییر باقی ماند.

مثال ۷: حذف تکراری‌ها با حفظ ترتیب

Section titled “مثال ۷: حذف تکراری‌ها با حفظ ترتیب”

آرایه‌ی انجمنی به‌عنوان «مجموعه‌ی دیده‌شده‌ها»: هر چیزی که قبلاً دیده‌ای را رد کن:

Terminal window
items=(b a c a b d a)
declare -A seen
unique=()
for x in "${items[@]}"; do
if [[ ! -v seen[$x] ]]; then
seen[$x]=1
unique+=("$x")
fi
done
echo "input: ${items[@]}"
echo "unique (in order): ${unique[@]}"
mapfile -t sorted_unique < <(printf '%s\n' "${items[@]}" | sort -u)
echo "sort -u (sorted): ${sorted_unique[@]}"
خروجی
input: b a c a b d a
unique (in order): b a c d
sort -u (sorted): a b c d

حلقه‌ی اول ترتیب اولین دیده‌شدن را نگه می‌دارد (b a c d)، در حالی که sort -u مرتب‌شده می‌دهد (a b c d). هر کدام را بسته به نیازت انتخاب کن.

آرایه‌ی ساده در Bash در واقع یک نقشه‌ی «اندیس ← مقدار» است، نه آرایه‌ی پشت‌سرهم در حافظه. برای همین «سوراخ‌دار» می‌شود و اندیس‌ها لازم نیست پیوسته باشند. declare -p همین ساختار را نشان می‌دهد ([0]="apple" [2]="cherry pie"). چند نتیجه‌ی عملی:

۱) آرایه به پروسه‌های فرزند نمی‌رسد. محیط (environment) فقط متن حمل می‌کند و export برای آرایه کار نمی‌کند:

Terminal window
rainbow=(red green blue)
export rainbow
bash -c 'echo "child sees: [${rainbow[0]}]"'
خروجی
child sees: []

فرزند چیزی نمی‌بیند. اگر باید آرایه را به یک اسکریپت دیگر بدهی، به‌صورت آرگومان ("${rainbow[@]}") یا خط‌به‌خط در فایل بده.

۲) آرایه در sh (dash) نیست:

Terminal window
sh -c 'a=(1 2 3); echo "${a[0]}"' 2>&1
خروجی
sh: 1: Syntax error: "(" unexpected

آرایه مخصوص bash است. اگر اسکریپت باید با sh کار کند، همان‌طور که در درس اولین اسکریپت دیدی، باید از رشته و "$@" استفاده کنی.

۳) ویرگول جداکننده نیست. اعضا را فاصله جدا می‌کند:

Terminal window
wrong=(1,2,3)
echo "count: ${#wrong[@]}"
right=(1 2 3)
echo "count: ${#right[@]}"
خروجی
count: 1
count: 3

آرایه‌ی ساده:

عبارت معنی
arr=(a b "c d") ساخت (عضو دارای فاصله در کوتیشن)
arr[2]=x تغییر یا ساخت یک عضو
arr+=(x y) افزودن به انتها
${arr[1]} / ${arr[-1]} یک عضو / آخرین عضو
"${arr[@]}" همه‌ی عضوها، هر کدام جدا
"${arr[*]}" همه‌ی عضوها در یک رشته (جداکننده‌ی IFS)
${#arr[@]} تعداد عضوها
"${!arr[@]}" اندیس‌های موجود
"${arr[@]:1:2}" برش: از اندیس ۱، دو عضو
unset 'arr[1]' حذف یک عضو (سوراخ می‌ماند)
arr=("${arr[@]}") بسته‌بندی مجدد (اندیس‌ها دوباره پیوسته)
mapfile -t arr < file هر خط فایل یک عضو
declare -p arr نمایش ساختار دقیق

آرایه‌ی انجمنی:

عبارت معنی
declare -A map تعریف (بدون -A آرایه‌ی ساده می‌شود!)
map[key]=value مقداردهی
declare -A m=([a]=1 [b]=2) تعریف با مقدار اولیه
"${map[key]}" خواندن
"${!map[@]}" / "${map[@]}" همه‌ی کلیدها / همه‌ی مقدارها
[[ -v map[key] ]] کلید وجود دارد؟
unset 'map[key]' حذف کلید
((count[$w]++)) شمارنده‌ی هر کلید

مثال ۱: echo $arr فقط عضو اول، ${#arr} طول عضو اول. راه‌حل: "${arr[@]}" و ${#arr[@]}.

مثال ۳: عضوهای دارای فاصله می‌شکنند. راه‌حل: همیشه کوتیشن.

۳) آرایه‌ی انجمنی بدون declare -A

Section titled “۳) آرایه‌ی انجمنی بدون declare -A”
Terminal window
n[apple]=1
n[banana]=2
declare -p n
خروجی
declare -a n=([0]="2")

بدون declare -A، Bash n را آرایه‌ی ساده می‌گیرد و apple و banana را مثل عبارت حسابی می‌سنجد (متغیرهای تعریف‌نشده، صفر)، پس هر دو در اندیس ۰ نوشته می‌شوند و فقط آخری می‌ماند: [0]="2". بی‌صدا و غلط. راه‌حل: همیشه اول declare -A n.

۴) arr=($(دستور)) برای خروجی‌های دارای فاصله

Section titled “۴) arr=($(دستور)) برای خروجی‌های دارای فاصله”

مثال ۴: two words دو عضو شد. راه‌حل: mapfile -t arr < <(دستور).

Terminal window
cd ~/bashlab
letters=(a b c)
touch arr1
unset arr[1]
echo "after unset arr[1] : ${letters[@]}"
unset 'letters[1]'
echo "after unset 'letters[1]' : ${letters[@]}"
rm arr1
خروجی
after unset arr[1] : a b c
after unset 'letters[1]' : a c

شل arr[1] را الگوی نام فایل حساب می‌کند. چون فایلی به اسم arr1 بود، دستور شد unset arr1 و آرایه دست‌نخورده ماند (و letters اصلاً ربطی نداشت). با کوتیشن تکی درست حذف شد. راه‌حل: همیشه unset 'arr[i]'.

پشت پرده: (1,2,3) یک عضو است.

۷) آرایه در sh یا macOS قدیمی

Section titled “۷) آرایه در sh یا macOS قدیمی”

پشت پرده و مثال ۵: Syntax error: "(" unexpected در sh و نبودن آرایه‌ی انجمنی در bash ۳.۲.

✎ تمرینآسان

آرایه‌ی shades را با چهار رنگ بساز (یکی دو کلمه‌ای: "light blue"). تعداد، عضو اول، عضو آخر و همه‌ی رنگ‌ها را هر کدام در یک خط شماره‌دار (1. red) چاپ کن.

دیدن جواب
Terminal window
shades=(red green "light blue" yellow)
echo "count: ${#shades[@]}"
echo "first: ${shades[0]}"
echo "last: ${shades[-1]}"
position=1
for c in "${shades[@]}"; do
echo "$position. $c"
((position++))
done
خروجی
count: 4
first: red
last: yellow
1. red
2. green
3. light blue
4. yellow
✎ تمرینمتوسط

تمرین اصلی درس: لیست سرورها را در آرایه بگذار و به هر کدام ping بزن. اسکریپت ping-servers.sh برای هر سرور بنویسد UP یا DOWN، در آخر خلاصه (چند سرور، چند بالا، چند پایین) چاپ کند، و فقط وقتی همه بالا بودند با کد ۰ تمام شود. (برای نتیجه‌ی قابل‌تکرار: 127.0.0.1 و localhost همیشه بالا هستند؛ 192.0.2.1 از بازه‌ی مخصوص مستندات است و جواب نمی‌دهد؛ و دامنه‌ی nonexistent.invalid هرگز وجود ندارد.)

دیدن جواب
cd ~/bashlab
cat > ping-servers.sh <<'EOF'
#!/usr/bin/env bash
# ping-servers.sh: ping every server in a list and report
servers=(127.0.0.1 localhost 192.0.2.1 nonexistent.invalid)
up=0
down=0
for server in "${servers[@]}"; do
if ping -c 1 -W 1 "$server" > /dev/null 2>&1; then
echo "UP $server"
((up++))
else
echo "DOWN $server"
((down++))
fi
done
echo "Summary: ${#servers[@]} servers, $up up, $down down"
(( down == 0 ))
EOF
chmod +x ping-servers.sh
./ping-servers.sh
echo "کد خروج: $?"
خروجی
UP 127.0.0.1
UP localhost
DOWN 192.0.2.1
DOWN nonexistent.invalid
Summary: 4 servers, 2 up, 2 down
کد خروج: 1

ping -c 1 -W 1 فقط یک بسته می‌فرستد و حداکثر یک ثانیه صبر می‌کند؛ > /dev/null 2>&1 خروجی را دور می‌ریزد و فقط کد خروج می‌ماند (۰ یعنی جواب گرفت). آخرین خط (( down == 0 )) خودش کد خروج اسکریپت می‌شود: ۰ اگر هیچ سروری پایین نبود. برای اضافه‌کردن سرور فقط یک کلمه به آرایه می‌افزایی.

✎ تمرینمتوسط

اسکریپت top-words.sh که یک متن (با echo/فایل) بگیرد و تعداد تکرار هر کلمه را بشمارد (حروف کوچک و بزرگ یکی)، و سه کلمه‌ی پرتکرار را با تعدادشان چاپ کند. از آرایه‌ی انجمنی استفاده کن.

دیدن جواب
cd ~/bashlab
cat > top-words.sh <<'EOF'
#!/usr/bin/env bash
# top-words.sh: count word frequencies (case-insensitive) and print the top 3
declare -A count
while read -r word; do
[[ -z $word ]] && continue
((count[${word,,}]++))
done < <(tr -s '[:space:][:punct:]' '\n' < "${1:-/dev/stdin}")
for w in "${!count[@]}"; do
echo "${count[$w]} $w"
done | sort -rn | head -n 3
EOF
chmod +x top-words.sh
echo "the cat and The dog and THE bird saw a cat" > text.txt
./top-words.sh text.txt
خروجی
3 the
2 cat
2 and

نکته‌ها: ${word,,} کلمه را به حروف کوچک تبدیل می‌کند (درس بعد)؛ tr -s متن را کلمه‌به‌کلمه (هر کلمه در یک خط) می‌کند؛ for کلیدها را چاپ می‌کند و sort -rn | head -n 3 سه تای اول را از نظر تعداد برمی‌دارد. (برای تساوی تعداد، ترتیب اسم‌ها بسته به sort است.)

✎ تمرینسخت

یک جدول دستور (dispatch table) بساز: آرایه‌ی انجمنی tasks که اسم کار ← اسم تابع را نگه دارد (date ← show_date، disk ← show_disk، user ← show_user). اسکریپت tasks.sh اسم کار را از آرگومان بگیرد و تابع مربوطه را اجرا کند؛ اگر اسم ناشناخته یا خالی بود، فهرست کارها را (مرتب‌شده) در راهنما چاپ کند و با کد ۱ تمام شود.

دیدن جواب
cd ~/bashlab
cat > tasks.sh <<'EOF'
#!/usr/bin/env bash
# tasks.sh: a dispatch table (associative array: task name -> function name)
show_date() { date +%F; }
show_disk() { df -h / | tail -n 1; }
show_user() { whoami; }
declare -A tasks=(
[date]=show_date
[disk]=show_disk
[user]=show_user
)
name="$1"
if [[ -z $name || ! -v tasks[$name] ]]; then
names=$(printf '%s\n' "${!tasks[@]}" | sort | paste -sd'|')
echo "Usage: $0 {$names}" >&2
exit 1
fi
"${tasks[$name]}"
EOF
chmod +x tasks.sh
./tasks.sh user
./tasks.sh date
./tasks.sh disk
./tasks.sh nothing; echo "کد خروج: $?"
./tasks.sh; echo "کد خروج: $?"
خروجی
ali
2026-10-04
overlay 453G 23G 408G 6% /
Usage: ./tasks.sh {date|disk|user}
کد خروج: 1
Usage: ./tasks.sh {date|disk|user}
کد خروج: 1

مزیت این الگو: اضافه‌کردن یک کار جدید یعنی یک تابع و یک خط در جدول؛ بدون case طولانی. و چون فقط اسم‌های موجود در جدول اجرا می‌شوند، ورودی کاربر هرگز مستقیم به‌عنوان دستور اجرا نمی‌شود (برخلاف eval) که امن‌تر است.

⚡ بررسی سریع

آرایه‌ای سه‌عضوی دارید: apple و «cherry pie» (دو کلمه) و date. دستورهای echo ${#arr[@]} و echo ${#arr[1]} چه چاپ می‌کنند؟

؟ آزمونک
  1. echo $arr وقتی arr=(a b c) است چه چاپ می‌کند؟

  2. چرا باید "${files[@]}" را با کوتیشن نوشت؟

  3. بعد از unset 'arr[1]' روی آرایه‌ی سه‌عضوی، ${!arr[@]} چه نشان می‌دهد؟

  4. اگر declare -A را فراموش کنی و بنویسی m[apple]=1; m[banana]=2 چه می‌شود؟

  5. کدام روش برای ساختن آرایه‌ای از خط‌های یک فایل (حتی خط‌های دارای فاصله) درست است؟

  6. export arr (که آرایه است) و بعد bash -c 'echo ${arr[0]}' چه نشان می‌دهد؟

  • آرایه‌ی ساده: arr=(a b "c d")؛ خواندن ${arr[i]}، همه با "${arr[@]}" (هر عضو یک آرگومان)، تعداد ${#arr[@]}، اندیس‌ها "${!arr[@]}"، افزودن +=(x)، حذف unset 'arr[i]'، برش ${arr[@]:1:2}.
  • همیشه کوتیشن: "${arr[@]}". بدون اندیس، $arr فقط عضو اول است و ${#arr} طول عضو اول.
  • ساختن از فایل و دستور: files=(*.txt) (امن برای فاصله)، mapfile -t arr < file، read -ra arr <<< "a,b". اجتناب از arr=($(cmd)).
  • آرایه‌ی انجمنی: declare -A map (حتماً)، map[key]=v، کلیدها "${!map[@]}" (بدون ترتیب)، [[ -v map[key] ]]، شمارنده ((map[$k]++))، جدول دستور (name ← function).
  • declare -p ساختار دقیق آرایه را نشان می‌دهد؛ برای دیباگ بهترین ابزار است.
  • آرایه به فرزندها نمی‌رسد (export کار نمی‌کند)، در sh نیست، و آرایه‌ی انجمنی bash ۴+ می‌خواهد (مک: ۳.۲).
  • وقتی آرایه را به تابع می‌دهی، "${arr[@]}"؛ برای برگرداندن، mapfile روی خروجی یا local -n.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
arr=(a b "c d") arr+=(e)ساخت / افزودن
"${arr[@]}" ${arr[0]} ${arr[-1]}همه (جدا جدا) / یک عضو / آخرین
${#arr[@]} "${!arr[@]}"تعداد / اندیس‌ها
for x in "${arr[@]}"; do ...; doneپیمایش مقدارها
for i in "${!arr[@]}"; do ...; doneپیمایش با اندیس
unset 'arr[1]' arr=("${arr[@]}")حذف (سوراخ) / فشرده‌سازی مجدد
files=(*.txt)آرایه از فایل‌ها (امن برای فاصله)
mapfile -t lines < fileهر خط فایل یک عضو
IFS=, read -ra parts <<< "a,b,c"شکستن رشته به آرایه
declare -A map; map[k]=vآرایه‌ی انجمنی
"${!map[@]}" [[ -v map[k] ]] ((map[$k]++))کلیدها / وجود کلید / شمارش
declare -p arrدیدن ساختار دقیق آرایه