۳۴ – رشته‌ها و اعداد (Strings And Numbers)

برنامه‌های کامپیوتری اساساً دربارهٔ کار با داده هستند. در فصل‌های گذشته، تمرکز ما روی پردازش داده‌ها در سطح فایل بود؛ اما بسیاری از مسائل برنامه‌نویسی نیازمند پردازش واحدهای کوچک‌تری مانند رشته‌ها (strings) و اعداد (numbers) هستند.

در این فصل، چندین قابلیت شِل را بررسی می‌کنیم که برای دستکاری رشته‌ها و اعداد مورد استفاده قرار می‌گیرند. شِل مجموعهٔ متنوعی از گسترش پارامترها (parameter expansions) دارد که عملیات رشته‌ای انجام می‌دهند.
علاوه بر گسترش عددی (Arithmetic Expansion) که در فصل ۷ به آن اشاره کردیم، برنامهٔ رایجی به نام bc نیز وجود دارد که محاسبات سطح بالا انجام می‌دهد.


گسترش پارامتر (Parameter Expansion)

در فصل ۷ به گسترش پارامتر اشاره کردیم، اما وارد جزئیات نشدیم زیرا بیشتر آن‌ها در اسکریپت‌ها استفاده می‌شوند، نه روی خط فرمان.

ما قبلاً با برخی از این گسترش‌ها کار کرده‌ایم؛ برای مثال، متغیرهای شِل.
ولی شِل امکانات بسیار بیشتری ارائه می‌کند.


پارامترهای ساده (Basic Parameters)

ساده‌ترین نوع گسترش پارامتر استفادهٔ معمولی از متغیرهاست. مثلاً:

$a

که هنگام گسترش، مقدار متغیر a را تولید می‌کند.

یا می‌توان متغیر را داخل آکولاد قرار داد:

${a}

این کار در برخی موارد لازم است؛ مثلاً زمانی که متغیر در کنار متن دیگری قرار می‌گیرد و ممکن است باعث سردرگمی شِل شود:

مشکل نمونه

[me@linuxbox ~]$ a="foo"
[me@linuxbox ~]$ echo "$a_file"

شِل تلاش می‌کند متغیر a_file را گسترش دهد، نه متغیر a.

راه حل

[me@linuxbox ~]$ echo "${a}_file"
foo_file

همچنین برای دسترسی به پارامترهای موضعی بالاتر از ۹، باید از آکولاد استفاده کرد:

${11}

گسترش‌ها برای مدیریت متغیرهای خالی (Expansions To Manage Empty Variables)

این گسترش‌ها برای حالت‌هایی مفید هستند که یک متغیر تعریف نشده یا خالی است.
اغلب برای مدیریت آرگومان‌های کم یا تنظیم مقدار پیش‌فرض به‌کار می‌روند.


۱. ${parameter:-word}

اگر parameter خالی یا تعریف نشده باشد → مقدار word برگردانده می‌شود.
اگر مقدار داشته باشد → مقدار خودش برگردانده می‌شود.

مثال:

foo=
echo ${foo:-"substitute value if unset"}
# خروجی: substitute value if unset

اگر foo مقدار داشته باشد:

foo=bar
echo ${foo:-"substitute value if unset"}
# خروجی: bar

۲. ${parameter:=word}

مانند مورد قبل است، اما word علاوه بر گسترش، به خود parameter نیز اختصاص داده می‌شود.

مثال:

foo=
echo ${foo:="default value if unset"}
# خروجی: default value if unset
echo $foo
# اکنون foo همین مقدار را دارد

توجه: پارامترهای موضعی (مثل $1 و …) را نمی‌توان با این روش مقداردهی کرد.


۳. ${parameter:?word}

اگر parameter خالی باشد:

مثال:

foo=
echo ${foo:?"parameter is empty"}
# خروجی: bash: foo: parameter is empty

اگر مقدار داشته باشد:

foo=bar
echo ${foo:?"parameter is empty"}
# خروجی: bar

۴. ${parameter:+word}

