۳۵ – آرایه‌ها (Arrays)

در فصل قبل دیدیم که شِل چگونه می‌تواند رشته‌ها و اعداد را پردازش کند.
نوع داده‌هایی که تاکنون بررسی کردیم در علوم کامپیوتر متغیرهای اسکالر (scalar variables) نامیده می‌شوند؛ یعنی متغیرهایی که تنها یک مقدار را در خود نگه می‌دارند.

در این فصل، با نوع دیگری از ساختار داده‌ها آشنا می‌شویم به نام آرایه (array) که می‌تواند چندین مقدار را هم‌زمان ذخیره کند.
تقریباً تمام زبان‌های برنامه‌نویسی از آرایه‌ها پشتیبانی می‌کنند.
Bash نیز از آرایه‌ها پشتیبانی می‌کند، هرچند با امکانات محدود.
اما همین مقدار محدود نیز برای حل بسیاری از مسائل برنامه‌نویسی کافی است.


آرایه‌ها چیستند؟

آرایه‌ها متغیرهایی هستند که بیش از یک مقدار را ذخیره می‌کنند.
آرایه‌ها مانند یک جدول (table) عمل می‌کنند.

مثال: صفحه‌گسترده (Spreadsheet)

یک فایل Excel مثل یک آرایهٔ دوبعدی است — هم سطر دارد هم ستون.
هر سلول توسط آدرس سطر و ستون آن مشخص می‌شود.

آرایه هم مشابه است:

بیشتر زبان‌ها آرایه‌های چندبعدی دارند (مثل آرایه‌های ۲بعدی و ۳بعدی).
اما:

در bash آرایه‌ها فقط یک‌بعدی هستند.

می‌توان آن‌ها را مانند یک جدول تک‌ستونه تصور کرد.
با اینکه محدود هستند، همچنان کاربردهای زیادی دارند.

آرایه‌ها از bash نسخهٔ ۲ معرفی شدند.
شل سنتی Unix یعنی sh اصلاً آرایه نداشت.


ساختن آرایه

نام‌گذاری متغیرهای آرایه مانند سایر متغیرهاست.
آرایه به‌محض استفاده، خودبه‌خود ساخته می‌شود:

[me@linuxbox ~]$ a[1]=foo
[me@linuxbox ~]$ echo ${a[1]}
foo

در دستور اول، عنصر ۱ از آرایهٔ a مقدار "foo" را دریافت می‌کند.
در دستور دوم، مقدار ذخیره‌شده در عنصر ۱ نمایش داده می‌شود.

نکته:
هنگام دسترسی به عناصر آرایه، باید از {} استفاده کنیم تا شل نام آرایه را با wildcard اشتباه نگیرد.

ساخت آرایه با declare

[me@linuxbox ~]$ declare -a a

گزینهٔ -a آرایهٔ a را ایجاد می‌کند.


انتساب مقدار به آرایه

دو روش برای مقداردهی وجود دارد:


۱. مقداردهی تک‌عنصر

name[subscript]=value

توجه: اولین عنصر آرایه اندیس صفر دارد، نه یک.


۲. مقداردهی چندعنصره

name=(value1 value2 value3 ...)

این کار باعث می‌شود:

ذخیره شوند.

مثلاً آرایهٔ روزهای هفته:

[me@linuxbox ~]$ days=(Sun Mon Tue Wed Thu Fri Sat)

یا مقداردهی با اندیس مشخص:

[me@linuxbox ~]$ days=([0]=Sun [1]=Mon [2]=Tue [3]=Wed [4]=Thu [5]=Fri [6]=Sat)

دسترسی به عناصر آرایه

آرایه‌ها برای انجام بسیاری از کارهای مرتبط با مدیریت داده کاربرد دارند.

یک مثال:
می‌خواهیم زمان آخرین تغییر فایل‌ها در یک دایرکتوری را بررسی کنیم و ببینیم در هر ساعت از روز چند فایل تغییر یافته است.

اسکریپت hours خروجی شبیه زیر تولید می‌کند:

[me@linuxbox ~]$ hours .
Hour Files   Hour Files
----------------------
00   0       12   11
01   1       13   7
02   0       14   1
03   0       15   7
04   1       16   6
05   1       17   5
06   6       18   4
07   3       19   4
08   1       20   1
09   14      21   0
10   2       22   0
11   5       23   0

Total files = 80

این خروجی نشان می‌دهد در هر یک از ساعات ۰ تا ۲۳ چند فایل آخرین بار اصلاح شده‌اند.


کد اسکریپت hours

#!/bin/bash
# hours : script to count files by modification time

usage () {
    echo "usage: $(basename $0) directory" >&2
}

# Check that argument is a directory
if [[ ! -d $1 ]]; then
    usage
    exit 1
fi

# Initialize array
for i in {0..23}; do hours[i]=0; done

