Bash Script Öğrenme Notlarım — 3

Semih Saydam
13 min readMay 5, 2024

--

Shell(Bash) Script öğrenirken aldığım notları sizlerle paylaşmaya devam ediyorum. Hadi vakit kaybetmeden öğrenmeye devam edelim. Zaman en değerli şey ^^

Meta Karakterler

Bu kısımda script’ler içerisinde sürekli kullandığımız için bazı meta karakterleri inceleyeceğiz.

Semicolon ( ; )

Bu metakarakter komutları yan yana yazabilmenizi sağlar.

Gördüğünüz üzere date / who ve echo komutlarını çalıştırdık. Başarılıysa, başarısızsa vb. diye bakmadan ne olursun olsun çalıştırır. Bunu görebilmemiz için araya “ — -” şeklinde anlamsız bir komut ekledim, ona rağmen sonraki komut olan echo ‘----’ komutunu çalıştırdı.

Asterisk ( * )

Örneğin masaüstünüzde “uygulama_1.sh, uygulama_2.sh …” şeklinde ve bu dosyaların yanında alakasız dosyalar da var. Siz “u ile başlasın da, sonu ne olursa olsun” dediğiniz dosyaları listelemek istiyorsunuz. ls -l u* yaptığınızda bu isteğinizi asterisk(*) ile yapabilirsiniz :

Bir örnek daha yapıp pekiştirelim :

Dosyalarımız arasından c* / c*h ve *r* kullanımları ile istediğimiz yapıdaki dosyaları * yardımıyla yakalıyoruz. c*c ile başlasın sonrasında ne gelirse gelsin | c*hc ile başlasın h ile bitsin, arada ne olursa olsun | *r*r olsun sağında ve solunda ne olursa olsun. Gibi ifadeler yazabiliyoruz, bunları çeşitlendirebiliriz.

Question Mark( ? )

Soru işaretiyle herhangi bir karakteri belirtmiş oluruz. Örneğin ls -lap /bin/?? → /bin klasörü altında bildiğiniz gibi ls cp vb. gibi uygulamalarımız bulunuyordu, bu yazdığımız ifade ile /bin altındaki iki karakterli olan dosya yada klasörleri listelemiş oluyoruz :

Square Brackets( [] )

Regex yazılarımı okuduysanız, kullandığımız [0–9]’u [A-Za-z]’yi hatırlarsınız. Bunlar herhangi bir sayı, herhangi büyük veya küçük harfi ifade eder. * meta karakteriyle beraber kullanımını çokça görürsünüz. Örneğin :

Gördüğünüz üzere sayı olan kısmı aldı. Şimdi regex yazısı üzerindeki tablodan da bunu hatırlayalım ve bir örnek yapalım :

Burada ls -l /etc/[de]* ile “d veya e ile başlasın ardından ne gelirse gelebilir” gibi bir tanım yapıyoruz ve yukarıdaki çıktı oluyor. Bu metakarakterle başka bir örnek daha görelim. a veya c ile başlasın, sonrasında her şey gelebilir fakat t ile bitsin dediğinizde şöyle bir şey ortaya çıkar :

Carrot ( ^ )

“ile başlayanlar” anlamını katar. [] içinde kullanılırsa da olumsuzluk anlamı katar :

Örneğin “u ile başlamayanları bana getir” demek için aşağıdaki komut işletilebilir :

Başka bir örnek daha görelim, bu ^pattern yani string’in başlangıcı anlamında kullanılmasıyla alakalı bir örnek olsun :

Buradaki komutla /etc/ssh altında konumlanıp ; ile diğer komuta geçip, ls komutunu çalıştırıp bu komutunu çıktısını grep içine veriyoruz( | ile bunu yapıyoruz). grep komutu içinde ^s diyerek de “ssh ile başlayanlar” demiş oluyoruz.

Curly Brackets( {} )

Süslü parantezler ile komutlara birden fazla değer verebiliyor ve işlerimizi kolaylaştırabiliyoruz. Örnekler üzerinden inceleyecek olursak :

