Neden önemli?
Anadolu Market'in operasyon ekibi her sabah üç betik çalıştırıyor: stok uyarısı, kargo gecikme raporu, iade özeti. Betiklerin içinde sabit yazılmış dosya adları ve eşik değerleri var; bir şeyi değiştirmek için kodu açmak gerekiyor. Bir komut satırı arayüzü (CLI, command-line interface) ile aynı betik şöyle kullanılır: stok-uyar satis.csv --esik 5 --bicim json. Artık kodu açmadan farklı dosya ve eşikle çalışır, zamanlanmış görevlere (cron) ve CI hatlarına eklenir, başka araçlarla boru (pipe) ile birleşir.
sys.argv ve temel argparse'ı standart kütüphane dersinde görmüştün; paketlemedeki "entry point" kısmını ileri seviyede göreceksin. Bu ders ikisinin arasındaki boşluğu doldurur: kullanıcı dostu, test edilebilir ve otomasyona uygun bir CLI nasıl tasarlanır?
Bir komutun anatomisi
$ stok ekle AM-101 --adet 3 --depo izmir -v │ │ │ │ │ └─ kısa bayrak (flag) │ │ │ │ └─ seçenek + değer (option) │ │ │ └─ seçenek + değer │ │ └─ konumsal argüman (positional) │ └─ alt komut (subcommand) └─ program
| Kavram | argparse | Örnek |
|---|---|---|
| Konumsal argüman | add_argument("dosya") | rapor satis.csv |
| Seçenek | add_argument("--limit", type=int, default=10) | --limit 5 |
| Bayrak | add_argument("--ozet", action="store_true") | --ozet |
| Sınırlı seçim | choices=["csv", "json"] | --bicim json |
| Çok değer | nargs="+" (en az 1), "*" (0+), "?" (0/1), 2 | --sehir İzmir Bursa |
| Sayaç | action="count" | -vv → 2 |
| Tekrarlanan | action="append" | --etiket a --etiket b |
argparse ileri: türler, seçimler, varsayılanlar
parse_args() argümansız çağrılınca sys.argv[1:]'i okur. Ona bir liste verirsen terminal olmadan test edebilirsin; bu derste hep bunu yapacağız.
import argparse
parser = argparse.ArgumentParser(prog="rapor", description="Anadolu Market satış raporu üretir.")
parser.add_argument("dosya", help="Satış CSV dosyası (stdin için -)")
parser.add_argument("--sehir", choices=["İzmir", "Ankara", "Bursa"], help="Yalnız bu şehir")
parser.add_argument("--limit", type=int, default=10, metavar="N", help="En fazla N satır (varsayılan: %(default)s)")
parser.add_argument("--ozet", action="store_true", help="Yalnızca toplamları göster")
parser.add_argument("-v", "--verbose", action="count", default=0, help="Ayrıntı düzeyi (-v, -vv)")
parser.add_argument("--etiket", nargs="*", default=[], help="Bir ya da daha çok etiket")
args = parser.parse_args(["satis.csv", "--sehir", "İzmir", "--limit", "5", "-vv", "--etiket", "kampanya", "ekim"])
print(args)
print(args.limit + 1, args.ozet, args.verbose)
print(vars(args)["etiket"])
type=dönüşümü argparse yapar ve hatalı değerde kullanıcıya düzgün bir hata mesajı gösterir. Kendi fonksiyonunu da verebilirsin; geçersiz değerdeargparse.ArgumentTypeErrorfırlatmalıdır.- Seçenek adındaki tire, nitelik adında alt çizgiye döner:
--min-stok→args.min_stok. - Hatalı girdide argparse kullanım bilgisini stderr'e yazar ve
SystemExit(2)fırlatır.--helpise yardım metnini basıpSystemExit(0)fırlatır.
import argparse
def pozitif_int(metin):
try:
n = int(metin)
except ValueError:
raise argparse.ArgumentTypeError(f"tam sayı değil: {metin!r}")
if n <= 0:
raise argparse.ArgumentTypeError(f"pozitif olmalı: {n}")
return n
p = argparse.ArgumentParser(prog="stok")
p.add_argument("--adet", type=pozitif_int, required=True)
grup = p.add_mutually_exclusive_group()
grup.add_argument("--json", action="store_true")
grup.add_argument("--csv", action="store_true")
print(p.parse_args(["--adet", "3", "--json"]))
for hatali in (["--adet", "-2"], ["--adet", "1", "--json", "--csv"]):
try:
p.parse_args(hatali)
except SystemExit as e:
print("çıkış kodu:", e.code)
Birbirini dışlayan grup (mutually exclusive group), aynı anda verilmemesi gereken seçenekleri (--json ile --csv, --sessiz ile --ayrinti) argparse düzeyinde engeller; kendi if kontrollerini yazmana gerek kalmaz.
Alt komutlar
git commit, pip install gibi araçlar alt komut kullanır. argparse'ta her alt komut kendi ayrıştırıcısına sahiptir; set_defaults(func=...) ile her alt komutu bir fonksiyona bağlamak en temiz kalıptır.
import argparse
STOK = {"AM-101": 4, "AM-102": 12}
def ekle(args):
STOK[args.sku] = STOK.get(args.sku, 0) + args.adet
print(f"{args.sku}: {STOK[args.sku]}")
return 0
def listele(args):
for sku, adet in sorted(STOK.items()):
if args.az is None or adet <= args.az:
print(sku, adet)
return 0
def main(argv=None):
parser = argparse.ArgumentParser(prog="stok")
alt = parser.add_subparsers(dest="komut", required=True)
p_ekle = alt.add_parser("ekle", help="Stoğa ürün ekle")
p_ekle.add_argument("sku")
p_ekle.add_argument("--adet", type=int, default=1)
p_ekle.set_defaults(func=ekle)
p_liste = alt.add_parser("listele", help="Stoğu listele")
p_liste.add_argument("--az", type=int, metavar="N", help="Adedi N ve altındakiler")
p_liste.set_defaults(func=listele)
args = parser.parse_args(argv)
return args.func(args)
print("kod:", main(["ekle", "AM-101", "--adet", "3"]))
print("kod:", main(["listele", "--az", "10"]))
Unix felsefesi: stdin, stdout, stderr ve çıkış kodu
İyi bir CLI başka araçlarla birleşebilir. Bunun dört kuralı vardır:
| Kanal | Ne yazılır? | Python |
|---|---|---|
| stdin | Girdi verisi (dosya yerine boru ile) | sys.stdin; dosya adı - ise stdin oku |
| stdout | Yalnızca sonuç verisi | print(...) |
| stderr | Uyarı, ilerleme, hata, günlük | print(..., file=sys.stderr), logging |
| Çıkış kodu | 0 başarı; 0 dışı hata | sys.exit(main()) |
$ cat siparisler.csv | python stok_uyar.py - --esik 5 --bicim csv > kritik.csv $ echo $? # 0: kritik ürün yok, 3: kritik ürün var, 2: kullanım hatası $ python stok_uyar.py siparisler.csv 2>hatalar.log | sort -t, -k3 -n
Günlük mesajlarını stdout'a yazarsan > kritik.csv dosyası bozulur, bir sonraki araç "Uyarı: ..." satırını veri sanır. Çıkış kodu ise cron'un, CI'ın ve kabuk betiklerinin "başarılı mı?" sorusunu yanıtlar. Yaygın sözleşme: 0 başarı, 1 genel hata, 2 kullanım hatası (argparse'ın kullandığı), 130 Ctrl+C ile kesilme.
import io
import sys
def sayilari_topla(akis, hata=sys.stderr):
toplam, atlanan = 0.0, 0
for no, satir in enumerate(akis, start=1):
satir = satir.strip()
if not satir or satir.startswith("#"):
continue
try:
toplam += float(satir.replace(",", "."))
except ValueError:
atlanan += 1
print(f"uyarı: {no}. satır sayı değil: {satir!r}", file=hata)
return toplam, atlanan
sahte_stdin = io.StringIO("# günlük tahsilat\n120,5\n80\n\nyok\n49.5\n")
sahte_stderr = io.StringIO()
print(sayilari_topla(sahte_stdin, sahte_stderr))
print("stderr'e giden:", sahte_stderr.getvalue().strip())
sys.stdin/sys.stdout'a doğrudan dokunmak yerine akışı parametre olarak alsın (akis=sys.stdin). Böylece testte io.StringIO verirsin; üretimde varsayılan değer kullanılır.import sys
def main(argv=None) -> int:
... # argparse, iş mantığı
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except KeyboardInterrupt:
print("iptal edildi", file=sys.stderr)
sys.exit(130)
Yapılandırma önceliği
Gerçek araçlarda bir ayar dört yerden gelebilir. Kabul görmüş öncelik sırası: komut satırı argümanı > ortam değişkeni > yapılandırma dosyası > varsayılan. En özel ve en geçici olan kazanır. collections.ChainMap bu katmanlamayı tek satırda yapar; ancak argparse'ın verilmemiş seçenekler için koyduğu None değerlerini önce süzmelisin.
from collections import ChainMap
varsayilan = {"esik": 5, "bicim": "tablo", "renk": True}
dosya = {"esik": 10, "renk": False} # ör. ~/.stokrc (TOML/JSON)
ortam_ham = {"STOK_BICIM": "json", "PATH": "/usr/bin"}
ortam = {k[5:].lower(): v for k, v in ortam_ham.items() if k.startswith("STOK_")}
argumanlar = {"esik": 3, "bicim": None} # argparse çıktısı (vars(args))
argumanlar = {k: v for k, v in argumanlar.items() if v is not None}
ayar = ChainMap(argumanlar, ortam, dosya, varsayilan)
print(dict(ayar))
print(ayar.maps.index(next(m for m in ayar.maps if "renk" in m)), "→ renk dosyadan geldi")
STOK_ESIK=7 Python'a "7" olarak gelir; STOK_RENK=false ise boş olmayan bir metin olduğu için bool("false") True verir. Ortamdan gelen değeri varsayılanın türüne göre açıkça çevir.click ve typer: daha az kodla CLI
click, seçenekleri dekoratörlerle (decorator) tanımlar; renkli çıktı, onay soruları, dosya türleri ve en önemlisi test için CliRunner sunar. Tarayıcı motorunda da çalışır:
import click
from click.testing import CliRunner
@click.command()
@click.argument("sehir")
@click.option("--esik", type=float, default=0.0, show_default=True, help="En düşük tutar")
@click.option("--detay", is_flag=True, help="Her siparişi listele")
def ozet(sehir, esik, detay):
"""Bir şehrin sipariş özetini yazar."""
siparisler = [("AM-1", "İzmir", 1200.0), ("AM-2", "İzmir", 80.0), ("AM-3", "Bursa", 500.0)]
secilen = [s for s in siparisler if s[1] == sehir and s[2] >= esik]
if detay:
for no, _, tutar in secilen:
click.echo(f" {no}: {tutar:.2f}")
click.echo(f"{sehir}: {len(secilen)} sipariş, {sum(s[2] for s in secilen):.2f} TL")
runner = CliRunner()
sonuc = runner.invoke(ozet, ["İzmir", "--esik", "100", "--detay"])
print(sonuc.exit_code)
print(sonuc.output)
print(runner.invoke(ozet, ["İzmir", "--esik", "yüz"]).exit_code)
typer ise click'in üzerine kurulu ve tip ipuçlarından (type hint) CLI üretir: fonksiyon imzası arayüzün kendisidir. Tarayıcıda yüklü değildir; kendi bilgisayarında dene.
import typer
app = typer.Typer(help="Anadolu Market stok aracı")
@app.command()
def uyar(dosya: str, esik: int = 5, bicim: str = "tablo", ayrinti: bool = False):
"""Stoğu eşiğin altındaki ürünleri listeler."""
typer.echo(f"{dosya} okunuyor, eşik={esik}, biçim={bicim}")
@app.command()
def ekle(sku: str, adet: int = typer.Option(1, min=1)):
typer.echo(f"{sku} +{adet}")
if __name__ == "__main__":
app() # python stok.py uyar satis.csv --esik 3 --ayrinti
pip install typer rich (click typer ile birlikte gelir). python stok.py --help ile otomatik yardım sayfasını gör; typer ayrıca kabuk tamamlama (shell completion) kurulumunu da sunar.| argparse | click | typer | |
|---|---|---|---|
| Kurulum | Standart kütüphane | pip install click | pip install typer |
| Tanım biçimi | Ayrıştırıcı nesnesi | Dekoratörler | Tip ipuçları |
| Test | parse_args(liste) | CliRunner | typer.testing.CliRunner |
| Ne zaman? | Bağımlılık istemeyen betikler, kütüphaneler | Büyük, çok komutlu araçlar | Hızlı, modern, tip odaklı araçlar |
rich: okunur terminal çıktısı
rich renkli metin, tablo, ilerleme çubuğu ve güzel hata izleri üretir. Console(file=io.StringIO()) ile çıktıyı bir metne yönlendirip test edebilirsin; terminal olmayan bir dosyaya yazarken renk kodu üretmez.
import io
from rich.console import Console
from rich.table import Table
tablo = Table(title="Kritik stok — İzmir deposu")
tablo.add_column("SKU", style="bold")
tablo.add_column("Ürün")
tablo.add_column("Adet", justify="right")
for sku, ad, adet in [("AM-101", "Çay bardağı", 2), ("AM-207", "Demlik", 0), ("AM-310", "Kupa", 4)]:
tablo.add_row(sku, ad, str(adet), style="red" if adet == 0 else None)
konsol = Console(file=io.StringIO(), width=50)
konsol.print(tablo)
print(konsol.file.getvalue())
from rich.progress import track
import time
for dosya in track(["ocak.csv", "subat.csv", "mart.csv"], description="İşleniyor..."):
time.sleep(0.5)
İlerleme çubukları ve renkler stderr'e ya da yalnızca terminale gitmelidir: Console(stderr=True). Çıktı bir dosyaya ya da boruya yönlendirildiğinde (sys.stdout.isatty() False) süslemeyi kapatıp düz CSV/JSON üretmek iyi bir CLI'ın işaretidir.
tahakkuk hesapla --donem 2026-10 --mahalle Konak --kuru-calistir biçiminde bir CLI'a çevirdi. --kuru-calistir (dry-run) veritabanına yazmadan ne yapılacağını gösteriyor; çıkış kodları gece çalışan zamanlayıcıya hatayı bildiriyor; tüm ayarlar TAHAKKUK_* ortam değişkenlerinden de verilebildiği için aynı araç test ve canlı ortamda kod değişmeden çalışıyor.İyi CLI tasarımı kontrol listesi
- Tahmin edilebilir adlar:
--output/-o,--verbose/-v,--quiet/-q,--dry-run,--force,--versiongibi alışılmış seçenekleri aynı anlamda kullan. - Tehlikeli işlemde onay: silme/üzerine yazma için onay iste; otomasyon için
--yesile atlanabilsin. - Idempotent çalıştırma: aynı komutu iki kez çalıştırmak ikinci kez zarar vermesin (aynı raporu ikinci kez eklememek gibi).
- Makine okunur çıktı: insanlar için tablo, makineler için
--json; JSON'u stdout'a, açıklamaları stderr'e yaz. - Anlamlı hata mesajı: ne oldu, neden, ne yapmalı: "satis.csv bulunamadı; yolu kontrol et ya da - ile stdin'den ver".
Paket olarak kurulum
pyproject.toml içindeki [project.scripts] bölümüne stok = "stokaraci.cli:main" yazınca pip install . sonrası stok komutu her yerde çalışır. Bu bağlantıyı ileri seviyedeki paketleme dersinde kuracaksın; main(argv=None) -> int imzası bu yüzden önemlidir.
Test stratejisi
İş mantığını saf fonksiyonlarda tut, CLI katmanı yalnızca ayrıştırsın ve çağırsın. CLI testlerinde main([...])'i çağır, çıkış kodunu ve io.StringIO'ya yönlendirilen stdout/stderr'i denetle; pytest kullanıyorsan capsys ve monkeypatch.setenv bu işi kolaylaştırır.