Node.js'te kullanıcıdan komut satırında girdi almak için node:readline modülünü kullanırsınız. Kısa cevap: node:readline/promises içinden createInterface({ input: process.stdin, output: process.stdout }) ile bir arayüz oluşturur, await rl.question('Soru? ') ile cevabı beklersiniz ve işiniz bitince rl.close() çağırırsınız. Aynı modül bir dosyayı ya da herhangi bir okunabilir stream'i satır satır okumanın da standart yoludur. Modül belgede Stability: 2 - Stable; Promise tabanlı API v17.0.0 ile geldi ve v24.0.0 ile v22.17.0 sürümlerinde kararlı olarak işaretlendi.

Bu yazıda tek soruluk basit bir örnekten başlayıp, girdi bir borudan (pipe) geldiğinde ikinci sorunun neden cevapsız kaldığını, bunun güvenli çözümünü, prompt() ve line olayıyla küçük bir etkileşimli komut satırı aracı yazmayı, sorulara zaman aşımı koymayı, Tab tamamlamayı ve büyük bir log dosyasını belleğe almadan satır satır işlemeyi görüyoruz. Örnekler Node.js v26.13.0 ile çalıştırıldı; etkileşimli olanlar, sonucu tekrarlanabilir kılmak için girdi borudan verilerek denendi.

İki API: readline ve readline/promises

İki API: readline ve readline/promises: input, output, prompt, terminal

Modülün iki giriş noktası vardır. node:readline/promises, question() metodunun bir Promise döndürdüğü sürümdür ve async/await ile en rahat bu kullanılır. node:readline ise geri çağırma tabanlı API'yi ve cursorTo(), clearLine() gibi senkron terminal yardımcılarını içerir. İki sürümde de arayüz bir EventEmitter gibi davranır; line, close, SIGINT gibi olaylar aynıdır. Promise ve await temelleri için async/await yazısına bakabilirsiniz.

SeçenekVarsayılanAçıklama
inputzorunluOkunacak Readable stream, örneğin process.stdin veya bir dosya stream'i
outputyokSoru ve istem metninin yazılacağı Writable stream
prompt> rl.prompt() çağrısında yazılan istem metni
terminaloutput.isTTY değeritrue ise girdi ve çıktı TTY gibi ele alınır, ANSI kaçış kodları yazılır
crlfDelay100Bu süreden uzun aralıklı CR ve LF ayrı satır sonu sayılır; Infinity ile CRLF her zaman tek satır sonudur
historySize30Saklanan geçmiş satır sayısı; yalnızca terminal modunda anlamlıdır
completeryokTab tamamlama fonksiyonu
signalyokArayüzü bir AbortSignal ile kapatmak için

Belge önemli bir davranışı baştan belirtir: arayüz oluşturulduktan sonra Node.js uygulaması, input stream'inden veri beklediği için arayüz kapatılana kadar sonlanmaz. Programınız işini bitirdiği halde terminalde asılı kalıyorsa ilk bakmanız gereken yer rl.close() çağrısının yapılıp yapılmadığıdır.

İlk soru: question() ile cevap almak

En basit kullanım tek bir soru sormak ve cevabı yazdırmaktır. question(), sorgu metnini output akışına yazar, kullanıcının Enter tuşuna basmasını bekler ve girilen satırı satır sonu karakteri olmadan döndürür.

javascript
import * as readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';

const rl = readline.createInterface({ input, output });

const ad = await rl.question('Adınız nedir? ');
console.log(`Merhaba ${ad.trim()}!`);

rl.close();
bash
printf 'Ayşe\n' | node soru.mjs
# çıktı: Adınız nedir? Merhaba Ayşe!

Girdi borudan geldiği için kullanıcının yazdığı "Ayşe" ekrana yansımadı; çıktıda soru ve cevap aynı satırda görünüyor. Gerçek bir terminalde kullanıcı yazdığını görür ve Enter'a basınca imleç alt satıra geçer. Belge ayrıca arayüz kapatıldıktan sonra çağrılan bir question() çağrısının reddedilen bir Promise döndürdüğünü belirtir.

