Alt kabuk (subshell), Bash'in kendi sürecinin bir kopyasıdır. ( komutlar ) parantezleri, $( ) komut ikamesi, & ile arka plana atılan işler ve çok elemanlı bir borunun elemanları alt kabukta çalışır. Alt kabuk ana kabuğun değişkenlerini, fonksiyonlarını ve seçeneklerini görür, ama içinde yapılan değişiklikler ana kabuğa geri dönmez. { komutlar; } süslü parantezleri ise komutları gruplar ama alt kabuk açmaz; değişiklikler kalıcıdır.

"Döngüde artırdığım sayaç neden sıfır çıkıyor?" ve "betikte cd yaptım ama terminalim aynı klasörde kaldı" gibi soruların cevabı bu ayrımda yatar. Bu yazıda kılavuzun 3.7.3 ve 3.7.4 bölümlerine dayanarak çalışma ortamının neleri kapsadığını, alt kabuğa neyin aktarılıp neyin aktarılmadığını, export ile ortam değişkenlerini ve BASH_SUBSHELL sayacını GNU Bash 5.2.37 ile denenmiş örneklerle anlatıyoruz. Konu sırası için Bash öğrenme rehberi sayfasına bakabilirsiniz.

Çalışma ortamı neleri kapsar

Çalışma ortamı neleri kapsar

Kılavuza göre kabuğun bir çalışma ortamı (execution environment) vardır ve şunlardan oluşur: başlangıçta devralınan ve exec ile değiştirilen açık dosyalar, cd ile ayarlanan çalışma dizini, umask ile belirlenen dosya oluşturma maskesi, trap ile kurulan trap'ler, değişkenler, fonksiyonlar, set ve shopt ile açılan seçenekler, takma adlar (alias) ve $$, $PPID gibi süreç numaraları. Alt kabuk bu ortamın bir kopyasını alır. Kopyanın tek önemli farkı trap'lerdir: kabuğun yakaladığı sinyaller alt kabukta kabuğun başlangıçta devraldığı değerlere döner.

Harici bir program (yerleşik komut ya da fonksiyon olmayan bir komut) çalıştırıldığında ise çok daha dar bir ortam aktarılır: açık dosyalar ve o komuta özel yönlendirmeler, çalışma dizini, dosya maskesi, dışa aktarılmış (export edilmiş) değişkenler ve fonksiyonlar. Dışa aktarılmamış değişkenler, takma adlar ve kabuk seçenekleri harici programa geçmez. Kılavuzun vurguladığı kural ikisi için de aynıdır: alt kabukta veya ayrı ortamda yapılan değişiklikler ana kabuğun ortamını etkilemez.

( ) ile { } farkı

İki gruplama biçimi de birden fazla komutu tek bir birim gibi çalıştırır ve ikisinde de grubun çıkış durumu, listenin son komutunun durumudur. Fark, grubun nerede çalıştığıdır. Parantez bir alt kabuk açar; süslü parantez komutları mevcut kabukta çalıştırır.

bash
sayi=1
( sayi=2; echo "alt kabukta: $sayi" )
echo "parantezden sonra: $sayi"
{ sayi=3; echo "grupta: $sayi"; }
echo "süslü parantezden sonra: $sayi"
# çıktı: alt kabukta: 2
# çıktı: parantezden sonra: 1
# çıktı: grupta: 3
# çıktı: süslü parantezden sonra: 3

Kılavuz sözdizimi açısından da küçük ama önemli bir farka dikkat çeker. Parantezler kabuk işleçleridir ve boşluk olmadan da tanınır: (echo a) geçerlidir. Süslü parantezler ise ayrılmış sözcüklerdir; listeden boşlukla ayrılmaları ve listenin noktalı virgül ya da yeni satırla bitmesi gerekir. {echo a} veya { echo a } yazmak hata verir; doğrusu { echo a; } biçimidir.

Alt kabuğun en yaygın meşru kullanımı, çalışma dizinini veya seçenekleri geçici olarak değiştirmektir. Parantez içinde yapılan cd, alt kabuk bitince kendiliğinden geri alınır; cd - ile geri dönmeyi unutma riski ortadan kalkar.

