SSD Nodes Learn 🎉 VPS $4.99/aydan başlayan
Rehberler Matt ConnorYazan Matt Connor · Güncellendi 2026-08-07

Bash Komut Ikamesi: $() ve Backticks Farkı

Bash komut ikamesinde $() ile backticks arasındaki temel farkları öğrenin. Subshell yapısı nedeniyle cd ve değişkenlerin neden kaybolduğunu ve iç içe yazım sorunlarını inceleyin.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, August 4, 2026.

Bash komut ikamesinin işlevi

Bash komut ikamesi, $(command) ifadesini, ilgili komutun standart çıktıya yazdırdığı metinle değiştirir. Geri tırnak işaretleri (backticks) ile yapılan eski yazım biçimi de aynı işi görür. İnsanları şaşırtan tüm durumlar iki temel gerçekten kaynaklanır: komut, subshell adı verilen ayrı bir süreçte çalıştırılır ve çıktının sonundaki tüm yeni satır karakterleri (newline) atılır.

mkdir -p /tmp/subst-demo
cd /tmp/subst-demo
printf 'alpha\nbeta\ngamma\n' > three.txt
count=$(wc -l < three.txt)
echo "$count"
3

Özelliğin tamamı bundan ibarettir. wc -l, 3 ifadesini ve ardından bir yeni satır karakterini yazdırdığında, bu yeni satır karakteri silinir ve count değişkeni istediğiniz iki karakteri tutar. wc -l < three.txt komutunun sadece bir sayı yazdırdığına dikkat edin; çünkü standart girdiyi okuyan GNU wc aracının yazdıracak bir dosya adı yoktur. Bunun yerine wc -l three.txt yazarsanız, 3 three.txt değerini yakalarsınız; bu farklı bir dizgidir ve daha sonra başarısız olan aritmetik işlemlerin yaygın bir nedenidir.

Bu kılavuzun geri kalanı, kimsenin beklemediği davranışları ele alır; çünkü bir subshell ayrı bir süreçtir ve ayrı bir süreç, komutları yazdığınız kabuğu (shell) değiştiremez.

$() kullanın ve backtick kullanımını bırakın

Her iki biçim de geçerlidir. $() POSIX standartlarında yer alır, bu nedenle dash, ash ve busybox sh bu biçimi destekler. Backtick kullanmayı gerektirecek hiçbir taşınabilirlik nedeni kalmamıştır; aksine, kullanmamak için iki somut neden mevcuttur.

Backtick'ler iç içe geçmez

echo "$(echo "$(echo hi)")"
echo "`echo `echo hi``"
hi
echo hi

İkinci satır, echo hi kelimelerini olduğu gibi yazdırdı. Shell, bir sonraki kaçış karakteri almamış backtick'i bulmak için ileriye doğru tarama yapar; bu nedenle yazdığınız ikinci backtick, ilkini kapattı. Çalıştırılan komut, argüman almayan echo oldu; bu komut boş bir satır yazdırdı ve ardından satır sonu karakteri silindi. echo hi kelimeleri düz metin olarak geride kaldı ve son backtick çifti boş bir komut çalıştırdı.

Backtick'leri iç içe geçirmek için her bir iç backtick'i kaçış karakteri ile belirtmeniz gerekir:

echo "`echo \`echo hi\``"
hi

Her ek seviye, kaçış karakteri ihtiyacını ikiye katlar. $() bu karmaşıklıkların hiçbirine ihtiyaç duymaz, çünkü ayrıştırıcı (parser) bir sınırlayıcı karakter aramak yerine parantezleri eşleştirir.

Backtick'ler komut çalışmadan önce ters eğik çizgilerinizi değiştirir

echo "$(echo 'a\\b')"
echo "`echo 'a\\b'`"
a\\b
a\b

