Shell Script #3 – Shell Script’lerin Çalışma Mantığı

Shell script’ler, özünde birer text dosyasıdır. Derlenmemiş hâlde bulunan bir dizi instruction’dan oluşur. Bu instruction’lar; shell’in yerleşik komutları olabileceği gibi (type, cd, echo vs.), sistemde kurulu programlar da (ls, dpkg, ping vs.) olabilir.

Yaygın kanı; shell’e yazılan komutların, bir text dosyasına yazılıp script hâline getirildikten sonra çalıştırılmasından farksız olacağı yönündedir. Bu durum çoğu zaman doğru olabilir. Fakat her zaman değil 🙂

Kod örneklerini paylaştığım GitHub reposu: Shell Scripting 101

Parent ve child shell'ler
Parent ve child shell’ler

Bir shell script’in çalışma sürecini aşağıdaki şekilde örnekleyebiliriz:

  1. Komut okunur.
  2. Okunan komut aranır.
    1. Komut bir kabuk yerleşiği mi? (built-in, internal)
      1. Eğer yerleşikse, ilgili kodları çalıştırılır ve sıradaki komuta geçilir.
    2. Komut bir kabuk yerleşiği değilse, external bir programsa, bir alt process (sub process) oluşturulur (Fork. Burası çok önemli. İleride lazım olacak.). Bu sırada parent process beklemeye başlar.
      1. Çekirdek, çağrılan programı CPU’ya yükler ve çalıştırır.
      2. Alt process görevini tamamlar ve durur. Parent process (shell) tekrar ayağa kalkar.
    3. Komut internal ya da external değilse, hata verilir. (Burada tüm döngü kontrolleri, karar yapıları vs. internal komut olarak düşünülebilir.)
  3. Script’in sonu gelmediyse, tüm süreç baştan başlar. Son komut çalıştırıldıktan sonra script sonlandırılır.

Şimdi bir deneme yapalım ve mevzunun, bizi şaşırtacağını düşündüğüm bazı noktalarına değinelim.

Önümde bir shell açık, GNU Bash. Bu shell’e bazı komutlar vereceğiz ve tepkisine bakacağız:

[root@localhost bashscript]# echo $deneme

[root@localhost bashscript]# deneme="Ali"
[root@localhost bashscript]# echo $deneme
Ali

“deneme” değişkenini yazdırmayı deniyoruz. Boş değer dönüyor. Çünkü değişkenin henüz bir değeri yok. Sonrasında “deneme” değişkenine “Ali” değerini atıyoruz ve değerini yazdırabiliyor.

Buraya kadar problem yok. Şimdi bu işi bir script hâline getirip öyle deneyelim:

[root@localhost bashscript]# vi degisken.sh
[root@localhost bashscript]# chmod 744 degisken.sh
[root@localhost bashscript]# cat degisken.sh
#!/bin/bash
echo $degisken_kapsami

degisken_kapsami="Test değeri"

echo $degisken_kapsami
[root@localhost bashscript]# ./degisken.sh

Test değeri
[root@localhost bashscript]# echo $degisken_kapsami

[root@localhost bashscript]# echo $deneme
Ali

Satır satır inceleyelim:

  1. vi editörü ile dosyayı oluşturuyoruz.
  2. Çalıştırabilmek için gerekli izinleri belirtiyoruz. (744 = rwxr–r–)
  3. cat ile script’in içeriğini okuyoruz.
    1. Önce “degisken_kapsami” değişkeninin değeri yazdırılmak istenmiş.
    2. Sonrasında bu değişkene bir değer atanmış.
    3. “degisken_kapsami” değişkeninin değeri tekrar yazdırılmış.
  4. Script’i çalıştırıyoruz.
  5. Boş değer dönüyor. Çünkü değişkene henüz bir değer verilmemişti.
  6. Ekrana “Test değeri” yazdırılmış. Bu, değişkene verdiğimiz değerdi.
  7. “degisken_kapsami” değişkeninin değerini yazdırmak istiyoruz..
  8. Boş çıktı dönüyor.
  9. Önceki örnekte tanımlanan “deneme” değişkeninin değerini yazdırmak istiyoruz.
  10. “Ali” değeri sorunsuzca yazdırılıyor.

Peki neden? Shell’e yazarak tanımlayabildiğimiz bir değişken, shell tarafından hatırlandı. Değeri saklandı. Script içinde tanımlanan bir değişken ise script çalıştırıldıktan sonra erişilmez hâle geldi.

Hani shell’de yazdığımız şeyleri dosyaya yazınca shell script oluyordu? 🙂

Yukarıdakinin neredeyse aynısı bir iş yapacağız. Sadece script’i çalıştırdığımız “./degisken.sh” kısmını, “. ./degisken.sh” olarak güncelleyeceğiz:

[root@localhost bashscript]# . ./degisken.sh