Node.js Learn rehberindeki komut satırı girdisi sayfası bir de şunu hatırlatır: parola gibi gizli bir değer istiyorsanız, girilen karakterleri ekrana yansıtmak yerine * göstermek daha doğrudur. readline bunu tek bir seçenekle yapmaz; terminal modunda çıktıyı kendiniz yönetmeniz gerekir.

Sık yaşanan sorun: borudan gelen girdide ikinci soru cevapsız kalır

Aynı betiğe iki soru eklediğinizde, terminalde elle yazarken her şey çalışır; ama girdiyi bir dosyadan ya da başka bir programdan boruyla verdiğinizde ikinci soru hiç cevaplanmaz. Aşağıdaki betik bunu gösteriyor.

javascript
import * as readline from 'node:readline/promises';

const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const ad = await rl.question('Adınız nedir? ');
const yas = await rl.question('Kaç yaşındasınız? ');
console.log(ad, yas);
rl.close();
bash
printf 'Ayşe\n32\n' | node iki-soru.mjs
# çıktı: Adınız nedir? Kaç yaşındasınız? Warning: Detected unsettled top-level await at file:///home/kullanici/proje/iki-soru.mjs:5
# çıktı: const yas = await rl.question('Kaç yaşındasınız? ');
# çıktı:             ^
echo $?
# çıktı: 13

Sebep şudur: borudan gelen veri tek seferde okunur ve readline iki satırı da hemen line olayı olarak yayar. İlk satır bekleyen ilk soruya cevap olur; ikinci satır yayıldığı anda henüz ikinci soru sorulmamıştır, bu yüzden satır hiçbir soruya bağlanmaz. Sonra girdi biter, ikinci question() çağrısının Promise'i hiç çözülmez ve Node.js çözülmemiş bir üst düzey await ile karşılaşır. Process belgesine göre 13 çıkış kodu tam olarak bu durumu, yani "Unsettled Top-Level Await" hatasını anlatır. Çıktıdaki dosya yolu kısaltıldı.

Belgenin async iterator bölümü de aynı riske dikkat çeker: createInterface() çağrıldığı anda girdiyi tüketmeye başlar ve arayüzü oluşturmak ile okumaya başlamak arasına giren asenkron işlemler satırların kaçırılmasına yol açabilir. Çözüm, bütün satırları tek bir async iterator'dan sırayla çekmektir; iterator gelen satırları siz isteyene kadar sırada tutar.

javascript
import { createInterface } from 'node:readline';

const rl = createInterface({ input: process.stdin });
// Satırları tek bir async iterator'dan sırayla çekmek: hiçbir satır kaybolmaz
const satirlar = rl[Symbol.asyncIterator]();

async function sor(soru) {
  process.stdout.write(soru);
  const { value, done } = await satirlar.next();
  return done ? '' : value.trim();
}

const ad = await sor('Adınız nedir? ');
const yas = Number(await sor('Kaç yaşındasınız? '));
console.log(Number.isNaN(yas)
  ? '\nYaş bir sayı olmalı.'
  : `\nMerhaba ${ad}, 10 yıl sonra ${yas + 10} yaşında olacaksınız.`);
rl.close();
bash
printf 'Ayşe\n32\n' | node sor.mjs
# çıktı: Adınız nedir? Kaç yaşındasınız? 
# çıktı: Merhaba Ayşe, 10 yıl sonra 42 yaşında olacaksınız.

Bu yaklaşımda sorunun metnini process.stdout.write() ile kendimiz yazıyoruz, cevabı satirlar.next() ile alıyoruz. Girdi erken biterse done değeri true olur ve boş dize döndürürüz; böylece program asılı kalmaz. Hem terminalde hem boruyla çalışır. Async iterator kavramının dil tarafını generator ve iterator yazısında anlattık.

Etkileşimli komut satırı: prompt() ve line olayı

Etkileşimli komut satırı: prompt() ve line olayı

Kullanıcıdan art arda komut alan bir araç (küçük bir kabuk, not defteri, yönetim konsolu) için question() yerine prompt() ve line olayı daha doğal bir yapıdır. setPrompt() ya da prompt seçeneğiyle istem metnini belirler, her satırı işledikten sonra rl.prompt() ile istemi tekrar gösterirsiniz. Belgedeki "Tiny CLI" örneği de bu deseni kullanır.