Aynı iç komut farklı çıktı üretti. Backtick'lerin içinde shell, iç metin ayrıştırılmadan önce bir katman ters eğik çizgi kaçışını kaldırır, bu nedenle tek tırnaklar hiçbir şeyi korumadı. $() içinde ise parantezler arasındaki metin sıradan bir betik gibi ayrıştırılır, bu nedenle tek tırnaklar beklediğiniz gibi davranır. Bu durum en çok sed ve awk tek satırlık komutlarında sorun yaratır; burada kaybolan bir ters eğik çizgi, çalışan bir kalıbı sessizce farklı bir kalıba dönüştürür.

Tırnak kullanımı $() içinde yeniden başlar; bu, çift tırnakları kaçış karakteri kullanmadan iç içe geçirebileceğiniz anlamına gelir:

path=/etc/nginx/nginx.conf
echo "$(dirname "$path")"
/etc/nginx

Backtick eşdeğeri, $path etrafında \" kullanımını gerektirir. Her kaçış karakteri, hata yapmaya açık bir noktadır.

$() içindeki cd komutu neden kabuğunuzu değiştirmez

Çünkü $(...) yeni bir süreç (process) oluşturur. Alt kabuk (subshell), değişkenlerinizin ve çalışma dizininizin bir kopyasını alır. Kendi kopyası üzerinde değişiklik yapar, bir çıktı üretir ve sonlanır. Bu kopya, süreçle birlikte yok olur.

cd /tmp/subst-demo
pwd
target=$(cd /etc && pwd)
echo "$target"
pwd
/tmp/subst-demo
/etc
/tmp/subst-demo

Hiçbir şey başarısız olmadı. cd çalıştı ve alt kabuk içindeki pwd gerçekten /etc çıktısını verdi. Bu değişikliğin ana sürece geri dönmesinin bir yolu yoktur; çünkü bir alt kabuğun üst sürece aktardığı tek şey standart çıktısı ve çıkış durumudur.

Değişken atamaları da aynı şekilde davranır:

count=0
msg=$(count=99; echo "inside: $count")
echo "$msg"
echo "outside: $count"
inside: 99
outside: 0

Aynı kural, insanların çok daha sık karşılaştığı ve komut ikamesi yerine bir pipeline içeren bu hatanın nedenini de açıklar:

n=0
cat three.txt | while read -r line; do n=$((n+1)); done
echo "$n"
0

Bir pipeline'ın her aşaması kendi alt kabuğunda çalışır, bu nedenle while döngüsü n değişkeninin bir kopyasını artırdı ve ardından sonlandı. Pipeline yerine bir yönlendirme (redirect) kullanırsanız döngü doğrudan sizin kabuğunuzda çalışır:

n=0
while read -r line; do n=$((n+1)); done < three.txt
echo "$n"
3

Bash, bir pipeline'ın son aşamasını mevcut kabukta shopt -s lastpipe ile çalıştırabilir; ancak bu yalnızca iş kontrolü (job control) kapalıyken mümkündür, bu durum ise etkileşimli bir kabukta hiçbir zaman geçerli değildir. Yönlendirme kullanın.

İstem nereye gitti? $() içindeki etkileşimli komutlar

Bir komut ikamesi (command substitution), standart çıktıyı bir boruya (pipe) yönlendirir ve standart girdiye dokunmaz. Sorusunu standart çıktıya yazdıran ve ardından bir yanıt bekleyen bir program, beklemeyi değil, soruyu kaybeder. Terminal donmuş gibi görünür.

ask() { printf 'Username: '; read -r u; printf '%s\n' "$u"; }
ask
Username: deploy
deploy

İlk satırdaki deploy kelimesi sizin yazdığınızdır. Şimdi aynı fonksiyonu bir ikame içinde çalıştırın:

v=$(ask)
deploy

Yalnızca kendi tuş vuruşlarınız görünür; bunlar program tarafından değil, terminal sürücüsü tarafından yansıtılır. İstem başka bir yere gitmiştir:

echo "$v"
Username: deploy