Test değeri
[root@localhost bashscript]# echo $degisken_kapsami
Test değeri
[root@localhost bashscript]# echo $deneme
Ali

Yine beklediğimiz gibi önce boş değer, sonra “Test değeri” geldi. Fakat önceki örnekten farklı olarak, script bittikten sonra da “degisken_kapsami” değişkeninin değerini okuyabildik.

Linux’ta Nokta (.) ile Başlayarak Çalıştırılan Script’ler

Konunun aslında oldukça basit bir açıklaması var. Yukarıda “burası önemli” dediğim noktaya dokunuyor.

Önünüzde bir shell açık. Bash. Bir program. Çalışır durumda olduğu için de bir process. Bu process’e komutlar veriyorsunuz. Ona değişkenler öğretiyor, bu değişkenlerin değerlerini geri vermesini istiyorsunuz.

Sonrasında “./degisken.sh” gibi bir komutla, çalıştırılabilir bir dosyayı gösteriyorsunuz. Elinizdeki ana process, dosyaya bakıyor. Shebang’ini görüyor. “Bunu /bin/bash ile çalıştırmam lazım” diyor ve kendisinden bir tane daha fork’layarak subshell açıyor. Dosyanın içindeki bütün komutlar, subshell içerisinde çalıştırılıyor. Çalıştıracak daha fazla bir şey kalmadığında alt process çıkış yapıyor, ana process kaldığı yerden devam ediyor. Yani size tekrar prompt getirip sıradaki komutunuzu bekliyor.

Özetle, sizin script’inizi şu an bulunduğunuz shell değil; shell’inizin çalıştırdığı yeni bir shell çalıştırıyor. Değişkenler onun üzerinde tanımlanıyor. Çıktıları o veriyor. Script bitince de subshell ölüyor. Dolayısıyla yapılan bütün tanımlamalar boşa gidiyor.

Script’in çalıştırılmasını sağlayan komutun, başında bir “nokta (.)” ile verilmesi ise, shell’imize şunu söylüyor: “Bu dosyayı alt process değil, sen çalıştıracaksın. Bu iş için yeni bir process fork’lama, kendin yap.”

Shell vs. Subshell

Konuyu biraz daha örneklendirmeye çalışalım. Shell’de “ps” komutunu çalıştırmak istediğimde ne olur?

Yukarıda özetlemeye çalıştığımız akışa göre, bu bir external command olduğu için Bash yeni bir process fork‘lar. Bu process’in içine de “ps” programını exec‘ler.

Parent-child ilişkisini gösterecek şekilde process’lerimizi listelediğimizde çıktımız şu şekilde oluyor:

[root@localhost bashscript]# ps --forest
  PID TTY          TIME CMD
 1409 pts/0    00:00:00 bash
 1850 pts/0    00:00:00  \_ ps

Görüldüğü gibi, çıktıyı veren “ps” programı, bash’in bir alt process’i olarak çalıştırılmış.

Örneklere devam:

[root@localhost bashscript]# sleep 10000 &
[1] 1852
[root@localhost bashscript]# ps --forest
  PID TTY          TIME CMD
 1409 pts/0    00:00:00 bash
 1852 pts/0    00:00:00  \_ sleep
 1853 pts/0    00:00:00  \_ ps

İlk komutta, arkaplanda çalışmasını istediğimiz bir “sleep” çalıştırıyoruz. Ağaca baktığımızda, kullanmakta olduğumuz shell’in iki çocuğu olduğunu görüyoruz: sleep ve ps.

Şimdi bu komutu bir shell script içine yazalım. Ayrı bir shell’e geçelim ve “ps” çalıştıralım:

[root@localhost bashscript]# cat sleep.sh
#!/bin/bash

sleep 10000

ps çıktısı ise şu şekilde görünüyor:

[root@localhost ~]# ps a --forest
  PID TTY      STAT   TIME COMMAND
 2097 pts/1    Ss     0:00 -bash
 2134 pts/1    R+     0:00  \_ ps a --forest
 1409 pts/0    Ss     0:00 -bash
 2131 pts/0    S+     0:00  \_ /bin/bash ./sleep.sh
 2132 pts/0    S+     0:00      \_ sleep 10000
  956 tty1     Ss+    0:00 -bash

En altta görünen, 956 PID’li bash; sanal makinenin kendi shell’i. Açık, bekliyor. 2097 PID numaralı bash ise, benim yeni açtığım SSH bağlantısı (ayrı bir shell’e geçelim demiştim ya hani). 2097’nin altındaki 2134 ise, bize yukarıdaki çıktıyı veren “ps” process’i.

Bir de 1409 PID’li bash var. İlk SSH bağlantımız. İçerisinde “/bin/bash” çalıştırmış. Tıpkı shebang’de yazdığı gibi. Onun içindeki bash ise “sleep” programını çağırmış.

Güzel mevzu değil mi? 🙂

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir