Shell nedir derseniz kısaca bir komut işleyici (command processor) diyebiliriz. Shell komutları kullanıcıdan komut satırından alabileceği gibi bir dosyadan da alabilir. Bu tür dosyalara script (betik) dosyası diyoruz.
Kali’de shell arayüzünü Terminal uygulaması ile başlatabiliriz.
Veya sol uygulama çubuğunun en altındaki seçeneğe tıkladıktan sonra uygulama ismini arayabiliriz.
Daha önce de belirttiğimiz gibi, normalde genel amaçlı Linux işletim sistemlerine ilk erişim root haklarına sahip olmayan bir kullanıcı ile bağlanılması önerilir. Sebebi gayet basit aslında, süper kullanıcı hakları ile ne kadar uzun süre geçirirseniz hata yaptığınızda sisteme zarar verme potansiyeliniz de artar. Ancak Kali’de özel bir kullanım ihtiyacımız var ve neredeyse sürekli root kullanıcı haklarına ihtiyacımız var. Bu nedenle başlangıçtan itibaren root kullanıcısı olarak sistemi kullanıyoruz.
Root kullanıcı prompt’u “#” işareti ile biter, normal kullanıcılarda ise bu işaret “$” işaretidir. Elbette bu bir sistemsel kural değil ve bu işaret çevresel değişkenler aracılığı ile (ör: PS1 çevresel değişkeni ile) ayarlanabilir. Ancak bu işaretler geleneksel olarak bu şekilde kullanılır. Çevresel değişken konusuna ayrıca değineceğiz. Yukarıdaki şekilden de görebileceğiniz gibi shell prompt’u root olduğumuzu hem kullanıcı adını prompt’un başında belirterek hem de # işareti ile belli ediyor.
Unix ve linux işletim sistemleri üzerinde birden fazla shell uygulaması bulabilirsiniz, bunlara csh (C shell), Bourne shell, bash (Bourne shell’in yerini alan uygulama, aynı zamanda Bourne-again Shell’in kısaltması), ksh (Korn shell) örnek olarak verilebilir. Shell uygulamaları arasında farklar nelerdir derseniz, fonksiyonaliteleri benzer olsa da her uygulama gibi kendilerine has ayar farklılıkları ve kullanım kolaylıkları bulunmaktadır. Bazı temel komutlar ve işlemler tüm shell uygulamalarında aynı olsa da kullandığınız shell ortamına göre shell ortamınızı ayarladığınız startup script dosyaları farklılaşacaktır. Bu nedenle rutin bir shell kullanıcısı iseniz ve her seferinde bazı environmental değişkenleri belirli değerlere ayarlamanız gerekiyorsa bu bilgiye ihtiyacınız olacaktır. Ancak sıradan bir kullanıcı için shell’ler arasındaki farkı bilme ihtiyacı çok düşüktür, temel linux bilgisi ile shell’inizi kullanabilirsiniz.
Kali’de Terminal uygulamasının açtığı shell uygulamasının ne olduğunu merak ederseniz “echo $0” komutunu çalıştırarak bunu görebilirsiniz.
Bu komutla ilgili olarak iki konudan bahsetmemiz lazım. Birincisi “echo” komutu, adından da anlaşılabileceği gibi bu komut kendisine parametre olarak verilen sabit değeri veya değişkenin değerini yazıyor. Böyle söylendiğinde kulağa çok basit geliyor ama o kadar temel bir işlevi yerine getiriyor ki bu nedenle shell script’lerinde, debug amaçlı olarak, bir dosyaya birşey yazmak için v.b. amaçlarla çok sık kullanılan bir komuttur. İkinci konu ise $0 parametresi. Bu parametre özel bir parametredir ve çalıştırılan uygulamaya verilen ilk parametrenin değerini içerir. Pek akla yakın gelmese de çalıştırılan uygulamaya verilen ilk parametre bu çalıştırılan uygulamanın adıdır. Sonraki parametreler $1, $2, $3 şeklinde gider. $0 parametresinin mantıklı bir kullanım alanını söyleyeyim, örneğin gömülü sistemlerde kaynak kısıtından dolayı uygulama kaynak kodları, kullanılan kütüphaneler v.d. pek çok noktada optimizasyona gitme ihtiyacı vardır. Gömülü sistemlerde standart shell komutlarının pek çoğu busybox adlı bir uygulamada barındırılır. Bu komutların tamamı da sembolik link ile (sembolik linki daha sonra açıklayacağım) “busybox” uygulama dosyası ile ilişkilendirilir. Bu komutlar çağrıldığında aslında “busybox” uygulaması çalıştırılır. “busybox” uygulaması bu parametre sayesinde aslında kullanıcının hangi komutu kullanmak istediğini anlayabilir.
Shell’e interaktif olarak erişebildiğimizden çalışma anında dinamik olarak shell uygulaması çağrılırken kullanılan parametreleri de tek tek inceleyebiliriz. Yukarıda yaptığımız işlem de shell uygulamasını çağırırken kullanılan uygulama adını görüntülemek oldu.
Peki işletim sistemi hangi shell uygulamasını başlatacağını nereden biliyor. Bu ayar “/etc/passwd” dosyası içinde her bir kullanıcı için belirtilen bir değerdir.
“cat” komutunu ve “pipe” (|) işaretini daha sonra açıklayacağım. “passwd” dosyasını da daha sonra açıklayacağım, ancak bu dosyada root kullanıcısı ile ilgili satırda en sonda “/bin/bash” ifadesine dikkatinizi çekmek istiyorum. Buradaki değer hangi kullanıcı için hangi shell ortamının kullanılacağını belirtiyor. İşte işletim sistemi bash ortamını başlatması gerektiğini bu değere bakarak anlıyor. Diğer kullanıcılar için genellikle “/usr/bin/nologin” değerini görüyoruz. Bu değer sayesinde bu kullanıcılara parola atanmışsa ve bu parola kırılmışsa bile bu kullanıcıların shell alamamaları sağlanıyor. Bu yöntem Unix ortamlarında güvenlik için kullanılan akıllıca yöntemlerden bir tanesi.
Nologin uygulamasını çağırdığımızda sadece bir mesaj yazarak sonlanıyor. Bir shell içinden farklı bir shell açıp açmadığımızı yine echo $0 komutu ile kontrol edebiliriz.
Startup Scriptleri
Her shell uygulamasının kendisine has startup script dosyaları bulunabileceğinden bahsetmiştik. Shell scripting konusundan daha sonra bahsedeceğiz, ancak startup scriptlerinin normal scriptlerden farkları şunlardır:
- Startup scriptlerinde (#!/bin/bash gibi) bir interpreter directive’i bulunmasına gerek yoktur, normal shell scriptlerinde eğer öntanımlı shell kullanılmayacaksa bu directive’in ilgili shell uygulamasına işaret edecek biçimde bulunması gereklidir.
- Startup scriptlerinin çalıştırılma haklarının tanımlanmasına gerek yoktur, shell scriptlerinin mutlaka çalıştırılabilir olması gereklidir. Bu konuya dosya erişim hakları bölümünde değineceğiz.
Kali’de bash’i kullandığımıza göre bu shell ile ilgili startup script dosyalarını inceleyelim.
İnteraktif login shell olarak başlatıldığında
Bash, interaktif login shell olarak başlatıldığında (yani diyelimki Kali’ye uzaktan ssh veya telnet gibi bir protokolle eriştiniz, login oldunuz ve bir shell ortamına ulaştınız) “/etc/profile” dosyasında bulunan komutlar çalıştırılır.
“/etc/profile” dosyasının içeriğine baktığınızda bir shell script barındırdığını görebilirsiniz. Burada yapılan ilk işlem kullanıcı id’sine göre PATH çevresel değişkeninin ayarlanması, bunun hemen altında da command prompt’un mevcut olup olmamasına göre bazı işlemler yapılıyor.
“/etc/profile” dosyası okunduktan ve çalıştırıldıktan sonra bash sırasıyla aşağıdaki dosyaları arar ve bulduğu ilk dosyadaki scripti çalıştırır:
• ~/.bash_profile
• ~/.bash_login
• ~/.profile
Neden hem .profile hem de .bash_profile v.d. dosyalar var derseniz, bu dosyaların kullanıcının $HOME dizininde bulunması gerektiğini söylemiştik. Eğer bu kullanıcı hesabı için login shell’i bash olarak ayarlanmışsa her ikisi de, farklı bir shell tanımlanırsa sadece .profile dosyasındaki komutlar çalıştırılacaktır. Bu nedenle bash’e özel syntax gerektiren komutların .bash_profile dosyasına yazılması, farklı bir shell kullanılacaksa .profile dosyasının kullanılması önerilir. Ancak bash kullanılması halinde .bash_profile dosyası varsa sadece bu dosyadaki komutlar çalıştırılacak, .profile dosyası dikkate alınmayacaktır.
Yukarıdaki dosya isimlerinde ilginç olabilecek iki konu var. Bunlardan birincisi baştaki “~” yani tilda işareti. Bu işaretin shell için özel bir anlamı vardır, o da kullanıcının HOME dizinini ifade etmesidir. HOME dizini daha önce de gördüğünüz passwd dosyasında her bir kullanıcı satırında sondan ikinci alanda belirtilen dizin adıdır. Örneğin root kullanıcısı için bu değerin “/root” olduğunu aşağıda görebilirsiniz.
İkinci ilginç konu ise dosya isimlerinin başındaki “.” yani nokta işareti. Shell de özel bir switch kullanmadan “ls” yani list komutunu çalıştırdığınızda listelediğiniz dizin veya dosya isimlerinin başında “.” geçen dosyalar listelenmez. Yani bir tür gizlenmiş dosya haline gelirler. Bu iyi niyetli olarak düşünürsek genellikle kritik dosyaların yanlışlıkla silinme veya bozulma ihtimalini azaltmak için geliştirilmiş bir yöntemdir.
Login shell’den çıkıldığında
Eğer varsa aşağıdaki script dosyası çalıştırılır:
- ~/.bash_logout
Interaktif shell olarak başlatıldığında (örneğin desktop ortamına login olduktan sonra Terminal uygulamasını başlatarak)
Eğer varsa aşağıdaki script dosyası çalıştırılır:
- ~/.bashrc
Sondaki “rc” harfleri “run commands” ifadesinin kısaltmasıdır.
Çevresel değişkenler (environmental variables)
Shell ortamında mevcut olan ilginç bir fonksiyonalite de çevresel değişkenler. Yukarıda bahsedilen startup script’lerin önemli bir bölümü de bu çevresel değişkenlerin değerlerinin belirlenmesine yönelik.
Önce çevresel değişkenler neye benzer onu görelim:
Set komutu lokal değişkenlerin oluşturulması için kullanılır. Herhangi bir argüman almadan kullanılırsa mevcut lokal ve çevresel değişkenleri listeler.
Yukarıdaki çevresel değişkenler elbette belli bir amaç için varlar. Öncelikle çevresel değişkenlerin ana kullanım alanlarından birisinde bahsedeyim. Shell scripting için çevresel değişkenler pratik bir parametre aktarım alanıdır. Bir shell oturumu içinde bir shell script’i çalıştırdığınızda shell prosesine (parent) bağlı bir alt (child) proses başlatılır. Ancak parent proseste tanımlı olan tüm çevresel değişkenler child prosese de aktarılır. Bu aktarım teknik olarak çevresel değişkenlerden oluşan bir array’in adresinin başlatılan prosese parametre olarak verilmesi şeklinde gerçekleşir. Böylece bazı önemli tanımların aktarımı için başlatılan bir script’e parametre verilmesi gerekmez. Aynı durum sadece shell script’ler için değil binary uygulamalar ve diğer dillerdeki scriptler için de geçerlidir. Shell script’ler dışındaki uygulamalardaki tek temel fark çevresel değişkenlere doğrudan erişememeleri, uygulamanın yazıldığı dilin sağladığı bir API fonksiyonunu kullanmaları gerektiğidir. Örneğin “C” dilinde bunun için “getenv” fonksiyonunun kullanılması gereklidir.
Shell script içinde ise aşağıdaki ifade ile bir çevresel değişkenin değerine erişilebilir:
Çevresel değişkenin başına eklenecek “$” işareti bu değişkenin değerine erişmek için yeterlidir. Teknik olarak yukarıda gerçekleşen işlem variable substitution işlemidir. Bu konuya daha sonra değineceğiz.
Yeni bir çevresel değişken tanımlamak içinse şu işlemlerin yapılması gereklidir:
“export” komutu ile tanımlanan bir değişken çevresel değişken olacak, bu shell içinden başlatılan başka bir proses bu çevresel değişkenin değerine erişebilecektir.
Değişken değerini aşağıdaki gibi tanımlamış olsaydım bu değişken sadece içinde bulunduğumuz shell’e özel olacaktı ve child prosesler tarafından erişilemeyecekti:
Değişken, variable substitution ve command substitution konularına shell scripting bölümünde daha detaylı değineceğiz.
Concurrent (eş zamanlı), batch (sıralı) ve conditional (koşula bağlı) çalıştırma
Shell ortamının sağladığı bir başka imkan da eş zamanlı, sıralı ve koşula bağlı çalıştırma imkanlarıdır.
Yukarıdaki işlemde birinci komut “&” işareti ile arka plana (background) atılmakta, ancak çalışmaya devam etmekte ve standart output (stdout) olarak ekranı kullanmaya devam etmektedir. (background ve stdout konusunu birazdan açıklayacağız)
İkinci komut ise ön planda (foreground) çalışmaya başlamakta ve bu şekilde devam etmektedir. Çıktılara dikkat ederseniz iki farklı prosesin çıktılarının birbirlerinin arasına girdiğini farkedebilirsiniz.
Sıralı çalıştırmada çok ilginç bir durum yok, önce birinci komut çalışıyor ve çalışmasını tamamlıyor, birinci proses tamamlandıktan hemen sonra ikinci komut çalışmaya başlıyor.
Buradaki sıralı (batch) çalıştırma script içinde komut çalıştırmaya benzer bir durum, ancak aynı satırda birden fazla komutu sırayla çalıştırmaktan bahsediyoruz.
Koşula bağlı çalıştırmada “&&” kullanılması halinde ikinci komut ancak birinci komut başarı ile sonuçlandığında (return kodu 0 olduğunda) çalışmaktadır. Eğer birinci komut başarı ile sonuçlanmazsa ikinci komut çalışmamaktadır.
Yukarıda ilk komutun hata aldığı durumda özel değişken “$?” son çalışan prosesin return kodunu içerir. Bu değere baktığımızda 0’dan farklı (1) bir değer taşıdığını görebiliriz. Bu durum prosesin hata ile sonuçlandığına işarettir.
Koşula bağlı çalıştırmada “||” kullanılması halinde ikinci komut ancak birinci komut başarısız olduğunda (return kodu 0’dan farklı olduğunda) çalışmaktadır.
Proses’leri arka plan ve ön planda çalıştırma imkanları
Bazen çalıştırmak istediğimiz proses çok uzun sürebilir. Ancak biz açmış olduğumuz shell’i kapattığımız anda bu shell’den başlattığımız prosesler de sonlanacaktır. Böyle bir durumda “nohup” komutunu kullanabiliriz. Bu “no hang up” kelimelerinin kısaltması olan bir ifadedir. Yaptığı işlem shell kapansa dahi prosesin (bilgisayar / sunucu shutdown edilmediği sürece) çalışmasına devam etmesini sağlamaktır.
Nohup komutu ile çalıştırılan komut biz shell’i kapatsaydık da çalışmaya devam edecekti. Nohup eğer çıktı (stdout) ve hata çıktısı (stderr) yönlendirmesi yapılmamışsa çalıştırdığı komutların çıktılarını içinde bulunulan dizin içinde oluşturacağı nohup.out dosyasına yazar. Eğer bu uyarıyı ve çıktının herhangi bir yere yazılmasını istemiyorsanız aşağıdaki işlemi yapmanız gerekir.
Bu basit satırın içinde ağır bir içerik var. Birincisi input ve output redirection konusu, ikincisi ise /dev/null gibi özel Unix / Linux dosyaları hakkında bilgi. Bu konularda örnekleri ayrıca vermek kaydıyla özet olarak bir giriş yapacağım.
Küçüktür (<) işareti ile belirttiğimiz dosya nohup komutu için bir input dosyası haline geliyor. Yani komut girdisini komut satırından değil de belirtilen dosya içeriğinden alıyor. Büyüktür ve ampersand (>&) işaretleri hem çalıştırılan komutun çıktısını hem de hata mesajlarını bu işaretlerden sonra belirtilen dosyaya yönlendiriyor, bu yüzden ekranda herhangi bir çıktı görmüyoruz. Stderr çıktılarının stdout çıktıları ile birlikte yönlendirilmesi için eski moda yol (2>&1) ifadesidir. Farklı bir kaynakta bu şekilde bir yönlendirme de görebilirsiniz. 2 ve 1 rakamlarının geldiği yer ise stdin file descriptor’ının 0, stdout file descriptor’ının 1 ve stderr file descriptor’ının 2 olmasıdır (Unix’te her şeyin bir dosya olduğunu hatırlayın). File descriptor nedir derseniz bu integer değer işletim sisteminin üzerinde çalışan uygulamalara bu dosyalara erişim için verdikleri “handle” değeridir. Uygulamalar bu değerleri dosyalara okuma, yazma, silme gibi erişimler için kullandıkları kütüphane fonksiyonlarına parametre olarak verirler.
/dev dizini filesystem hierarchy standard’a göre device dosyalarının tutulduğu dizin:
Unix’te her şey bir dosyadır. /dev/null ise özel bir dosyadır. Bu dosyayı kara delik olarak düşünebilirsiniz. Bu dosyaya yazdığınız her şey boşluğa gider, bu dosyadan aldığınız girdi ise hiçbir şeydir. Buna benzer diğer dosyalara şunları örnek verebiliriz:
- /dev/zero (sürekli sıfır üretir)
- /dev/random (rastgele sayı üretir)
Bir prosesi baştan arka planda başlatabileceğiniz gibi ön planda başladıktan sonra da arka plana gönderebilirsiniz. Ancak bunun için öncelikle ön planda çalışan prosesi Ctrl+Z kısayol tuşu ile duraklatmanız (suspend etmeniz) gerekir. (Teknik olarak prosese SIGTSTP sinyali göndermekten bahsediyoruz).
Yukarıda öncelikle toplam 100 defa 192.168.163.1 IP adresine ping atacak komutu başlattıktan ve çıktısını da /dev/null dosyasına yönlendirdikten sonra (evet biliyorum biraz anlamsız oldu, ama ekranı kirletmesin istedim) Ctrl+Z komutu ile bu prosesi duraklattım. Daha sonra “bg” komutu ile ön planda olan prosesi arka plana attım. Hemen ardından bu defa 100 adet ping (echo request) işlemini gerçekleştirmek üzere farklı bir komut çalıştırdım. Bunu da arka plana attıktan sonra “jobs” komutu ile arka plandaki tüm işleri listeledim. “fg %1” komutu ile bunlardan birinci sırada olanını ön plana geri aldım.
Proses’lere sinyal gönderme
Şimdi sinyal konusuna ve bunun shell ortamı ile ilgili uygulamasına geçelim. Diyelim ki shell’den bir proses’i başlattınız ama çok uzun sürdü, siz de bu proses’i sonlandırmak istiyorsunuz. Elbette shell erişimini sağlayan Terminal uygulamasını çarpı düğmesinden kapatmak bir çözüm, çünkü terminal uygulamasından başlatılan shell uygulaması (bash) ve bundan başlatılan proses’iniz birbirleri ile parent child process ilişkisine sahip ve bu nedenle terminal uygulaması kapandığında başlattığınız proses’de (nohup komutuyla başlatmadıysanız) sonlanacaktır. Ancak bu pek pratik bir yöntem değil. Onun yerine işletim sistemine artık prosesi sonlandırmak istediğinizi iletmenin daha pratik bir yolu olması lazım. Bu ve benzeri diğer ihtiyaçlar nedeniyle Linux (ve diğer işletim sistemleri) sinyal (signal) altyapısını sunuyor.
Sinyal altyapısı çalışmakta olan proses’lere bir interrupt gönderme imkanı sunuyor. Yani prosesin kendi iç dinamiklerinden bağımsız başka bir proses (veya kullanıcının yapacağı bir işlem diyelim) diğer proses’e standart bir mesaj gönderiyor. Bu teknik olarak asenkron bir mesaj ve bir software interrupt. Hardware interrupt konusu daha çok işlemci mimarisi ile birlikte anlaşılabilecek bir konu, bu yüzden gerekli genel bilgi ihtiyacı daha fazla, ayrıca shell kullanımı ile de bir ilgisi yok.
Kendisine bir sinyal gönderilen bir proses bu sinyale aşağıdaki şekillerde tepki verebilir:
- Sinyali bilerek ve isteyerek görmezden gelebilir
- Sinyali yakalayarak kendi istediği bir biçimde yanıt verebilir
- Kendi içinde hiçbir düzenleme yok ise sinyale öntanımlı tepkiyi verebilir
Yukarıdaki ilk iki işlemin nasıl yapılacağı uygulamanın geliştirildiği dil ve bu dilin kullandığı kütüphane imkanları ile ilgili bir konu, bu yüzden detayına girmeyeceğiz. Ancak bir prosesin sinyali yakalaması ve buna öntanımlı tepkinin dışında kendi istediği tepkiyi veren bir uygulama örneği olarak “hping” uygulamasını örnek verebiliriz. “hping” uygulaması Ctrl+Z tuşları ile kendisine suspend (SIGTSTP) sinyali gönderildiğinde bu sinyali gönderdiği paketlerin TTL değerlerini artırmak veya azaltmak için kullanabilmektedir. Aslında bu oldukça akıllıca bir yöntem, çünkü kullanıcıdan uygulama çalışırken girdi almak için bu asenkron kanal zaten işletim sistemi tarafından sağlanmış.
Yukarıdaki örnekte hping’i www.btrisk.com sunucusuna yönelik olarak TCP SYN (-S) bayrağı işaretli, başlangıç TTL değeri 0 (-t 0), 80 portuna (-p 80) yönelik ve Ctrl+Z tuşları ile TTL değeri her seferinde 1 artacak biçimde (-z) paket göndermeye başlıyoruz. Her Ctrl+Z tuşlarına basıldığında TTL değeri 1 artıyor. TTL değeri 2 olduğunda normalde “TTL 0 during transit” yanıtını görmeye devam etmemiz lazım çünkü www.btrisk.com sunucusu 2 hop’tan daha uzakta. Ancak görünen o ki sanal makine’den çıkan paketlerin TTL değerleri orijinal değerlerinden farklılaşarak host makinenin öntanımlı değerini alıyorlar. Bu konu kafanızı karıştırmasın diye açıklamak istedim.
Tekrar sinyal konusuna dönersek, normalde bir proses’e komut satırından bir sinyal göndermek istersek “kill” komutunu kullanabiliriz.
“kill” komutunun öntanımlı davranışı SIGTERM sinyali göndermek şeklinde, ama ismi yanıltıcı olabilir, her durumda prosesi sonlandırmak amacıyla kullanılması şart değil. Sinyal türlerini yukarıda görebilirsiniz, signal.h dosyasında sinyallerin anlamlarına ilişkin comment bilgileride görülebilir.
Arka plandaki (background) bir prosese sinyal göndermenin tek yolu “kill” komutudur. Shell ortamında ise yukarıda gördüğünüz Ctrl+Z tuşlarının yanı sıra Ctrl+C tuşları ile de ön plandaki (foreground) prosese sinyal gönderme imkanı vardır. Ctrl+C ile gönderilen sinyal SIGINT sinyali olup bu sinyalin öntanımlı etkisi prosesin terminate edilmesidir, elbette uygulama kodu içinde ele alınabilir veya ihmal edilebilir.
Yukarıda ping komutu 100 adet istek göndermek üzere başlatılmış, ancak bu sayı tamamlanmadan Ctrl+C tuşları ile ön planda bulunan bu proses sonlandırılmıştır.
Yine shell ortamından kullanılan tuş kombinasyonlarından Ctrl+D tuşlarıda sıklıkla kullanılan tuşlardandır. Bu tuş kombinasyonu aktif proses’e EOF (dosya sonu) karakteri gönderir. Bu karakterin proses üzerinde oluşturacağı etki tamamen prosesle ilgilidir. Bu tuş kombinasyonu genellikle shell’in “exit” komutuyla kapatılmadan Ctrl+D tuşları ile kapatılması için kullanılır.
Standard input (stdin), standard output (stdout), standard error (stderr), girdi ve çıktı yönlendirme
Unix’te (ve dolayısıyla Linux’ta) herşeyin bir dosya olduğundan söz etmiştik. Uygulamalar elbette dosyadan, ağdan gelen isteklerden, komut satırından, çevresel değişkenlerden v.d. kaynaklardan girdi alabilir ve bu kanallara çıktı iletebilir, ancak komut satırından kullanılan pek çok uygulama girdi olarak klavyeden komut satırına yazılan verileri kullanır ve çıktılar ile hata mesajlarını da komut satırına yazar.
Ancak shell ortamı normalde komut satırından okunan girdileri bir dosyadan okumayı ve yine çıktılar ile hata mesajlarını da bir dosyaya yazmayı destekler.
Yukarıda btrisk.txt adlı dosyanın içeriğini “cat” komutu ile görüntülüyoruz. “cat” komutu normalde stdout ve stderr hedefleri olarak shell ortamını kullanıyor.
Şimdi çıktı yönlendirme yöntemini kullanarak btrisk.txt dosyasının içeriğini cikti.txt dosyasına yazıyoruz. Gördüğünüz gibi bu işlem sırasında shell’de herhangi bir dosya içeriği görülmüyor. Daha sonra cikti.txt dosyamıza göz attığımızda btrisk.txt dosyamızın içeriğinin bu dosyaya yazılmış olduğunu görüyoruz.
Bu denemede “olmayandosya.txt” adlı bir dosyanın içeriğini listelemeye ve bu çıktıyı da “cikti.txt” dosyasına yönlendirmeye çalışıyoruz. Ancak bu defa shell’de bir hata mesajı ile karşılaşıyoruz. Demek ki uygulama hata mesajlarını stdout kanalına yazmıyor, stderr halen shell ortamını işaret ediyor. “cikti.txt” dosyamızın içeriğinde de herhangi bir veri göremiyoruz.
Hem hata mesajlarını hem de standart çıktıyı tek bir dosyaya yönlendirmek için “>” işareti yerine “&>” karakterlerini kullanabiliriz. Özellikle arka planda (background) çalıştırdığınız işlerde ön planda yaptığınız çalışmanın etkilenmemesini istiyorsanız arka plana attığınız işin hem çıktılarını hem de hata mesajlarını bir dosyaya yönlendirmek isteyebilirsiniz.
Bu şekilde yönlendirme yaptıktan sonra hata mesajının da “cikti.txt” dosyasına yazıldığını görebiliriz. Bu kullanımdan daha önce hem Unix hem de Linux ortamlarında kullanılan shell uygulamalarında desteklenen bir diğer yöntem ise çıktı yönlendirme işlemini yaptıkdan sonra komut satırının sonuna “2>&1” karakterlerini kullanmaktır. Bu komutu şöyle anlamlandırabiliriz; birinci yönlendirme işareti (>) çıktıyı yönlendirirken satırın sonundaki karakterler (2>&1) stderr’ye (dosya handle’ı 2) yazılan çıktıları da stdout’a (dosya handle’ı 1) yönlendirecek, böylece hata mesajları da aynı dosyaya yazılacaktır.
Girdi yönlendirme için çok mantıklı bir örnek bulamadım, çünkü zaten komutlar genellikle girdi olarak kendilerine parametre olarak verilen dosya isimlerini kullanabiliyorlar. Ancak yukarıdaki işlemi bir örnek olarak verirsek “btrisk.txt” dosyasını “sort” komutuna girdi olarak veriyoruz, bu işlemin çıktısını da “sortlubtrisk.txt” dosyasına yönlendiriyoruz. “sortlubtrisk.txt” dosyasına göz attığımızda satırların sıralı bir şekilde saklandığını görebiliriz.
Daha önce yaptığımız yönlendirmeler eğer bir dosya yoksa bunu oluşturuyor ve içeriğini yazıyor, ancak dosya zaten mevcutsa da içeriğini son oluşturulan içerik ile eziyordu. Eğer dosya içeriğini koruyarak yeni çıktıyı da dosyaya eklemek isterseniz bunun için ekleme (append etme) işaretini (>>) kullanabilirsiniz. Append etme işareti de eğer dosya yoksa oluşturuyor, ancak eğer dosya mevcutsa mevcut dosyanın içeriğini ezmiyor ve çıktıyı dosyanın sonuna ekliyor.
Şimdi bir sistem yöneticisinin en büyük yardımcılarından birisine geliyoruz, “pipe”lar. Pipe işareti (|) kendisinden önce çalışan komutun çıktılarını kendisinden bir sonraki komuta girdi olarak yönlendiriyor. Bir başka deyişle çıktılar bir dosyaya yönlendirilmek yerine yine shell ortamına yönlendiriliyor, ancak bir çıktılar bir başka komut tarafından yeni bir satırda yeni bir komut yazmadan hemen girdi olarak kullanılabiliyor. Bu yöntem birden fazla işlemi herhangi bir ara dosya kullanmadan tek satırda halletmemize imkan sağlıyor.
Yukarıda “ls” komutu ile içinde bulunduğumuz dizindeki dosya ve dizinleri listeliyoruz, ancak çıktıyı “wc” komutuna parametre olarak aktarıyoruz. “wc” (word count) uygulaması işlediği girdi veya dosyadaki satır sayısı, kelime sayısı ve karakter sayısını hesaplar ve bize görüntüler. Pipe sayesinde “ls” komutunun çıktısını bir dosyaya yönlendirip daha sonra bu dosyayı “wc” komutuyla işlemek yerine tek bir satırda bu işlemi gerçekleştirebildik. Buna göre içinde bulunduğumuz dizinde tam 21 adet dosya veya dizin var.
Daha önce girdi ve çıktı yönlendirme işlemine değinmiştik. Çıktı yönlendirmede çıktılar artık shell ortamında görünmüyor sadece dosyaya yazılıyordu. Pipe işareti ve “tee” komutuyla birlikte çıktılar bir dosyaya aktarılırken aynı zamanda shell ortamında da görüntülenebiliyor.
Unix ve linux sistemlerde “wc” ve “tee” gibi daha pek çok kullanışlı uygulama var, bu uygulamalara filter adı veriliyor. Bu uygulamalar dosyaları girdi olarak alabildiği gibi “pipe” vasıtasıyla bir önceki komutun çıktısı olarak shell ortamından da alabilir. Bunlardan bir kısmını aşağıda bulabilirsiniz:
- sort: Adından da anlaşılacağı üzere almış olduğu girdiyi sıralayarak çıktı üretir
- uniq: Sıralanmış bir girdi dizisinden tekrarlayan satırları çıkararak satırları tekil biçimde tekrar üretir
- grep: En sık kullanılan filtrelerden birisi olan “grep” komutu girdilerin içinde belirli bir veri yapısını arar ve bu veri yapısının bulunduğu satırın tamamını çıktı olarak tekrarlar. Yani veri yapısının görülmediği satırları eler. Veri yapısı sabit bir metin olabileceği gibi bir “regular expression” da olabilir.
- head: Genellikle büyük bir dosya inceleneceği zaman sadece ilk birkaç satırını görmek için kullanılır.
- tail: Genellikle büyük bir dosya inceleneceği zaman (örneğin bir log dosyası gibi) dosyanın sadece son birkaç satırını görmek için kullanılır. Tail komutu özellikle log dosyaları gibi sık değişen dosyaların sonuna eklenen satırların sürekli izlenmesi için “-f” switch’i ile birlikte kullanılır.
- sed: Adı “stream editor”ün kısaltması olan “sed” komutu aldığı girdi içinde gerekli değişiklik ve dönüşümleri yaparak çıktı üretir. Örneğin bir dosyanın içindeki “2015” tarihini “2016”ya dönüştürmek ve yeni bir dosyaya yazmak için şu komut kullanılabilir: “sed ‘s/2015/2016’ < girdi.txt > cikti.txt”. Tabi sed’in özellikleri bununla kısıtlı değil, kendi karakter eşleşme ve regular expression desteği de var.
- awk: awk hakkında kısaca bir tanımlama yapmak kolay değil, ama awk’ı girdi işleme ve dönüştürme için bir programlama dili de içeren bir araç olarak düşünebilirsiniz. Girdi işleme ve dönüştürme ihtiyaçlarınız karmaşıklaştığında “perl” programlama dilinden önce değerlendirilebilecek bir komuttur.
Shellshock
Bash ile ilgili belirtilmesi gereken konulardan birisi de bu shell’in sağladığı fonksiyon export etme imkanı. Öncelikle shell içinden kullanabileceğiniz bir fonksiyonu komut satırından tanımlayabiliyorsunuz. Bu işlemin syntax’ına girmeyeceğim ama kabaca açıklayacağım. Bu fonksiyonu tanımladıktan sonra export ederek artık bir çevresel değişken (fonksiyonu bir değişken olarak düşünebilirseniz) olarak tanımlıyoruz. Daha sonra mevcut shell’in içinden veya bu shell’in içindeyken çağırdığınız diğer child shell’lerden (yine bash komutuyla yeni bir shell daha açabilirsiniz ve bu bir child process olarak üstteki parent process’e bağlı olarak çalışır) bu fonksiyonu kullanabilirsiniz. Buraya kadar güzel ve diğer shell’lerden (csh, ksh, v.d.) farklı bir fonksiyonalite. Ne yazık ki bu fonksiyonalite kötüye kullanıldı ve shellshock adıyla meşhur olan açıklığa imkan tanıdı. Konuyla ilgili detaylı açıklık ve yama bilgilerine internetten ulaşabilirsiniz. Bu açıklık çevresel değişkenleri uzaktan etkileyebildiğiniz bir uygulama açıklığı ile birlikte kullanıldığında çok etkili sonuçlar doğurabiliyor, bu açıdan bash’ten bahsederken atlanmaması gereken bir konu.
<<Önceki Bölüm Sonraki Bölüm>>