O que Construção de Aplicações CLI em Dart faz?
Conteúdo
- 1. Arquitetura Central e Ciclo de Vida do Processo
- 2. Streams, Diagnósticos e Formatação
- 3. Configuração e Empacotamento do Projeto
- 4. Análise de Argumentos e Roteamento de Comandos
- 5. Async Nativo e Stack Traces Modernos
- 6. Criação de Subprocessos e Resiliência AOT
- 7. Manipulação de Sinais e Desligamento do Terminal
- 8. Testando Aplicações CLI
- 9. Compilação e Distribuição Modernas
- 10. Fluxos de Trabalho e Lista de Verificação
- Referências e Exemplos
1. Arquitetura Central e Ciclo de Vida do Processo
Evite Saídas Destrutivas (exit(N))
Chamar exit(int code) de dart:io invoca Platform::Exit(code) no runtime C++. Ele termina o processo do SO imediatamente sem desenrolar a pilha Dart:
- Desconexão do Depurador: Quando iniciado com
--pause-isolates-on-exit, o VM Service pausa os isolates antes do encerramento para permitir a inspeção na IDE.exit()termina o processo do SO antes que o VM Service possa pausar ou inspecionar o estado. - Perda de Cobertura:
package:coverageconsulta linhas de execução através de RPCs do VM Service durante o estado pausado no exit.exit()destrói o processo antes da extração das RPCs, resultando em cobertura de 0%. - Truncamento de Buffer:
stdoutestderrsão streamsIOSinkassíncronas com buffer.exit()descarta bytes não descarregados (flushed). - Vazamento de Recursos: Blocos
finally(fechando locks, deletando diretórios temporários) são ignorados.
Regra: Evite chamar exit(code) diretamente durante a execução normal; defina exitCode = code ou retorne um código de saída inteiro de CommandRunner<int> (de package:args) e permita que a função main() assíncrona retorne naturalmente. Não chame exit() em erros não tratados; lance um Error ou exceção não tratada para que o runtime desenrole de forma limpa e saia com um status diferente de zero.
Códigos de saída POSIX padrão (/usr/include/sysexits.h):
0: Sucesso (EX_OK/ExitCode.success.code)64: Erro de uso da linha de comando (EX_USAGE/ExitCode.usage.code)65: Erro de formato de dados (EX_DATAERR/ExitCode.data.code)70: Falha interna de software (EX_SOFTWARE/ExitCode.software.code)78: Erro de configuração (EX_CONFIG/ExitCode.config.code)
Nota: Prefira importar package:io/io.dart e usar as constantes ExitCode
(ex., ExitCode.usage.code, ExitCode.software.code) em vez de números
inteiros mágicos. Para scripts independentes mínimos sem dependências de pacotes,
os inteiros POSIX padrão (0, 64, 70) podem ser usados.
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:io/io.dart' show ExitCode; // Fornece constantes POSIX padrão ExitCode
Future<void> main(List<String> args) async {
final runner = CommandRunner<int>('tool', 'Descrição da ferramenta CLI.');
try {
final status = await runner.run(args);
exitCode = status ?? ExitCode.success.code;
} on UsageException catch (e) {
stderr
..writeln(e.message)
..writeln(e.usage);
exitCode = ExitCode.usage.code;
}
}
O Padrão de Ponto de Entrada Enxuto (bin/ vs. lib/src/)
Mantenha os arquivos bin/*.dart estritamente como trampolins de ponto de entrada mínimos (instanciar runner, passar args, aguardar código de saída). Coloque todas as definições de comando, analisadores de argumentos, formatadores e lógica de negócios dentro de lib/src/.
- Justificativa: Código em
bin/não pode ser importado de forma limpa via URIspackage:. Mover a lógica paralib/src/permite testar unitariamente todo o runner de comandos, hierarquia de subcomandos e lógica de negócios em memória em milissegundos (< 2ms) sem gerar subprocessos do SO.
// bin/my_cli.dart — Trampolim de ponto de entrada enxuto
import 'dart:io';
import 'package:my_cli/src/cli.dart';
Future<void> main(List<String> args) async {
exitCode = await runCli(args);
}
2. Saída, Diagnósticos e Formatação
- Dados vs. Diagnósticos: Escreva resultados pretendidos do programa e dados legíveis por máquina exclusivamente no
stdout. Escreva avisos, mensagens de erro e logs de depuração exclusivamente nostderr. - A Regra de Erro de Uso: Quando ocorrer um erro de análise de argumentos ou de opção obrigatória (
FormatException,UsageException, ouArgumentErrorlançado ao acessar uma opçãomandatory: trueausente viaresults.option(...)), tanto a mensagem de erro quanto o texto de uso devem escrever parastderr, e o código de saída64(EX_USAGE/ExitCode.usage.code) deve ser retornado.stdoutsó deve receber ajuda de uso quando o usuário a solicitar explicitamente via--helpou-h. - Sem
print()em Manipuladores de Erro:print()roteia parastdout. Usestderr.writeln()para todas as notificações de falha. Para saída padrão, prefirastdout.writeln()em vez deprint()para cumprir a regra de lintavoid_print(a menos queanalysis_options.yamlconfigure explicitamenteavoid_print: false). - Detecção de Capacidade do Terminal e
NO_COLOR: Verifiquestdout.hasTerminal,stdout.supportsAnsiEscapes, e!Platform.environment.containsKey('NO_COLOR')antes de emitir códigos de escape ANSI de cor ou cursor:bool get useAnsi => stdout.hasTerminal && stdout.supportsAnsiEscapes && !Platform.environment.containsKey('NO_COLOR'); - Modos Legíveis por Máquina: Quando os sinalizadores
--jsonou--machineforem passados, formate os dados como JSON parastdoute direcione os logs parastderr.
3. Configuração e Empacotamento do Projeto
Scaffolding e Mapeamento de Executáveis no Pubspec (executáveis:)
Crie o scaffolding de novos projetos de linha de comando usando dart create -t console <package_name>, que inicializa o layout padrão bin/ e lib/. Sempre declare executáveis em pubspec.yaml sob executables: para mapear nomes de comandos para scripts em bin/, permitindo invocação limpa via dart run <command> (sem especificar bin/...dart) e configurando links simbólicos binários globais para dart install:
name: my_cli
description: Ferramenta CLI de alto desempenho.
version: 1.0.0
executables:
my_cli: # Mapeia para bin/my_cli.dart
secondary_cmd: helper # Mapeia para bin/helper.dart
Versionamento de Fonte Única (package:build_version)
Evite codificar strings --version em bin/*.dart ou sincronizar manualmente arquivos de constantes. Use package:build_version para gerar lib/src/version.dart contendo const packageVersion = 'x.y.z'; diretamente de pubspec.yaml durante as compilações.
Convenções de Cache
Armazene arquivos de cache transitórios em .dart_tool/<package_name>/. Nunca escreva arquivos de cache persistentes diretamente na raiz do projeto.
4. Análise de Argumentos e Roteamento de Comandos
Importe package:args para gerenciar argumentos de linha de comando:
- Scripts Simples: Use
ArgParserdiretamente comaddFlag()eaddOption(). - Ferramentas com Múltiplos Comandos: Implemente
CommandRunner<int>e estendaCommand<int>para cada subcomando, retornando códigos de saída POSIX diretamente. - Acessadores Type-Safe: Use
results.flag('name'),results.option('name')eresults.multiOption('name')(disponíveis nopackage:args2.5+) em vez da indexação por mapaoperator []para eliminar casts de tipo manuais (as bool,as String?). - Modelos de Opções Complexas: Para aplicações com muitas flags, use
package:build_clipara gerar classes de opções fortemente tipadas. Aproveite substituições de padrão nomeadas (ex.{String? hostDefaultOverride}) para mesclar limpa e eficientemente arquivos de configuração com flags da CLI.
5. Async Nativo e Stack Traces Modernos
- Evite
Chain.capture(): A VM Dart preserva nativamente os quadros de pilha assíncronos entre pontos de suspensãoawait.Chain.captureenvolve o loop de eventos em Zonas personalizadas, incorrendo em sobrecarga substancial de alocação e capturando erros através de limites de Zona. - Sanitize com
Trace.from(st).terse: Use utilitários estáticos depackage:stack_traceem erros não capturados sem capturar zonas:
import 'dart:io';
import 'package:io/io.dart' show ExitCode;
import 'package:stack_trace/stack_trace.dart';
Future<void> runMain(List<String> args) async {
try {
await executeLogic(args);
exitCode = ExitCode.success.code;
} catch (e, st) {
stderr.writeln('Erro fatal: $e');
if (args.contains('-v') || args.contains('--verbose')) {
stderr.writeln(Trace.from(st).terse);
}
exitCode = ExitCode.software.code;
}
}
6. Criação de Subprocessos e Resiliência AOT
Ao gerar subprocessos do Dart SDK ou executar outras ferramentas Dart (ex., dart format, dart test, build_runner):
- Não presuma que
Platform.resolvedExecutableouPlatform.executableaponte para o executável de linha de comandodart: Em binários autônomos compilados AOT (dart install/dart compile exe),resolvedExecutableaponta para o binário da aplicação compilada em si, causando loops de auto-invocação recursiva ou falhas de rejeição de flag. - Use
package:cli_util: Resolva o executável do SDK Dart usandocli_util.dartExecutableoucli_util.sdkPathem vez de escrever scrapers de diretório ou PATH personalizados. - Consulte os requisitos de versão e o guia técnico detalhado em referências/aot_sdk_discovery.md.
7. Manipulação de Sinais e Desligamento do Terminal
Se sua CLI alterar modos de terminal, exibir spinners ou abrir sockets de escuta:
- Proteção para Windows: No Windows,
ProcessSignal.sigterm.watch()lançaUnsupportedError. Protejasigtermcomif (!Platform.isWindows). - Desligamento de Eco e Modo de Linha: Se estiver definindo
stdin.echoMode = falseoustdin.lineMode = false, verifiqueif (!stdin.hasTerminal) return;primeiro e instale um listenerSIGINTe um blocofinallypara restaurá-los, para que as teclas do usuário permaneçam visíveis após a saída. - Visibilidade do Cursor: Se emitir ANSI para esconder cursor (
\x1B[?25l), sempre restaure a visibilidade do cursor (\x1B[?25h) ao sair ou cancelar. - Limpeza de Socket: Feche explicitamente instâncias de
HttpServerouServerSocketem escuta (server.close(force: true)) em sinais de término para liberar portas do SO imediatamente. - Consulte padrões detalhados em referências/sinais_e_terminal.md.
8. Testando Aplicações CLI
Estruture os testes em duas camadas distintas:
- Testes Unitários (Em Memória,
< 5ms): Teste classes de comando, análise de opções e lógica de negócios diretamente importandopackage:<pkg>/src/...emtest/. - Testes de Integração (Subprocessos): Use
package:test_processepackage:test_descriptorpara verificar a execução binária de ponta a ponta, streaming de E/S do processo e códigos de saída do SO:
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;
import 'package:test_process/test_process.dart';
void main() {
test('CLI processa entrada e sai de forma limpa', () async {
await d.file('input.txt', 'olá').create();
final process = await TestProcess.start('dart', [
'run',
'bin/my_cli.dart',
'--input',
d.path('input.txt'),
]);
await expectLater(process.stdout, emitsThrough('Processamento concluído.'));
await process.shouldExit(0);
});
}
9. Compilação e Distribuição Modernas
Dart 3.12+ padroniza a distribuição de CLI em torno de dart run e dart install (afastando-se de dart pub global activate):
- Execução Efêmera (JIT):
dart run <package>@<version> [args]baixa e executa a CLI sob demanda. - Instalação Global (AOT Nativo):
dart install <package>compila o ponto de entrada do pacote em um binário nativo autônomo rápido em~/.dart/install/bin/. - Desenvolvimento Local: Use
dart run <command>(resolve viaexecutables:empubspec.yaml) oudart run bin/cli.dart. - Agrupando Bibliotecas Dinâmicas e Assets de Código: Use
dart build cli. Saída do pacote parabuild/cli/_/bundle/. - Compilação de Executável Autônomo: Use
dart compile exe bin/cli.dart -o <caminho_saida>.
10. Fluxos de Trabalho e Lista de Verificação
Fluxo de Trabalho de Implementação
- Declare pontos de entrada em
pubspec.yamlsobexecutables:. - Mantenha
bin/*.dartcomo um ponto de entrada enxuto; coloque a lógica do comando emlib/src/. - Retorne códigos de saída inteiros ou defina
exitCode = N; eviteexit(N)bruto. - Direcione erros, avisos e uso em falha de análise para
stderr. - Use
results.flag(),results.option()eresults.multiOption()para segurança de tipo. - Valide
useAnsi(verificandostdout.hasTerminal,supportsAnsiEscapeseNO_COLOR) antes de emitir códigos ANSI. - Gere ferramentas filhas usando
cli_util.dartExecutable, nuncaPlatform.resolvedExecutable. - Teste unitariamente os runners de comando em memória; teste a execução binária de ponta a ponta com
test_process.
Referências e Exemplos
- Modelo de Ferramenta de Comando Único: exemplos/ferramenta_comando_unico.dart
- Modelo de Runner de Múltiplos Comandos: exemplos/runner_multiplos_comandos.dart
- Descoberta de SDK AOT e Criação de Subprocessos: referências/descoberta_sdk_aot.md
- Manipulação de Sinais e Desligamento do Terminal: referências/sinais_e_terminal.md