eval, kendisine verilen argümanları aralarına birer boşluk koyarak tek bir metinde birleştirir, sonra bu metni sanki yeni bir komut satırıymış gibi Bash'e yeniden okutup çalıştırır. Bu yüzden metnin içindeki tırnaklar, noktalı virgüller ve $ işaretleri ikinci kez yorumlanır. Değişken adını çalışma anında oluşturmak gibi işler için güçlü bir araçtır, ama dışarıdan gelen bir girdi eval içine düştüğünde o girdi komut olarak çalışabilir. Çoğu durumda ${!isim} dolaylı genişletmesi, declare -n isim referansı (nameref) veya printf -v aynı işi riskleri olmadan yapar.
Bu yazıda önce eval komutunun gerçekte ne yaptığını, sonra neden tehlikeli olabileceğini ve güvenli kullanım yollarını, ardından eval gerektirmeyen dolaylı değişken tekniklerini anlatıyoruz. Örneklerin hepsi GNU Bash 5.2.37 ile çalıştırıldı. Tırnak kurallarını hatırlamak için tek tırnak ve çift tırnak farkı yazısına, serinin geri kalanı için Bash öğrenme rehberi sayfasına bakabilirsiniz.
eval ne yapar?

Kılavuzun tanımı kısadır: argümanlar boşlukla birleştirilip tek bir komut olur, Bash bu komutu okur, çalıştırır ve onun çıkış kodunu eval komutunun çıkış kodu olarak döndürür. Argüman yoksa ya da hepsi boşsa dönüş kodu 0'dır. Bunun pratik anlamı, komut satırının iki kez işlenmesidir: ilk turda Bash eval satırını her zamanki gibi genişletir; ikinci turda eval bu sonucu yeniden ayrıştırır. Aşağıdaki örnek farkı gösteriyor:
#!/usr/bin/env bash
komut='echo "iki boşluk korunur"; echo ikinci komut'
eval "$komut"
# çıktı:
# iki boşluk korunur
# ikinci komut
$komut
# çıktı: "iki boşluk korunur"; echo ikinci komut$komut yazımında değişken genişletilir ve kelimelere bölünür, ama ortaya çıkan metindeki tırnaklar ve noktalı virgül artık sözdizimi değildir; hepsi echo komutuna sıradan argüman olarak gider. eval "$komut" ise metni yeniden ayrıştırdığı için tırnaklar boşlukları korur ve noktalı virgül iki ayrı komut oluşturur. Komutu bir değişkende saklayıp sonra çalıştırma ihtiyacı çoğu zaman bir fonksiyonla ya da bir diziyle daha temiz çözülür: komut=(echo "iki boşluk") ve "${komut[@]}" yazımı, argümanları ikinci bir ayrıştırma olmadan aynen korur. Diziler için Bash dizi yazısına bakın.
Argümanların boşlukla birleştirilmesi, tırnak içine almadığınız metnin şekil kaybetmesine yol açar. İlk turda tek tırnak kaldırılır, argümanlar tek boşlukla birleşir ve ikinci turda a b artık iki ayrı kelimedir:
#!/usr/bin/env bash
eval echo 'a b' "c"
# çıktı: a b c
eval 'echo "a b"'
# çıktı: a b
eval
echo "boş eval: \$? = $?"
# çıktı: boş eval: $? = 0
eval false
echo "eval false: \$? = $?"
# çıktı: eval false: $? = 1Bu yüzden eval argümanını genellikle tek bir tırnaklı metin olarak verirsiniz. Tek tırnak, metnin ilk turda hiç dokunulmadan eval komutuna ulaşmasını sağlar; genişletmeler ikinci turda yapılır.
Klasik kullanım: adı çalışma anında oluşan değişken
eval komutunun eskiden en yaygın kullanımı, adı başka bir değişkene bağlı olan bir değişkeni okumaktı. Aşağıdaki örnekte ilk turda \$ kaçışı korunur ve $secim yerine kirmizi gelir; ikinci turda deger=$renk_kirmizi ataması yapılır:
#!/usr/bin/env bash
renk_kirmizi='#ff0000'
secim=kirmizi
eval "deger=\$renk_$secim"
echo "$deger"
# çıktı: #ff0000Çalışıyor, ama hangi $ işaretinin hangi turda genişleyeceğini kafanızda takip etmeniz gerekiyor. Aynı iş için Bash'in kendi dolaylı genişletmesi hem daha okunaklı hem de daha güvenlidir; aşağıda ona geçeceğiz.
Bash, kendi ürettiği ve yeniden okunmak üzere tasarlanmış birkaç çıktı biçimi sunar; bunlar eval için doğal ve güvenli girdilerdir. shopt -p ve set +o seçenekleri yeniden kuran komutlar yazar. Kılavuza göre ${degisken@A} dönüşümü de değişkeni nitelikleri ve değeriyle yeniden oluşturacak bir atama ya da declare komutu üretir; eval ile değerlendirildiğinde değişken aynen geri gelir. Bu tür metinler kabuğun kendisinden geldiği için tırnaklama hataları içermez. Bir değişkenin durumunu kaydedip sonra geri yüklemek istediğinizde, metni elle birleştirmek yerine bu biçimlere başvurun.
eval neden tehlikeli?
Sorun, ikinci turdaki ayrıştırmanın metnin içeriğini kod olarak yorumlamasıdır. Metin sizin yazdığınız sabit bir dize olduğu sürece bu kontrol altındadır. Metnin bir parçası kullanıcı girdisinden, bir dosyadan, bir HTTP isteğinden veya bir dosya adından geliyorsa, o parçadaki noktalı virgül, ters tırnak ya da $( ) yeni bir komut başlatır. Aşağıdaki örnekte zararsız bir echo kullanıyoruz, ama aynı yere herhangi bir komut yazılabilirdi:
#!/usr/bin/env bash
girdi='x; echo "BURADA ZARARLI BİR KOMUT ÇALIŞABİLİRDİ"'
eval "ad=$girdi"
echo "ad: $ad"
# çıktı:
# BURADA ZARARLI BİR KOMUT ÇALIŞABİLİRDİ
# ad: xBenzer bir risk source için de geçerlidir: source edilen dosya da sizin kabuğunuzda kod olarak çalışır. Ayrıntılar için source ve nokta komutu yazısına bakabilirsiniz.
eval kullanmak zorundaysanız: %q ve @Q ile tırnaklama

