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.
- GitHub: https://github.com/dahlia/optique
- Dokümantasyon: https://optique.dev/
- npm: https://www.npmjs.com/package/@optique/core
- JSR: https://jsr.io/@optique/core
1 yorum
Vay canına, çok güzel görünüyor! Paylaştığınız için teşekkürler. Ben de CLI yaparken denemeliyim.