اگر parameter خالی باشد → هیچ چیزی خروجی نمی‌دهد.
اگر مقدار داشته باشد → مقدار word خروجی می‌شود (اما مقدار parameter تغییر نمی‌کند).

مثال:

foo=
echo ${foo:+"substitute value if set"}
# (خروجی ندارد)

foo=bar
echo ${foo:+"substitute value if set"}
# خروجی: substitute value if set

گسترش‌هایی که نام متغیرها را برمی‌گردانند

${!prefix*}
${!prefix@}

هر دو فرم، فهرست نام متغیرهایی را که با prefix شروع می‌شوند برمی‌گردانند.

مثال:

echo ${!BASH*}

خروجی شامل همهٔ متغیرهای محیطی شروع‌شونده با BASH خواهد بود.


عملیات روی رشته‌ها (String Operations)

بخش مهمی از parameter expansion به دستکاری رشته‌ها اختصاص دارد.


۱. ${#parameter} – طول رشته

foo="This string is long."
echo "'$foo' is ${#foo} characters long."

خروجی:

'This string is long.' is 20 characters long.

اگر parameter برابر @ یا ***** باشد، تعداد پارامترهای موضعی برگردانده می‌شود.


۲. برش (Substring)

فرم‌ها

${parameter:offset}
${parameter:offset:length}

مثال‌ها:

foo="This string is long."
echo ${foo:5}
# خروجی: string is long.

echo ${foo:5:6}
# خروجی: string

offset منفی

echo ${foo: -5}
# خروجی: long.

echo ${foo: -5:2}
# خروجی: lo

۳. حذف قسمت‌هایی از ابتدای رشته

${parameter#pattern}   # کوتاه‌ترین برداشت
${parameter##pattern}  # بلندترین برداشت

مثال:

foo=file.txt.zip
echo ${foo#*.}
# txt.zip

echo ${foo##*.}
# zip

۴. حذف از انتهای رشته

${parameter%pattern}
${parameter%%pattern}

مثال:

echo ${foo%.*}
# file.txt

echo ${foo%%.*}
# file

۵. جستجو و جایگزینی (Search & Replace)

${parameter/pattern/string}   # فقط اولین
${parameter//pattern/string}  # همهٔ موارد
${parameter/#pattern/string}  # فقط اگر ابتدای رشته باشد
${parameter/%pattern/string}  # فقط اگر انتهای رشته باشد

مثال:

foo=JPG.JPG
echo ${foo/JPG/jpg}
# jpg.JPG

echo ${foo//JPG/jpg}
# jpg.jpg

echo ${foo/#JPG/jpg}
# jpg.JPG

echo ${foo/%JPG/jpg}
# JPG.jpg

افزایش کارایی با جایگزینی دستورهای خارجی

مثال از فصل قبل: برنامه longest-word

به‌جای:

len=$(echo $j | wc -c)

می‌توان از:

len=${#j}

استفاده کرد که سریع‌تر و کارآمدتر است.

نسخهٔ بهینه‌شده:

len=${#j}

مقایسهٔ سرعت

بهبود بسیار قابل توجه.


تبدیل حروف بزرگ/کوچک (Case Conversion)

نسخه‌های جدیدتر bash از تبدیل رشته‌ها به حروف بزرگ یا کوچک پشتیبانی می‌کنند.
برای این کار، bash چهار گسترش پارامتر (parameter expansion) و دو گزینه برای دستور declare ارائه می‌دهد.

اما تبدیل حروف به چه درد می‌خورد؟

غیر از جنبهٔ ظاهری، این کار نقش مهمی در برنامه‌نویسی دارد.
مثلاً تصور کنید می‌خواهیم یک مقدار ورودی کاربر را در یک پایگاه داده جستجو کنیم. ممکن است کاربر آن مقدار را به‌صورت:

وارد کند.
ما نمی‌خواهیم پایگاه دادهٔ خود را با هزاران حالت متفاوت از حروف بزرگ/کوچک پر کنیم.

راه‌حل: نرمال‌سازی (normalize) ورودی کاربر

یعنی قبل از انجام جستجو، تمام حروف ورودی را به یک شکل استاندارد تبدیل کنیم.
مثلاً همیشه به lowercase یا همیشه به UPPERCASE.

bash امکان انجام این کار را فراهم می‌کند.


تبدیل حروف با declare

با دستور declare می‌توانیم تعیین کنیم که یک متغیر همیشه به صورت uppercase یا lowercase ذخیره شود:

#!/bin/bash
# ul-declare: demonstrate case conversion via declare

declare -u upper
declare -l lower

if [[ $1 ]]; then
    upper="$1"
    lower="$1"
    echo $upper
    echo $lower
fi

در این اسکریپت:

مثال اجرا:

[me@linuxbox ~]$ ul-declare aBc
ABC
abc

ورودی کاربر (“aBc”) نرمال‌سازی شده است.


۴ نوع گسترش پارامتری برای تبدیل حروف

Table 34-1: Case Conversion Expansions

فرمت نتیجه
${parameter,,} تبدیل کل رشته به حروف کوچک
${parameter,} تبدیل فقط اولین حرف به کوچک
${parameter^^} تبدیل کل رشته به حروف بزرگ
${parameter^} تبدیل فقط اولین حرف به بزرگ (capitalization)

نمونه اسکریپت

#!/bin/bash
# ul-param - demonstrate case conversion via parameter expansion

if [[ $1 ]]; then
    echo ${1,,}
    echo ${1,}
    echo ${1^^}
    echo ${1^}
fi

اجرا:

[me@linuxbox ~]$ ul-param aBc
abc
aBc
ABC
ABc

در اینجا، از موقعیت پارامتری $1 استفاده شده، اما این گسترش‌ها می‌توانند روی هر رشته یا متغیری اعمال شوند.


ارزیابی و گسترش حسابی (Arithmetic Evaluation and Expansion)

در فصل ۷ با گسترش حسابی آشنا شدیم. این گسترش برای انجام عملیات ریاضی روی اعداد صحیح استفاده می‌شود:

$((expression))

این همان چیزی است که در دستورات شرطی arithmetic evaluation مثل:

(( expression ))

نیز استفاده می‌شود.

در ادامه، به فهرست کامل‌تری از قابلیت‌های ریاضی bash می‌پردازیم.


مبنای اعداد (Number Bases)

در فصل ۹ دربارهٔ اعداد هشت‌هشتی (octal) و شانزده‌شانزدهی (hexadecimal) توضیح داده شد.
در گسترش‌های حسابی bash می‌توان از اعداد صحیح در هر مبنایی استفاده کرد.

Table 34-2: مشخص‌کردن مبنای عدد

نشانه‌گذاری توضیح
number مبنای ۱۰ (ده‌دهی) – حالت پیش‌فرض
0number مبنای ۸ (octal)
0xnumber مبنای ۱۶ (hexadecimal)
base#number عدد در مبنای دلخواه

مثال‌ها:

echo $((0xff))
# 255

echo $((2#11111111))
# 255

عملگرهای یگانی (Unary Operators)

عملگرهای یگانی + و - نشانهٔ مثبت یا منفی بودن عدد هستند:

مثلاً:

-5

عملیات حسابی ساده

Table 34-3: Arithmetic Operators

عملگر عملیات
+ جمع
- تفریق
* ضرب
/ تقسیم صحیح
** توان
% باقیمانده (modulo)

تقسیم صحیح در bash

bash فقط با اعداد صحیح کار می‌کند، بنابراین:

echo $((5 / 2))
# 2

برای همین تعیین باقیمانده اهمیت دارد:

echo $((5 % 2))
# 1

استفاده از modulo در حلقه‌ها

باقیماندهٔ تقسیم برای ایجاد رفتارهای دوره‌ای مفید است.
مثلاً برجسته‌کردن مقادیر مضرب ۵:

#!/bin/bash
# modulo : demonstrate the modulo operator

for ((i = 0; i <= 20; i = i + 1)); do
    remainder=$((i % 5))
    if (( remainder == 0 )); then
        printf "<%d> " $i
    else
        printf "%d " $i
    fi
done
printf "\n"

نتیجه:

<0> 1 2 3 4 <5> 6 7 8 9 <10> 11 12 13 14 <15> 16 17 18 19 <20>

انتساب (Assignment)

هرچند استفاده‌هایش شاید در نگاه اول خیلی واضح نباشد، عبارات حسابی می‌توانند عمل انتساب هم انجام دهند.

ما تا الان بارها عمل انتساب انجام داده‌ایم؛ هر بار که به یک متغیر مقدار می‌دهیم، داریم انتساب انجام می‌دهیم.
اما این کار را می‌توانیم داخل عبارات حسابی هم انجام دهیم:

[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo $foo

[me@linuxbox ~]$ if (( foo = 5 )); then echo "It is true."; fi
It is true.
[me@linuxbox ~]$ echo $foo
5

در مثال بالا:

  1. اول یک مقدار خالی به متغیر foo می‌دهیم و خالی بودنش را چک می‌کنیم.
  2. بعد در دستور if از عبارت حسابی (( foo = 5 )) استفاده می‌کنیم.

این کار دو اتفاق هم‌زمان را رقم می‌زند:

نکته مهم:
در عبارت بالا، = یعنی انتساب (assignment)، نه مقایسه.

این موضوع گیج‌کننده است، چون دستور test (یا [ ]) برای مقایسهٔ رشته‌ای از یک = هم استفاده می‌کند.
یکی از دلایل ترجیح دادن [[ ]] و (( )) به جای test همین تفاوت‌هاست.


عملگرهای انتساب (Assignment Operators)

علاوه بر =, شِل چندین شکل کوتاه‌شدهٔ انتساب دارد:

Table 34-4: Assignment Operators

این‌ها شورت‌کات‌های بسیار مفیدی برای عملیات حسابی متداول هستند.
به‌خصوص ++ و -- که از زبان C گرفته شده‌اند و در بسیاری از زبان‌ها (از جمله bash) استفاده می‌شوند.


تفاوت ++ قبل و بعد از متغیر

این عملگرها می‌توانند قبل یا بعد از نام متغیر قرار بگیرند، و هر دو مقدار متغیر را یک واحد کم/زیاد می‌کنند،
اما رفتارشان متفاوت است:

مثال:

[me@linuxbox ~]$ foo=1
[me@linuxbox ~]$ echo $((foo++))
1
[me@linuxbox ~]$ echo $foo
2

اینجا:

اما اگر عملگر را قبل بگذاریم:

[me@linuxbox ~]$ foo=1
[me@linuxbox ~]$ echo $((++foo))
2
[me@linuxbox ~]$ echo $foo
2

این‌بار:

برای بیشتر استفاده‌های شِل، پیش‌وندی (++foo) طبیعی‌تر و مفیدتر است.


استفاده در حلقه‌ها – بهبود اسکریپت modulo

عملگرهای ++ و -- معمولاً همراه حلقه‌ها به‌کار می‌روند.

نسخهٔ بهبودیافتهٔ اسکریپت قبلی:

#!/bin/bash
# modulo2 : demonstrate the modulo operator

for ((i = 0; i <= 20; ++i )); do
    if (((i % 5) == 0 )); then
        printf "<%d> " $i
    else
        printf "%d " $i
    fi
done
printf "\n"

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


عملیات بیتی (Bit Operations)

یک دسته از عملگرها روی اعداد در سطح بیت کار می‌کنند.
این‌ها برای کارهای سطح پایین مثل تنظیم یا خواندن فلگ‌های بیتی استفاده می‌شوند.

Table 34-5: Bit Operators

برای بیشتر این‌ها، نسخه‌های انتسابی هم وجود دارد، مثل <<= و غیره (به جز ~).

مثال: تولید توان‌های ۲ با شیفت بیتی

[me@linuxbox ~]$ for ((i=0;i<8;++i)); do echo $((1<<i)); done
1
2
4
8
16
32
64
128

هر بار با 1 << i عدد ۱ را i بیت به چپ شیفت می‌دهیم، که معادل 2**i است.


منطق و عملگرهای مقایسه (Logic and Comparison)

در فصل ۲۷ دیدیم که دستور مرکب (( )) از عملگرهای مقایسه‌ای مختلف پشتیبانی می‌کند.
اینجا لیست کامل‌تری از آن‌ها را می‌بینیم:

Table 34-6: Comparison Operators

در منطق حسابی:

(( )) نتیجه را به کدهای خروج (exit code) شِل تبدیل می‌کند:

[me@linuxbox ~]$ if ((1)); then echo "true"; else echo "false"; fi
true

[me@linuxbox ~]$ if ((0)); then echo "true"; else echo "false"; fi
false

عملگر سه‌تایی (Ternary Operator)

این عملگر شبیه if/then/else است اما در قالب یک عبارت:

expr1 ? expr2 : expr3

مثال روی خط فرمان:

[me@linuxbox ~]$ a=0
[me@linuxbox ~]$ ((a<1?++a:--a))
[me@linuxbox ~]$ echo $a
1
[me@linuxbox ~]$ ((a<1?++a:--a))
[me@linuxbox ~]$ echo $a
0

در این مثال، یک Toggle ساخته‌ایم:

و هر بار مقدار a بین ۰ و ۱ تغییر می‌کند.

نکته:
قرار دادن انتساب‌ (assignment) داخل این عبارت‌ها کمی tricky است و اگر مستقیم بنویسید، bash خطا می‌دهد:

[me@linuxbox ~]$ a=0
[me@linuxbox ~]$ ((a<1?a+=1:a-=1))
bash: ((: a<1?a+=1:a-=1: attempted assignment to non-variable (error token is "-=1")

راه‌حل: انتساب را داخل پرانتز قرار دهید:

[me@linuxbox ~]$ ((a<1?(a+=1):(a-=1)))

نمونهٔ کامل: جدول اعداد

در این اسکریپت از عملگرهای حسابی استفاده شده تا یک جدول از اعداد و مربع و مکعب آن‌ها چاپ کند:

#!/bin/bash
# arith-loop: script to demonstrate arithmetic operators

finished=0
a=0

printf "a\ta**2\ta**3\n"
printf "=\t====\t====\n"

until ((finished)); do
    b=$((a**2))
    c=$((a**3))
    printf "%d\t%d\t%d\n" $a $b $c
    ((a<10?++a:(finished=1)))
done

روند کار:

خروجی:

a       a**2    a**3
=       ====    ====
0       0       0
1       1       1
2       4       8
3       9       27
4       16      64
5       25      125
6       36      216
7       49      343
8       64      512
9       81      729
10      100     1000

bc – یک زبان ماشین‌حساب با دقت دلخواه (Arbitrary Precision Calculator Language)

دیدیم که شل می‌تواند همهٔ انواع محاسبات صحیح (integer) را انجام دهد؛
اما اگر بخواهیم محاسبات پیچیده‌تر انجام دهیم، یا حتی فقط با اعداد اعشاری (floating point) کار کنیم چه؟

پاسخ این است که:
نمی‌توانیم.
حداقل نه مستقیم با خود شل.

برای انجام چنین کارهایی باید از برنامه‌های خارجی استفاده کنیم.
می‌توانیم از اسکریپت‌های Perl یا AWK استفاده کنیم، اما این‌ها خارج از محدودهٔ این کتاب هستند.

راه دیگر استفاده از یک برنامهٔ مخصوص ماشین‌حساب است.
یکی از این برنامه‌ها که تقریباً در همهٔ سیستم‌های لینوکسی وجود دارد، bc است.


bc چیست؟

برنامهٔ bc فایلی را که به زبان مخصوص خودش (مشابه C) نوشته شده، می‌خواند و اجرا می‌کند.
یک اسکریپت bc می‌تواند:

bc امکانات زیادی دارد:

در اینجا یک آشنایی کوتاه ارائه می‌شود؛ راهنمای کامل آن در man page موجود است.


یک مثال ساده

یک اسکریپت bc که ۲+۲ را حساب می‌کند:

/* A very simple bc script */
2 + 2

در زبان bc، کامنت‌ها مثل C هستند:
هر چیزی بین /* و */ یک کامنت است.


اجرای bc

اگر این اسکریپت را به‌صورت foo.bc ذخیره کنیم:

[me@linuxbox ~]$ bc foo.bc
...
4

نتیجه (۴) در پایین خروجی نمایش داده می‌شود.
برای حذف پیام خوش‌آمدگویی از گزینهٔ -q استفاده می‌کنیم.


اجرای تعاملی bc

[me@linuxbox ~]$ bc -q
2 + 2
4
quit

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


ارسال اسکریپت از طریق استاندارد ورودی

[me@linuxbox ~]$ bc < foo.bc
4

این یعنی می‌توانیم:

برای ارسال کد به bc استفاده کنیم.

مثال here string:

[me@linuxbox ~]$ bc <<< "2+2"
4

یک اسکریپت نمونهٔ واقعی: محاسبهٔ قسط ماهانهٔ وام

در مثال زیر، یک اسکریپت bash می‌نویسیم که از bc استفاده می‌کند تا قسط یک وام را محاسبه کند:

#!/bin/bash
# loan-calc : script to calculate monthly loan payments

PROGNAME=$(basename $0)

usage () {
cat <<- EOF
Usage: $PROGNAME PRINCIPAL INTEREST MONTHS
Where:
PRINCIPAL is the amount of the loan.
INTEREST is the APR as a number (7% = 0.07).
MONTHS is the length of the loan's term.
EOF
}

if (($# != 3)); then
    usage
    exit 1
fi

principal=$1
interest=$2
months=$3

bc <<- EOF
scale = 10
i = $interest / 12
p = $principal
n = $months
a = p * ((i * ((1 + i) ^ n)) / (((1 + i) ^ n) - 1))
print a, "\n"
EOF

اجرا:

[me@linuxbox ~]$ loan-calc 135000 0.0775 180
1270.7222490000

اسکریپت بالا میزان قسط ماهانهٔ یک وام ۱۳۵٬۰۰۰ دلاری با نرخ بهرهٔ ۷.۷۵٪ و مدت ۱۸۰ ماه (۱۵ سال) را محاسبه می‌کند.

نکته مهم: scale

در bc، مقدار متغیر داخلی scale تعداد رقم‌های اعشار محاسبات را تعیین می‌کند.
مثلاً:

scale = 10

یعنی ۱۰ رقم اعشار.


جمع‌بندی

در این فصل، با بسیاری از ابزارهای کوچک اما مهمی آشنا شدیم که برای انجام «کار واقعی» در اسکریپت‌ها استفاده می‌شوند.
هرچه تجربهٔ ما در اسکریپت‌نویسی بیشتر شود، توانایی دستکاری مؤثر رشته‌ها و اعداد اهمیت بیشتری پیدا می‌کند.

اسکریپت loan-calc نمونه‌ای نشان می‌دهد که چگونه حتی یک اسکریپت کوتاه می‌تواند کارهای بسیار مفیدی انجام دهد.


تمرین اضافی (Extra Credit)

اسکریپت loan-calc هنوز کامل نیست.
برای بهبود آن، می‌توانید:


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

● مقالهٔ Bash Hackers Wiki دربارهٔ parameter expansion:
http://wiki.bash-hackers.org/syntax/pe

● Bash Reference Manual:
http://www.gnu.org/software/bash/manual/bashref.html#Shell-Parameter-Expansion

● مقالهٔ ویکی‌پدیا دربارهٔ bit operations:
http://en.wikipedia.org/wiki/Bit_operation

● مقالهٔ ویکی‌پدیا دربارهٔ ternary operations:
http://en.wikipedia.org/wiki/Ternary_operation

● فرمول محاسبهٔ وام که در اسکریپت loan-calc استفاده شده:
http://en.wikipedia.org/wiki/Amortization_calculator