Ferramentas de linha de comando precisam ler respostas do usuário, exibir prompts, oferecer autocomplete e processar arquivos de texto linha por linha. O módulo Readline no Node.js, disponível como node:readline, conecta streams de entrada e saída para criar interfaces interativas sem dependências externas.
Embora seja simples fazer uma pergunta, aplicações reais precisam tratar encerramento, Ctrl+C, histórico, dados inválidos, saída não interativa e arquivos grandes. Também é importante não bloquear o event loop nem acumular todo o conteúdo em memória.
Neste guia, você aprenderá a criar interfaces, usar a API de Promises, validar respostas, implementar autocomplete, ocultar dados sensíveis, processar arquivos linha por linha e testar CLIs.
Criando a primeira interface
const readline = require('node:readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('Qual é o seu nome? ', answer => {
console.log(`Olá, ${answer}!`);
rl.close();
});process.stdin é uma Readable Stream e process.stdout é uma Writable Stream. A interface mantém o processo aberto até ser fechada.
A documentação oficial de Readline apresenta interfaces, eventos e métodos. A documentação de Process explica stdin, stdout e sinais.
Para aprofundar o fluxo de dados, consulte Streams no Node.js e o que é Node.js.
API de Promises
const readline = require('node:readline/promises');
const { stdin: input, stdout: output } = require('node:process');
async function main() {
const rl = readline.createInterface({ input, output });
try {
const name = await rl.question('Nome: ');
console.log(`Bem-vindo, ${name}`);
} finally {
rl.close();
}
}
main().catch(console.error);O bloco finally garante o fechamento mesmo quando a validação ou outra etapa falha.
Fazendo várias perguntas
async function collectSettings() {
const rl = readline.createInterface({ input, output });
try {
const host = await rl.question('Host: ');
const port = await rl.question('Porta: ');
const secure = await rl.question('Usar TLS? [s/N] ');
return {
host: host.trim(),
port: Number(port),
secure: secure.trim().toLowerCase() === 's'
};
} finally {
rl.close();
}
}Todas as respostas chegam como strings. Converta e valide antes de usar.
Validação com repetição
async function askPort(rl) {
while (true) {
const raw = await rl.question('Porta [3000]: ');
const value = raw.trim() || '3000';
const port = Number(value);
if (Number.isInteger(port) && port > 0 && port <= 65535) {
return port;
}
console.log('Informe uma porta entre 1 e 65535.');
}
}Defina também uma forma de cancelar. Um loop que não aceita saída torna a ferramenta frustrante.
Evento line
Para processar várias linhas digitadas:
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: '> '
});
rl.prompt();
rl.on('line', line => {
const command = line.trim();
if (command === 'exit') {
rl.close();
return;
}
console.log(`Comando recebido: ${command}`);
rl.prompt();
});O evento é emitido quando a entrada encontra um fim de linha. A sequência depende da plataforma, mas o módulo normaliza o comportamento para o consumidor.
Evento close
rl.on('close', () => {
console.log('Interface encerrada');
});O evento pode ocorrer após rl.close(), fim da entrada ou sinais tratados. Não tente continuar fazendo perguntas depois dele.
Tratando Ctrl+C
rl.on('SIGINT', () => {
rl.question('Deseja realmente sair? [s/N] ', answer => {
if (answer.trim().toLowerCase() === 's') {
rl.close();
} else {
rl.prompt();
}
});
});Quando não há listener de SIGINT na interface, o comportamento padrão pode encerrar ou propagar o sinal conforme o ambiente. Para aplicações com recursos abertos, integre ao Graceful Shutdown no Node.js.
Autocomplete
const commands = [
'help',
'status',
'start',
'stop',
'exit'
];
function completer(line) {
const hits = commands.filter(command => command.startsWith(line));
return [hits.length ? hits : commands, line];
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
completer
});O completer deve responder rapidamente. Não faça uma consulta de rede lenta a cada tecla. Para fontes dinâmicas, mantenha um cache local atualizado.
Histórico
A interface interativa mantém histórico em memória. Opções como historySize, removeHistoryDuplicates e histórico inicial ajudam a controlar o comportamento:
const rl = readline.createInterface({
input,
output,
historySize: 100,
removeHistoryDuplicates: true
});Não armazene senhas, tokens ou comandos sensíveis no histórico. Se a aplicação persiste histórico em arquivo, proteja permissões e permita desativá-lo.
Detectando terminal interativo
if (!process.stdin.isTTY || !process.stdout.isTTY) {
console.error('Este comando exige um terminal interativo.');
process.exitCode = 1;
}Em pipelines e CI, stdin pode vir de arquivo e stdout pode ser redirecionado. Uma CLI bem projetada oferece flags não interativas para automação.
Modo interativo e modo automático
async function resolveConfig(args) {
if (args.yes) {
return {
port: args.port || 3000,
confirmed: true
};
}
return collectSettings();
}Não exija perguntas quando todos os valores podem ser fornecidos por flags ou variáveis de ambiente.
Ocultando senhas
O Readline padrão não possui uma API de alto nível universal para senha mascarada. É possível controlar terminal e escrita, mas implementações manuais são fáceis de quebrar em plataformas diferentes. Prefira uma biblioteca madura para prompts secretos ou leia o valor por arquivo/secret manager.
Nunca exiba, registre ou mantenha a senha no histórico. Limpe Buffers temporários quando possível; veja Buffer no Node.js.
Lendo arquivo linha por linha
const fs = require('node:fs');
const readline = require('node:readline');
async function processFile(filename) {
const stream = fs.createReadStream(filename, {
encoding: 'utf8'
});
const rl = readline.createInterface({
input: stream,
crlfDelay: Infinity
});
for await (const line of rl) {
await processLine(line);
}
}crlfDelay: Infinity trata CRLF como um único fim de linha. O arquivo é processado progressivamente, sem carregar tudo na memória. Consulte File System no Node.js.
Backpressure ao processar linhas
O loop for await aguarda processLine() antes de buscar a próxima linha, criando processamento sequencial. Isso é adequado quando a ordem importa ou a dependência possui limite baixo.
Para paralelismo, use uma fila com concorrência limitada. Não dispare Promises ilimitadas para milhões de linhas.
const pending = new Set();
const concurrency = 5;
for await (const line of rl) {
const task = processLine(line).finally(() => {
pending.delete(task);
});
pending.add(task);
if (pending.size >= concurrency) {
await Promise.race(pending);
}
}
await Promise.all(pending);Linhas muito grandes
Readline acumula bytes até encontrar um fim de linha. Um arquivo com uma única linha enorme pode consumir muita memória. Se a entrada não é confiável, aplique limite de tamanho antes ou use um parser que controle chunks e comprimento.
Arquivos CSV
Não processe CSV apenas dividindo cada linha por vírgula. Campos podem conter vírgulas e quebras de linha entre aspas. Use um parser CSV que trabalhe por stream e siga a especificação do formato.
Comandos e processos externos
Uma CLI pode iniciar ferramentas externas, mas nunca concatene a linha digitada em uma string de shell. Mapeie comandos permitidos e use argumentos separados com spawn(). Veja Child Process no Node.js.
const handlers = {
status: showStatus,
sync: runSync,
exit: () => rl.close()
};
const handler = handlers[command];
if (!handler) {
console.log('Comando desconhecido');
} else {
await handler();
}Saída e cursor
O módulo oferece métodos como clearLine(), clearScreenDown(), cursorTo() e moveCursor() para atualizar o terminal:
readline.cursorTo(process.stdout, 0);
readline.clearLine(process.stdout, 0);
process.stdout.write('Progresso: 50%');Use apenas quando stdout é TTY. Em logs redirecionados, sequências de controle podem poluir a saída.
AbortSignal em question()
A API de Promises pode aceitar um sinal conforme a versão:
const controller = new AbortController();
setTimeout(() => controller.abort(), 30_000);
const answer = await rl.question('Resposta: ', {
signal: controller.signal
});Confirme o suporte na versão mínima. Consulte AbortController no Node.js.
Tratamento de erros
Streams de entrada podem emitir erro. Registre listeners quando a origem é arquivo ou socket:
stream.on('error', error => {
logger.error({ error }, 'Falha ao ler entrada');
});Em CLI, retorne um código de saída diferente de zero para falhas. Não exponha stack trace por padrão ao usuário final; permita um modo verbose.
Separando interface e lógica
Não coloque toda a regra dentro de listeners. Crie funções que recebem valores e retornam resultados. A interface coleta entrada e exibe saída:
function createUser(input, dependencies) {
validateUser(input);
return dependencies.repository.create(input);
}Essa separação simplifica testes e permite reutilizar a lógica em API ou worker.
Como testar uma CLI
Injete streams:
const { PassThrough } = require('node:stream');
const input = new PassThrough();
const output = new PassThrough();
const rl = readline.createInterface({ input, output });
input.write('Ana\n');
Capture a saída e confirme prompts e resultados. Para lógica principal, teste funções sem terminal. O guia de Node Test Runner mostra mocks e recursos temporários.
Observabilidade
Não envie cada tecla ou resposta para telemetria. Registre nome do comando, duração e resultado, removendo argumentos sensíveis. Para processamento de arquivo, acompanhe linhas processadas, erros e taxa, sem registrar conteúdo completo.
Erros comuns
- Esquecer rl.close(): o processo permanece aberto.
- Não validar respostas: strings inválidas chegam à lógica.
- Exigir modo interativo no CI: o comando trava aguardando entrada.
- Guardar senha no histórico: o segredo fica exposto.
- Carregar arquivo inteiro: memória cresce desnecessariamente.
- Executar linha como shell: permite injeção de comandos.
- Paralelizar sem limite: milhões de Promises são criadas.
- Usar controle de cursor sem TTY: logs recebem caracteres especiais.
Boas práticas para produção
- Prefira a API de Promises em fluxos sequenciais.
- Feche a interface em
finally. - Valide e converta cada resposta.
- Ofereça flags para execução não interativa.
- Não armazene segredos no histórico.
- Use
for awaitpara arquivos grandes. - Limite concorrência e tamanho de linha.
- Mapeie comandos permitidos.
- Trate SIGINT e erros de stream.
- Separe interface da lógica de negócio.
Conclusão
O Readline no Node.js permite criar prompts, shells simples e processadores de texto usando streams nativas. A API de Promises torna perguntas sequenciais fáceis de organizar, enquanto o iterador assíncrono processa arquivos linha por linha.
Uma CLI confiável precisa funcionar em terminais e automações, validar entradas, proteger segredos e fechar recursos. Com lógica separada e limites de concorrência, Readline atende desde perguntas simples até ferramentas administrativas e importadores de grandes arquivos.




