Back to Blog
Node.jsReadlineCLIInput

Node.js Readline Module Guide

Master the Node.js readline module for interactive CLI applications and line-by-line file processing.

B
Bootspring Team
Engineering
February 3, 2020
6 min read

The readline module enables interactive command-line interfaces and line-by-line input processing.

Basic Usage#

1const readline = require('readline'); 2 3// Create interface 4const rl = readline.createInterface({ 5 input: process.stdin, 6 output: process.stdout, 7}); 8 9// Ask a question 10rl.question('What is your name? ', (answer) => { 11 console.log(`Hello, ${answer}!`); 12 rl.close(); 13}); 14 15// With promise wrapper 16function askQuestion(query) { 17 return new Promise((resolve) => { 18 rl.question(query, resolve); 19 }); 20} 21 22// Using promises 23async function main() { 24 const name = await askQuestion('Name: '); 25 const age = await askQuestion('Age: '); 26 console.log(`${name} is ${age} years old`); 27 rl.close(); 28} 29 30main();

Promises API (Node 17+)#

1const readline = require('readline/promises'); 2 3async function main() { 4 const rl = readline.createInterface({ 5 input: process.stdin, 6 output: process.stdout, 7 }); 8 9 const name = await rl.question('What is your name? '); 10 const age = await rl.question('How old are you? '); 11 12 console.log(`${name} is ${age} years old`); 13 14 rl.close(); 15} 16 17main();

Event-Based Interface#

1const readline = require('readline'); 2 3const rl = readline.createInterface({ 4 input: process.stdin, 5 output: process.stdout, 6 prompt: '> ', 7}); 8 9// Display prompt 10rl.prompt(); 11 12// Handle each line 13rl.on('line', (line) => { 14 const input = line.trim(); 15 16 switch (input) { 17 case 'hello': 18 console.log('Hello!'); 19 break; 20 case 'exit': 21 console.log('Goodbye!'); 22 rl.close(); 23 return; 24 default: 25 console.log(`You said: ${input}`); 26 } 27 28 rl.prompt(); 29}); 30 31// Handle close 32rl.on('close', () => { 33 console.log('Interface closed'); 34 process.exit(0); 35}); 36 37// Handle SIGINT (Ctrl+C) 38rl.on('SIGINT', () => { 39 rl.question('Are you sure you want to exit? (y/n) ', (answer) => { 40 if (answer.toLowerCase() === 'y') { 41 rl.close(); 42 } else { 43 rl.prompt(); 44 } 45 }); 46});

Reading Files Line by Line#