javascript
import { createInterface } from 'node:readline';

const rl = createInterface({
  input: process.stdin,
  output: process.stdout,
  prompt: 'not> ',
});

const notlar = [];
rl.prompt();
rl.on('line', (satir) => {
  const komut = satir.trim();
  if (komut === 'liste') {
    console.log(notlar.length ? notlar.join(', ') : '(boş)');
  } else if (komut === 'çık') {
    rl.close();
    return;
  } else if (komut) {
    notlar.push(komut);
  }
  rl.prompt();
}).on('close', () => {
  console.log(`\n${notlar.length} not kaydedildi.`);
});
bash
printf 'süt al\nfaturayı öde\nliste\nçık\n' | node not.mjs
# çıktı: not> not> not> süt al, faturayı öde
# çıktı: not> 
# çıktı: 2 not kaydedildi.

Belgeye göre close olayı şu durumlarda yayılır: rl.close() çağrıldığında, input stream'i end olayını aldığında, kullanıcı Ctrl+D ile iletim sonunu bildirdiğinde ve Ctrl+C geldiğinde arayüzde hiç SIGINT dinleyicisi yoksa. Bu yüzden kapanış işlerini (kaydetme, özet yazdırma) close olayına koymak, kullanıcı hangi yolla çıkarsa çıksın çalışmalarını garanti eder. Bir ayrıntı: rl.close() çağrısı, kuyrukta bekleyen line olaylarının yayılmasını hemen durdurmaz; bu yüzden örnekte çık komutundan sonra return kullandık.

Ctrl+C'ye özel bir davranış istiyorsanız (örneğin "Çıkmak istediğinize emin misiniz?" sorusu) arayüzde SIGINT olayını dinlersiniz. Belge, SIGTSTP (Ctrl+Z) ve SIGCONT olaylarının Windows'ta desteklenmediğini not eder. Terminale renkli çıktı vermek için console ve util.styleText yazısına, komut satırı argümanlarını okumak için process.argv ve parseArgs yazısına bakabilirsiniz.

Soruya zaman aşımı koymak: AbortSignal

Promise tabanlı question(), ikinci parametre olarak { signal } kabul eder. Sinyal iptal edildiğinde soru da iptal olur ve Promise bir AbortError ile reddedilir. AbortSignal.timeout(ms) ile birleştirince, kullanıcı belli bir süre cevap vermezse varsayılan bir davranışa geçmek kolaylaşır; otomasyonda çalışan, bazen insansız kalan betiklerde işe yarar.

javascript
import { createInterface } from 'node:readline/promises';

const rl = createInterface({ input: process.stdin, output: process.stdout });

try {
  const cevap = await rl.question('Devam edilsin mi (e/h)? ', {
    signal: AbortSignal.timeout(300),
  });
  console.log('cevap:', cevap);
} catch (err) {
  console.log('\nsoru iptal edildi:', err.name);
} finally {
  rl.close();
}
bash
# stdin açık ama hiç veri gelmiyor: 300 ms sonra iptal
(sleep 1) | node onay.mjs
# çıktı: Devam edilsin mi (e/h)? 
# çıktı: 
# çıktı: soru iptal edildi: AbortError

printf 'e\n' | node onay.mjs
# çıktı: Devam edilsin mi (e/h)? cevap: e

finally bloğundaki rl.close(), iki durumda da arayüzün kapanmasını ve programın beklemeden çıkmasını sağlıyor. AbortController ve AbortSignal'ın Node.js'teki diğer kullanım alanlarını AbortController yazısında bulabilirsiniz.

Tab ile otomatik tamamlama

completer seçeneğine verdiğiniz fonksiyon, kullanıcı Tab tuşuna bastığında o ana kadar yazılmış satırla çağrılır. Belgeye göre iki elemanlı bir dizi döndürmelidir: eşleşen tamamlama adayları ve eşleştirmede kullanılan alt dize. Fonksiyon bir Promise de döndürebilir, yani asenkron olabilir. Tamamlama yalnızca terminal (TTY) modunda etkileşimli olarak çalışır; aşağıda fonksiyonun döndürdüğü değeri doğrudan çağırarak görüyoruz.

