Ana içeriğe geç
Kesit Python
Orta · Otomasyon, web ve araçlar

Komut satırı araçları: argparse, click, rich ve boru hatları

Betiklerini gerçek komut satırı aracına dönüştürürsün: argparse ile alt komut, tür, seçenek ve grup; click ve typer ile kısa CLI; rich ile tablo; stdin/stdout, çıkış kodları ve yapılandırma önceliğiyle boru hattına uyumlu araç yazarsın.

Uygulamada aç, 18 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

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
KavramargparseÖrnek
Konumsal argümanadd_argument("dosya")rapor satis.csv
Seçenekadd_argument("--limit", type=int, default=10)--limit 5
Bayrakadd_argument("--ozet", action="store_true")--ozet
Sınırlı seçimchoices=["csv", "json"]--bicim json
Çok değernargs="+" (en az 1), "*" (0+), "?" (0/1), 2--sehir İzmir Bursa
Sayaçaction="count"-vv → 2
Tekrarlananaction="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"])
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:

KanalNe yazılır?Python
stdinGirdi verisi (dosya yerine boru ile)sys.stdin; dosya adı - ise stdin oku
stdoutYalnızca sonuç verisiprint(...)
stderrUyarı, ilerleme, hata, günlükprint(..., file=sys.stderr), logging
Çıkış kodu0 başarı; 0 dışı hatasys.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())
Akışı parametre yapFonksiyonların 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")
Ortam değişkenleri hep metindirSTOK_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
Kendi bilgisayarında denepip 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.
argparseclicktyper
KurulumStandart kütüphanepip install clickpip install typer
Tanım biçimiAyrıştırıcı nesnesiDekoratörlerTip ipuçları
Testparse_args(liste)CliRunnertyper.testing.CliRunner
Ne zaman?Bağımlılık istemeyen betikler, kütüphanelerBüyük, çok komutlu araçlarHı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.

Gerçek işteBir belediyenin bilgi işlem ekibi su faturası tahakkuk betiğini 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.
Uzmanlaş

İyi CLI tasarımı kontrol listesi

  • Tahmin edilebilir adlar: --output/-o, --verbose/-v, --quiet/-q, --dry-run, --force, --version gibi alışılmış seçenekleri aynı anlamda kullan.
  • Tehlikeli işlemde onay: silme/üzerine yazma için onay iste; otomasyon için --yes ile 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.

Alıştırmalara geç (18)