Bazen eval gerçekten gerekir; örneğin shopt -p çıktısını geri yüklerken ya da başka bir programın ürettiği kabuk atamalarını okurken. Değişken içeriğini eval metnine eklemeniz gerekiyorsa, onu önce yeniden girdi olarak güvenle kullanılabilecek bir biçime çevirin. Kılavuz iki araç sunar: printf '%q' argümanı "kabuk girdisi olarak yeniden kullanılabilecek" biçimde yazar; ${degisken@Q} dönüşümü de değeri aynı amaçla tırnaklar:
#!/usr/bin/env bash
girdi='x; echo "BURADA ZARARLI BİR KOMUT ÇALIŞABİLİRDİ"'
eval "ad=$(printf '%q' "$girdi")"
echo "ad: $ad"
# çıktı: ad: x; echo "BURADA ZARARLI BİR KOMUT ÇALIŞABİLİRDİ"
eval "ad2=${girdi@Q}"
[[ $ad2 == "$girdi" ]] && echo "@Q ile de değer aynen korundu"
# çıktı: @Q ile de değer aynen korundu
printf '%q\n' "$girdi"
# çıktı: x\;\ echo\ \"BURADA\ ZARARLI\ BİR\ KOMUT\ ÇALIŞABİLİRDİ\"
echo "${girdi@Q}"
# çıktı: 'x; echo "BURADA ZARARLI BİR KOMUT ÇALIŞABİLİRDİ"'İki yöntem de girdiyi tek bir veri parçası olarak korudu; noktalı virgül artık komut ayırıcı değil, değerin bir karakteri. Çıktılara bakarsanız %q ters eğik çizgiyle kaçış yaparken @Q değeri tek tırnak içine aldı. Kılavuza göre %q, gerekirse $'...' biçimini de kullanır. Bu araçlar yalnızca değer için uygundur: değişken veya fonksiyon adını kullanıcıdan alıyorsanız onu ayrıca bir desenle doğrulamanız gerekir (aşağıdaki örneğe bakın). Parametre dönüşümlerinin diğerleri için string işlemleri yazısına, printf biçimleri için echo ve printf rehberine göz atabilirsiniz.
eval yerine dolaylı genişletme: ${!isim}
Kılavuza göre ${ işaretinden hemen sonra bir ünlem gelirse (ve değişken bir nameref değilse) bir dolaylılık seviyesi eklenir: Bash ünlemden sonraki kısmın değerini yeni bir değişken adı olarak kullanır ve onun değerini getirir. Buna dolaylı genişletme (indirect expansion) denir. Kod metni yeniden ayrıştırılmadığı için girdi komut olarak çalışmaz:
#!/usr/bin/env bash
renk_kirmizi='#ff0000'
renk_mavi='#0000ff'
secim=mavi
isim="renk_$secim"
echo "${!isim}"
# çıktı: #0000ff
echo "${!isim:-varsayılan}"
# çıktı: #0000ff
isim=renk_yesil
echo "${!isim:-tanımsız renk}"
# çıktı: tanımsız renkDolaylı genişletme, :- gibi varsayılan değer operatörleriyle birlikte de çalışır; işaret edilen değişken yoksa varsayılan metin gelir. Dolaylı ad bir dizi elemanını da gösterebilir: ref='sunucular[1]' için ${!ref} ikinci elemanı, ref='sunucular[@]' için tüm elemanları verir:
#!/usr/bin/env bash
sunucular=(web db cache)
ref='sunucular[1]'
echo "${!ref}"
# çıktı: db
ref='sunucular[@]'
echo "${!ref}"
# çıktı: web db cacheÜnlemin yeri önemlidir: kılavuz, ünlemin dolaylılık anlamı taşıması için sol süslü parantezin hemen ardından gelmesi gerektiğini söyler. Ayrıca ${!dizi[@]} ve ${!onek*} biçimleri dolaylı genişletme değildir, başka anlamları vardır.
Belirli önekle başlayan değişkenleri listelemek
${!onek*} ve ${!onek@}, adı verilen önekle başlayan tüm değişkenlerin adlarını döndürür. @ biçimi çift tırnak içindeyse her ad ayrı bir kelime olur. Bunu dolaylı genişletmeyle birleştirerek bir grup ayarı tek döngüde dolaşabilirsiniz:
#!/usr/bin/env bash
renk_kirmizi='#ff0000'
renk_mavi='#0000ff'
renk_yesil='#00ff00'
for ad in "${!renk_@}"; do
echo "$ad = ${!ad}"
done
# çıktı:
# renk_kirmizi = #ff0000
# renk_mavi = #0000ff
# renk_yesil = #00ff00Sıralama genişletmenin kendi sırasıdır; belirli bir sıraya güvenmek istiyorsanız çıktıyı kendiniz sıralayın. Aynı kalıbı anahtarlı verilerde kullanmak istiyorsanız ilişkisel diziler genellikle daha iyi bir seçimdir; ilişkisel dizi yazısına bakın.
Dolaylı atama: printf -v ve declare -n
${!isim} yalnızca okumak içindir; ona değer atayamazsınız. ${!isim}=5 yazdığınızda Bash önce sol tarafı genişletir ve ortaya çıkan kelimeyi komut adı olarak çalıştırmaya çalışır. Değişken adı da bir komut satırının başında atama olarak tanınabilmesi için geçerli bir isim olmalıdır:
#!/usr/bin/env bash
isim=sayac
${!isim}=5
# çıktı: ornek.sh: line 3: =5: command not found
eval "1abc=5"
# çıktı: ornek.sh: line 5: 1abc=5: command not foundAdı değişkende tutulan bir değişkene değer atamanın iki temiz yolu vardır. printf -v ad biçim değer, çıktıyı ekrana yazmak yerine adı verilen değişkene koyar. declare -n (veya fonksiyon içinde local -n) ise bir isim referansı (nameref) oluşturur: referans üzerinde yaptığınız her okuma ve atama, kılavuzun deyişiyle, aslında referansın gösterdiği değişken üzerinde yapılır:
#!/usr/bin/env bash
isim=sayac
printf -v "$isim" '%d' 42
echo "printf -v sonrası: $sayac"
# çıktı: printf -v sonrası: 42
declare -n ref=$isim
ref=43
echo "nameref sonrası: $sayac"
# çıktı: nameref sonrası: 43
echo "${!ref}"
# çıktı: sayacSon satır kılavuzdaki bir uyumluluk kuralını gösteriyor: değişken bir nameref ise ${!ref} tam dolaylı genişletme yapmaz, referansın gösterdiği değişkenin adını döndürür. nameref'in en yaygın kullanımı, bir fonksiyonun sonucunu çağıranın seçtiği değişkene yazmasıdır:
#!/usr/bin/env bash
sonuc_yaz() {
local -n _hedef=$1
_hedef="işlem tamam: $2"
}
sonuc_yaz durum "yedek alındı"
echo "$durum"
# çıktı: işlem tamam: yedek alındıFonksiyon içindeki referans adını (_hedef) çağıranın kullanabileceği adlardan farklı seçmek iyi bir alışkanlıktır. Çağıran da _hedef adını verirse referans kendisini göstermeye çalışır ve Bash bu durumu bir uyarıyla bildirir. Kapsam kurallarının ayrıntısı için local değişken ve kapsam yazısına, declare nitelikleri için declare ve readonly yazısına bakın.
Kullanıcıdan gelen değişken adını doğrulamak
printf -v veya declare -n için hedef değişken adını dışarıdan alıyorsanız, önce adın geçerli bir kabuk değişken adı olduğunu kontrol edin. Kılavuz, adın harf, rakam ve alt çizgiden oluştuğunu ve rakamla başlayamayacağını belirtir. Bunu bir [[ =~ ]] denetimiyle sağlayabilirsiniz:
#!/usr/bin/env bash
ata() {
if [[ $1 =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
printf -v "$1" '%s' "$2"
else
echo "geçersiz değişken adı: $1" >&2
return 1
fi
}
ata port 8080 && echo "port=$port"
# çıktı: port=8080
ata 'x;echo hack' 1
# çıktı: geçersiz değişken adı: x;echo hackDüzenli ifadelerin Bash içindeki kullanımı için Bash regex eşleşme yazısına bakabilirsiniz.
eval içindeki sözdizimi hataları
eval metni çalışma anında ayrıştırıldığı için bir yazım hatası betik yüklenirken değil, o satır çalıştığında ortaya çıkar. Hata mesajında eval kelimesi görünür ve satır numarası betiğin satırını değil, değerlendirilen metnin içindeki konumu gösterir. Varsayılan modda betik devam eder; POSIX modunda ise kılavuza göre etkileşimli olmayan kabuk eval metnindeki bir sözdizimi hatasında çıkar:
#!/usr/bin/env bash
eval 'if true; then echo eksik'
echo "\$? = $?"
# çıktı:
# ornek.sh: eval: line 3: syntax error: unexpected end of file
# $? = 2bash -n ile yapılan sözdizimi kontrolü de eval içindeki metni göremez, çünkü o metin ancak çalışma anında oluşur. Bu, eval ağırlıklı betiklerin test edilmesini zorlaştıran bir başka nedendir. Hata ayıklama teknikleri için set -x ile hata ayıklama yazısına göz atın.
Hangi durumda hangi araç?

| İhtiyaç | Önerilen yöntem | eval gerekir mi |
|---|---|---|
| Adı değişkende olan değişkeni okumak | ${!isim} | Hayır |
| Adı değişkende olan değişkene yazmak | printf -v ad veya declare -n | Hayır |
| Fonksiyondan sonucu çağıranın değişkenine vermek | local -n | Hayır |
| Önekle başlayan değişken adlarını listelemek | ${!onek@} | Hayır |
| Komutu argümanlarıyla saklayıp çalıştırmak | Dizi: "${komut[@]}" | Hayır |
| shopt -p veya declare -p çıktısını geri yüklemek | eval "$kayit" | Evet, metin kabuğun kendi çıktısı |
| Kabuk kodu üreten bir aracın çıktısını uygulamak | eval, kaynağa güveniyorsanız | Evet |
Sık yapılan hatalar
- Tırnaksız eval:
eval $metinyazımı ilk turda kelime bölmesi ve joker genişletme yapar;eval "$metin"yazın. - Kullanıcı girdisini doğrudan eval etmek: Değerler için
printf '%q'veya${v@Q}, adlar için desen doğrulaması kullanın. - ${!isim} ile atama yapmaya çalışmak: Okuma içindir; yazmak için
printf -vveyadeclare -nkullanın. - Ünlemi yanlış yere koymak:
${!isim}dolaylı genişletmedir,${isim!}ise geçersizdir. - eval ile dizi taklidi yapmak:
eval "sunucu_$i=..."gibi numaralı değişkenler yerine gerçek diziler kullanın.
Sık Sorulan Sorular
eval ne zaman güvenlidir?
Değerlendirilen metnin tamamı sizin kontrolünüzde olduğunda: sabit dizeler, kabuğun kendi ürettiği shopt -p veya declare -p çıktısı ya da %q ile tırnaklanmış değerler. Kaynağını bilmediğiniz metin güvenli değildir.
${!isim} ile declare -n arasındaki fark nedir?
${!isim} her kullanımda dolaylı okuma yapar ve yalnızca okur. declare -n kalıcı bir referans oluşturur; o referans üzerinden hem okuyabilir hem yazabilir, hatta unset uygulayabilirsiniz. nameref'i kaldırmak için unset -n kullanılır.
Değişken adı değil de dizi elemanı dolaylı okunabilir mi?
Evet. Dolaylı ad dizi[1] veya dizi[@] biçiminde olabilir; örnekte ikisi de çalıştı.
eval çıkış kodu neyi gösterir?
Değerlendirilen komutun çıkış kodunu. eval false 1, boş eval 0 döndürür; sözdizimi hatasında denediğimiz sürüm 2 döndürdü.
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 · local ve nameref · declare ve readonly · tırnak farkı · string işlemleri · source komutu