İstem artık değişkenin içindedir ve yanıta yapışmıştır; çünkü $(), fonksiyonun standart çıktıya yazdığı her şeyi yakalamıştır. Standart girdiye hiç dokunulmadığı için yazdıklarınız yine de read'e ulaşmıştır. Bu, "betiğim donuyor ve hiçbir şey yazdırmıyor" diyen hata raporlarının tam imzasını oluşturur.

Bazı araçlar istemleri standart hataya veya doğrudan /dev/tty'ya yazar, böylece bu durumdan etkilenmezler. Birçoğu ise bunu yapmaz. Eğer bir fonksiyon mutlaka bir ikame içinde kalmalıysa, istemini standart hataya kendiniz gönderin:

ask() { printf 'Username: ' >&2; read -r u; printf '%s\n' "$u"; }
v=$(ask)
echo "$v"
Username: deploy
deploy

Standart hata yakalanmadığı için istem terminalinize ulaşır ve yalnızca yanıt v'ye yerleşir.

ls komutu $() içinde neden farklı görünür?

Çünkü ls, dosya tanımlayıcı 1 üzerinde isatty çağrısı yapar ve yanıtına göre çıktı biçimini değiştirir. Komut satırında bu tanımlayıcı terminalinizdir, bu yüzden ls isimleri satır boyunca sütunlar halinde yayar. Bir komut ikamesi içinde ise bu bir pipe (boru) hattıdır, bu yüzden ls her satıra bir isim gelecek şekilde geçiş yapar.

mkdir -p /tmp/tty-demo
cd /tmp/tty-demo
touch alpha beta delta gamma
echo "$(ls)"
alpha
beta
delta
gamma

Aynı denetim grep --color=auto içindeki renkleri ve git içindeki sayfalayıcıyı (pager) kapatır. Bu bir özelliktir. Bir betiğin, özel olarak talep etmesine gerek kalmadan kararlı ve makine tarafından okunabilir bir çıktı alması anlamına gelir.

Bu durum, insanların pipe hatları hakkında sorduğu bir soruyu da yanıtlar. Komut satırında ls | sort ve $(ls | sort) aynı çıktıyı verir, çünkü her iki durumda da ls çıktısında bir pipe hattı vardır. Komut ikamesi içinde değişen şey, pipe hattının son aşamasıdır. sort asla terminal denetimi yapmaz, bu yüzden çıktısı asla değişmez. Pipe hattının sonuna terminal duyarlı bir komut koyduğunuzda çıktı değişir; gözle test ettiğiniz bir pipe hattının, onu $() içine aldığınız anda farklı davranmasının nedeni budur.

Bundan kaynaklanan bir uyarı: kullanışlı görünse bile bir betik içerisinde ls çıktısını ayrıştırmayın (parse etmeyin). Dosya isimleri boşluk ve yeni satır karakterleri içerebilir. Bunun yerine bir glob kullanın veya read -d '' ile birlikte find -print0 tercih edin.

Sessizce kaybolan sondaki satır sonları

Komut ikamesi (command substitution), çıktının sonundaki tüm satır sonlarını kaldırır. Sadece sonuncuyu değil, hepsini.

cd /tmp/subst-demo
printf 'hello\n\n\n' > blanks.txt
wc -c < blanks.txt
v=$(cat blanks.txt)
printf '%s' "$v" | wc -c
8
5

Dosya hello ve üç satır sonu içerir, yani 8 bayttır. Değişken ise hello değerini tutar, yani 5 bayttır. Üç bayt hiçbir uyarı olmaksızın kaybolmuştur.

Bu kırpma işlemi kasıtlıdır ve genellikle faydalıdır. stamp=$(date -u +%Y%m%dT%H%M%SZ) komutunun, satır sonu içeren bir isim yerine kullanılabilir bir dosya adı parçası üretmesini sağlayan şey budur; bu nedenle bu kalıp zamanlanmış bir restic yedekleme betiği gibi yerlerde güvenlidir:

stamp=$(date -u +%Y%m%dT%H%M%SZ)
printf 'backup-%s.tar.gz\n' "$stamp"
backup-20260804T031500Z.tar.gz

Zaman damganız farklı olacaktır. Önemli olan ismin tek bir satırdan oluşmasıdır.

Bunun bedeli, bir dosyanın baytlarını tam olarak aktarmak için $() kullanamamanızdır. Eğer sondaki satır sonlarına ihtiyacınız varsa, ikame içine bir işaretçi karakter ekleyin ve sonrasında bunu kırpın:

v=$(cat blanks.txt; printf x)
v=${v%x}
printf '%s' "$v" | wc -c
8

x ifadesi satır sonlarından sonra yer alır, bu yüzden kırpılacak bir satır sonu kalmaz. ${v%x} daha sonra işaretçi karakteri kaldırır ve orijinal baytları bırakır.

İlgili iki detay daha mevcuttur. Bir dosyanın tamamını okumak için v=$(<blanks.txt), cat çalıştırmadan aynı işi yapar, çünkü bash dosyayı kendisi açar. Sondaki satır sonlarını aynı şekilde kırpar. "Here-string" yapıları ise ters yönde çalışır ve yazmadığınız bir satır sonunu ekler:

wc -c <<< 'abc'
4

Sonucu tırnak içine alın, aksi takdirde bash bunu bölecek ve globbing işlemine tabi tutacaktır

Tırnak içine alınmamış bir değişken genişletmesi, kelime bölme ve ardından yol adı genişletme işlemlerinden geçer. Tırnak içine alınmış bir genişletme ise bu işlemlerin hiçbirinden geçmez.

printf 'a b\tc\nd\n' > words.txt
echo $(cat words.txt)
echo "$(cat words.txt)"
a b c d
a b	c
d

Tırnak kullanılmadığında bash, çıktıyı varsayılan olarak boşluk, sekme ve yeni satır karakterlerinden oluşan IFS değerine göre böler ve echo bu dört parçayı tek boşluklarla birleştirir. Tırnak kullanıldığında ise metin, sekme ve dahili yeni satır karakterleri korunarak tek bir kelime olarak iletilir.

Globbing, bu sürecin daha tehlikeli olan kısmıdır:

mkdir -p /tmp/glob-demo
cd /tmp/glob-demo
touch one.txt two.txt
printf '*\n' > pattern.txt
p=$(cat pattern.txt)
echo $p
echo "$p"
one.txt pattern.txt two.txt
*

Atama işleminin kendisi güvenlidir, çünkü atamalar kelime bölme veya globbing yapmaz. Hasar, echo $p kısmında, * ifadesinin mevcut dizine göre genişletilmesiyle meydana gelir. Bir yapılandırma dosyasından desen okuyan ve tırnak işaretlerini unutan bir betik, görebildiği her dosya üzerinde işlem yapacaktır. Her genişletmeyi tırnak içine alın; bu sayede bu tür hataların tamamı ortadan kalkacaktır. Tırnak işaretlerini yalnızca kelime bölme işlemini gerçekten istediğiniz durumlarda kaldırın ki bu oldukça nadirdir.

Neden local x=$(cmd) her zaman 0 değerini döndürür?

Çünkü local başlı başına bir komuttur ve $?, içerdiği komut ikamesinin durumunu değil, local komutunun durumunu raporlar.

check_bad() { local out=$(false); echo "status: $?"; }
check_bad
status: 0

false 1 hata koduyla sonlandı, local değişkeni tanımlama işlemini başarıyla tamamladı ve 1 değeri göz ardı edildi. declare, export, typeset ve readonly komutlarının tümü aynı şekilde davranır. set -e de bu durumu yakalamayacaktır, çünkü kabuk (shell) açısından başarısız olan hiçbir şey yoktur.

Tanımlama ve atama işlemlerini birbirinden ayırın:

check_good() { local out; out=$(false); echo "status: $?"; }
check_good
status: 1

En üst seviyede yapılan basit bir atama işlemi, zaten son komut ikamesinin durumunu raporlar:

out=$(exit 3)
echo $?
3

Bu durum, en çok bir systemd servisi ve zamanlayıcısı altında çalışan bir sağlık kontrolünde önem taşır; çünkü maskelenmiş bir çıkış kodu, yapılması gereken iş gerçekleşmemiş olsa bile birimin her çalışmada başarılı raporu vermesine neden olur.

Kabuk içinde aslında ihtiyaç duyduğunuz alternatifler

Çoğu kullanıcı, veriyi bir değişkene almak istediği için $(...) komutuna başvurur. Ancak genellikle ihtiyaç duyulan şey yakalama değil, girdidir. Aşağıdaki dört yöntem, durumu mevcut kabuğunuzda korur.

Döngü üzerinde yönlendirme

cd /tmp/subst-demo
while read -r line; do printf 'got: %s\n' "$line"; done < three.txt
got: alpha
got: beta
got: gamma

Girdi için ayrı bir süreç oluşturulmaz; bu nedenle döngü gövdesinde atanan her değer, döngü sonrasında da geçerliliğini korur.

Süreç ikamesi (Process substitution)

while read -r line; do printf 'got: %s\n' "$line"; done < <(sort -r three.txt)
got: gamma
got: beta
got: alpha

