Cron dostu komut satırı aracı tasarımı: argparse, çıkış kodları ve kuru çalıştırma
Cron dostu komut satırı aracı tasarımı: argparse iskeleti, sysexits uyumlu çıkış kodları, stdout/stderr ayrımı, --dry-run modu ve flock ile güvenli crontab satırı.
Komut satırı aracı tasarımı, bir otomasyon betiğinin yalnız elle çalıştırıldığında değil, gece üçte cron tarafından başlatıldığında da doğru davranmasını sağlayan kararların toplamıdır. Çoğu otomasyon küçük bir Python dosyası olarak başlar: dosyayı çalıştırırsınız, rapor üretilir, iş biter. Aynı dosya zamanlayıcıya bağlandığında ise kimse ekranı izlemez, yanlış giden bir şeyi soracak kişi yoktur ve betiğin yapabileceği tek şey çıkış koduyla ve günlüğüyle konuşmaktır.
Bu yüzden cron dostu bir araç ile elle çalıştırılan bir betik arasındaki fark arayüzde ve çıkış davranışında ortaya çıkar. Araç parametrelerini açık biçimde alır, girdisi eksikse soru sormaz, başarıyı sessizce ve hatayı gürültüyle bildirir, aynı dakikada iki kez başlatılırsa birbirine karışmaz ve ne yapacağını önce göstermeden yapmaz.
Bu yazıda bu davranışları adım adım kuruyorum: cron dostu aracın ölçütleri, argparse ile öngörülebilir bir arayüz, anlamlı çıkış kodları ve hata sınıfları, kuru çalıştırma ile stdout ve stderr ayrımı, son olarak kilitli ve günlüğe yazan bir crontab satırı. Örnekler Python 3.8 ve üzeri sürümlerle, standart kütüphane dışında bağımlılık olmadan çalışır.
Cron dostu araç nedir
Cron dostu araç, terminal ve klavye olmadan, önceden belirlenmiş parametrelerle başlatılıp sonucunu çıkış kodu ve standart akışlar üzerinden bildiren bir programdır. Etkileşimli soru sormaz, çalışma dizinine güvenmez, başarılı olduğunda gereksiz çıktı üretmez ve başarısız olduğunda nedenini tek bakışta anlaşılır biçimde yazar. Zamanlayıcı yalnız bu sözleşmeyi görür; betiğin içinde ne kadar iyi kod olduğunu bilmez.
Cron ortamının elle çalıştırmadan farkları somuttur ve her biri klasik bir arızaya karşılık gelir:
- Kısıtlı ortam:
PATHkısadır, kabuk profilleri yüklenmez;pythonkomutu bulunamaz ya da sanal ortam dışındaki yorumlayıcı çalışır. - Terminal yok:
input()çağrısı anında hata verir, renkli çıktı günlüğe kaçış kodu olarak düşer. - Göreli yol yok: çalışma dizini kullanıcının ev dizinidir;
veri/giris.csvgibi yollar başka yere bakar. - Çakışma: önceki çalıştırma bitmeden yenisi başlar ve aynı dosyaya iki süreç yazar.
- Sessiz hata: çıktı hiçbir yere yönlendirilmezse hata mesajı kaybolur ya da yerel posta kutusunda birikir.
Araç bu beş durumu kendi tarafında kapatır: yolları mutlak alır ya da yapılandırmadan okur, etkileşim gerektirmez, günlüğünü stderr'e yazar ve çalışma sonucunu çıkış koduyla bildirir. Kilit ve yönlendirme ise crontab satırının işidir; son bölümde ikisini birleştiriyoruz.
argparse ile öngörülebilir arayüz
argparse, Python standart kütüphanesinin komut satırı argümanlarını tanımlayan, doğrulayan ve otomatik yardım metni üreten modülüdür. Cron dostu araçta argparse'ın değeri yalnız kolaylık değildir: tip dönüşümünü ve varsayılan değerleri tek yerde toplar, eksik ya da bozuk argümanda programı kullanım hatası koduyla durdurur ve aracın altı ay sonra --help ile kendini açıklamasını sağlar.
Aşağıdaki iskelet, bir klasördeki satış CSV dosyalarını günlük rapora çeviren varsayımsal bir araç için arayüzü kurar. Tarih argümanı özel bir dönüştürücüyle doğrulanır; yanlış biçim, yığın izi yerine tek satırlık bir kullanım hatası üretir:
import argparse
import datetime as dt
import pathlib
def tarih(metin):
"""YYYY-AA-GG biçimindeki metni date nesnesine çevirir; bozuksa kullanım hatası üretir."""
try:
return dt.date.fromisoformat(metin)
except ValueError:
raise argparse.ArgumentTypeError("tarih YYYY-AA-GG biçiminde olmalı: %r" % metin)
def ayristirici():
p = argparse.ArgumentParser(
prog="rapor",
description="Satış CSV dosyalarını günlük rapora dönüştürür.",
)
p.add_argument("kaynak", type=pathlib.Path, help="okunacak CSV klasörü (mutlak yol)")
p.add_argument(
"--since", type=tarih, default=dt.date.today() - dt.timedelta(days=1),
help="bu tarihten itibaren değişen dosyalar (varsayılan: dün)",
)
p.add_argument(
"--out", type=pathlib.Path, default=pathlib.Path("/srv/rapor/gunluk.html"),
help="çıktı dosyası",
)
p.add_argument(
"--dry-run", action="store_true",
help="hiçbir dosya yazma, yapılacakları listele",
)
p.add_argument(
"-v", "--verbose", action="count", default=0,
help="ayrıntı düzeyi; -vv hata ayıklama çıktısı verir",
)
return p
Birkaç tasarım kuralı bu iskeletin arkasında durur. Zorunlu girdi konumsal argümandır; seçenekler ise mantıklı varsayılanlarla gelir, böylece crontab satırı kısa kalır. Yollar pathlib.Path tipinde alınır ve karşılaştırma, var olma kontrolü gibi işler metin birleştirmeyle değil nesne üzerinden yapılır. Varsayılan değerler yardım metninde görünür; aracı devralan kişi kaynak koda bakmadan davranışı öğrenir. Ortam değişkenleri yalnız sır niteliğindeki değerler için kullanılır; iş parametreleri argümanla gelir ki crontab satırı neyin çalıştığını açıkça göstersin.
Çıkış kodları ve hata sınıfları
Çıkış kodu, bir programın işletim sistemine döndürdüğü ve zamanlayıcı, kabuk betiği ya da izleme aracı tarafından okunan tek sayısal sonuçtur. Sıfır başarı, sıfır dışı hata demektir; ancak hatanın türünü ayırmak için tek bir hata kodu yetmez. Bir araç girdi bulamadığını, geçici bir ağ sorunu yaşadığını ve beklenmeyen bir istisnayla çöktüğünü farklı kodlarla bildirirse, onu çağıran taraf yeniden deneme ile insan müdahalesini birbirinden ayırabilir.
Kendi kod şemasını uydurmak yerine BSD kökenli sysexits.h geleneğini kullanmak işi kolaylaştırır: 64 ile 78 arasındaki değerler kullanım hatası, girdi yok, yapılandırma hatası ve geçici hata gibi anlamlar taşır ve birçok posta ve sistem aracı tarafından aynı biçimde yorumlanır. Aşağıdaki main fonksiyonu hata sınıflarını bu kodlara eşler ve günlük yapılandırmasını ayrıntı düzeyine bağlar:
import logging
import sys
EXIT_OK = 0
EXIT_HATA = 1
EXIT_GIRDI_YOK = 66 # sysexits.h: EX_NOINPUT
EXIT_YAPILANDIRMA = 78 # EX_CONFIG
EXIT_GECICI = 75 # EX_TEMPFAIL: yeniden denenebilir
class GirdiHatasi(Exception):
pass
class GeciciHata(Exception):
pass
def rapor_uret(dosyalar, cikti):
"""İş mantığı burada yaşar; örnek olması için dosya adlarını yazar."""
cikti.write_text("\n".join(str(d) for d in dosyalar), encoding="utf-8")
def main(argv=None):
args = ayristirici().parse_args(argv)
seviye = logging.WARNING - 10 * min(args.verbose, 2)
logging.basicConfig(
stream=sys.stderr, level=seviye,
format="%(asctime)s %(levelname)s rapor: %(message)s",
)
log = logging.getLogger("rapor")
try:
if not args.kaynak.is_dir():
raise GirdiHatasi("kaynak klasörü yok: %s" % args.kaynak)
dosyalar = sorted(
d for d in args.kaynak.glob("*.csv")
if dt.date.fromtimestamp(d.stat().st_mtime) >= args.since
)
if not dosyalar:
log.warning("işlenecek dosya yok (since=%s)", args.since)
return EXIT_OK
if args.dry_run:
for d in dosyalar:
print("işlenecek:", d)
return EXIT_OK
rapor_uret(dosyalar, args.out)
log.info("%d dosya işlendi, çıktı: %s", len(dosyalar), args.out)
return EXIT_OK
except GirdiHatasi as e:
log.error("%s", e)
return EXIT_GIRDI_YOK
except GeciciHata as e:
log.error("geçici hata, sonraki çalıştırmada yeniden denenecek: %s", e)
return EXIT_GECICI
except Exception:
log.exception("beklenmeyen hata")
return EXIT_HATA
if __name__ == "__main__":
sys.exit(main())
İki karar tartışmaya açıktır ve bilinçli verilmelidir. İşlenecek dosya olmaması burada başarı sayılıyor ve yalnız uyarı yazılıyor; raporun her gün mutlaka girdi alması gerekiyorsa bunu EXIT_GIRDI_YOK ile hata yapmak daha doğrudur. Beklenmeyen istisnalar ise yutulmaz: log.exception yığın izini stderr'e yazar ve genel hata koduyla çıkılır, çünkü yığın izi olmayan bir hata günlüğünün gece üçte hiçbir değeri yoktur.
Kuru çalıştırma ve stdout ile stderr ayrımı
Kuru çalıştırma, aracın yapacağı değişiklikleri hiçbirini uygulamadan listelediği moddur. Zamanlayıcıya bağlanacak her araçta bu mod bulunmalıdır, çünkü yeni bir sunucuda ya da yeni bir parametreyle ilk çalıştırmayı güvenle denemenin başka yolu yoktur. Doğru kurulmuş bir kuru çalıştırma, gerçek çalıştırmayla aynı kod yolundan geçer ve yalnız yazma adımlarını atlar; ayrı bir kod yolu olursa zamanla gerçek davranıştan sapar.
Standart akışların ayrımı bununla birlikte düşünülür. Stdout aracın ürünüdür: liste, tablo, bir sonraki komuta aktarılacak veri. Stderr ise aracın günlüğüdür: ilerleme, uyarı, hata. Bu ayrım korunursa rapor /srv/satis --dry-run | wc -l gibi bir boru doğru sonuç verir, crontab satırı iki akışı farklı yerlere yönlendirebilir ve günlük biçimi değiştiğinde çıktıyı okuyan betikler bozulmaz.
Günlük biçimini zenginleştirmek isterseniz, her çalıştırmaya bir korelasyon kimliği verip satırları JSON olarak yazmanın yolunu yapılandırılmış loglama yazısında anlattım; buradaki basicConfig çağrısının yerine o yazıdaki biçimlendirici geçer, geri kalanı değişmez. Ayrıntı düzeyini -v sayısıyla yönetmek de aynı ilkeye hizmet eder: cron sessiz çalışır, sorun ararken aynı komuta -vv eklenir.
Kuru çalıştırma ile birlikte düşünülmesi gereken son özellik yeniden çalıştırılabilirliktir. Araç yarıda kesildiğinde ikinci çalıştırma işi bozmadan tamamlamalı, tamamlanmış işi de yinelememelidir. Bunu sağlayan kilit, filigran ve idempotency desenlerini zamanlanmış iş tasarımı yazısında ayrıntılı ele aldım; buradaki araç o desenlerin içine oturacak dış kabuktur.
Crontab satırı: kilit, ortam ve günlük
Crontab satırı, aracı hangi zamanda, hangi kullanıcıyla, hangi ortam değişkenleriyle çalıştıracağını ve çıktısının nereye gideceğini belirleyen tek satırlık sözleşmedir. Araç ne kadar iyi tasarlanmış olsa da bu satır yanlış kurulduğunda ya iki kopya aynı anda çalışır ya da hata mesajları hiç kimseye ulaşmaz. Satırı /etc/cron.d altında ayrı bir dosyada tutmak hem sürüm kontrolünü hem de devir teslimi kolaylaştırır. Komut tek satırda yazılır; cron, kabuktaki ters bölü ile satır devamını tanımaz.
# /etc/cron.d/rapor -- her gün 06:15, tek örnek, stdout günlüğe, stderr postaya
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
[email protected]
15 6 * * * rapor flock -n /var/tmp/rapor.lock /opt/rapor/.venv/bin/python /opt/rapor/rapor.py /srv/satis --out /srv/rapor/gunluk.html >>/var/log/rapor/gunluk.log
Satırın her parçası yukarıdaki arızalardan birini kapatır. Yorumlayıcı sanal ortamın içinden mutlak yolla çağrılır; PATH açıkça yazılır. flock -n kilit dosyasını alamazsa aracı hiç başlatmaz, böylece uzayan bir çalıştırmanın üstüne ikincisi binmez. Stdout günlük dosyasına eklenir; stderr yönlendirilmez ve cron bu akışta bir şey birikirse MAILTO adresine gönderir. Araç başarıda stderr'e yazmadığı için posta yalnız uyarı ve hata olduğunda gelir.
Dağıtım öncesi şu kontrol listesi işe yarar:
- Komutu önce cron kullanıcısıyla ve boş ortamla deneyin:
sudo -u rapor env -i PATH=/usr/bin:/bin /opt/rapor/.venv/bin/python /opt/rapor/rapor.py /srv/satis --dry-run. - Günlük klasörünün ve kilit dosyasının cron kullanıcısı tarafından yazılabilir olduğunu doğrulayın.
- Crontab içinde yüzde işaretinin satır sonu anlamına geldiğini unutmayın; tarih biçimi gibi değerleri araca argümanla değil, aracın içinde üretin.
- Sunucunun saat dilimini ve cron servisinin hangi dilimde çalıştığını kontrol edin; AlmaLinux ve Ubuntu'da varsayılan sistem dilimi kullanılır.
- Günlük dosyasına
logrotatekuralı ekleyin; aksi halde aylar sonra disk dolar.
Sonuç: sözleşmeyi önce kurmak
Cron dostu komut satırı aracı, iş mantığının etrafına kurulan dört parçalı bir sözleşmedir: argparse ile açık ve kendini açıklayan bir arayüz, türüne göre ayrılmış çıkış kodları, ürünü stdout'a ve günlüğü stderr'e yazan bir akış düzeni, kuru çalıştırma modu. Bu sözleşme kurulduğunda crontab satırı kilit ve yönlendirmeyle tamamlanır ve araç, kimse izlemezken de ne yaptığını anlatabilen bir bileşen olur.
İlk adım olarak elinizde zamanlayıcıya bağlı betiklerden birini seçin ve üç soruya cevap verin: başarısız olduğunda bunu nereden öğreniyorsunuz, aynı anda iki kez çalışsa ne olur, bir sonraki kişi parametreleri nereden öğrenir. Cevapların eksik olduğu yer, yukarıdaki iskeletin uygulanacağı yerdir. Önce main fonksiyonunu ve çıkış kodlarını ekleyin; argüman arayüzü ve crontab düzeni onun arkasından gelir.
Mevcut betiklerinizi bu sözleşmeye taşımak, bir otomasyon envanterini gözden geçirmek ya da devir teslime hazır bir araç seti kurmak isterseniz iletişim bölümünden ulaşabilirsiniz.