javascript
import { createInterface } from 'node:readline';

const KOMUTLAR = ['liste', 'ekle', 'sil', 'çık'];

// Tab tuşuna basıldığında çağrılır: [eşleşenler, eşleştirilen parça]
function tamamla(satir) {
  const eslesen = KOMUTLAR.filter((k) => k.startsWith(satir));
  return [eslesen.length ? eslesen : KOMUTLAR, satir];
}

console.log(tamamla('e'));
console.log(tamamla('x'));

// Gerçek kullanımda arayüze verilir; terminal (TTY) varsa Tab ile çalışır
const rl = createInterface({ input: process.stdin, output: process.stdout, completer: tamamla });
rl.close();
// çıktı: [ [ 'ekle' ], 'e' ]
// çıktı: [ [ 'liste', 'ekle', 'sil', 'çık' ], 'x' ]

Eşleşme yoksa bütün komutları döndürmek, belgedeki örnekte de kullanılan yaygın bir tercihtir: kullanıcı Tab'a bastığında hangi komutların var olduğunu görür. Kendi etkileşimli kabuğunuzu sıfırdan yazmak yerine Node.js'in yerleşik REPL'ini özelleştirmek isterseniz REPL yazısına bakın.

Terminal modu ve komut geçmişi

terminal seçeneği verilmezse readline, arayüz oluşturulduğu anda output akışının isTTY değerine bakarak karar verir. Gerçek bir terminalde bu değer true olur ve ok tuşlarıyla geçmişte gezinme, satır içi düzenleme, Tab tamamlama gibi özellikler devreye girer. Çıktı bir dosyaya ya da boruya yönlendirildiğinde ise false olur; bu durumda arayüz yalnızca satır okuyan sade bir okuyucu gibi davranır. Bu yüzden betiğinizi terminalde ve bir CI ortamında çalıştırdığınızda farklı görünmesi normaldir.

Geçmiş özellikleri yalnızca terminal modunda anlamlıdır. history seçeneği başlangıç geçmişini verir, historySize (varsayılan 30) saklanacak satır sayısını belirler, 0 değeri geçmişi kapatır; removeHistoryDuplicates: true aynı satırın eski kopyasını siler. Belgeye göre geçmiş her değiştiğinde yayılan history olayının temel amacı, geçmişi bir dosyaya kaydetmenize imkân vermektir. Dinleyici geçmiş dizisini değiştirebildiği için, parola gibi bazı satırların geçmişe girmesini de bu olayda engelleyebilirsiniz.

Dosyayı satır satır okumak

readline yalnızca klavye girdisi için değildir; input olarak herhangi bir okunabilir stream verebilirsiniz. Belgenin "Read file stream line-by-line" bölümü, fs.createReadStream() ile for await...of döngüsünü birleştiren deseni gösterir. Bu yöntem dosyanın tamamını belleğe almadığı için gigabaytlık log dosyalarında da çalışır. Windows satır sonlu (\r\n) dosyalarda crlfDelay: Infinity vermek, her CR LF çiftinin tek satır sonu sayılmasını garanti eder.

javascript
import { createReadStream, writeFileSync } from 'node:fs';
import { createInterface } from 'node:readline';

// Windows satır sonlu (\r\n) küçük bir erişim günlüğü üretelim
writeFileSync('erisim.log', 'GET /anasayfa 200 12\r\nGET /urunler 200 40\r\nPOST /sepet 500 7\r\nGET /urunler 404 3');

const rl = createInterface({
  input: createReadStream('erisim.log', { encoding: 'utf8' }),
  crlfDelay: Infinity, // \r\n tek satır sonu sayılsın
});

let satir = 0;
let hata = 0;
let toplamMs = 0;
for await (const line of rl) {
  satir++;
  const [, , durum, ms] = line.split(' ');
  if (Number(durum) >= 400) hata++;
  toplamMs += Number(ms);
}
console.log({ satir, hata, ortalamaMs: toplamMs / satir });
// çıktı: { satir: 4, hata: 2, ortalamaMs: 15.5 }