1const readline = require('readline'); 2const fs = require('fs'); 3 4async function processFile(filename) { 5 const fileStream = fs.createReadStream(filename); 6 7 const rl = readline.createInterface({ 8 input: fileStream, 9 crlfDelay: Infinity, // Recognize all CR LF sequences 10 }); 11 12 let lineNumber = 0; 13 14 for await (const line of rl) { 15 lineNumber++; 16 console.log(`Line ${lineNumber}: ${line}`); 17 } 18 19 console.log(`Total lines: ${lineNumber}`); 20} 21 22processFile('data.txt'); 23 24// With error handling 25async function processFileWithErrors(filename) { 26 try { 27 const fileStream = fs.createReadStream(filename); 28 29 fileStream.on('error', (error) => { 30 console.error('File error:', error.message); 31 }); 32 33 const rl = readline.createInterface({ 34 input: fileStream, 35 crlfDelay: Infinity, 36 }); 37 38 for await (const line of rl) { 39 // Process line 40 processLine(line); 41 } 42 } catch (error) { 43 console.error('Processing error:', error.message); 44 } 45}

Interactive Menu#

1const readline = require('readline'); 2 3class Menu { 4 constructor(options) { 5 this.options = options; 6 this.rl = readline.createInterface({ 7 input: process.stdin, 8 output: process.stdout, 9 }); 10 } 11 12 display() { 13 console.log('\n=== Main Menu ==='); 14 this.options.forEach((option, index) => { 15 console.log(`${index + 1}. ${option.label}`); 16 }); 17 console.log('0. Exit\n'); 18 } 19 20 async run() { 21 while (true) { 22 this.display(); 23 24 const choice = await this.askQuestion('Select option: '); 25 const index = parseInt(choice, 10); 26 27 if (index === 0) { 28 console.log('Goodbye!'); 29 this.rl.close(); 30 break; 31 } 32 33 if (index > 0 && index <= this.options.length) { 34 await this.options[index - 1].action(); 35 } else { 36 console.log('Invalid option'); 37 } 38 } 39 } 40 41 askQuestion(query) { 42 return new Promise((resolve) => { 43 this.rl.question(query, resolve); 44 }); 45 } 46} 47 48// Usage 49const menu = new Menu([ 50 { 51 label: 'Say Hello', 52 action: () => console.log('Hello, World!'), 53 }, 54 { 55 label: 'Show Date', 56 action: () => console.log(new Date().toLocaleDateString()), 57 }, 58 { 59 label: 'Calculate', 60 action: async () => { 61 // Can do async operations 62 }, 63 }, 64]); 65 66menu.run();

Password Input#

1const readline = require('readline'); 2 3function askPassword(prompt) { 4 return new Promise((resolve) => { 5 const rl = readline.createInterface({ 6 input: process.stdin, 7 output: process.stdout, 8 }); 9 10 // Mute output 11 const stdin = process.stdin; 12 stdin.on('data', (char) => { 13 char = char.toString(); 14 switch (char) { 15 case '\n': 16 case '\r': 17 case '\u0004': 18 stdin.pause(); 19 break; 20 default: 21 process.stdout.write('*'); // Show asterisks 22 break; 23 } 24 }); 25 26 rl.question(prompt, (answer) => { 27 console.log(); // New line after password 28 rl.close(); 29 resolve(answer); 30 }); 31 }); 32} 33 34// Simpler approach with readline directly 35function askPasswordSimple(prompt) { 36 return new Promise((resolve) => { 37 const rl = readline.createInterface({ 38 input: process.stdin, 39 output: new (require('stream').Writable)({ 40 write: (chunk, encoding, callback) => callback(), 41 }), 42 terminal: true, 43 }); 44 45 process.stdout.write(prompt); 46 47 rl.question('', (answer) => { 48 console.log(); 49 rl.close(); 50 resolve(answer); 51 }); 52 }); 53}

Auto-Complete#

1const readline = require('readline'); 2 3const commands = ['help', 'hello', 'history', 'exit', 'clear', 'status']; 4 5function completer(line) { 6 const hits = commands.filter((c) => c.startsWith(line)); 7 return [hits.length ? hits : commands, line]; 8} 9 10const rl = readline.createInterface({ 11 input: process.stdin, 12 output: process.stdout, 13 completer: completer, 14}); 15 16rl.prompt(); 17 18rl.on('line', (line) => { 19 const cmd = line.trim(); 20 21 switch (cmd) { 22 case 'help': 23 console.log('Available commands:', commands.join(', ')); 24 break; 25 case 'exit': 26 rl.close(); 27 return; 28 case 'clear': 29 console.clear(); 30 break; 31 default: 32 console.log(`Unknown command: ${cmd}`); 33 } 34 35 rl.prompt(); 36}); 37 38// Async completer 39function asyncCompleter(line, callback) { 40 // Could fetch from database, API, etc. 41 setTimeout(() => { 42 const hits = commands.filter((c) => c.startsWith(line)); 43 callback(null, [hits.length ? hits : commands, line]); 44 }, 100); 45}

Cursor Control#

1const readline = require('readline'); 2 3// Move cursor 4readline.cursorTo(process.stdout, 0, 0); // Move to position 5readline.moveCursor(process.stdout, 2, 0); // Move relative 6 7// Clear lines 8readline.clearLine(process.stdout, 0); // Clear current line 9readline.clearScreenDown(process.stdout); // Clear screen below 10 11// Progress indicator 12function showProgress(current, total) { 13 readline.cursorTo(process.stdout, 0); 14 const percent = Math.round((current / total) * 100); 15 const bar = '█'.repeat(percent / 2) + '░'.repeat(50 - percent / 2); 16 process.stdout.write(`Progress: [${bar}] ${percent}%`); 17} 18 19// Usage 20let progress = 0; 21const interval = setInterval(() => { 22 progress += 5; 23 showProgress(progress, 100); 24 25 if (progress >= 100) { 26 clearInterval(interval); 27 console.log('\nDone!'); 28 } 29}, 100);

History#

1const readline = require('readline'); 2 3const rl = readline.createInterface({ 4 input: process.stdin, 5 output: process.stdout, 6 historySize: 100, 7 removeHistoryDuplicates: true, 8}); 9 10// History is available via arrow keys automatically 11 12// Access history programmatically 13rl.on('line', (line) => { 14 console.log('History:', rl.history); 15 rl.prompt(); 16}); 17 18// Save/load history 19const fs = require('fs'); 20 21function saveHistory(rl, filename) { 22 fs.writeFileSync(filename, rl.history.join('\n')); 23} 24 25function loadHistory(filename) { 26 try { 27 return fs.readFileSync(filename, 'utf8').split('\n').filter(Boolean); 28 } catch { 29 return []; 30 } 31} 32 33// Initialize with saved history 34const savedHistory = loadHistory('.history'); 35const rlWithHistory = readline.createInterface({ 36 input: process.stdin, 37 output: process.stdout, 38 history: savedHistory, 39});

Best Practices#

Interface: ✓ Use promises API when available ✓ Handle close events properly ✓ Implement SIGINT handling ✓ Clean up resources File Processing: ✓ Use async iterator for large files ✓ Set crlfDelay for cross-platform ✓ Handle stream errors ✓ Process in chunks if needed CLI: ✓ Provide clear prompts ✓ Implement auto-complete ✓ Support command history ✓ Handle edge cases Avoid: ✗ Forgetting to close interface ✗ Blocking event loop ✗ Ignoring input validation ✗ Memory issues with large files

Conclusion#

The readline module enables interactive CLI applications and efficient line-by-line file processing. Use the promises API for cleaner async code, implement auto-complete for better UX, and leverage cursor control for progress indicators. Always handle cleanup and edge cases properly.

Share this article

Help spread the word about Bootspring