Bash'te aritmetik işlem yapmanın üç ana yolu vardır: sonucu metne yerleştiren $(( ifade )) aritmetik genişletmesi, sonucu çıkış durumuna çeviren (( ifade )) komutu ve eski let yerleşik komutu. Üçü de aynı kuralları kullanır: hesaplar yalnızca tamsayılarla yapılır, operatörler ve öncelikleri C dilindekiyle aynıdır, değişkenlere $ koymadan adlarıyla başvurulabilir ve sıfıra bölme bir hata olarak yakalanır. Ondalıklı sayı hesabı Bash'in kendi aritmetiğinde yoktur.

Bu yazıda kılavuzun 6.5 Shell Arithmetic ve 3.5.5 Arithmetic Expansion bölümlerine dayanarak operatörleri, öncelik sırasını, sayı tabanlarını (16#ff, 0x1F, 2#1010), başında sıfır olan sayıların yol açtığı "value too great for base" hatasını, (( )) komutunun dönüş durumu tuzağını ve tamsayı sınırlarını göreceksiniz. Örnekler GNU Bash 5.2.37 ile çalıştırıldı.

Aritmetik genişletme: $(( ))

Aritmetik genişletme: $(( ))

$(( ifade )) biçimi ifadeyi hesaplar ve sonucu yazıldığı yere koyar; tıpkı bir değişken ya da komut ikamesi gibi. Kılavuza göre ifade, çift tırnak içindeymiş gibi parametre genişletmesinden, komut ikamesinden ve tırnak kaldırmadan geçer, ardından aritmetik ifade olarak değerlendirilir. Aritmetik genişletmeler iç içe de yazılabilir. İfadenin içindeki boşluklar serbesttir; okunabilirlik için operatörlerin çevresinde boşluk bırakabilirsiniz.

bash
#!/usr/bin/env bash
adet=7
fiyat=45

echo "toplam: $(( adet * fiyat ))"
# çıktı: toplam: 315
echo "bölüm: $(( 17 / 5 )), kalan: $(( 17 % 5 ))"
# çıktı: bölüm: 3, kalan: 2
echo "üs: $(( 2 ** 10 ))"
# çıktı: üs: 1024
echo "iç içe: $(( $(( 2 + 3 )) * 4 ))"
# çıktı: iç içe: 20

Bölme işlemi tamsayı bölmesidir: 17 / 5 işleminin sonucu 3'tür ve kalan kısmı % operatörü verir. Sonuç, aritmetik genişletme bir değişken atamasının sağ tarafında da kullanılabilir: toplam=$(( adet * fiyat )). Bu durumda tırnak gerekmez, çünkü sonuç her zaman boşluk içermeyen bir sayıdır.

Değişkenler ve $ işareti

Kılavuz, aritmetik ifade içinde kabuk değişkenlerine $ kullanmadan, doğrudan adlarıyla başvurulabileceğini söyler. Bir değişkene adıyla başvurulduğunda değeri bir aritmetik ifade olarak değerlendirilir; boş ya da tanımsız değişkenler 0 sayılır. Değişkenin önceden tamsayı niteliğiyle tanımlanmış olması gerekmez. $x yazmak da çalışır, ama x yazmak hem daha kısadır hem de değişken boşken sözdizimi hatası almanızı önler.

bash
#!/usr/bin/env bash
x=10
bos=""
echo $(( x + 5 ))
# çıktı: 15
echo $(( bos + 1 )) $(( tanimsiz + 2 ))
# çıktı: 1 2

a=b
b=5
echo $(( a + 1 ))
# çıktı: 6

Son örnek ilginçtir: a değişkeninin değeri "b" metnidir; bu metin de bir aritmetik ifade olarak değerlendirildiğinde b değişkenine, yani 5'e ulaşılır. Bu özellik bazen işe yarar ama dışarıdan gelen bir değeri doğrudan aritmetiğe sokmanın riskini de gösterir: değer bir sayı değil bir değişken adı ya da ifade ise, Bash onu değerlendirir. Kullanıcı girdisini hesaplamaya sokmadan önce yalnızca rakamlardan oluştuğunu kontrol etmek bu yüzden önemlidir; bunun bir örneği test ve [[ ]] farkı yazısında var.

(( )) komutu ve dönüş durumu

(( ifade )) biçimi bir sonuç üretmez, bir komuttur. Kılavuza göre ifadenin değeri sıfırdan farklıysa dönüş durumu 0, sıfırsa 1 olur. Bu sayede if (( a > b )) ya da while (( i < 10 )) gibi koşullar doğal biçimde yazılır: karşılaştırma doğruysa 1 değerini, yanlışsa 0 değerini üretir ve (( )) bunu kabuk mantığına çevirir. Değişken artırma, atama gibi yan etkili işlemler için de (( )) kullanılır.

bash
#!/usr/bin/env bash
echo $(( 5 > 3 )) $(( 5 < 3 ))
# çıktı: 1 0

if (( 5 > 3 )); then echo "beş üçten büyük"; fi
# çıktı: beş üçten büyük

sayac=0
(( sayac += 5 ))
echo "sayac: $sayac"
# çıktı: sayac: 5

(( 0 ))
# $? = 1
(( 3 - 3 ))
# $? = 1
bash
$ cat sayac.sh
set -e
i=0
((i++))
echo "bu satır görünmez"
$ bash sayac.sh; echo "çıkış kodu: $?"
çıkış kodu: 1

let ve declare -i

let yerleşik komutu, kendisine verilen her argümanı bir aritmetik ifade olarak değerlendirir. Kılavuza göre son ifadenin değeri 0 ise 1, değilse 0 döndürür; yani (( )) ile aynı dönüş kuralını izler. Her argüman ayrı değerlendirildiği için boşluk içeren bir ifadeyi tırnağa almanız gerekir: let "x = 2 * 3". Tırnaksız yazılan * ise dosya adı genişletmesine uğrayabilir. Bu yüzden yeni betiklerde let yerine genellikle (( )) tercih edilir.

declare -i ile bir değişkene tamsayı niteliği verirseniz, o değişkene yapılan her atama otomatik olarak aritmetik ifade olarak değerlendirilir. Bu kullanışlı ama gizli bir davranıştır; kodu okuyan biri n="3+4" satırının bir metin değil 7 atadığını fark etmeyebilir. Değişken nitelikleri declare ve readonly yazısında ayrıntılı olarak anlatılıyor.

bash
#!/usr/bin/env bash
let "x = 2 * 3" y=x+1
echo "$x $y"
# çıktı: 6 7
let 0
# $? = 1

declare -i n
n="3+4"
echo "$n"
# çıktı: 7
n="abc"
echo "$n"
# çıktı: 0

Son atamada "abc" metni bir değişken adı olarak yorumlandı; böyle bir değişken olmadığı için sonuç 0 oldu. Tamsayı niteliği, hatalı girdiyi hata vermeden sıfıra çevirebildiği için doğrulama yerine kullanılmamalıdır.

Operatörler ve öncelik sırası

Operatörler ve öncelik sırası: id++ id--, ++id --id, Tekli eksi ve artı, Mantıksal ve bit düzeyinde değil

Kılavuz operatörleri öncelik sırasına göre gruplar ve bunların C dilindekilerle aynı olduğunu belirtir. Aşağıdaki tablo yüksekten düşüğe doğru sıralanmıştır; aynı satırdaki operatörler eşit önceliktedir. Parantez içindeki alt ifadeler her zaman önce değerlendirilir ve öncelik kurallarını geçersiz kılar.

OperatörlerAnlamı
id++ id--Son ek artırma ve azaltma (önce eski değeri döndürür)
++id --idÖn ek artırma ve azaltma (önce değiştirir)
- +Tekli eksi ve artı
! ~Mantıksal ve bit düzeyinde değil
**Üs alma
* / %Çarpma, tamsayı bölme, kalan
+ -Toplama, çıkarma
<< >>Bit kaydırma
<= >= < >Karşılaştırma
== !=Eşitlik, eşitsizlik
& ^ |Bit düzeyinde ve, özel veya, veya (bu sırayla)
&& ||Mantıksal ve, mantıksal veya (bu sırayla)
a ? b : cKoşul operatörü
= += -= *= /= %= <<= >>= &= ^= |=Atama
,Virgül: ifadeleri sırayla değerlendirir
bash
#!/usr/bin/env bash
echo $(( 2 + 3 * 4 )) $(( (2 + 3) * 4 ))
# çıktı: 14 20
echo $(( 2 ** 3 ** 2 ))
# çıktı: 512
echo $(( -2 ** 2 ))
# çıktı: 4

i=5
echo $(( i++ )) $i $(( ++i )) $i
# çıktı: 5 6 7 7

not=72
echo $(( not >= 50 ? 1 : 0 ))
# çıktı: 1
echo $(( 5 & 3 )) $(( 5 | 3 )) $(( 5 ^ 1 )) $(( 1 << 4 ))
# çıktı: 1 7 4 16

Üç sonuç dikkat çekici. 2 ** 3 ** 2 ifadesi 512 verdi: üs alma sağdan sola gruplandı, yani 2 ** (3 ** 2) olarak hesaplandı. -2 ** 2 ise 4 verdi, çünkü tabloya göre tekli eksi üs almadan daha yüksek önceliktedir ve ifade (-2) ** 2 olarak okunur. Matematikteki alışkanlıkla -4 bekliyorsanız -(2 ** 2) yazmalısınız. Son ek i++ önce 5 değerini döndürüp sonra artırdı, ön ek ++i önce artırıp yeni değeri döndürdü.

Sayı tabanları ve başında sıfır tuzağı

Kılavuza göre tamsayı sabitleri C kurallarına uyar: başında 0 olan sayılar sekizlik, 0x ya da 0X ile başlayanlar on altılık tabanda yorumlanır. Bunun dışında taban#sayı biçimiyle 2 ile 64 arasında herhangi bir taban kullanılabilir. 10'dan büyük rakamlar için sırasıyla küçük harfler, büyük harfler, @ ve _ kullanılır; taban 36 veya daha küçükse küçük ve büyük harf aynı rakamı gösterir.

bash
#!/usr/bin/env bash
echo $(( 16#ff )) $(( 0x1F )) $(( 2#1010 )) $(( 017 ))
# çıktı: 255 31 10 15
echo $(( 36#z )) $(( 36#Z )) $(( 64#_ ))
# çıktı: 35 35 63

ay="08"
echo $(( ay + 1 ))
# çıktı: ornek.sh: line 8: 08: value too great for base (error token is "08")
echo $(( 10#$ay + 1 ))
# çıktı: 9

Bu hata, tarih ve saat değerleriyle çalışırken çok sık görülür. "08" ve "09" değerleri sıfırla başladığı için sekizlik sayı olarak okunur, 8 ve 9 ise sekizlik tabanda geçerli rakam değildir. 10# öneki, değeri açıkça on tabanında yorumlamasını söyler. Dikkat edin, 10# önekinin arkasına değişkenin adını değil değerini koymanız gerekir; bu yüzden örnekte 10#$ay yazdık. "07" gibi değerler hata vermez ama sekizlik okunduğu için "010" gibi bir değer 8 olarak hesaplanır; hata vermeyen bu durum daha da tehlikelidir.

Sonucu farklı bir tabanda yazdırmak için aritmetik değil printf kullanılır: printf '%x' 255 on altılık, printf '%o' 8 sekizlik gösterim üretir. Biçimlendirme seçenekleri echo ve printf yazısında anlatılıyor.

Sıfıra bölme ve hatalı ifadeler

Kılavuza göre sıfıra bölme yakalanır ve bir hata olarak işaretlenir. Bir aritmetik genişletme geçersizse Bash standart hataya bir mesaj yazar, ikameyi yapmaz ve o genişletmeyi içeren komutu çalıştırmaz. Aşağıdaki örnekte hatalı satırdaki echo hiç çalışmadı, ama betik bir sonraki satırdan devam etti:

bash
#!/usr/bin/env bash
bolen=0
echo "sonuç: $(( 10 / bolen ))"
# çıktı: ornek.sh: line 3: 10 / bolen : division by 0 (error token is "bolen ")
echo "betik devam etti"
# çıktı: betik devam etti

if (( bolen != 0 )); then
  echo "sonuç: $(( 10 / bolen ))"
else
  echo "bölen sıfır, işlem atlandı"
fi
# çıktı: bölen sıfır, işlem atlandı

Kullanıcıdan ya da dosyadan gelen bölenleri her zaman önceden kontrol edin. Hata mesajındaki "error token" kısmı, ifadenin hangi noktasında sorun bulunduğunu gösterir; bu bilgi uzun ifadelerde hatayı bulmayı kolaylaştırır.

Tamsayı sınırları ve taşma

Kılavuza göre hesaplar, sistemdeki en geniş sabit genişlikli tamsayı türüyle yapılır ve taşma kontrolü yoktur. Örnekleri çalıştırdığımız 64 bit sistemde bu tür işaretli 64 bitlik tamsayıydı; en büyük değeri 9223372036854775807'dir. Bu değere 1 eklediğinizde hata almazsınız, sonuç sessizce en küçük negatif değere döner:

bash
#!/usr/bin/env bash
echo $(( 9223372036854775807 + 1 ))
# çıktı: -9223372036854775808
echo $(( -7 / 2 )) $(( -7 % 3 ))
# çıktı: -3 -1

İkinci satır, negatif sayılarda bölmenin sıfıra doğru yuvarlandığını ve kalanın bölünen sayının işaretini aldığını gösteriyor; C dilindeki davranış da budur. Çok büyük sayılarla ya da hassas hesaplarla çalışıyorsanız Bash aritmetiği yerine harici bir araç kullanmanız gerekir.

Ondalıklı sayılar

Bash'in aritmetiği yalnızca tamsayıları destekler; $(( 3.5 + 1 )) bir sözdizimi hatası verir. Ondalıklı sonuca ihtiyaç duyduğunuz basit durumlarda iki yaygın yol vardır. Birincisi ölçeklemedir: hesabı 100 ya da 1000 ile çarpılmış tamsayılarla yapıp sonucu yazdırırken ondalık noktasını kendiniz yerleştirirsiniz. İkincisi, yalnızca gösterim için printf komutunun %f biçimini kullanmaktır. Daha karmaşık hesaplar için bc ya da awk gibi harici araçlar kullanılır; bunlar bu serinin kapsamı dışındadır.

bash
#!/usr/bin/env bash
kullanilan=37
toplam=120

yuzde_x100=$(( kullanilan * 10000 / toplam ))
printf "kullanım: %d.%02d%%\n" $(( yuzde_x100 / 100 )) $(( yuzde_x100 % 100 ))
# çıktı: kullanım: 30.83%

printf "%.2f\n" 3.14159
# çıktı: 3.14

İlk yöntemde oran 10000 ile çarpılarak iki ondalık basamak korundu, sonra tam ve ondalık kısım ayrı ayrı yazdırıldı. Çarpmayı bölmeden önce yapmak önemlidir; kullanilan / toplam * 10000 yazsaydınız ilk bölme 0 verir ve sonuç da 0 olurdu.

Küçük bir uygulama: saniyeyi süreye çevirmek

Aşağıdaki fonksiyon bir saniye değerini gün, saat, dakika ve saniyeye ayırıyor. Tamsayı bölme ve kalan operatörleri tam olarak bu tür işler içindir. Sıfırla doldurulmuş çıktı için printf kullanıldı. Fonksiyonun girdisi önce bir desenle doğrulanıyor, böylece sayı olmayan bir değer aritmetiğe hiç girmiyor.

bash
#!/usr/bin/env bash
sure_yaz() {
  local s=$1
  if [[ $s != +([0-9]) ]]; then
    echo "geçersiz: $s"
    return 1
  fi
  s=$(( 10#$s ))
  local gun=$(( s / 86400 ))
  local saat=$(( s % 86400 / 3600 ))
  local dk=$(( s % 3600 / 60 ))
  local sn=$(( s % 60 ))
  printf "%d gün %02d:%02d:%02d\n" "$gun" "$saat" "$dk" "$sn"
}

sure_yaz 59
sure_yaz 3725
sure_yaz 090061
sure_yaz 1.5
# çıktı:
#   0 gün 00:00:59
#   0 gün 01:02:05
#   1 gün 01:01:01
#   geçersiz: 1.5

s % 86400 / 3600 ifadesinde % ve / eşit öncelikte olduğu için soldan sağa değerlendirilir: önce günden artan saniye bulunur, sonra saate bölünür. "090061" girdisi 10# sayesinde sekizlik sayı sanılmadan 90061 olarak işlendi. Fonksiyondaki local kullanımı için local değişken ve kapsam yazısına bakabilirsiniz.

Sık yapılan hatalar

Sık yapılan hatalar: Köşeli parantezde aritmetik beklemek, Tek parantez kullanmak, expr alışkanlığı, Başında sıfır olan değerler
  • Köşeli parantezde aritmetik beklemek: [ 3+4 -eq 7 ] hata verir; [ ] yalnızca düz tamsayı kabul eder. (( 3 + 4 == 7 )) yazın.
  • Tek parantez kullanmak: $( 2 + 3 ) aritmetik değil, komut ikamesidir ve 2 adlı bir komut çalıştırmaya çalışır. Aritmetik için çift parantez gerekir.
  • expr alışkanlığı: Harici expr komutu her işlem için ayrı bir süreç başlatır; Bash içinde $(( )) yeterlidir.
  • Başında sıfır olan değerler: "08", "09" hata verir, "010" sessizce 8 olur. 10# kullanın.
  • Bölmeden sonra çarpmak: Tamsayı bölmesi küsuratı atar; oranlarda önce çarpın.
  • set -e ile (( i++ )): Değer 0 iken betik durur; (( ++i )) ya da (( i += 1 )) kullanın.

Sık Sorulan Sorular

Bash ile iki sayıyı nasıl toplarım?

toplam=$(( a + b )) yazın. Sonucu doğrudan yazdırmak için echo $(( a + b )) yeterlidir. Değişkenlerin önüne $ koymanıza gerek yoktur.

Bash ondalıklı sayılarla hesap yapabilir mi?

Kendi aritmetiğiyle hayır; yalnızca tamsayılarla çalışır. Ölçekleme yöntemiyle tamsayılardan ondalıklı bir gösterim üretebilir ya da harici bir hesap aracı kullanabilirsiniz. printf '%.2f' yalnızca var olan bir ondalıklı metni biçimlendirir, hesap yapmaz.

$(( )) ile (( )) arasındaki fark nedir?

$(( )) bir genişletmedir ve sonucu metin olarak yerine koyar; echo ya da atama içinde kullanılır. (( )) bir komuttur, sonucu çıkış durumuna çevirir ve if, while ya da yan etkili işlemler ((( i++ ))) için kullanılır.

Rastgele sayı nasıl üretirim?

Bash'in RANDOM değişkeni her okunduğunda 0 ile 32767 arasında bir tamsayı verir. Belirli bir aralık için kalan operatörünü kullanabilirsiniz: $(( RANDOM % 6 + 1 )) 1 ile 6 arasında bir değer üretir. Kabuk değişkenleri Bash değişkenler yazısında anlatılıyor.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Bash öğrenme rehberi · if elif else · for döngüsü · declare ve readonly · echo ve printf · test ve [[ ]] farkı