echo komutunda gördüğünüz üzere hem ali hem de veli için komut çalıştı, boşluk bırakmak için escape(\) kullandığımıza dikkat edelim. Bunu daha da anlamlı örneklerde kullanacak olursak :

touch ile Dosya oluşturma işlemi yaparken gördüğünüz üzere süslü parantez yardımıyla birden fazla dosyayı oluşturabildik 😊Burada özellikle ikinci örnekteki {a, b, c}.{1, 2, 3} kullanımını iyi inceleyelim.

Burada tabii ki bir aralık da verebiliyoruz, örneğin 15 tane dosya oluşturmak istiyorsanız şu şekilde bir aralık verilebilir :

Direkt 1'den 15'e gitmesin de aralıklı gitsin isterseniz {başlangıç, bitiş, aralık} şeklinde kullanabilirsiniz :

rm -f vb. için de {}’leri kullandığımıza ve hepsini birden silebildiğimize dikkat edelim. Klasördeki koşulsuz her şeyi silmek için alternatif olarak rm -f * da kullanabileceğimizi artık öğrendik. touch file{01..10..2}.txt komutunda 2 aralıkla artış yaptıracaktır başlangıçtan itibaren. “01” yazdığımız için artışı 03,05,… gibi yaptığına dikkat edelim :)

Tırnak işaretleri

`` komut için kullanılır. ‘’ tek tırnak ifadeyi yorumlamadan ekrana çıktı verir. “” çift tırnak ise içerisindeki ifadeyi yorumlayarak çıktısını verir.

Burada gördüğünüz üzere `` içinde komut işletebiliyoruz, bunun yanında ‘’ tırnak ve “” arasındaki fark örnekle daha anlaşılır oluyor. “” içindeki komutu işletirken ‘’ işletmiyor ve dümdüz yazıyor.

Yönlendirme (Redirection)

Bu kısımda öğreneceğimiz “yönlendirme”yi çokça görmüşsünüzdür veya şimdiden sonra göreceksinizdir. Öncelikle üç adet iletişim kanalımızı öğrenerek başlayalım :

1 INPUT  ----> Klavye < (0)
2 OUTPUT ----> EKRAN (Standart output - çıktı) > (1)
2 OUTPUT ----> EKRAN (Standart error - hata ) 2> (2)

İşletim sisteminde yukarıda görülen 3 adet iletişim kanalı var. Bu kanallardan “0” olan input kanalı klavyeyi bize temsil eder ve veri girişini sağlar. “1” ve “2" kanalları da output kanalı olup ekranı temsil eder ve veri çıkışını/ekrana çıktıyı verir. “1” kanalı hata olmayan çıktıları verirken, “2” kanalı hata verilen çıktıları verir. Yönlendirme için kullandığımız operatörler ve açıklamaları aşağıdaki gibidir :

komut > dosya      (Komutun çıktısı dosyaya yazdırılır)
komut >> dosya (Komutun çıktısı dosya sonuna yazdırılır)
komut < dosya (Komutun girdisi dosyadan okunur)
komut >| dosya (noclobber set edilmiş olsa dahi komut çıktısı dosyaya yazdırılır)
komut 2> dosya (komutun hataları dosyaya yazdırılır)
komut > dosya 2>&1 (Komutun çıktısı ve hataları aynı dosyaya yazılır)
komut &> dosya (Komutun çıktısı ve hataları aynı dosyaya yazılır)
komut &>> dosya (Komutun çıktısı ve hataları aynı dosyanın sonuna eklenir)
komut > dosya 2> dosya2 (Komutun çıktısı dosya`ya hataları dosya2`ye yazılır.)

Şimdi > ve >> için bir örnek yapalım :

history komutu ile komut geçmişimizi alabiliyorduk, bu komutu işlettikten sonra “gecmis” adında bir dosyaya bu komutun çıktılarını > ile yazmış olduk. Ardından bu dosyadaki verileri ezmeden, yeni bir veri eklemek için >> yönlendirmesini kullandık.