<(command), komutun çıktısını okuyan /dev/fd/63 benzeri bir yol sağlar. Komut kendi sürecinde çalışmaya devam eder. while döngüsü ise ayrı bir süreçte çalışmaz; zaten amaç da budur. < <( içindeki boşluk zorunludur: <<( ifadesi bir here-document başlangıcı olarak algılanır ve ayrıştırılamaz. Süreç ikamesi bir bash özelliğidir; bu nedenle #!/bin/sh içeren bir betik, Ubuntu veya Debian üzerinde dash ile çalıştırılırsa hata verir. Bunun yerine #!/bin/bash kullanın.

Here-strings

read -r first rest <<< 'alpha beta gamma'
echo "$first"
echo "$rest"
alpha
beta gamma

<<<, bir komutun standart girdisine tek bir dizge besler. read mevcut kabuğunuzda çalışır, bu sayede her iki değişken de kullanabileceğiniz bir konumda tanımlanmış olur. read -r a b <<< "$(some-command)", tek satırlık bir çıktıdan iki alanı ayıklamanın standart yoludur.

Tüm dosyalar için mapfile

mapfile -t lines < three.txt
echo "${#lines[@]}"
echo "${lines[1]}"
3
beta

readarray olarak da yazılabilen mapfile, bir dosyayı mevcut kabuktaki bir diziye okur. -t, her öğenin sonundaki yeni satır karakterini kaldırır. Bu komut bash 4 veya daha yeni bir sürüm gerektirir; Ubuntu 24.04, bash 5.2 ile geldiğinden güncel tüm sunucu imajlarında kullanılabilir.

Betiklerinizi kaydetmeden önce kontrol listesi

  • $(command) yazın ve özel olarak bölme işlemi istemediğiniz sürece bunu "$(command)" olarak tırnak içine alın.
  • Sondaki yeni satır karakterlerinin silindiğini varsayın. Bunlara tekrar ihtiyaç duyarsanız bir işaretçi karakteri ekleyin.
  • Etkileşimli komutları yer değiştirmelerin dışında tutun veya istemlerini standart hata çıktısına gönderin.
  • out=$(command) çıkış durumunun önemli olduğu durumlarda local out ifadesini kendi satırında yazın.
  • Girdiden değişken atamak için pipe yerine yönlendirme veya süreç yer değiştirmeyi kullanın.

Bu yapılar, insanların yeni bir VPS kurarken yazdıkları ilk küçük betiklerde ortaya çıkar ve hatalar sessiz kalır. Bir istemi dosya adına kaydeden yedekleme betiği veya bir çıkış kodunu maskeleyen sağlık kontrolü, başarı rapor etmeye devam eder. Aynı betiği birden fazla sunucuda çalıştırdığınızda maliyet artar; çünkü hiç okumadığınız çıktı, artık yirmi makinede hiç okumadığınız bir çıktı haline gelir.

FAQ

$() içindeki cd komutu neden mevcut dizinimi değiştirmiyor?

$(...), komutunu çalışma dizininizin ve değişkenlerinizin bir kopyasını tutan ayrı bir süreç olan bir alt kabukta (subshell) çalıştırır. cd bu kopyayı değiştirir, ardından süreç sonlanır ve kopya silinir. Bir alt kabuk yalnızca standart çıktısını ve çıkış durumunu döndürebilir; bu nedenle dizin değişikliğinin ana kabuğa yansıması için bir mekanizma yoktur. Dizin yolunu almak istiyorsanız, bunu target=$(cd /etc && pwd) ile yakalayın ve "$target" kullanın. Kabuğunuzun dizin değiştirmesini istiyorsanız, cd komutunu herhangi bir alt kabuk içine almadan doğrudan çalıştırın.

bash içinde $() ile ters tırnaklar (backticks) arasındaki fark nedir?

Basit komutlar için aynı sonucu üretirler ancak önemli iki noktada farklılık gösterirler. $() doğrudan iç içe geçebilir çünkü ayrıştırıcı parantezleri eşleştirir; ters tırnaklar ise her iç içe geçme seviyesi için kaçış karakteri gerektirir. Ters tırnaklar ayrıca iç komut ayrıştırılmadan önce bir katman ters eğik çizgi kaçışını kaldırır, bu yüzden ` echo 'a\\b' prints a\b while $(echo 'a\\b') prints a\\b. $() is in POSIX and works in dash and busybox sh` örneklerinde görüldüğü gibi, ters tırnakların taşınabilirlik açısından bir avantajı yoktur.

Bir komut soru sorduğunda betiğim neden komut satırı istemi olmadan asılı kalıyor?

Komut ikamesi, standart çıktıyı bir boruya (pipe) yönlendirir ancak standart girdiyi terminalinize bağlı bırakır. İstemini standart çıktıya yazdıran bir programın istemi değişkene yakalanırken, arkasındaki read hala sizden girdi bekler. Terminal yalnızca yazdığınız karakterleri gösterir; bunlar terminal sürücüsü tarafından yansıtılır. Soruyu ikamenin dışına taşıyın veya istemin standart hata çıktısına yazılmasını sağlayarak printf 'Username: ' >&2 ile yakalanmamasını temin edin.

Değişkenimin sonundaki boş satırlar neden kayboldu?

Komut ikamesi, yalnızca sonuncuyu değil, sondaki tüm yeni satır karakterlerini kaldırır. printf 'hello\n\n\n' > f; v=$(cat f), dosya sekiz bayt içerirken v değişkeninin beş bayt tutmasına neden olur. Bunları korumak için ikamenin içine bir belirteç ekleyin ve sonrasında v=$(cat f; printf x) ile v=${v%x} kullanarak bu belirteci temizleyin. Belirteç yeni satırlardan sonra durduğu için, bash'in kaldırabileceği bir boşluk kalmaz.

local out=$(cmd) neden her zaman başarı rapor ediyor?

local başlı başına bir komuttur ve o satırdan sonraki $?, local komutunun değişkeni tanımlamada başarılı olup olmadığını rapor eder. İkamenin çıkış durumu tüketilir ve atılır; bu da set -e komutunun betiği durdurmayacağı anlamına gelir. declare, export, typeset ve readonly aynı şekilde davranır. local out komutunu bir satıra, out=$(cmd) komutunu bir sonraki satıra yazın; böylece $? gerçek durumu rapor edecektir.

#bash#shell-scripting#linux#subshell#coreutils