Mert Özen Her Satırda Daha İleri
DevOps Bash Scripting — #01

İlk Bash Script'i: Shebang, Çalıştırma İzni, Exit Code ve Ne Zaman Script Yazmamalı

Mert Özen 15 Kas 2026 15 dk 5 görüntülenme
İlk Bash Script'i: Shebang, Çalıştırma İzni, Exit Code ve Ne Zaman Script Yazmamalı

Bir Bash script'i shebang satırıyla yorumlayıcısını seçer, çalıştırma izniyle çalışır ve çağıranla exit code üzerinden konuşur. Bookstore API'sinin health endpoint'ini kontrol eden 10 satırlık bir script'le shebang ve dash farkını, ./script.sh, bash ve source arasındaki farkı, exit code'ları, PATH'i, script iskeletini, CRLF tuzağını ve bir işin ne zaman Python, PowerShell ya da .NET 10 file-based app'e verilmesi gerektiğini gösteriyoruz.

Sürüm notu: Bu yazıdaki örnekler Ubuntu 24.04 LTS (uzun süreli destek sürümü) üzerinde Bash 5.2.21 (Ubuntu paketi) ve kaynaktan derlenen Bash 5.3.20 ile, dash 0.5.12, curl 8.5 ve .NET SDK 10.0.115 kullanılarak denenmiştir.

Önce Tahmin Et

Bir arkadaşın sunucuda küçük bir kontrol script'i yazmış. Elle çalıştırınca sorun yok:

$ cat check.sh
#!/bin/sh
if [[ -f /etc/os-release ]]; then echo "found"; fi
$ bash check.sh
found

Aynı dosyayı chmod +x verip ./check.sh diye çalıştırıyor. Sence ekranda ne çıkar, ve echo $? kaç yazar? Cevap shebang bölümünde; ipucu: ikinci sayı, birincisinden daha tehlikeli.

Bu yazıyla Bash Scripting serisine başlıyoruz. Komut satırını zaten kullanıyorsun; yayındaki Linux ve Shell serisi sunucuda file system, permission, process ve systemd gibi konuları etkileşimli olarak anlattı. Bu seri, o komutları dosyaya yazıp her seferinde aynı şekilde çalışan, okunabilir ve bakımı yapılabilir script'lere dökmekle ilgili. İlk yazıda iskeleti kuruyoruz: shebang, çalıştırma izni, script'i çalıştırmanın üç yolu, exit code ve bir script'in standart görünümü. Son bölümde de bir işi ne zaman Bash'e değil Python'a, PowerShell'e ya da tek dosyalık bir C# uygulamasına vermek gerektiğine bakıyoruz.

İlk Script: Bookstore'un Health Endpoint'ini Kontrol Etmek

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) API'si Linux ve Shell serisindeki sunucuda 127.0.0.1:5000'de dinliyor ve /health'e {"status":"Healthy"} dönüyor. Deploy'dan sonra, cron'dan ya da bir izleme aracından "API ayakta mı?" diye soracak 10 satırlık bir script yazalım. Seri boyunca script'leri bookstore-ops adlı bir klasörde, scripts/ altında topluyoruz:

#!/usr/bin/env bash
# Bookstore API'nin health endpoint'ini kontrol eder; exit code 0 sağlıklı demek.
url="${1:-http://127.0.0.1:5000/health}"

if curl -fsS --max-time 5 -o /dev/null "$url"; then
    echo "OK $url"
else
    echo "FAIL $url" >&2
    exit 1
fi

İlk satır shebang; script'i hangi programın yorumlayacağını söylüyor. url variable'ı ilk argümanı alıyor, argüman yoksa varsayılan adresi (${1:-...} kalıbını bir sonraki yazıda ayrıntılı göreceğiz). curl'ün bayrakları burada önemli: -f HTTP 4xx/5xx cevabında curl'ün hata koduyla çıkmasını sağlıyor, -sS ilerleme çubuğunu kapatıp hata mesajını açık bırakıyor, --max-time 5 toplam süreyi sınırlıyor, -o /dev/null cevap gövdesini atıyor. if doğrudan curl'ün exit code'una bakıyor. Hata mesajı >&2 ile stderr'e gidiyor; bunun neden önemli olduğunu Üretimde Dikkat bölümünde konuşacağız.