Yukarıdaki açıklamalarda “noclobber” set edilmesinden bahsetmişiz. set -o noclobber ile noclobber modunu etkinleştirebilirsiniz. Bu mod ile birlikte eğer dosya var ve siz bu dosyayı ezip içindeki veriyi değiştirmek istiyorsanız (>) bu mod size izin vermeyecektir. Böylelikle dosyalarınız başkaları tarafından ezilmesini engelleyebilirsiniz. Bu modu geri kapatmak için set +o noclobber komutunu işletebilirsiniz. Hadi yukarıda oluşturduğumuz “gecmis” noclobber açıkken/kapalıyken ezmeye çalışalım :

Gördüğünüz gibi kapalı durumdayken ezebildik ve istediğimiz yazıyı içine koyduk. Şimdi “1” ve “2” kanalları için bir kaç örnek yapalım :

find /etc -name network şeklinde örnek bir komut işlettiğimizde çıktısında bazı dosyalar için başarılı çalıştığını fakat bazı dosyalara yetki olmadığı için “Permission denied” hatası olduğunu görüyorsunuz, 2 kanalından hatalı çıktıları almayı bekliyoruz. Bu komuttaki sadece hatasız çıktılar için > yönlendirmesini, sadece hatalılar için 2> yönlendirmesini kullandık. Ardından hem hatalı hem hatasızları aynı anda görmek için komut > dosya 2>&1 yönlendirme kalıbını kullandık. Bununla aynı işi yapan &> yönlendirmesi ile de aynı çıktıyı alabildiğimizi kontrol ettik.

Kurallı İfadeler(Regex)

Bu kısımda sizlerle beraber terminal üzerinde bazı kurallı ifadeler yazıp, mantığını anlamaya çalışacağız.

^   Satır başı anlamına gelir.
$ Satır sonu anlamına gelir.
. Herhangi bir karakter demektir.
* Kendisinden önceki karaterleri tekrarlatır.
[] Köşeli parantez içerisindeki karakterlerden biri gelebilir.
[^] Köşeli parantez içerisindeki karakter haricinde bir karakter gelebilir.

Terminal’den words isimli hazır dosyamızı buluyoruz, bu dosya üzerindeki tüm kelimeler üzerinden regex ifadelerimizi rahatça deneyebiliriz :

usr altında bir yerde olduğunu bildiğimiz dosyanın tam konumunu find komutu ile bulduktan sonra, konuma gidip; dosya içeriğini yazdırıyoruz. Regex için işimize yarayacak bir dosya olduğunu görüyoruz. Şimdi bu dosya üzerinden bazı regex’ler deneyelim.

cat words | grep ^kara komutu ile words’un içeriğini grep komutuna | ile girdi olarak veriyoruz. Burada grep komutu ile filtreleme yapıyoruz ve gelen kelimeler arasında regex ile ihtiyacımız olan kalıp(pattern)’e uyanları filtreliyoruz. ^kara ile “kara” ile başlayanları, kara$ ile de “kara” ile bitenleri getir demiş oluyoruz. Kullanılan -i flag’i de büyük küçük harf önemsememesini söylüyoruz, dolayısıyla da artık gördüğünüz gibi “Kara” ile başlayanları da getirmeye başlıyor.

Aşağıda . ve * için örnekler verip, örnekler üzerinden anlamaya çalışalım :

Şimdi bir de -v flag’ini görelim. Bu flag ile olumsuzluk anlamı katıyoruz. Örneğin ^kar “kar” ile başlasın demek oluyorken önüne -v gelince “kar ile başlamasın” oluyor :

İçinde “kar” olacak, “kar” ile başlamayacak ve “kar” ile bitemeyeceği için; ortalarında bir yerlerde “kar” olanları getirecektir. Bulduğumuz ifadelerin satır sayısını öğrenmek için de -c flag’ini kullanabiliriz :

-n flag’i ise o satırın, satır numarasını gösterir :

Debugging(Hata Ayıklama)

