readline 与命令行交互
本教程共 76 篇 · 第 22 篇 · 更新于 2026-07-25 · 约 5 分钟阅读
22. readline 与命令行交互
本节目标:readline 逐行读取、命令行交互和构建简单 CLI 工具。
命令行工具是 Node.js 的强项之一。无论是脚手架工具、DevOps 脚本还是交互式问卷,你都需要从终端读取用户输入、逐行处理日志文件。readline 模块就是干这个的。自 v17.0.0 起,它还提供了基于 Promise 的接口,写起来更顺手。
22.1 逐行读取文件
日志分析是最常见的场景。一个 10GB 的日志文件,用 readFile 直接读会炸内存,用 readline 逐行处理就很稳:
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
const fileStream = createReadStream('access.log', 'utf8');
const rl = createInterface({
input: fileStream,
crlfDelay: Infinity // 兼容 Windows 的 \r\n
});
let errorCount = 0;
for await (const line of rl) {
if (line.includes('ERROR')) {
errorCount++;
}
}
console.log(`共发现 ${errorCount} 条错误日志`);
for await...of 在这里同样适用,readline.Interface 是个异步可迭代对象。
22.2 交互式提问
v17 引入的 readline/promises 让写问答脚本变得很像现代代码:
import { createInterface } from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
const rl = createInterface({ input, output });
const name = await rl.question('项目名称: ');
const port = await rl.question('监听端口 [3000]: ');
console.log(`即将创建 ${name},运行在 ${port || 3000} 端口`);
rl.close();
question() 返回 Promise,等待用户输入并按下回车。注意最后要手动 rl.close(),否则进程不会退出。
老版本的回调写法
如果你维护的是 v16 或更早的代码,写法长一些:
import { createInterface } from 'node:readline';
import { stdin, stdout } from 'node:process';
const rl = createInterface({ input: stdin, output: stdout });
rl.question('项目名称: ', name => {
console.log(`你输入了: ${name}`);
rl.close();
});
22.3 自定义提示与补全
修改提示符
import { createInterface } from 'node:readline/promises';
import { stdin, stdout } from 'node:process';
const rl = createInterface({
input: stdin,
output: stdout,
prompt: 'my-cli> '
});
rl.prompt();
const answer = await rl.question('');
console.log(answer);
Tab 补全
import { createInterface } from 'node:readline';
import { stdin, stdout } from 'node:process';
const commands = ['init', 'build', 'test', 'deploy', 'help'];
const rl = createInterface({
input: stdin,
output: stdout,
completer: (line) => {
const hits = commands.filter(c => c.startsWith(line));
return [hits.length ? hits : commands, line];
}
});
rl.question('输入命令: ', cmd => {
console.log(`执行: ${cmd}`);
rl.close();
});
completer 函数接收当前输入,返回 [补全候选项数组, 原始输入]。用户按 Tab 时,readline 会展示候选项。
22.4 处理标准输入的原始数据
有时候你需要处理管道输入,比如 cat file.txt | node script.js:
import { createInterface } from 'node:readline';
import { stdin } from 'node:process';
const rl = createInterface({ input: stdin });
for await (const line of rl) {
console.log(`处理: ${line.toUpperCase()}`);
}
这种写法让脚本既能直接运行交互,也能接受管道数据,和 Unix 工具的行为一致。
22.5 隐藏密码输入
用户输密码时不该回显到屏幕上。readline 本身没有内建隐藏功能,但可以临时关闭 stdout 的显示:
import { createInterface } from 'node:readline/promises';
import { stdin, stdout } from 'node:process';
const rl = createInterface({ input: stdin, output: stdout });
async function hiddenQuestion(prompt) {
stdout.write(prompt);
stdin.setRawMode(true);
let password = '';
for await (const chunk of stdin) {
const key = chunk.toString();
if (key === '\n' || key === '\r') break;
if (key === '\u0003') process.exit(); // Ctrl+C
if (key === '\u007f') { // Backspace
password = password.slice(0, -1);
continue;
}
password += key;
}
stdin.setRawMode(false);
stdout.write('\n');
return password;
}
const pwd = await hiddenQuestion('密码: ');
console.log(`密码长度: ${pwd.length}`);
rl.close();
生产环境可以考虑用成熟的 inquirer 或 prompts 包,它们把密码隐藏、选择列表、多选框这些交互都封装好了。
22.6 实战:简易 CLI 工具
把上面的知识点串起来,写一个项目初始化脚手架:
import { createInterface } from 'node:readline/promises';
import { stdin, stdout } from 'node:process';
import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
const rl = createInterface({ input: stdin, output: stdout });
async function main() {
const name = await rl.question('项目名称: ');
if (!name.trim()) {
console.error('项目名称不能为空');
rl.close();
return;
}
const type = await rl.question('类型 (esm/cjs) [esm]: ');
const isEsm = (type.trim() || 'esm') === 'esm';
await mkdir(name, { recursive: true });
const pkg = {
name,
version: '1.0.0',
type: isEsm ? 'module' : 'commonjs',
main: isEsm ? 'index.js' : 'index.js',
scripts: { start: 'node index.js' }
};
await writeFile(
join(name, 'package.json'),
JSON.stringify(pkg, null, 2)
);
const indexContent = isEsm
? "console.log('Hello, ESM!');\n"
: "console.log('Hello, CommonJS!');\n";
await writeFile(join(name, 'index.js'), indexContent);
console.log(`项目 ${name} 初始化完成`);
rl.close();
}
main().catch(err => {
console.error(err);
rl.close();
process.exit(1);
});