Lab notu: Bu yazıdaki çıktılar, Ubuntu 24.04 üzerinde 127.0.0.1:5000/health'e Bookstore.Api ile aynı cevabı veren küçük bir test servisiyle alındı; /tmp/bookstore-unhealthy dosyası varken 503 dönüyor.

$ ls -l scripts/bookstore-health.sh
-rw-r--r-- 1 user user 248 Nov 15 03:30 scripts/bookstore-health.sh
$ ./scripts/bookstore-health.sh
bash: ./scripts/bookstore-health.sh: Permission denied
$ echo $?
126
$ chmod +x scripts/bookstore-health.sh
$ ./scripts/bookstore-health.sh
OK http://127.0.0.1:5000/health
$ echo $?
0

Dosyayı editörde oluşturduğunda çalıştırma izni (x) yoktur; ./ ile çalıştırmak için önce chmod +x gerekir. Permission'ların ayrıntısı, umask ve chown Linux ve Shell serisinin üçüncü yazısında. Şimdi iki hata durumunu deneyelim: dinlemeyen bir port ve 503 dönen API.

$ ./scripts/bookstore-health.sh http://127.0.0.1:5099/health
curl: (7) Failed to connect to 127.0.0.1 port 5099 after 0 ms: Couldn't connect to server
FAIL http://127.0.0.1:5099/health
$ echo $?
1
$ touch /tmp/bookstore-unhealthy
$ ./scripts/bookstore-health.sh
curl: (22) The requested URL returned error: 503
FAIL http://127.0.0.1:5000/health
$ echo $?
1

curl iki durumda farklı kodla çıkıyor (7: bağlanamadı, 22: HTTP hatası), script ise ikisini tek bir anlama indiriyor: 1, yani sağlıksız. Script'i çağıran taraf için sözleşme bu.

Shebang: Script'i Kim Çalıştıracak?

./check.sh yazdığında dosyayı bash açmaz; kernel açar. Dosya #! ile başlıyorsa satırın geri kalanını yorumlayıcı olarak alır ve script'i ona argüman olarak verir. Önce Tahmin Et'teki script'in shebang'i #!/bin/sh; Ubuntu'da /bin/sh ise bash değil:

$ ./check.sh
./check.sh: 2: [[: not found
$ echo $?
0
$ ls -l /bin/sh
lrwxrwxrwx 1 root root 4 Mar 31  2024 /bin/sh -> dash

Ubuntu ve Debian'da /bin/sh, POSIX'e (Unix benzeri sistemler için ortak shell ve komut standardı) yakın, küçük ve hızlı bir shell olan dash'e bağlıdır. [[ ]] bir bash eklentisi (bashism); dash onu tanımaz, [[ adında bir komut arar, bulamaz. Asıl tehlikeli kısım exit code: if'in koşulu başarısız olduğu için gövde atlanır, script'in son komutu if olur ve script 0 ile, yani "başarılı" diye biter. Kontrol yapılmadı ama kimse fark etmedi. bash check.sh çalıştı, çünkü o durumda yorumlayıcıyı sen seçtin ve shebang sıradan bir yorum satırı olarak atlandı.

Shebang için üç yaygın seçenek var:

  • #!/usr/bin/env bash: env, PATH'teki (komutların arandığı klasörlerin listesi) ilk bash'i bulup çalıştırır. Bash'in /bin dışında kurulu olduğu sistemlerde (macOS'ta Homebrew bash, BSD'ler (FreeBSD, OpenBSD gibi Unix türevleri), NixOS) de doğru bash'i bulur. Bu serideki varsayılan bu.
  • #!/bin/bash: Yolu sabittir; PATH'ten bağımsız olduğu için root ile çalışan ve PATH'e güvenmek istemediğin sistem script'lerinde tercih edilir. Linux dağıtımlarında bash hemen her zaman buradadır.
  • #!/bin/sh: Yalnız POSIX shell özelliklerini kullanacağına söz veriyorsun demektir. Bash'e özgü bir şey yazacaksan ([[ ]], array, ${var//a/b}) bu shebang'i kullanma.

Bash Reference Manual, satırdaki yorumlayıcı adından sonraki argümanların nasıl bölüneceğinin sistemden sisteme değiştiğini, bu yüzden birden fazla argümana güvenmenin taşınabilir olmadığını belirtiyor. Pratikte #!/usr/bin/env bash -e gibi bir satır Linux'ta çalışmaz; shell ayarlarını shebang yerine script'in içinde yapacağız.

Çalıştırmanın Üç Yolu: ./script.sh, bash script.sh ve source

Farkı görmek için bulunduğu klasörü değiştiren ve kendi PID'ini (process kimlik numarası) yazan iki satırlık bir script yeterli:

$ cat where.sh
#!/usr/bin/env bash
cd /opt
echo "script: pid=$$ pwd=$PWD"
$ echo "shell: pid=$$ pwd=$PWD"
shell: pid=10454 pwd=/home/user/lab
$ ./where.sh
script: pid=10457 pwd=/opt
$ bash where.sh
script: pid=10459 pwd=/opt
$ pwd
/home/user/lab
$ source where.sh
script: pid=10454 pwd=/opt
$ pwd
/opt

01-run-modes-tr.png

./where.sh ve bash where.sh yeni bir process açar; script'in içindeki cd o process'te kalır ve sen hâlâ eski klasördesin. source (kısa yazımı .) ise dosyadaki komutları senin shell'inde, aynı PID ile çalıştırır: cd de, tanımlanan variable'lar da sende kalır. Bu yüzden source environment dosyaları ve function library'leri için var; araç script'leri her zaman ayrı process'te çalışmalı. source edilen dosyadaki exit, script'i değil seni çıkarır:

$ cat bye.sh
echo "loading settings"
exit 3
$ bash -c 'source ./bye.sh; echo "after source"'
loading settings
$ echo $?
3
$ bash -c './bye.sh; echo "after ./bye.sh"'
loading settings
after ./bye.sh

İlk komutta after source hiç yazılmadı: exit 3 çağıran shell'i bitirdi. Etkileşimli terminalde aynı şeyi yaparsan terminal penceren ya da SSH (uzak bağlantı protokolü) oturumun kapanır.

Script'in ne yaptığını adım adım görmek için bash -x her komutu, expansion'lar yapılmış hâliyle başına + koyarak yazar; bash -n ise script'i çalıştırmadan yalnız syntax kontrolü yapar:

$ bash -x scripts/bookstore-health.sh
+ DEFAULT_URL=http://127.0.0.1:5000/health
+ main
+ '[' 0 -gt 1 ']'
+ local url=http://127.0.0.1:5000/health
+ curl -fsS --max-time 5 -o /dev/null http://127.0.0.1:5000/health
+ echo 'OK http://127.0.0.1:5000/health'
OK http://127.0.0.1:5000/health
+ exit
$ bash -n scripts/bookstore-health.sh; echo "syntax exit=$?"
syntax exit=0

Bu çıktı script'in birazdan göreceğimiz son hâlinden; main function'ını iskelet bölümünde açıklıyoruz.

Exit Code: Script'in Tek Kelimelik Cevabı

Her komut bitince 0 ile 255 arasında bir sayı döner; bash son komutun değerini $?'de tutar. 0 başarı, geri kalan her şey başarısızlık. Bash Reference Manual birkaç değere özel anlam veriyor: bulunamayan komut 127, bulunup çalıştırılamayan komut 126, N numaralı signal ile ölen komut 128+N. Yukarıda 126'yı (izin yok) gördük; PATH'te olmayan bir script'i adıyla çağırınca 127 gelir. exit'e verdiğin değerin yalnız son 8 bit'i kalır:

$ bash -c 'exit 3'; echo $?
3
$ bash -c 'exit 256'; echo $?
0
$ bash -c 'exit 300'; echo $?
44
$ bash -c 'exit -1'; echo $?
255
$ bash -c 'sleep 30 & kill -TERM $!; wait $!; echo $?'
143
$ timeout 1 sleep 5; echo $?
124

exit 256'nın 0'a dönmesi, "hata sayısını exit code olarak dönen" script'lerin 256 hatada başarılı görünmesi demek. SIGTERM (15) ile öldürülen sleep 143 (128+15) verdi; timeout ise süre dolduğunda kendi kodu olan 124'ü döner. Bu sayıları bir deploy log'unda gördüğünde neyin olduğunu anlamak için ezberlemeye değer.

Exit code'u okumanın en kısa yolu && ve ||: && sağındaki komutu yalnız soldaki başarılıysa, || yalnız başarısızsa çalıştırır.

$ ./scripts/bookstore-health.sh && echo "deploy can continue"
OK http://127.0.0.1:5000/health
deploy can continue
$ ./scripts/bookstore-health.sh http://127.0.0.1:5099/health || echo "health check failed, rolling back"
curl: (7) Failed to connect to 127.0.0.1 port 5099 after 0 ms: Couldn't connect to server
FAIL http://127.0.0.1:5099/health
health check failed, rolling back

Kendi script'lerinde anlamlı ve belgelenmiş kodlar kullan: 0 başarı, 1 genel hata, 2 yanlış kullanım (bash builtin'leri de yanlış kullanımda 2 döner). Daha fazlası gerekiyorsa script'in başındaki yorumda yaz.

PATH'e Koymak

./ yazmadan, her klasörden çalıştırmak için script'i PATH'teki bir klasöre koyman gerekir. Kendi kullanıcın için ~/.local/bin, sunucudaki herkes için /usr/local/bin (bunun için sudo install) uygun yerler. .sh uzantısını atmak yaygın bir alışkanlık; Google Shell Style Guide da çalıştırılabilir dosyalarda .sh uzantısını ya da uzantısızlığı kabul ediyor, source edilen library dosyalarında ise .sh uzantısını şart koşuyor.

$ mkdir -p ~/.local/bin && install -m 755 scripts/bookstore-health.sh ~/.local/bin/bookstore-health
$ echo "$PATH"
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
$ PATH="$HOME/.local/bin:$PATH"
$ command -v bookstore-health
/home/user/.local/bin/bookstore-health
$ bookstore-health
OK http://127.0.0.1:5000/health

install, kopyalamayla izin vermeyi tek adımda yapar. Ubuntu'nun varsayılan ~/.profile'ı, ~/.local/bin varsa onu login sırasında PATH'e ekler; klasörü yeni oluşturduysan yeniden login olana kadar elle eklemen gerekir. command -v bir adın neye çözüldüğünü gösterir; script yerine yanlış bir kopyanın çalıştığından şüphelendiğinde ilk bakacağın yer burası.

Script İskeleti: Header, main ve "$@"

10 satırlık script çalışıyor ama büyüdükçe argüman kontrolü, yardım metni ve function'lar eklenecek. Seri boyunca bütün script'ler aynı iskeleti kullanacak:

#!/usr/bin/env bash
#
# bookstore-health.sh - Bookstore API'nin health endpoint'ini kontrol eder.
#
# Kullanım:   bookstore-health.sh [URL]
# Varsayılan: http://127.0.0.1:5000/health
# Exit code:  0 sağlıklı, 1 sağlıksız ya da ulaşılamıyor, 2 yanlış kullanım

DEFAULT_URL="http://127.0.0.1:5000/health"

usage() {
    echo "Usage: ${0##*/} [URL]" >&2
}

main() {
    if [ "$#" -gt 1 ]; then
        usage
        exit 2
    fi
    local url="${1:-$DEFAULT_URL}"

    if curl -fsS --max-time 5 -o /dev/null "$url"; then
        echo "OK $url"
    else
        echo "FAIL $url" >&2
        exit 1
    fi
}

main "$@"; exit

Header yorumu üç soruya cevap veriyor: script ne yapar, nasıl çağrılır, hangi exit code ne demek. Altı ay sonra bu dosyayı açan kişi (büyük ihtimalle sen) kodu okumadan bu kadarını bilmeli. Sabitler en üstte, iş main function'ında, dosyanın son satırı da main "$@"; exit. "$@" script'e gelen bütün argümanları, boşluk içerseler bile tek tek ve bozulmadan main'e aktarır (neden tırnak içinde olduğunu bir sonraki yazıda göreceğiz). local, variable'ı function'a ait yapar. Google Shell Style Guide da en az bir function içeren script'ler için main function'ını istiyor.

$ ./scripts/bookstore-health.sh http://127.0.0.1:5000/health extra; echo "exit=$?"
Usage: bookstore-health.sh [URL]
exit=2

Son satırdaki ; exit gereksiz görünebilir; neden orada olduğunu Sık Düşülen Tuzaklar'da, çalışan bir script'in üzerine yazınca göreceğiz. İskelette bilerek olmayan bir şey de var: set -euo pipefail. Birçok şablon bu satırla başlıyor, ama davranışı sanıldığı kadar basit değil; altıncı yazıda gerçek davranışını gördükten sonra ekleyeceğiz.

Ne Zaman Script Yazmamalı

Bash, komutları ve dosyaları birbirine bağlamakta çok iyi: sunucuda kurulu, başlangıç süresi milisaniyeler, her aracı doğrudan çağırıyor. Veri işlemeye geldiğinde ise zorlanıyor. Google Shell Style Guide'a göre 100 satırı geçen ya da akışı karmaşıklaşan bir script, daha yapılı bir dile taşınmalı. Pratikte karar şöyle:

AraçEn çok işe yaradığı yerZorlandığı yer
BashKomut ve dosya orkestrasyonu, deploy/backup adımları, birkaç düzine satırJSON ve iç içe veri, karmaşık hata yönetimi, Windows
PythonJSON/CSV/HTTP işleme, karmaşık mantık, geniş standart librarySunucudaki Python sürümü ve bağımlılık yönetimi
PowerShell 7Object pipeline, Windows yönetimi, ekip zaten Windows'taysaLinux sunucularda genelde kurulu değil
.NET 10 file-based appEkip C# biliyorsa, tip güvenliği, NuGet paketleri, Bookstore koduyla paylaşımSDK gerekir, ilk çalıştırmada derleme

PowerShell'in kendisi Terminal ve Command Line serisinin dördüncü yazısında. .NET 10 ile gelen file-based app'ler son satırı ilginç kılıyor: tek bir .cs dosyası, proje dosyası olmadan, shebang ile çalışabiliyor. Aynı kontrolün C# hâli:

#!/usr/bin/env dotnet
// health.cs - aynı kontrol, .NET 10 file-based app olarak
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;

var url = args.Length > 0 ? args[0] : "http://127.0.0.1:5000/health";
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
try
{
    Health? health = await http.GetFromJsonAsync(url, HealthJson.Default.Health);
    Console.WriteLine($"{health?.Status} {url}");
    return health?.Status == "Healthy" ? 0 : 1;
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
{
    Console.Error.WriteLine($"FAIL {url}: {ex.Message}");
    return 1;
}

record Health(string Status);

[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
[JsonSerializable(typeof(Health))]
partial class HealthJson : JsonSerializerContext;
$ chmod +x health.cs
$ ./health.cs; echo "exit=$?"
Healthy http://127.0.0.1:5000/health
exit=0
$ ./health.cs http://127.0.0.1:5099/health; echo "exit=$?"
FAIL http://127.0.0.1:5099/health: Connection refused (127.0.0.1:5099)
exit=1
$ time ./health.cs
Healthy http://127.0.0.1:5000/health

real	0m0.126s
user	0m0.114s
sys	0m0.021s

Bash'teki gibi exit code ile konuşuyor, ama JSON'u gerçekten okuyup Status'a bakıyor; bash script'i yalnız HTTP koduna bakıyordu. İlk denemede GetFromJsonAsync<Health>(url) yazdım ve "Reflection-based serialization has been disabled" exception'ı aldım: .NET 10'da file-based app'ler varsayılan olarak native AOT (uygulamayı önceden makine koduna derlemek) hedefliyor ve reflection tabanlı JSON serialization kapalı geliyor. Çözüm yukarıdaki gibi source generator kullanmak ya da dosyaya #:property PublishAot=false eklemek. Süre tarafında, lab'da ilk çalıştırma (derleme dahil) yaklaşık 6 saniye sürdü, sonraki çalıştırmalar derlenmiş çıktıyı kullanarak 0,13 saniye; bash script'i 0,04 saniyede bitiyor. Kural olarak: adımlar komut çağırmaksa Bash, veriyi anlamak gerekiyorsa Python ya da C#.

Üretimde Dikkat

Script'i elle çalıştırmak kolay; asıl çalışacağı yer cron, systemd timer, CI job'ı ya da deploy aracı. Bu ortamların environment'ı senin terminalinden farklıdır: PATH daha kısadır, ~/.bashrc okunmaz, çalışma klasörü beklediğin yer değildir. Linux ve Shell serisinin systemd ve cron yazısı bu farkları gösteriyor. Script'in içinde komutlara tam yol ver ya da PATH'i en başta kendin ayarla, göreli yollara güvenme.

Exit code script'in çağıranla sözleşmesi. systemd bir service'i exit code'una göre başarısız sayar, CI adımı sıfır olmayan kodla kırmızıya döner, izleme aracı alarm üretir. Hata durumunda 0 ile çıkan bir script, hiç çalışmayan bir script'ten daha kötüdür, çünkü sorunu gizler. Önce Tahmin Et'teki check.sh tam olarak bunu yaptı.

Ağa çıkan her komuta süre sınırı koy. curl varsayılan olarak toplam süre sınırı koymaz; yanıt vermeyen bir API'ye bağlanan health check script'i, onu çağıran deploy'u da bekletir. --max-time, timeout komutu ya da aracın kendi timeout ayarı burada iş görür. Son olarak, çıktıyı ikiye ayır: sonuç stdout'a, hata ve log mesajları stderr'e. Böylece script'in çıktısını başka bir komuta pipe ile verdiğinde hata mesajları veriye karışmaz; beşinci yazıda bu ayrımı ayrıntılı kullanacağız.

Sık Düşülen Tuzaklar

Windows'ta düzenlenen dosyada CRLF (carriage return + line feed, Windows'un satır sonu). Editör ya da Git satır sonlarını \r\n yaparsa kernel shebang'deki yorumlayıcı adını bash\r diye okur. Hata mesajı shebang'e göre değişir:

$ ./crlf.sh
/usr/bin/env: ‘bash\r’: No such file or directory
/usr/bin/env: use -[v]S to pass options in shebang lines
$ ./crlf2.sh
bash: ./crlf2.sh: cannot execute: required file not found
$ file crlf.sh
crlf.sh: Bourne-Again shell script, ASCII text executable, with CRLF line terminators
$ bash crlf3.sh
crlf3.sh: line 2: cd: $'/opt/bookstore\r': No such file or directory
now in /home/user/lab

İkinci mesaj Bash 5.2'nin; aynı dosyayı Bash 5.3'ten çalıştırınca tanıdık mesaj geri geliyor: bash: ./crlf2.sh: /bin/bash^M: bad interpreter: No such file or directory. Üçüncü örnek daha sinsi: bash crlf3.sh ile shebang atlanınca \r her satırın sonuna yapışıyor, cd başarısız oluyor ve script yanlış klasörde devam ediyor. Düzeltmek için sed -i 's/\r$//' crlf.sh yeterli. Kalıcı çözüm repoda bir .gitattributes:

# Shell script'leri Windows'ta düzenlense bile LF satır sonuyla kalmalı
*.sh text eol=lf

Shebang'siz script. Dosyada #! satırı yoksa bash'ten çalıştırınca yine çalışır, çünkü bash kernel'in "bu çalıştırılabilir bir format değil" cevabını alınca dosyayı kendisi yorumlar. Aynı dosyayı başka bir program çalıştırınca durum değişir:

$ ./noshebang.sh
bash ran me
$ sh -c ./noshebang.sh
./noshebang.sh: 1: [[: not found
$ python3 -c 'import subprocess; subprocess.run(["./noshebang.sh"])' 2>&1 | tail -n 1
OSError: [Errno 8] Exec format error: './noshebang.sh'

Cron, systemd ya da bir uygulamanın başlattığı script senin terminalindeki bash'te çalışmaz. Shebang'i her zaman yaz.

Çalışan script'in üzerine yazmak. Bash script'i baştan sona okuyup sonra çalıştırmaz; satır satır okur ve dosyadaki konumunu hatırlar. Script çalışırken dosyayı yerinde değiştirirsen bash yeni dosyada eski konumdan okumaya devam eder:

$ cat live-v1.sh
#!/usr/bin/env bash
echo "step 1: stop service"
sleep 2
echo "step 2: copy files"
echo "step 3: start service"
$ ./live.sh & sleep 1; cp live-v2.sh live.sh; wait
[1] 10975
step 1: stop service
./live.sh: line 4: dded: command not found
step 1: stop service
step 2: copy files
step 3: start service
$ cp live-v1.sh live.sh
$ ./live.sh & sleep 1; cp live-v2.sh live.new && mv live.new live.sh; wait
[1] 10984
step 1: stop service
step 2: copy files
step 3: start service

live-v2.sh başına tek bir yorum satırı eklenmiş aynı script. cp dosyayı yerinde değiştirdi; bash satırın ortasından okumaya devam etti, anlamsız bir komut çalıştırdı ve "stop service" adımını ikinci kez yaptı. mv ise dosyayı yeni bir inode ile değiştirdiği için çalışan bash eski dosyayı okumayı sürdürdü. Script'leri sunucuda yerinde düzenleme; yeni sürümü yanına yazıp mv ile değiştir. İskeletteki main function'ı da yardım ediyor: function'ın gövdesi çağrılmadan önce tamamen okunduğu için çalışırken dosya değişse bile etkilenmiyor. Ama tek başına yetmiyor; aynı denemeyi main "$@" ile biten bir script'te yaptığımda main bittikten sonra bash dosyanın yeni hâlini okumaya devam etti, "step 3" satırını ikinci kez çalıştırdı ve syntax hatası verdi. Son satırı main "$@"; exit yapınca, satırın tamamı önceden okunduğu için bash main'den döner dönmez çıkıyor ve sorun kayboluyor.

Araç script'ini source ile çalıştırmak. İçinde exit ya da cd olan bir script'i source edersen SSH oturumun kapanır ya da başka bir klasörde kalırsın. source yalnız environment ve library dosyaları için.

Küçük Bir Deneme

★☆☆ check.sh'yi #!/usr/bin/env bash ile düzelt, sonra [[ -f /etc/os-release ]] koşulunu var olmayan bir dosyaya çevirip script'in exit code'unu kontrol et. Koşul yanlışken script neden yine 0 dönüyor ve doğru davranış ne olmalı?

★★☆ bookstore-health.sh'ye ikinci bir parametre ekle: saniye cinsinden timeout. Varsayılanı 5 olsun, --max-time'a bu değeri ver. Bağlantıyı kabul edip hiç cevap vermeyen bir port açmak için başka bir terminalde nc -l 127.0.0.1 5098 çalıştır, sonra timeout 3 ./scripts/bookstore-health.sh http://127.0.0.1:5098/health 10 çağrısında hangi exit code'u gördüğünü ve neden o olduğunu açıkla.

★★★ health.cs'yi uzantısız olarak ~/.local/bin/bookstore-health-cs adıyla kur ve her klasörden çalıştır. Sonra bash ve C# sürümlerini bir döngüde 20'şer kez çalıştırıp time ile karşılaştır. Hangi senaryoda C# sürümünü tercih ederdin?

Script'in iskeleti hazır, ama "$url"'deki tırnakları, ${1:-...}'i ve ${0##*/}'yi henüz açıklamadık. Bir sonraki yazıda variable'lar, quoting ve expansion'lara geçiyoruz: bash'in bir satırı çalıştırmadan önce hangi sırayla dönüştürdüğünü, word splitting'in boşluklu dosya adlarını nasıl böldüğünü ve rm -rf "$DIR/"* satırının boş bir variable ile neye dönüştüğünü göreceğiz.