23 puan yazan hongminhee 2025-08-23 | 1 yorum | WhatsApp'ta paylaş

Merhaba! TypeScript ile sık sık CLI araçları geliştirdiğim için mevcut kütüphanelerin sınırları beni rahatsız ediyordu; bu yüzden yeni bir CLI ayrıştırıcısı yapmaya karar verdim. İlgilenenlere tanıtmak için bu yazıyı paylaşıyorum.

CLI uygulamaları geliştirirken beni sürekli zorlayan bir nokta vardı. Mevcut CLI ayrıştırıcı kütüphanelerinin çoğu, CLI yapısını yapılandırma nesneleri ya da buyurgan API’lerle tanımlıyor; bu da yalnızca tip güvenliğini değil, karmaşık CLI yapılarını ifade etmeyi de zorlaştırıyor.

Özellikle birbirini dışlayan (mutually exclusive) seçenek gruplarını ifade etmek için doğrulama mantığını kodun farklı yerlerine dağıtmak gerekiyordu. “Şu seçenek ile bu seçenek aynı anda kullanılamaz”, “Bu modda yalnızca şu seçeneklere izin verilir” gibi kısıtları kodda temiz biçimde ifade etmek zordu. Ayrıca TypeScript kullansanız bile, ayrıştırma sonucunun tipini çoğu zaman elle tanımlamak gerekiyordu.

İşlevsel parser combinator yaklaşımı

Bu yüzden Haskell’deki optparse-applicative’den ilham alarak TypeScript için işlevsel parser combinator yaklaşımıyla bir CLI ayrıştırıcısı geliştirdim.

Mevcut yaklaşım:

// Mevcut kütüphanelerin tipik yaklaşımı  
const program = new Command()  
  .option('-p, --port <number>', 'port number')  
  .option('-h, --host <string>', 'hostname')  
  .action((options) => {  
    // options tipi any olur ya da elle tanımlanması gerekir  
  });  

Optique yaklaşımı:

// Küçük ayrıştırıcıları birleştirerek daha büyük bir yapı kurma  
const serverConfig = object({  
  port: option("-p", "--port", integer({ min: 1, max: 65535 })),  
  host: option("-h", "--host", string()),  
  verbose: option("-v", "--verbose")  
});  
  
// TypeScript tipi otomatik olarak çıkarır!  
// { port: number, host: string, verbose: boolean }  
const config = run(serverConfig);  

Fark 1: Birbirini dışlayan seçenekleri yapının içinde ifade etme

En büyük fark, birbirini dışlayan seçenek gruplarını doğal biçimde ifade edebilmesi. Mevcut kütüphanelerde bu tür kısıtlar ayrı doğrulama mantığıyla ele alınmak zorundayken, Optique or() combinator’ü ile bu kısıtları doğrudan yapının içine yerleştirebiliyor.

// Sunucu modu vs istemci modu - tamamen farklı seçenek kümeleri  
const parser = or(  
  object({  
    mode: constant("server"),  
    port: option("-p", "--port", integer()),  
    workers: option("-w", "--workers", integer()),  
    ssl: option("--ssl")  
  }),  
  object({  
    mode: constant("client"),   
    connect: option("-c", "--connect", string()),  
    timeout: option("-t", "--timeout", integer()),  
    retries: option("--retries", integer())  
  })  
);  
  
// TypeScript otomatik olarak discriminated union üretir  
// { mode: "server", port: number, workers: number, ssl: boolean } |   
// { mode: "client", connect: string, timeout: number, retries: number }  

Mevcut kütüphanelerde bu tür doğrulamaları elle yapmak gerekirdi:

// Mevcut yaklaşımın zahmeti  
if (options.mode === "server" && options.connect) {  
  throw new Error("--connect sunucu modunda kullanılamaz");  
}  
if (options.mode === "client" && options.workers) {  
  throw new Error("--workers istemci modunda kullanılamaz");  
}  

Fark 2: Tam otomatik tip çıkarımı

const gitLike = or(  
  command("add", object({  
    type: constant("add"),  
    files: multiple(argument(string())),  
    all: option("-A", "--all")  
  })),  
  command("commit", object({  
    type: constant("commit"),  
    message: option("-m", "--message", string()),  
    amend: option("--amend")  
  }))  
);  
  
// Sonuç discriminated union olarak otomatik çıkarılır  
const result = run(gitLike);  
if (result.type === "add") {  
  // TypeScript tip daraltmayı otomatik yapar  
  console.log(`Adding ${result.files.join(", ")}`);  
}  

Fark 3: Modülerlik ve yeniden kullanılabilirlik

merge() combinator’ü sayesinde seçenek grupları yeniden kullanılabiliyor; böylece ortak seçenekleri birden fazla komut arasında kolayca paylaşmak mümkün oluyor.

// Yeniden kullanılabilir seçenek grupları tanımı  
const networkOptions = object({  
  host: option("--host", string()),  
  port: option("--port", integer())  
});  
  
const authOptions = object({  
  username: option("-u", "--user", string()),  
  password: optional(option("-p", "--password", string()))  
});  
  
// Gerektiğinde birleştirme  
const devMode = merge(networkOptions, object({ debug: option("--debug") }));  
const prodMode = merge(networkOptions, authOptions, loggingOptions);  

Fark 4: Zengin yerleşik doğrulama

Değer ayrıştırıcıları, basit tip dönüşümünün ötesinde anlamlı doğrulamalar sunuyor.

const parser = object({  
  // Dosya sisteminde gerçekten var olup olmadığını kontrol etme  
  inputFile: option("--input", path({ mustExist: true })),  
  
  // Port numarası aralığı doğrulaması  
  port: option("-p", "--port", integer({ min: 1, max: 65535 })),  
  
  // URL protokolü kısıtlama  
  api: option("--api", url({ allowedProtocols: ["https:"] })),  
  
  // Seçenek kümesini sınırlama  
  logLevel: option("--log", choice(["debug", "info", "warn", "error"]))  
});  

Runtime desteği

  • @optique/core: tüm JavaScript runtime’larını destekler (tarayıcı, edge function vb.)
  • @optique/run: Node.js, Bun ve Deno için piller dahil sürüm

Kurulum:

deno add --jsr @optique/core @optique/run  
npm  add       @optique/core @optique/run  
pnpm add       @optique/core @optique/run  
yarn add       @optique/core @optique/run  
bun  add       @optique/core @optique/run  

Kapanış

Mevcut CLI kütüphaneleri “yapılandırma üzerinden ayrıştırıcı oluşturma” yaklaşımını benimserken, Optique “küçük ayrıştırıcıları birleştirerek büyük ayrıştırıcı kurma” şeklinde işlevsel bir yaklaşım sunuyor.

Özellikle birbirini dışlayan seçenek gruplarını ifade ederken bu fark çok net ortaya çıkıyor. Karmaşık CLI kısıtlarını ayrı doğrulama mantığı olmadan doğrudan ayrıştırıcı yapısının içinde ifade edebildiğiniz için, aynı anda hem tip güvenliği hem de daha sade kod elde edilebiliyor.

Elbette proje hâlâ erken geliştirme aşamasında ve API değişebilir; ancak işlevsel parser combinator yaklaşımının zarafetini TypeScript CLI geliştirmeye taşımak isteyenler için denemeye değer olabilir.

1 yorum

 
spilist2 2025-08-25

Vay canına, çok güzel görünüyor! Paylaştığınız için teşekkürler. Ben de CLI yaparken denemeliyim.