Bu kısımda hatalı yeri bulmak adına incelemeler yapacağız ve hangi yollarla hata ayıklayabiliriz beraber bakacağız :) Öncelikle örnek bir kod oluşturalım :

#!/bin/bash

sayi=0

while((sayi<=10))
do
sleep 2
echo $sayi
((sayi++))
done ~

Bu kod 10'a kadar sayıyı artırıp, 2 şer saniye bekleyerek sayıyı yazdıracaktır :

Şimdi kodumuzda bilerek bir hata yapalım ve while((sayi<=10)) satırının sonundaki parantezi silelim → while((sayi<=10) kodu tekrar çalıştıralım :

Gördüğünüz üzere hata aslında while döngüsü parantezindeyken gelen hata bizi done satırına yönlendirdi. Şimdi burada aldığımız hatayı ayıklamak için 3 farklı yolumuz var, bunları inceleyelim.

Birinci yol olarak bash -x ./<script_dosya_isminiz>.sh yazıyoruz ve hatanın tam yerini saptamaya çalışıyoruz :

Komutu yazdıktan sonra görüyoruz ki sayi=0 satırı uygulanmış ama sonraki satır çalışmamış. Demek ki sonraki satırda bir sorun var diyoruz ve hatalı olan satırı yakalayabilmiş oluyoruz. Bu kod ile aslında biz debug mode’u açmış oluyoruz, bu moddayken hatanızı düzelttikten sonra kodu çalıştırırsanız da şöyle gözükür :

İkinci yol olarak aslında aynı şeyi yapacağız fakat sürekli terminalden yazmak istemezseniz direkt olarak shell script’iniz içine yazabilirsiniz :

Shell Script’iniz içindeki shebang(#!/bin/bash)’e -x yazarak da debug mode’u açabilmiş olduk.

Üçünü yol olarak biraz daha farklı bir yöntem göreceğiz. Kodun bir kısmından eminsek ve sadece belirli bir kısmını debug mode’a sokmak istiyorsak; Debug mode’u sadece orası için açmayı göreceğiz :

Gördüğünüz üzere set -x ile sadece while döngüsünden önce debug mode’u açıp set +x ile geri kapattık. sayi=0 da hata olmayacağından eminiz o yüzden sadece hata olabilecek while döngüsü için debug mode açmış olduk.

Değişken İşlemleri

Bu kısımda değişkenlerle ilgili işlemler yapacağız. İşlerimizi kolaylaştıracak ve script’lerimizin içinde kullanabileceğimiz kullanımları inceleyeceğiz. Hadi başlayalım :

Öncelikle sehir adında bir değişkene değer atadık. Ardından echo ${sehir:-karaman} şeklinde bir ifade yazdık. Bu ifade sayesinde, eğer sehir değişkeninin bir değeri yoksa “karaman” yazdıracaktır; Değişkenimizin bir değeri olduğu için “konya” yazdırdı. Peki değeri olmasaydı? Bunu denemek için de echo ${il:-karaman} yazıyoruz. il değişkeninin bir değer ataması yapılmadığı ve değeri olmadığı için “karaman” yazdığını görüyoruz. Fakat karaman yazması, il değişkenine karaman ataması yapmış olması anlamına gelmez. Eğer atama da yapsın isterseniz echo ${il:=karaman} yapmanız gerekir(zaten = genelde atama olarak kullanılır bildiğiniz gibi). Bu ifadeyi çalıştırdıktan sonra “karaman” yazdırmanın yanında il değişkenine atama da yaptığını kontrol etmek için echo ${il} yaparak kontrol ediyoruz.

:? ve :+ operatörlerine de bir bakalım. echo ${nehir:?tanimsiz} gibi bir tanımlama yaptığınızda; nehir değişkenine atanmış bir değer varsa yazdıracaktır, yoksa hata mesajı fırlatacaktır. echo ${sehir:+kirsehir} gibi bir kullanımda; sehir değişkeninin bir değeri varsa onu değil, sağ tarafa yazılan ifadeyi çıktı verir fakat o değeri değişkene atamaz(echo ${sehir}). Eğer değişkenin değeri yoksa hiçbir şey yazdırmaz. Şimdi başka kullanımlara geçelim :

il değişkenine “karaman” değerini atamıştık. Bu değerin uzunluğu çekmek için echo ${#il} şeklinde # kullanabiliriz. Eğer bir değişkenin değerinin bir kısmını almak istiyorsak; Örneğin echo ${sehir:3} yaparsak “ya” çıktısını verecektir, aynı şekilde echo ${sehir:1:3} de “ony” çıktısını verir. Bunun sebebi :

Değişkenin değerinin her harfini bir dizinin elemanları olarak düşünürsek, ilk harf “0”dan başlar. sehir:3 dediğinizde 3'ten başla sona kadar git demiş oluyoruz. sehir:1:3 de ise 1 ve 3 arasını al demiş oluyoruz. Şimdi başka bir kullanıma geçelim ⛷️:

dosya adında bir değişken oluşturup değerine bir path veriyoruz. Burada # ve % üzerinden işlemler yapacağız. Bu parametreler genellikle dosya adları veya dosya yolları manipülasyonları için kullanılıyor. # soldan başlayarak işlem yaparken, % sağdan başlar. ## ve %% uzun yolu, # ve % ise kısa yolu kullanır. Örneğin /home/semih/Desktop/test.sh ifadesinde echo ${dosya##/*/} yazdığınızda soldaki / dan başlayacaktır test.sh öncesi / a kadar gidecektir. Burayı silecek ve geriye sadece test.sh kalacaktır. echo ${dosya%%.*} örneğinde de gördüğünüz gibi farklı regex ifadeler de yazabilirsiniz. Çizimi ve kodları inceleyip diğer örnekleri de sizler anlamaya çalışın, pratik yapmış olalım :) İncelediyseniz başka kullanımlara geçelim :

Bu kullanımda ise ^ ve , kullanarak harf büyütme ve küçültme işlemleri yapıyoruz. ^^ ve ,, kullanımı tüm harfleri etkilerken, ^ ve , kullanımı ise sadece baş harfleri etkiler. Harf büyütmeyi ^ li olanlarla, küçültmeyi ise , li olanlarla yaparız. Örneğin echo ${baskent,*} kullanımı “ANKARA” kelimesinin ilk harfini küçülterek “aNKARA” yazmış olacaktır. Diğer örnekleri de sizler inceleyip, pratik yapınız lütfen ^^ Sonraki kullanımımıza ışınlanalım 🚀:

Burada ! kullanarak guzelsehir’in değeri olan baskent’i değişken olarak algılatmış olduk. ${!guzelsehir} — ${baskent} — ANKARA şeklinde bir geçiş yapmış olduk. Son olarak bir kullanımı daha inceleyelim :

Elinizde bir path var diyelim, önceki örneklerde bu path’in bir kısmını sağdan veya soldan silmiştik. Bu kullanımda ise path’in içindeki bir kısmı değiştirmek istiyoruz gibi düşünebilirsiniz. Tabii tüm kullanım alanı path ile sınır değil, yukarıda verilen 3 örnek ile bu kullanımı anlamaya çalışalım. userline değişkenine /etc/passwd altında bulunan “semihsay” kullanıcı bilgilerini attık, ardından newuserline adında bir değişkende ${userline//semih/semih/esra} şeklinde bir işlem yaptık. Bu “semihsay” yazılı yerleri “esrasay” yapıyor olacaktır. Aynı şekilde ${float/.} ile de float içindeki noktayı sildik. Bu kullanımı yaparken # ve % ile de harmanlayabiliyoruz, ${filename/#sh/bash} ile “shscript.sh” değerindeki soldaki(# sol) sh’ı gidip bash ile değiştirecektir.

Sinyaller(SIGINT, SIGTSTP, SIGKILL) ve Trap

Bu bölümde sinyalleri incelemeye çalışacağız. Bir script’i durdurmak için daha önce eminim ki Control + C kısayolunu kullanmışsınızdır. Aslında burada siz uygulamaya/script’e bir sinyal gönderiyorsunuz. Bu sinyallerin detaylarına man signal yazarak ulaşabiliriz :

Bu sinyallerden bazılarını inceleyeceğiz, incelemek adına bize process’i gösteren ve bir süre bekleyen(kesme sinyali göndermemizi bekleyecek) bir script oluşturalım :

#/bin/bash

echo "pid $$ dır"

sayi=0

while((sayi<10))
do
sleep 10
((sayi++))
echo $sayi
done

exit 0

Bu script’teki echo “pid $$ dır” kısmı $$ ile process’in id’sini bize gösterir. Her script/uygulamanın bir process id’si vardır. while kısmında ise programı bekletiyoruz.

ps -ef | grep _process-id_ ile çalışan process’ler arasından bizim script’imizi buluyoruz. Ardından çalışan script’i Ctrl + C kısayolu ile durduruyoruz ve tekrar process izleme komutunu yazdığımızda process’in kapandığını görüyoruz. Peki bu Ctrl + C neden process’i durduruyor?

Ctrl + C yaptığınızda aslında SIGINT komutu yolluyorsunuz. Bu bir “terminiate” komutudur :

Bu komutları bu kısımdaki ilk komutta görebilirsiniz. Ctrl+Z ile process’i durdurur :

SIGTSTP durdurma komutu Ctrl + Z’ye denk gelir. SIGCONT ise durdurduğunuz process’i devam ettirir :

Gördüğünüz üzere script’imizi çalıştırdıktan sonra kill -TSTP process-id (SIGTSTP yerine TSTP yazılabilir) komutu ile çalışan process’i durduruyoruz. Ardından tekrar process’i çalıştırmaya devam ettirmek için kill -CONT process-id yazıyoruz ve üst terminalde çalışmaya devam ettiğini görüyoruz.

SIGKILL sinyali ise process’i direkt öldürür, hiç acıması yoktur. Hatta şimdi öğreneceğimiz trap komutu bile bu komutu ele geçiremez.

trap komutu üst paragraftan da anlayacağız üzere komutları sinyalleri ele geçirebiliyor. Örneğin siz bir program yazdınız ve bu programı kullanıcının CTRL+Z ile durdurmasını istemiyorsunuz. Bu durumda trap ile SIGTSTP sinyalini ele geçiriyorsunuz. Örneğin :

#!/bin/bash

trap 'echo "Ctrl+C ile çıkamazsınız"' INT
trap 'echo "Ctrl+Z ile çıkamazsınız"' TSTP

echo "Çıkmak için yeter yazın"

while((1))
do
echo -n "Devam Ediyorum..."
read str
if [ "$str" = "yeter" ]
then
break
fi
done

echo "Çıkış saglandı."

Burada trap '<sinyal gelince yapılacak islem>' <ele gecirilecek sinyal> komutu ile SIGINT ve SIGTSTP sinyallerini ele geçiriyoruz, yani kullanıcı Ctrl + Z veya Ctrl + C ile sinyal yollayıp uygulamayı durduramayacak. Eğer uygulamadan çıkmak isterse, buradaki koda göre “yeter” yazması gerekecek :

Kodunuza trap ‘echo “Ctrl+C ile çıkamazsınız” ’ INT komutunu yazsanız bile etki etmeyecektir çünkü SIGKILLkomutunu ele geçiremezsiniz.

Uzun bir yolculuğun sonuna geldik. Öğrenme ve bildiklerimi aktarma yolculuklarımda bana katıldığınız için teşekkür ederim. Bu arada kaynakçada bulunan Göhkan Şengün’ün yazı serisini de vakti olanlar okuyabilir, burada öğrendiklerinizi orada farklı bakış açısıyla da pekiştirebilirsiniz. Sonraki yazılarda görüşmek üzere, sağlıkla/mutlulukla kalın :)

--

--