# Collect data
for i in $(stat -c %y "$1"/* | cut -c 12-13); do
    j=${i/#0}
    ((++hours[j]))
    ((++count))
done

# Display data
echo -e "Hour\tFiles\tHour\tFiles"
echo -e "----\t-----\t----\t-----"

for i in {0..11}; do
    j=$((i + 12))
    printf "%02d\t%d\t%02d\t%d\n" $i ${hours[i]} $j ${hours[j]}
done

printf "\nTotal files = %d\n" $count

توضیح کد

بخش اول: بررسی ورودی

اسکریپت چک می‌کند که آرگومان وجود داشته باشد و یک دایرکتوری باشد؛ در غیر این صورت پیام راهنما چاپ شده و برنامه متوقف می‌شود.


بخش دوم: مقداردهی اولیهٔ آرایه

آرایهٔ hours با ۲۴ عنصر ایجاد می‌شود و همهٔ عناصر مقدار ۰ می‌گیرند.

این کار ضروری نیست، چون bash آرایه را خودکار می‌سازد؛
اما در اینجا لازم است مطمئن شویم هیچ عنصر خالی وجود ندارد.


بخش سوم: جمع‌آوری داده‌ها

برای هر فایل:

  1. دستور stat زمان اصلاح فایل را چاپ می‌کند.
  2. cut دو رقم ساعت را جدا می‌کند.
  3. پیش‌صفرهای ساعت حذف می‌شود تا مقادیر "00" مثل اعداد اکتال تفسیر نشوند.
  4. مقدار عنصر مربوط به همان ساعت یکی افزایش می‌یابد.
  5. شمارندهٔ کل فایل‌ها (count) نیز افزایش می‌یابد.

بخش چهارم: نمایش نتیجه

نتایج در دو ستون چاپ می‌شوند (۰ تا ۱۱ و ۱۲ تا ۲۳).
در پایان، تعداد کل فایل‌ها چاپ می‌شود.


در ادامه، ترجمهٔ کامل، دقیق و روان بخش Array Operations – عملیات روی آرایه‌ها آورده شده است:


عملیات روی آرایه‌ها (Array Operations)

آرایه‌ها در اسکریپت‌نویسی کاربردهای زیادی دارند.
کارهایی مانند حذف آرایه، پیدا کردن اندازهٔ آرایه، مرتب‌سازی آن و غیره بسیار رایج‌اند.


نمایش تمام عناصر یک آرایه

برای دسترسی به تمام عناصر یک آرایه، می‌توان از زیرنویس‌های * و @ استفاده کرد.
مانند پارامترهای موقعیتی، شکل @ معمولاً مفیدتر است.

مثال:

[me@linuxbox ~]$ animals=("a dog" "a cat" "a fish")

اکنون چهار حلقهٔ مختلف را اجرا می‌کنیم:

[me@linuxbox ~]$ for i in ${animals[*]}; do echo $i; done
a
dog
a
cat
a
fish
[me@linuxbox ~]$ for i in ${animals[@]}; do echo $i; done
a
dog
a
cat
a
fish
[me@linuxbox ~]$ for i in "${animals[*]}"; do echo $i; done
a dog a cat a fish
[me@linuxbox ~]$ for i in "${animals[@]}"; do echo $i; done
a dog
a cat
a fish

تحلیل نتیجه

این رفتار، مشابه همان چیزی است که دربارهٔ آرگومان‌های خط فرمان دیدیم.


تشخیص تعداد عناصر یک آرایه

برای پیدا کردن تعداد عناصر یک آرایه، از گسترش پارامتر استفاده می‌کنیم:

[me@linuxbox ~]$ a[100]=foo
[me@linuxbox ~]$ echo ${#a[@]}     # تعداد عناصر آرایه
1
[me@linuxbox ~]$ echo ${#a[100]}   # طول عنصر شماره ۱۰۰
3

توضیح:

این برخلاف برخی زبان‌هاست که همهٔ خانه‌های خالی را هم می‌شمارند.


پیدا کردن اندیس‌های موجود در آرایه

چون آرایه‌های bash می‌توانند «خانه‌های خالی» داشته باشند،
گاهی لازم است بدانیم کدام اندیس‌ها واقعاً وجود دارند.

برای این کار:

${!array[*]}
${!array[@]}

مثال:

[me@linuxbox ~]$ foo=([2]=a [4]=b [6]=c)

نمایش مقادیر:

[me@linuxbox ~]$ for i in "${foo[@]}"; do echo $i; done
a
b
c

نمایش اندیس‌ها:

[me@linuxbox ~]$ for i in "${!foo[@]}"; do echo $i; done
2
4
6

در حالت quoted، هر اندیس به‌عنوان یک کلمه جداگانه گسترش می‌یابد.


افزودن عناصر به انتهای آرایه

گاهی لازم است مقدار جدیدی به انتهای آرایه اضافه شود.
چون تعداد عناصر الزاماً برابر با بیشترین اندیس نیست،
نمی‌توان با شمارش آرایه تصمیم گرفت کجا اضافه کنیم.

راه‌حل: استفاده از عملگر +=

مثال:

[me@linuxbox ~]$ foo=(a b c)
[me@linuxbox ~]$ echo ${foo[@]}
a b c

افزودن ۳ مقدار دیگر:

[me@linuxbox ~]$ foo+=(d e f)
[me@linuxbox ~]$ echo ${foo[@]}
a b c d e f

bash به‌طور خودکار عناصر جدید را در اولین اندیس خالی بعدی قرار می‌دهد.


مرتب‌سازی آرایه

bash تابع داخلی برای مرتب‌سازی ندارد،
اما خیلی راحت می‌توان با یک حقهٔ کوچک این کار را انجام داد:

#!/bin/bash
# array-sort : Sort an array

a=(f e d c b a)
echo "Original array: ${a[@]}"

a_sorted=($(for i in "${a[@]}"; do echo $i; done | sort))

echo "Sorted array:   ${a_sorted[@]}"

خروجی:

Original array: f e d c b a
Sorted array:   a b c d e f

در اینجا:

  1. تمام عناصر چاپ می‌شوند
  2. ورودی به sort ارسال می‌شود
  3. خروجی مرتب‌شده دوباره وارد یک آرایهٔ جدید می‌شود

این تکنیک را می‌توان برای انواع پردازش‌ها به‌کار برد.


حذف آرایه

برای حذف کامل یک آرایه:

[me@linuxbox ~]$ foo=(a b c d e f)
[me@linuxbox ~]$ unset foo
[me@linuxbox ~]$ echo ${foo[@]}

مقداری چاپ نمی‌شود، زیرا آرایه حذف شده است.

حذف یک عنصر خاص

[me@linuxbox ~]$ foo=(a b c d e f)
[me@linuxbox ~]$ unset 'foo[2]'
[me@linuxbox ~]$ echo ${foo[@]}
a b d e f

رفتار عجیب: مقداردهی خالی آرایه را پاک نمی‌کند

[me@linuxbox ~]$ foo=(a b c d e f)
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo[@]}
b c d e f

در bash:

زیرا اگر آرایه را بدون زیرنویس صدا بزنید، اشاره به عنصر صفر دارد.

مثال:

[me@linuxbox ~]$ foo=(a b c d e f)
[me@linuxbox ~]$ foo=A
[me@linuxbox ~]$ echo ${foo[@]}
A b c d e f

آرایه‌های انجمنی (Associative Arrays)

نسخه‌های جدید bash از آرایه‌های انجمنی پشتیبانی می‌کنند.

مثال:

declare -A colors
colors["red"]="#ff0000"
colors["green"]="#00ff00"
colors["blue"]="#0000ff"

برای دسترسی:

echo ${colors["blue"]}

نکته: بر خلاف آرایه‌های عددی، آرایه‌های انجمنی حتماً باید با دستور declare ساخته شوند.

در فصل بعد، اسکریپتی خواهیم دید که از آرایه‌های انجمنی استفادهٔ جالبی می‌کند.


جمع‌بندی

اگر در صفحهٔ manual به دنبال «array» بگردیم، می‌بینیم bash در بسیاری از جاها از آرایه‌ها استفاده می‌کند —
هرچند بسیاری از آن‌ها کاربردهای تخصصی دارند.

به‌طور کلی، موضوع آرایه‌ها در bash کمتر استفاده شده است،
چون شل‌های سنتی مثل sh اصلاً آرایه نداشتند.

این در حالی است که در بسیاری از زبان‌ها آرایه‌ها یک ابزار بسیار قدرتمند و حیاتی هستند.

نکتهٔ مهم

آرایه‌ها و حلقه‌ها رابطهٔ نزدیکی دارند.
به‌ویژه حلقهٔ:

for ((expr; expr; expr))

برای پیمایش اندیس‌های آرایه بسیار مناسب است.


در ادامه، ترجمهٔ بخش "Further Reading" آورده شده است:


مطالعهٔ بیشتر (Further Reading)

● دو مقاله از ویکی‌پدیا دربارهٔ ساختارهای داده‌ای مطرح‌شده در این فصل:

Scalar (computing)
http://en.wikipedia.org/wiki/Scalar_(computing)

این مقاله مفهوم «اسکالر» را توضیح می‌دهد؛ یعنی متغیری که تنها یک مقدار را نگه می‌دارد — بر خلاف آرایه‌ها که چند مقدار را ذخیره می‌کنند.


Associative array
http://en.wikipedia.org/wiki/Associative_array

این مقاله ساختار «آرایهٔ انجمنی» را شرح می‌دهد؛ آرایه‌ای که در آن اندیس‌ها رشته هستند نه اعداد — مشابه دیکشنری‌ها در پایتون یا map در بسیاری از زبان‌ها.