bash
mkdir -p ./proje/derleme
once=$PWD
( cd ./proje/derleme && echo "alt kabukta dizin: ${PWD##*/}" )
[[ $PWD == "$once" ]] && echo "ana kabuğun dizini değişmedi"
( shopt -s nullglob; loglar=(./*.log); echo "nullglob açık, log sayısı: ${#loglar[@]}" )
loglar=(./*.log)
echo "dışarıda nullglob kapalı: ${loglar[0]}"
# çıktı: alt kabukta dizin: derleme
# çıktı: ana kabuğun dizini değişmedi
# çıktı: nullglob açık, log sayısı: 0
# çıktı: dışarıda nullglob kapalı: ./*.log

İkinci bölümde nullglob yalnızca alt kabukta açıldı; eşleşmeyen desen orada boş listeye dönüştü, dışarıda ise desen olduğu gibi kaldı. shopt seçenekleri için Bash shopt seçenekleri, desenlerin davranışı için Bash joker karakterler yazısına bakabilirsiniz.

exit alt kabukta yalnızca alt kabuğu kapatır

exit, çalıştığı kabuğu sonlandırır. Parantez içindeki bir exit bu yüzden yalnızca alt kabuğu kapatır ve verdiği değer grubun çıkış durumu olur; betik devam eder. Süslü parantez içindeki exit ise betiğin kendisini sonlandırır. Kısa hata işleyicilerinde [[ -r $f ]] || { echo "okunamadı" >&2; exit 1; } kalıbında süslü parantez kullanılmasının nedeni budur; aynı satırı parantezle yazarsanız mesaj yazılır ama betik durmaz.

bash
( echo "alt kabuktan çıkılıyor"; exit 5 )
echo "alt kabuğun durumu: $?"
[[ -r ./yok.txt ]] || { echo "dosya okunamadı, betik duruyor" >&2; exit 1; }
echo "buraya gelinmez"
# çıktı: alt kabuktan çıkılıyor
# çıktı: alt kabuğun durumu: 5
# çıktı: dosya okunamadı, betik duruyor
# $? = 1

BASH_SUBSHELL, $$ ve BASHPID

BASH_SUBSHELL, $$ ve BASHPID

Bir kodun alt kabukta mı çalıştığını anlamanın en kolay yolu BASH_SUBSHELL değişkenidir. Kılavuza göre başlangıç değeri 0'dır ve her alt kabuk ya da alt kabuk ortamı başladığında bir artar. $$ ise kılavuzun özellikle belirttiği gibi alt kabukta alt kabuğun değil, onu başlatan kabuğun süreç numarasını verir. Gerçek süreç numarasını görmek için BASHPID kullanılır; bu değer yeniden başlatılmayan alt kabuklarda $$ değerinden farklıdır.

bash
echo "ana kabuk: $BASH_SUBSHELL"
( echo "parantez: $BASH_SUBSHELL"; ( echo "iç içe parantez: $BASH_SUBSHELL" ) )
ikame=$(echo "komut ikamesi: $BASH_SUBSHELL")
echo "$ikame"
echo veri | { read -r _; echo "boru elemanı: $BASH_SUBSHELL"; }
{ echo "süslü parantez: $BASH_SUBSHELL"; }
[[ $$ == "$BASHPID" ]] && echo "ana kabukta \$\$ ile BASHPID eşit"
( [[ $$ != "$BASHPID" ]] && echo "alt kabukta \$\$ ile BASHPID farklı" )
# çıktı: ana kabuk: 0
# çıktı: parantez: 1
# çıktı: iç içe parantez: 2
# çıktı: komut ikamesi: 1
# çıktı: boru elemanı: 1
# çıktı: süslü parantez: 0
# çıktı: ana kabukta $$ ile BASHPID eşit
# çıktı: alt kabukta $$ ile BASHPID farklı

Süreç numaralarının kendisini yazdırmadık; her çalıştırmada farklı oldukları için yalnızca eşitliklerini karşılaştırdık. Bu ayrım, alt kabukta geçici dosya adı üretirken önemlidir: $$ tüm alt kabuklarda aynı değeri verdiği için paralel çalışan alt kabuklar aynı dosya adını üretebilir.

Boru içindeki değişken kaybı

Kılavuz, çok elemanlı bir borudaki her komutun kendi alt kabuğunda çalıştığını belirtir. Bu, en sık karşılaşılan alt kabuk sürprizinin kaynağıdır: bir komutun çıktısını while read döngüsüne borulayıp döngü içinde bir sayacı artırırsanız, döngü alt kabukta çalıştığı için sayaç döngü bitince kaybolur.

bash
sayac=0
printf 'a\nb\nc\n' | while read -r satir; do
  sayac=$(( sayac + 1 ))
done
echo "boru ile: $sayac"

sayac=0
while read -r satir; do
  sayac=$(( sayac + 1 ))
done < <(printf 'a\nb\nc\n')
echo "süreç ikamesi ile: $sayac"

shopt -s lastpipe
sayac=0
printf 'a\nb\nc\n' | while read -r satir; do
  sayac=$(( sayac + 1 ))
done
echo "lastpipe ile: $sayac"
# çıktı: boru ile: 0
# çıktı: süreç ikamesi ile: 3
# çıktı: lastpipe ile: 3

İki çözüm görüyorsunuz. Süreç ikamesi <( ), komutun çıktısını bir dosya gibi döngünün girdisine bağlar; döngü ana kabukta çalışır. lastpipe seçeneği ise kılavuza göre iş denetimi etkin değilken borunun son elemanını mevcut kabukta çalıştırır. Betiklerde iş denetimi varsayılan olarak kapalı olduğu için çalışır, ama etkileşimli terminalde iş denetimi açık olduğundan orada etkisizdir. Süreç ikamesi için Bash process substitution, boruların diğer ayrıntıları için Bash pipe ve PIPESTATUS yazısına bakabilirsiniz.

Aynı kural komut ikamesi için de geçerlidir. $( ) içinde çağrılan bir fonksiyon global bir değişkeni değiştirse bile bu değişiklik ikame bitince kaybolur; yalnızca fonksiyonun standart çıktısı geri gelir.

bash
sayac=0
say_ve_yaz() {
  sayac=$(( sayac + 1 ))
  echo "çağrı $sayac"
}
cikti=$(say_ve_yaz)
echo "$cikti, dışarıdaki sayac: $sayac"
say_ve_yaz > /dev/null
echo "doğrudan çağrıdan sonra sayac: $sayac"
# çıktı: çağrı 1, dışarıdaki sayac: 0
# çıktı: doğrudan çağrıdan sonra sayac: 1

Bir fonksiyondan hem değer döndürmek hem de durum değiştirmek istiyorsanız, değeri $( ) ile almak yerine declare -n ile bir isim referansı kullanmayı düşünebilirsiniz; bu kalıp Bash local değişken ve kapsam yazısında anlatılıyor. Komut ikamesinin diğer özellikleri için Bash komut ikamesi yazısına bakın.

Ortam değişkenleri ve export

Kılavuzun 3.7.4 bölümüne göre bir program başlatıldığında ona ad=değer çiftlerinden oluşan bir ortam (environment) verilir. Bash başlarken kendi ortamındaki her adı bir değişkene dönüştürür ve otomatik olarak dışa aktarılmış sayar. Yeni bir değişkeni çocuk süreçlere geçirmek için export veya declare -x kullanırsınız; unset ve export -n ortamdan çıkarır. Bir komutun önüne yazılan atama (AD=değer komut) yalnızca o komutun ortamına eklenir ve kabuğun kendi değişkenini değiştirmez. Fonksiyonlar da export -f ile dışa aktarılabilir; böylece ayrı bir Bash çağrısı tarafından başlatılan çocuk kabuklarda tanımlı olurlar.

Aşağıdaki örnekte cocuk.sh, kendisine hangi değişkenlerin ve fonksiyonların ulaştığını yazar. Ana betik ilk argüman olarak kendi SHLVL değerini geçirir; kılavuza göre bu değişken her yeni Bash örneği başladığında bir artar, bu yüzden aradaki fark iç içe kabuk derinliğini gösterir.

bash
#!/usr/bin/env bash
# cocuk.sh
echo "çocuk: YEREL=${YEREL:-yok}, ORTAK=${ORTAK:-yok}, TEK=${TEK:-yok}"
if declare -F selamla > /dev/null; then selamla; else echo "çocuk: selamla fonksiyonu yok"; fi
echo "çocuk: SHLVL farkı $(( SHLVL - $1 ))"
bash
YEREL="yalnızca kabukta"
export ORTAK="dışa aktarıldı"
selamla() { echo "çocuk: selamla çalıştı"; }

bash ./cocuk.sh "$SHLVL"
echo "---"
export -f selamla
TEK="yalnızca bu komut için" bash ./cocuk.sh "$SHLVL"
echo "ana kabukta TEK: ${TEK:-tanımsız}"
# çıktı: çocuk: YEREL=yok, ORTAK=dışa aktarıldı, TEK=yok
# çıktı: çocuk: selamla fonksiyonu yok
# çıktı: çocuk: SHLVL farkı 1
# çıktı: ---
# çıktı: çocuk: YEREL=yok, ORTAK=dışa aktarıldı, TEK=yalnızca bu komut için
# çıktı: çocuk: selamla çalıştı
# çıktı: çocuk: SHLVL farkı 1
# çıktı: ana kabukta TEK: tanımsız

YEREL hiçbir zaman çocuğa ulaşmadı, çünkü dışa aktarılmadı. TEK yalnızca ikinci çağrının ortamına eklendi ve ana kabukta tanımsız kaldı. BASH_SUBSHELL ile SHLVL farklı şeyleri sayar: ilki aynı Bash sürecinin kopyalarını, ikincisi yeni başlatılan Bash örneklerini. Değişken atamanın ve export komutunun temeli Bash değişkenler ve atama yazısında.

Trap'ler ve errexit alt kabukta

Alt kabuk ortamı kopyalarken trap'leri olduğu gibi almaz. Kılavuza göre kabuğun yakaladığı sinyaller alt kabukta kabuğun başlangıçta devraldığı değerlere döner; yok sayılan sinyaller ise yok sayılmaya devam eder. Ana kabuğun EXIT trap'i bu nedenle alt kabuk bittiğinde çalışmaz. Benzer biçimde POSIX modu dışında komut ikamesi için açılan alt kabuklarda -e seçeneği temizlenir; bu davranışı shopt -s inherit_errexit değiştirir.

bash
trap 'echo "EXIT trap: BASH_SUBSHELL=$BASH_SUBSHELL"' EXIT
( echo "alt kabuk çalıştı ve bitti" )
echo "ana kabuk bitiyor"
# çıktı: alt kabuk çalıştı ve bitti
# çıktı: ana kabuk bitiyor
# çıktı: EXIT trap: BASH_SUBSHELL=0

EXIT trap'i yalnızca bir kez, ana kabuk bitince çalıştı. Alt kabuğun kendi temizliğe ihtiyacı varsa trap'i parantezin içinde ayrıca kurmanız gerekir. Trap kuralları Bash trap ile sinyal ve temizlik, errexit ve alt kabuk ilişkisi Bash set -euo pipefail yazısında.

Grubun tamamına yönlendirme

Kılavuz, bir bileşik komuta bağlanan yönlendirmenin içindeki tüm komutlara uygulandığını belirtir. Birden fazla komutun çıktısını tek bir dosyaya yazmanın en temiz yolu, komutları bir grup içine alıp yönlendirmeyi bir kez yapmaktır. Bunun için alt kabuk açmak gerekmez; { } yeterlidir ve grup içindeki atamalar da kalıcı olur.

bash
{
  echo "rapor başlığı"
  satir_sayisi=2
  echo "satır sayısı: $satir_sayisi"
} > ./rapor.txt
echo "dosya içeriği:"
cat ./rapor.txt
echo "grup sonrası değişken: $satir_sayisi"
# çıktı: dosya içeriği:
# çıktı: rapor başlığı
# çıktı: satır sayısı: 2
# çıktı: grup sonrası değişken: 2

Yönlendirme işleçlerinin tamamı Bash yönlendirme yazısında anlatılıyor.

Hangi yapı alt kabuk açar: özet

YapıAlt kabuk açar mıDeğişiklikler ana kabukta kalır mı
( liste )EvetHayır
{ liste; }HayırEvet
$( komut ) ve ters tırnakEvetHayır, yalnızca çıktı döner
komut &EvetHayır
komut1 | komut2Her eleman için evet (lastpipe ile son eleman hariç)Hayır
Fonksiyon çağrısıHayırEvet (local olmayan değişkenler)
bash betik.sh veya ./betik.shYeni Bash örneğiHayır
source betik.shHayırEvet

Sık yapılan hatalar

Sık yapılan hatalar: Borulanmış while döngüsünde sayaç artırmak., Hata işleyicisinde ( ) kullanmak., Fonksiyonu $( ) içinde çağırıp global değişikliği beklemek., Değişkeni export...
  • Borulanmış while döngüsünde sayaç artırmak. Döngü alt kabukta çalışır; süreç ikamesi veya lastpipe kullanın.
  • Hata işleyicisinde ( ) kullanmak. || ( echo hata; exit 1 ) yalnızca alt kabuğu kapatır; { ...; exit 1; } yazın.
  • Fonksiyonu $( ) içinde çağırıp global değişikliği beklemek. Değişiklikler ikame bitince kaybolur.
  • Değişkeni export etmeden çocuk betikte kullanmak. Dışa aktarılmayan değişkenler harici programlara geçmez.
  • { } sözdizimini boşluksuz yazmak. Süslü parantezler ayrılmış sözcüktür; { komut; } biçiminde boşluk ve noktalı virgül gerekir.
  • Alt kabukta $$ değerini benzersiz sanmak. $$ ana kabuğun numarasıdır; alt kabuğun kendi numarası BASHPID değişkenindedir.

Sık Sorulan Sorular

Alt kabuk performansı etkiler mi?

Alt kabuk, kabuk sürecinin bir kopyasıdır ve kılavuz harici komutların ayrı bir ortamda çalıştığını belirtir. Çok sık tekrarlanan bir döngüde her adımda komut ikamesi ya da boru kullanmak, her seferinde yeni bir kopya oluşturmak anlamına gelir. Basit işler için parametre genişletmesi gibi kabuğun kendi içinde çalışan araçları tercih etmek bu maliyeti azaltır; ayrıntılar Bash string işlemleri yazısında.

Alt kabukta hesaplanan bir değeri ana kabuğa nasıl aktarırım?

Alt kabuk değişkenleri geri gönderemez, ama çıktı üretebilir. En yaygın yol, değeri standart çıktıya yazıp ana kabukta deger=$( ... ) ile yakalamaktır. Birden fazla değer gerekiyorsa her birini ayrı satıra yazıp mapfile veya read ile okuyabilir, ya da alt kabuğa hiç ihtiyaç duymayacak biçimde kodu süslü parantez veya süreç ikamesiyle yeniden düzenleyebilirsiniz. Çıkış durumu ise küçük bir sayı olarak her zaman $? ile geri gelir.

Fonksiyon gövdesini parantezle yazabilir miyim?

Evet. Kılavuza göre fonksiyon gövdesi herhangi bir bileşik komut olabilir; f() ( komutlar ) biçiminde yazılan bir fonksiyon her çağrıldığında alt kabukta çalışır. Bu, fonksiyonun içindeki cd ve değişken değişikliklerinin dışarı taşmasını kesin olarak engeller, ama fonksiyon artık global değişkenleri değiştiremez. Fonksiyon tanımlama biçimleri için Bash fonksiyon tanımlama yazısına bakın.

Arka plandaki bir işin girdisi nereden gelir?

Kılavuza göre iş denetimi etkin değilken ve açık bir yönlendirme yoksa, & ile başlatılan bir komutun standart girdisi boş dosya olan /dev/null olur. Arka plandaki iş klavyeden okumaya çalışırsa veri alamaz. Arka plan işlerinin yönetimi Bash arka plan işleri yazısında.

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 · PIPESTATUS ve lastpipe · Process substitution · Komut ikamesi · Bash değişkenler · source komutu