Python argparse ile Profesyonel Komut Satırı Aracı Geliştirme

Sistem yönetimi işlerinde er ya da geç şu noktaya geliyorsunuz: Yazdığınız Python script’i büyüdü, parametreler çoğaldı ve artık sys.argv[1], sys.argv[2] gibi ilkel yöntemlerle uğraşmak hem sizi hem de scripti kullanan meslektaşlarınızı çileden çıkarmaya başladı. İşte tam bu noktada Python’ın standart kütüphanesinin en değerli araçlarından biri devreye giriyor: argparse.

Bu yazıda sıfırdan profesyonel düzeyde komut satırı araçları nasıl geliştirilir, gerçek dünya senaryolarıyla birlikte ele alacağız.

Neden argparse?

Python’da komut satırı argümanlarını işlemek için birkaç seçenek var. sys.argv ile manuel parse etmek, getopt kullanmak ya da üçüncü parti click veya typer kütüphanelerine başvurmak bunların başında geliyor. Ancak argparse, standart kütüphanede geldiği için ek bağımlılık gerektirmez, olgun ve kararlıdır, otomatik yardım metni üretir ve sysadmin araçları için gereken hemen her özelliği barındırır.

Basit bir sys.argv kullanımıyla argparse karşılaştırmasına bakalım:

# sys.argv ile - kötü yol
python backup.py /var/www yedek_klasor gunluk

# argparse ile - doğru yol
python backup.py --source /var/www --dest /backup/www --mode daily --verbose

İkinci kullanımda ne yapıldığı okunabilir, parametreler sıra bağımsız çalışır ve --help ile belgeler otomatik gelir.

Temel Kullanım

En basit haliyle bir argparse script’i şu şekilde görünür:

#!/usr/bin/env python3
import argparse

parser = argparse.ArgumentParser(
    description='Sistem bilgilerini toplayan araç',
    epilog='Örnek: sysinfo.py --cpu --memory --disk'
)

parser.add_argument('--cpu', action='store_true', help='CPU bilgisini göster')
parser.add_argument('--memory', action='store_true', help='Bellek bilgisini göster')
parser.add_argument('--disk', action='store_true', help='Disk kullanımını göster')
parser.add_argument('--output', choices=['text', 'json', 'csv'], 
                    default='text', help='Çıktı formatı (varsayılan: text)')

args = parser.parse_args()

if args.cpu:
    print("CPU bilgisi alınıyor...")
if args.memory:
    print("Bellek bilgisi alınıyor...")
if args.disk:
    print("Disk bilgisi alınıyor...")

Bu kadar basit bir yapıyla bile python sysinfo.py --help komutunu çalıştırdığınızda tamamen oluşturulmuş bir yardım metni elde edersiniz. Kullanıcılarınız script’i nasıl kullanacaklarını öğrenmek için sizi aramak zorunda kalmaz.

Argüman Tipleri ve Önemli Parametreler

add_argument() metodunun sunduğu parametreler oldukça zengin. Sysadmin işlerinde en çok kullanacaklarınız şunlar:

  • type: Argümanın Python tipini belirler (int, float, str, dosya nesnesi vb.)
  • required: Zorunlu argüman tanımlar (True/False)
  • default: Argüman verilmediğinde kullanılacak değer
  • choices: Kabul edilebilir değerler listesi
  • nargs: Kaç tane değer alacağını belirler (+, *, ?, ya da sayı)
  • action: Argüman işlendiğinde yapılacak eylem (store_true, append, count vb.)
  • metavar: Yardım metninde gösterilecek değer ismi

Pratik bir örnek üzerinde bunları görelim:

#!/usr/bin/env python3
import argparse

parser = argparse.ArgumentParser(description='Log analiz aracı')

# Pozisyonel argüman - zorunlu, isimsiz
parser.add_argument('logfile', type=str, help='Analiz edilecek log dosyası')

# Seçenek argümanları
parser.add_argument('-n', '--lines', type=int, default=100,
                    metavar='SATIR_SAYISI',
                    help='Analiz edilecek son satır sayısı (varsayılan: 100)')

parser.add_argument('-l', '--level', 
                    choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
                    default='ERROR',
                    help='Minimum log seviyesi')

parser.add_argument('-p', '--pattern', type=str, nargs='+',
                    help='Aranacak kalıplar (birden fazla verilebilir)')

parser.add_argument('-v', '--verbose', action='count', default=0,
                    help='Ayrıntı seviyesi (-v, -vv, -vvv)')

parser.add_argument('--output', type=argparse.FileType('w'), 
                    default='-',
                    help='Çıktı dosyası (varsayılan: stdout)')