Dikkat ederseniz son satırın sonunda satır sonu yoktu ama yine de işlendi; belgeye göre stream satır sonu olmadan biterse son veri de bir line olayı olarak yayılır. Belge iki performans notu da düşer: for await...of döngüsü klasik line olayına göre biraz daha yavaştır ve hız kritikse line dinleyicisi ile events.once(rl, 'close') beklemesini birleştiren karma bir yaklaşım önerilir. Ayrıca async iterator, girdi stream'indeki hataları döngüye iletmez; dosya bulunamadı gibi hataları yakalamak için arayüzün error olayını dinleyin. Döngüden break ile çıkarsanız rl.close() otomatik çağrılır. Dosya okumanın diğer yolları için readFile yazısına, stream kavramı için stream yazısına bakın.

Sık yapılan hatalar

Sık yapılan hatalar: rl.close() çağırmayı unutmak, Borudan gelen girdide art arda question() kullanmak, Arayüzü oluşturduktan sonra await ile beklemek, Windows dosyalarında...
  • rl.close() çağırmayı unutmak: Arayüz açık kaldıkça program stdin'i beklediği için kapanmaz.
  • Borudan gelen girdide art arda question() kullanmak: İkinci satır kaybolabilir ve program 13 koduyla çıkar. Satırları tek bir async iterator'dan çekin.
  • Arayüzü oluşturduktan sonra await ile beklemek: createInterface() hemen okumaya başlar; arada yapılan asenkron işler satır kaçırılmasına yol açabilir.
  • Windows dosyalarında crlfDelay vermemek: CR ve LF arasında gecikme olursa ayrı satır sonu sayılabilir; dosya okurken crlfDelay: Infinity kullanın.
  • Döngüde dosya hatası beklemek: Async iterator girdi stream'inin hatalarını iletmez; error olayını ayrıca dinleyin.
  • Cevabı sayıya çevirmeden kullanmak: question() her zaman dize döndürür; Number() ile çevirip Number.isNaN() ile denetleyin.
  • Gizli değeri ekrana yansıtmak: Parola isterken girilen karakterleri olduğu gibi göstermeyin.

Sık Sorulan Sorular

process.stdin ile doğrudan okumak yerine neden readline kullanmalıyım?

process.stdin size ham veri parçaları verir; bir parçanın bir satıra denk geleceğinin garantisi yoktur. readline bu parçaları satır sonlarına göre böler, \n, \r ve \r\n sonlarını tanır, terminal modunda geçmiş, imleç hareketi ve Tab tamamlama gibi özellikler ekler.

Kullanıcı Ctrl+C'ye bastığında ne olur?

Arayüzde SIGINT dinleyicisi yoksa belgeye göre arayüz kapanır ve close olayı yayılır. Dinleyici eklerseniz kontrol sizdedir; örneğin onay sorusu sorabilirsiniz. Belge ayrıca dinleyici yokken Ctrl+C geldiğinde pause olayının yayıldığını belirtir.

readline ile büyük bir CSV dosyası işlenebilir mi?

Evet, satır satır işleme dosyanın tamamını belleğe almadığı için uygundur. Ancak tırnak içinde satır sonu içeren alanlar barındıran CSV dosyalarında satır, kayıtla birebir örtüşmez; o durumda gerçek bir CSV ayrıştırıcısı gerekir.

Arayüzü otomatik kapatmanın bir yolu var mı?

Belgeye göre v23.10.0 ve v22.15.0 ile eklenen rl[Symbol.dispose](), rl.close() için bir takma addır. Bu sayede arayüzü bir using bildirimiyle oluşturursanız, kapsamdan çıkıldığında kapatma çağrısı kendiliğinden yapılır. Eski sürümleri de desteklemeniz gerekiyorsa try...finally içinde rl.close() çağırmak aynı güvenceyi verir.

question() kullanıcının cevabını hangi biçimde döndürür?

Satır sonu karakteri çıkarılmış bir dize olarak döndürür. Baştaki ve sondaki boşlukları temizlemek için trim(), büyük küçük harf duyarsız karşılaştırma için Türkçe yerel ayarlı dönüşüm kullanmak isteyebilirsiniz.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Node.js öğrenme rehberi · process.argv ve çıkış kodları · console ve styleText · Node.js dosya okuma · AbortController ile iptal