args = parser.parse_args()
print(f"Log dosyası: {args.logfile}")
print(f"Verbosity seviyesi: {args.verbose}")

Burada action='count' kullanımına dikkat edin. Bu sayede kullanıcılar -v, -vv veya -vvv şeklinde ayrıntı seviyesini ayarlayabilir, tıpkı rsync ya da ssh komutlarında olduğu gibi.

Gerçek Dünya Senaryosu: Sunucu Yedekleme Aracı

Şimdi gerçekten işe yarar bir araç yazalım. Üretim ortamında kullandığım, MySQL veritabanı ve dosya sistemi yedeklemelerini yöneten bir script:

#!/usr/bin/env python3
"""
backup_manager.py - Sunucu yedekleme yönetim aracı
Kullanım: backup_manager.py [backup|restore|list] [seçenekler]
"""
import argparse
import sys
import os
from datetime import datetime

def create_parser():
    parser = argparse.ArgumentParser(
        description='Sunucu yedekleme yönetim aracı',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Örnekler:
  backup_manager.py backup --type db --host localhost --db myapp
  backup_manager.py backup --type files --source /var/www --compress
  backup_manager.py restore --file /backup/db_2024_01_15.sql.gz --db myapp
  backup_manager.py list --type db --days 7
        """
    )
    
    # Global seçenekler
    parser.add_argument('--config', type=str, 
                        default='/etc/backup_manager/config.ini',
                        help='Yapılandırma dosyası yolu')
    parser.add_argument('--dry-run', action='store_true',
                        help='Gerçek işlem yapmadan simüle et')
    parser.add_argument('-v', '--verbose', action='store_true',
                        help='Ayrıntılı çıktı')
    
    # Alt komutlar
    subparsers = parser.add_subparsers(dest='command', metavar='KOMUT')
    subparsers.required = True
    
    # backup komutu
    backup_parser = subparsers.add_parser('backup', help='Yedek al')
    backup_parser.add_argument('--type', required=True,
                               choices=['db', 'files', 'full'],
                               help='Yedek tipi')
    backup_parser.add_argument('--dest', type=str,
                               default='/backup',
                               help='Yedek hedef dizini')
    backup_parser.add_argument('--compress', action='store_true',
                               help='Yedeği sıkıştır')
    backup_parser.add_argument('--retention', type=int, default=7,
                               metavar='GUN',
                               help='Yedek saklama süresi (gün)')
    
    # restore komutu
    restore_parser = subparsers.add_parser('restore', help='Yedekten geri yükle')
    restore_parser.add_argument('--file', required=True,
                                help='Geri yüklenecek yedek dosyası')
    restore_parser.add_argument('--target', type=str,
                                help='Hedef konum (belirtilmezse orijinal konuma)')
    restore_parser.add_argument('--force', action='store_true',
                                help='Onay sormadan geri yükle')
    
    # list komutu
    list_parser = subparsers.add_parser('list', help='Yedekleri listele')
    list_parser.add_argument('--type', choices=['db', 'files', 'full', 'all'],
                             default='all', help='Listelenecek yedek tipi')
    list_parser.add_argument('--days', type=int, default=30,
                             help='Son kaç günün yedekleri (varsayılan: 30)')
    list_parser.add_argument('--sort', choices=['date', 'size', 'name'],
                             default='date', help='Sıralama kriteri')
    
    return parser

def main():
    parser = create_parser()
    args = parser.parse_args()
    
    if args.verbose:
        print(f"[{datetime.now():%Y-%m-%d %H:%M:%S}] Komut: {args.command}")
        print(f"[{datetime.now():%Y-%m-%d %H:%M:%S}] Config: {args.config}")
    
    if args.dry_run:
        print("*** DRY-RUN MODU AKTIF - Gerçek işlem yapılmayacak ***")
    
    if args.command == 'backup':
        print(f"Yedek alınıyor: tip={args.type}, hedef={args.dest}")
    elif args.command == 'restore':
        print(f"Geri yükleniyor: {args.file}")
    elif args.command == 'list':
        print(f"Son {args.days} günün yedekleri listeleniyor...")

if __name__ == '__main__':
    main()

Bu yapıda subparsers kullanımı kritik bir nokta. Git, docker, kubectl gibi profesyonel araçların hepsinin kullandığı alt komut yapısını kendi script’lerinize uygulamak için bu yöntem kullanılır.

Özel Tip Doğrulama

Argparse’ın en güçlü özelliklerinden biri, type parametresine özel fonksiyonlar vererek kendi doğrulama mantığınızı yazabilmenizdir. Bu, kullanıcıların geçersiz değer girmesini script’in içinde kontrol etmek yerine argparse seviyesinde yakalamak anlamına gelir:

#!/usr/bin/env python3
import argparse
import ipaddress
import os

def valid_ip(value):
    """IP adresi doğrulama"""
    try:
        return str(ipaddress.ip_address(value))
    except ValueError:
        raise argparse.ArgumentTypeError(f"Geçersiz IP adresi: {value}")

def valid_port(value):
    """Port numarası doğrulama"""
    try:
        port = int(value)
        if not (1 <= port <= 65535):
            raise ValueError
        return port
    except ValueError:
        raise argparse.ArgumentTypeError(
            f"Port 1-65535 arasında olmalıdır: {value}"
        )

def valid_directory(value):
    """Mevcut dizin doğrulama"""
    if not os.path.isdir(value):
        raise argparse.ArgumentTypeError(f"Dizin bulunamadı: {value}")
    if not os.access(value, os.W_OK):
        raise argparse.ArgumentTypeError(f"Dizine yazma izni yok: {value}")
    return value

def positive_int(value):
    """Pozitif tam sayı doğrulama"""
    try:
        num = int(value)
        if num <= 0:
            raise ValueError
        return num
    except ValueError:
        raise argparse.ArgumentTypeError(f"Pozitif tam sayı gerekli: {value}")

# Kullanım
parser = argparse.ArgumentParser(description='Port tarama aracı')
parser.add_argument('--host', type=valid_ip, required=True, help='Hedef IP adresi')
parser.add_argument('--port', type=valid_port, default=80, help='Hedef port')
parser.add_argument('--output-dir', type=valid_directory, default='/tmp',
                    help='Çıktı dizini')
parser.add_argument('--timeout', type=positive_int, default=5,
                    help='Bağlantı zaman aşımı (saniye)')

args = parser.parse_args()
print(f"Taranacak hedef: {args.host}:{args.port}")

Bu yaklaşımla kullanıcı --port 99999 girdiğinde script çalışmaya başlamadan önce anlamlı bir hata mesajı alır.

Ortam Değişkenleri ile Entegrasyon

Production ortamında script’leri çoğunlukla cron job olarak ya da CI/CD pipeline içinde çalıştırırsınız. Bu senaryolarda hassas bilgileri (şifreler, API anahtarları) komut satırı argümanı olarak geçmek güvenli değildir, çünkü ps aux çıktısında görünür hale gelir. Ortam değişkenlerini fallback olarak kullanmak bu sorunu çözer:

#!/usr/bin/env python3
import argparse
import os

def get_env_default(env_var, default=None):
    """Ortam değişkeninden varsayılan değer al"""
    return os.environ.get(env_var, default)

parser = argparse.ArgumentParser(
    description='Veritabanı yönetim aracı',
    formatter_class=argparse.ArgumentDefaultsHelpFormatter
)

# DB bağlantı parametreleri - ortam değişkenleri ile fallback
parser.add_argument('--db-host',
                    default=get_env_default('DB_HOST', 'localhost'),
                    help='Veritabanı sunucusu (DB_HOST env)')

parser.add_argument('--db-port',
                    type=int,
                    default=int(get_env_default('DB_PORT', '3306')),
                    help='Veritabanı portu (DB_PORT env)')

parser.add_argument('--db-user',
                    default=get_env_default('DB_USER', 'root'),
                    help='Veritabanı kullanıcısı (DB_USER env)')

parser.add_argument('--db-pass',
                    default=get_env_default('DB_PASSWORD'),
                    help='Veritabanı şifresi (DB_PASSWORD env - ÖNERİLİR)')

parser.add_argument('--db-name',
                    required=not bool(get_env_default('DB_NAME')),
                    default=get_env_default('DB_NAME'),
                    help='Veritabanı adı (DB_NAME env)')

args = parser.parse_args()

# Şifre hala verilmemişse interaktif sor
if not args.db_pass:
    import getpass
    args.db_pass = getpass.getpass("Veritabanı şifresi: ")

print(f"Bağlanılıyor: {args.db_user}@{args.db_host}:{args.db_port}/{args.db_name}")

Böylece hem python db_tool.py --db-name myapp şeklinde çalıştırabilir, hem de ortam değişkenlerini ayarlayarak python db_tool.py şeklinde parametre vermeden çalıştırabilirsiniz. formatter_class=argparse.ArgumentDefaultsHelpFormatter kullanımıyla da varsayılan değerler otomatik olarak yardım metninde görünür.

Yapılandırma Dosyası ile argparse Entegrasyonu

Büyük araçlarda argümanları hem komut satırından hem de yapılandırma dosyasından okuyabilmek istersiniz. Komut satırı argümanlarının her zaman öncelik alması gerekir. Bu pattern’i şu şekilde uygulayabilirsiniz:

#!/usr/bin/env python3
import argparse
import configparser
import os

def load_config(config_file):
    """Config dosyasından varsayılanları yükle"""
    config = configparser.ConfigParser()
    defaults = {}
    
    if os.path.exists(config_file):
        config.read(config_file)
        if 'DEFAULT' in config:
            defaults.update(dict(config['DEFAULT']))
        if 'monitor' in config:
            defaults.update(dict(config['monitor']))
    
    return defaults

# Önce sadece config dosyası argümanını parse et
pre_parser = argparse.ArgumentParser(add_help=False)
pre_parser.add_argument('--config', default='/etc/monitor/config.ini')
pre_args, _ = pre_parser.parse_known_args()

# Config'den varsayılanları yükle
config_defaults = load_config(pre_args.config)

# Ana parser'ı oluştur
parser = argparse.ArgumentParser(
    description='Sistem izleme aracı',
    parents=[pre_parser]
)

parser.set_defaults(**config_defaults)

parser.add_argument('--interval', type=int, default=60,
                    help='Kontrol aralığı (saniye)')
parser.add_argument('--threshold-cpu', type=float, default=90.0,
                    metavar='YÜZDE',
                    help='CPU alarm eşiği')
parser.add_argument('--threshold-mem', type=float, default=85.0,
                    metavar='YÜZDE',
                    help='Bellek alarm eşiği')
parser.add_argument('--alert-email', type=str,
                    help='Alarm e-posta adresi')
parser.add_argument('--slack-webhook', type=str,
                    help='Slack webhook URL')

args = parser.parse_args()
print(f"İzleme başlıyor: her {args.interval} saniyede bir kontrol")
print(f"CPU eşiği: %{args.threshold_cpu}, Bellek eşiği: %{args.threshold_mem}")

Bu yaklaşımda öncelik sırası şöyle işler: Komut satırı argümanları > Config dosyası > Kod içindeki default değerler. Production ortamlarında bu üç katmanlı yapı son derece esneklik sağlar.

Mutex Grupları ve Bağımlı Argümanlar

Bazı durumlarda birbirini dışlayan argümanlar tanımlamanız gerekir. Örneğin kullanıcı ya --enable ya da --disable seçeneğini kullanabilmeli, ikisini birden kullanamamalı:

#!/usr/bin/env python3
import argparse

parser = argparse.ArgumentParser(description='Servis yönetim aracı')
parser.add_argument('service', help='Yönetilecek servis adı')

# Birbirini dışlayan grup
action_group = parser.add_mutually_exclusive_group(required=True)
action_group.add_argument('--start', action='store_true', help='Servisi başlat')
action_group.add_argument('--stop', action='store_true', help='Servisi durdur')
action_group.add_argument('--restart', action='store_true', help='Servisi yeniden başlat')
action_group.add_argument('--status', action='store_true', help='Servis durumunu göster')

# Çıktı formatı için başka bir mutex grup
output_group = parser.add_mutually_exclusive_group()
output_group.add_argument('-q', '--quiet', action='store_true',
                          help='Sadece hataları göster')
output_group.add_argument('-v', '--verbose', action='store_true',
                          help='Ayrıntılı çıktı')

# Ek seçenekler
parser.add_argument('--timeout', type=int, default=30,
                    help='İşlem zaman aşımı (saniye)')
parser.add_argument('--no-deps', action='store_true',
                    help='Bağımlı servisleri etkileme')

args = parser.parse_args()

action = 'başlatılıyor' if args.start else 
         'durduruluyor' if args.stop else 
         'yeniden başlatılıyor' if args.restart else 'durumu kontrol ediliyor'

if not args.quiet:
    print(f"Servis '{args.service}' {action}...")

Bu script’te --start, --stop, --restart ve --status seçeneklerinden biri zorunlu ama ikisi birden kullanılamaz. Kullanıcı --start --stop girerse argparse otomatik olarak hata verir ve kullanım bilgisini gösterir.

Argparse’i Production’a Hazırlamak

Script’leri gerçek ortamlarda kullanılabilir hale getirmek için birkaç önemli detay daha var:

#!/usr/bin/env python3
"""
cleanup_tool.py - Disk temizleme aracı
Versiyon: 2.1.0
"""
import argparse
import sys
import logging

VERSION = '2.1.0'

def setup_logging(verbose, quiet):
    """Logging seviyesini argümanlara göre ayarla"""
    if quiet:
        level = logging.ERROR
    elif verbose:
        level = logging.DEBUG
    else:
        level = logging.INFO
    
    logging.basicConfig(
        level=level,
        format='%(asctime)s [%(levelname)s] %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )

def main():
    parser = argparse.ArgumentParser(
        description='Disk temizleme ve arşivleme aracı',
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    
    # Versiyon bilgisi
    parser.add_argument('--version', action='version',
                        version=f'%(prog)s {VERSION}')
    
    parser.add_argument('path', nargs='+', help='Temizlenecek dizin(ler)')
    
    parser.add_argument('--older-than', type=int, default=30,
                        metavar='GUN',
                        dest='older_than',
                        help='Kaç günden eski dosyalar silinsin')
    
    parser.add_argument('--min-size', type=int, default=0,
                        metavar='MB',
                        help='Minimum dosya boyutu (MB)')
    
    parser.add_argument('--exclude', nargs='*', default=[],
                        metavar='PATTERN',
                        help='Hariç tutulacak dosya kalıpları')
    
    parser.add_argument('--archive', type=str, metavar='HEDEF_DIZIN',
                        help='Silmek yerine bu dizine arşivle')
    
    parser.add_argument('--dry-run', action='store_true',
                        help='Gerçek silme işlemi yapma')
    
    output_group = parser.add_mutually_exclusive_group()
    output_group.add_argument('-v', '--verbose', action='store_true')
    output_group.add_argument('-q', '--quiet', action='store_true')
    
    args = parser.parse_args()
    setup_logging(args.verbose, args.quiet)
    
    logger = logging.getLogger(__name__)
    
    # Mantıksal doğrulama - argparse seviyesinde yapılamayacak kontroller
    if args.archive and args.dry_run:
        parser.error("--archive ve --dry-run birlikte kullanılamaz")
    
    for path in args.path:
        import os
        if not os.path.exists(path):
            parser.error(f"Dizin bulunamadı: {path}")
    
    logger.info(f"Temizlik başlıyor: {', '.join(args.path)}")
    logger.debug(f"Parametreler: {vars(args)}")
    
    if args.dry_run:
        logger.warning("DRY-RUN modu: Gerçek işlem yapılmayacak")
    
    return 0

if __name__ == '__main__':
    sys.exit(main())

Burada birkaç önemli pratik var. parser.error() kullanımı hem hata mesajı yazdırır hem de yardım bilgisini gösterir ve uygun çıkış kodu ile sonlanır. sys.exit(main()) pattern’i ise script’in her zaman düzgün bir çıkış kodu döndürmesini sağlar. Cron job’lardan ve monitoring sistemlerinden bu çıkış kodlarını kontrol ederseniz script’in başarılı olup olmadığını anlayabilirsiniz.

Sonuç

argparse, Python ile sistem araçları yazmanın temel taşlarından biri. Basit sys.argv kullanımından bu modüle geçmek başlangıçta biraz fazla iş gibi görünebilir, ancak bir kez alıştığınızda başka türlü yazamaz hale geliyorsunuz.

Özellikle dikkat etmeniz gereken noktaları şöyle özetleyeyim:

  • Subparser kullanın: Araç büyüdükçe tek bir düz argüman listesi yönetilemez hale gelir. Git, docker, kubectl gibi araçların neden subparser kullandığını düşünün.
  • Özel tip fonksiyonları yazın: Doğrulamayı iş mantığınıza değil argparse’a bırakın. Script’in içine girmeden önce hataları yakalayın.
  • Ortam değişkeni entegrasyonu yapın: Hassas bilgileri komut satırından değil, ortam değişkenlerinden alın.
  • parser.error() kullanın: Kendi print + sys.exit kombinasyonunuzu yazmayın, argparse’ın sağladığı bu metodu kullanın.
  • Dry-run modu ekleyin: Production’da çalışacak her araçta mutlaka --dry-run seçeneği bulunmalı. Bir gün sizi büyük bir felaketten kurtaracak.

Bu yaklaşımları benimsediğinizde yazdığınız araçlar sadece sizin değil, tüm ekibin ve hatta gelecekteki siz’in de kolayca kullanabileceği, belgelenmiş, güvenilir araçlara dönüşüyor. Ve şunu söyleyeyim: Bir script’i altı ay sonra açıp --help çıktısından ne yaptığını anlayabilmek, o an ne kadar değerli olduğunu kendinize kanıtlamanın en iyi yolu.

Yorum yapın