Rust — Artigo #40
Rust para CLI — Construindo Ferramentas de Linha de Comando Profissionais com clap
Por Prof. Dr. Marcelo Fontes | Série: Dominando Rust em 1 Ano
Ferramentas de linha de comando são onde Rust brilha de forma imediata e visível. Binários pequenos, startup instantâneo, sem runtime, sem JVM, sem interpretador — você distribui um único executável que roda em qualquer máquina com a mesma arquitetura. Ferramentas como ripgrep, fd, bat, exa, delta, e hyperfine demonstraram que CLIs em Rust podem ser simultaneamente mais rápidas e mais agradáveis de usar que suas equivalentes em C ou scripts shell.
Neste artigo construímos uma CLI completa e profissional usando clap, com subcomandos, configuração, output colorido, barras de progresso, e instalação via cargo install.
Dependências do projeto
[package]
name = "projctl"
version = "0.1.0"
edition = "2021"
description = "Gerenciador de projetos de desenvolvimento"
license = "MIT OR Apache-2.0"
[[bin]]
name = "projctl"
path = "src/main.rs"
[dependencies]
# Parser de argumentos — a crate mais popular para CLIs
clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] }
# Output colorido e formatado
colored = "2"
indicatif = "0.17" # barras de progresso
console = "0.15" # terminal utilities
# Serialização de configuração
serde = { version = "1", features = ["derive"] }
toml = "0.8"
serde_json = "1"
# Erros
anyhow = "1"
thiserror = "1"
# Utilitários
chrono = { version = "0.4", features = ["serde"] }
dirs = "5" # diretórios do sistema (home, config, etc.)
glob = "0.3"
walkdir = "2"
dialoguer = "0.11" # prompts interativos
tabled = "0.15" # tabelas formatadas
Estrutura do projeto
projctl/
├── Cargo.toml
├── src/
│ ├── main.rs — ponto de entrada
│ ├── cli.rs — definição da CLI com clap
│ ├── config.rs — configuração persistente
│ ├── commands/
│ │ ├── mod.rs
│ │ ├── init.rs
│ │ ├── list.rs
│ │ ├── status.rs
│ │ └── build.rs
│ ├── output.rs — helpers de output
│ └── error.rs — tipos de erro
Definindo a CLI com clap derive
// src/cli.rs
use clap::{Args, Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
/// projctl — Gerenciador de projetos de desenvolvimento
///
/// Gerencie seus projetos Rust com facilidade: inicialize,
/// liste, verifique status e construa projetos.
#[derive(Parser, Debug)]
#[command(
name = "projctl",
version, // lê de Cargo.toml automaticamente
author, // lê de Cargo.toml
about, // lê de Cargo.toml
long_about = None,
propagate_version = true, // --version disponível em subcomandos
subcommand_required = true,
arg_required_else_help = true, // mostra help se sem argumentos
)]
pub struct Cli {
/// Nível de verbosidade (-v, -vv, -vvv)
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
pub verbose: u8,
/// Saída em formato JSON (para scripts)
#[arg(long, global = true)]
pub json: bool,
/// Desabilita cores no output
#[arg(long, global = true, env = "NO_COLOR")]
pub no_color: bool,
#[command(subcommand)]
pub comando: Comando,
}
#[derive(Subcommand, Debug)]
pub enum Comando {
/// Inicializa um novo projeto
Init(ArgsInit),
/// Lista projetos gerenciados
#[command(alias = "ls")]
List(ArgsList),
/// Verifica o status de um projeto
Status(ArgsStatus),
/// Constrói o projeto
Build(ArgsBuild),
/// Gerencia a configuração
Config(ArgsConfig),
/// Exibe estatísticas dos projetos
Stats,
}
// ── Init ─────────────────────────────────────────────
/// Argumentos para o comando init
#[derive(Args, Debug)]
pub struct ArgsInit {
/// Nome do projeto
pub nome: String,
/// Diretório onde criar o projeto
/// [padrão: diretório atual]
#[arg(short, long)]
pub dir: Option<PathBuf>,
/// Template a usar
#[arg(short, long, value_enum, default_value_t = Template::Lib)]
pub template: Template,
/// Inicializa repositório git
#[arg(long, default_value_t = true)]
pub git: bool,
/// Abre o projeto no editor após criar
#[arg(long)]
pub abrir: bool,
}
#[derive(ValueEnum, Debug, Clone)]
pub enum Template {
/// Biblioteca (lib)
Lib,
/// Binário (bin)
Bin,
/// Workspace com lib e CLI
Workspace,
}
impl std::fmt::Display for Template {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Template::Lib => write!(f, "lib"),
Template::Bin => write!(f, "bin"),
Template::Workspace => write!(f, "workspace"),
}
}
}
// ── List ─────────────────────────────────────────────
#[derive(Args, Debug)]
pub struct ArgsList {
/// Filtrar por nome (suporta glob)
pub filtro: Option<String>,
/// Ordenar por campo
#[arg(short, long, value_enum, default_value_t = OrdenarPor::Nome)]
pub ordenar: OrdenarPor,
/// Ordem inversa
#[arg(short, long)]
pub reverso: bool,
/// Exibir apenas projetos com mudanças pendentes
#[arg(long)]
pub pendentes: bool,
}
#[derive(ValueEnum, Debug, Clone)]
pub enum OrdenarPor {
Nome,
Data,
Tamanho,
}
// ── Status ────────────────────────────────────────────
#[derive(Args, Debug)]
pub struct ArgsStatus {
/// Nome do projeto (padrão: projeto no diretório atual)
pub projeto: Option<String>,
/// Verifica dependências desatualizadas
#[arg(long)]
pub deps: bool,
/// Roda testes e inclui resultado
#[arg(long)]
pub testes: bool,
}
// ── Build ─────────────────────────────────────────────
#[derive(Args, Debug)]
pub struct ArgsBuild {
/// Nome do projeto
pub projeto: Option<String>,
/// Perfil de compilação
#[arg(short, long, value_enum, default_value_t = Perfil::Dev)]
pub perfil: Perfil,
/// Features a ativar
#[arg(long, value_delimiter = ',')]
pub features: Vec<String>,
/// Ativa todas as features
#[arg(long)]
pub all_features: bool,
/// Número de jobs paralelos
#[arg(short, long)]
pub jobs: Option<u32>,
}
#[derive(ValueEnum, Debug, Clone)]
pub enum Perfil {
Dev,
Release,
#[value(name = "release-debug")]
ReleaseDebug,
}
// ── Config ────────────────────────────────────────────
#[derive(Args, Debug)]
pub struct ArgsConfig {
#[command(subcommand)]
pub acao: AcaoConfig,
}
#[derive(Subcommand, Debug)]
pub enum AcaoConfig {
/// Exibe a configuração atual
Show,
/// Define um valor
Set {
chave: String,
valor: String,
},
/// Remove um valor
Unset {
chave: String,
},
/// Abre o arquivo de configuração no editor
Edit,
}
Configuração persistente
// src/config.rs
use anyhow::{Context, Result};
use dirs::config_dir;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct Config {
pub editor: String,
pub dir_projetos: PathBuf,
pub git_usuario: Option<String>,
pub git_email: Option<String>,
pub template_padrao: String,
pub mostrar_ocultos: bool,
}
impl Default for Config {
fn default() -> Self {
Config {
editor: std::env::var("EDITOR")
.unwrap_or_else(|_| "vim".to_string()),
dir_projetos: dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("projetos"),
git_usuario: None,
git_email: None,
template_padrao: "lib".to_string(),
mostrar_ocultos: false,
}
}
}
impl Config {
pub fn caminho() -> PathBuf {
config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("projctl")
.join("config.toml")
}
pub fn carregar() -> Result<Self> {
let caminho = Self::caminho();
if !caminho.exists() {
return Ok(Config::default());
}
let conteudo = std::fs::read_to_string(&caminho)
.with_context(|| format!("Falha ao ler config: {}", caminho.display()))?;
toml::from_str(&conteudo)
.with_context(|| "Configuração inválida")
}
pub fn salvar(&self) -> Result<()> {
let caminho = Self::caminho();
if let Some(dir) = caminho.parent() {
std::fs::create_dir_all(dir)
.with_context(|| format!("Falha ao criar dir: {}", dir.display()))?;
}
let conteudo = toml::to_string_pretty(self)
.context("Falha ao serializar configuração")?;
std::fs::write(&caminho, conteudo)
.with_context(|| format!("Falha ao salvar config: {}", caminho.display()))
}
pub fn set(&mut self, chave: &str, valor: &str) -> Result<()> {
match chave {
"editor" => self.editor = valor.to_string(),
"git.usuario" => self.git_usuario = Some(valor.to_string()),
"git.email" => self.git_email = Some(valor.to_string()),
"template_padrao" => self.template_padrao = valor.to_string(),
"mostrar_ocultos" => {
self.mostrar_ocultos = valor.parse()
.context("Valor deve ser 'true' ou 'false'")?;
}
_ => anyhow::bail!("Chave desconhecida: '{chave}'"),
}
Ok(())
}
pub fn get(&self, chave: &str) -> Result<String> {
Ok(match chave {
"editor" => self.editor.clone(),
"git.usuario" => self.git_usuario.clone().unwrap_or_default(),
"git.email" => self.git_email.clone().unwrap_or_default(),
"template_padrao" => self.template_padrao.clone(),
"mostrar_ocultos" => self.mostrar_ocultos.to_string(),
_ => anyhow::bail!("Chave desconhecida: '{chave}'"),
})
}
}
Output colorido e tabelas
// src/output.rs
use colored::*;
use indicatif::{ProgressBar, ProgressStyle};
use std::time::Duration;
pub struct Output {
pub json: bool,
pub verbose: u8,
pub cores: bool,
}
impl Output {
pub fn novo(json: bool, verbose: u8, no_color: bool) -> Self {
if no_color {
colored::control::set_override(false);
}
Output { json, verbose, cores: !no_color }
}
pub fn sucesso(&self, msg: &str) {
if !self.json {
println!("{} {}", "✓".green().bold(), msg);
}
}
pub fn erro(&self, msg: &str) {
eprintln!("{} {}", "✗".red().bold(), msg.red());
}
pub fn aviso(&self, msg: &str) {
if !self.json {
eprintln!("{} {}", "⚠".yellow().bold(), msg.yellow());
}
}
pub fn info(&self, msg: &str) {
if !self.json {
println!("{} {}", "→".blue(), msg);
}
}
pub fn detalhe(&self, msg: &str) {
if !self.json && self.verbose > 0 {
println!(" {}", msg.dimmed());
}
}
pub fn secao(&self, titulo: &str) {
if !self.json {
println!("
{}", titulo.bold().underline());
}
}
pub fn barra_progresso(&self, tamanho: u64, msg: &str) -> ProgressBar {
let pb = ProgressBar::new(tamanho);
pb.set_style(
ProgressStyle::with_template(
"{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}"
)
.unwrap()
.progress_chars("=>-"),
);
pb.set_message(msg.to_string());
pb.enable_steady_tick(Duration::from_millis(100));
pb
}
pub fn spinner(&self, msg: &str) -> ProgressBar {
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::with_template("{spinner:.green} {msg}")
.unwrap()
.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "),
);
pb.set_message(msg.to_string());
pb.enable_steady_tick(Duration::from_millis(80));
pb
}
pub fn tabela<T: tabled::Tabled>(&self, dados: &[T]) {
if self.json { return; }
if dados.is_empty() {
self.aviso("Nenhum resultado encontrado.");
return;
}
use tabled::Table;
println!("{}", Table::new(dados));
}
}
Implementando os comandos
// src/commands/init.rs
use crate::{cli::{ArgsInit, Template}, config::Config, output::Output};
use anyhow::{Context, Result};
use dialoguer::{Confirm, Input, Select};
use std::process::Command;
pub fn executar(args: ArgsInit, config: &Config, out: &Output) -> Result<()> {
let dir_base = args.dir.unwrap_or_else(|| config.dir_projetos.clone());
let dir_projeto = dir_base.join(&args.nome);
if dir_projeto.exists() {
anyhow::bail!(
"Diretório já existe: {}",
dir_projeto.display()
);
}
out.info(&format!("Criando projeto '{}'...", args.nome.bold()));
out.detalhe(&format!("Diretório: {}", dir_projeto.display()));
out.detalhe(&format!("Template: {}", args.template));
// Cria com cargo
let spinner = out.spinner("Inicializando com cargo...");
let tipo_flag = match args.template {
Template::Lib => "--lib",
Template::Bin => "--bin",
Template::Workspace => "--lib", // customizado depois
};
let status = Command::new("cargo")
.args(["new", tipo_flag, dir_projeto.to_str().unwrap()])
.output()
.context("Falha ao executar cargo new")?;
spinner.finish_and_clear();
if !status.status.success() {
let stderr = String::from_utf8_lossy(&status.stderr);
anyhow::bail!("cargo new falhou: {}", stderr);
}
// Git
if args.git {
out.detalhe("Inicializando repositório git...");
let _ = Command::new("git")
.args(["init", dir_projeto.to_str().unwrap()])
.output();
// .gitignore básico
std::fs::write(
dir_projeto.join(".gitignore"),
"/target
.env
*.log
",
)?;
}
// README básico
std::fs::write(
dir_projeto.join("README.md"),
format!("# {}
Descrição do projeto.
", args.nome),
)?;
out.sucesso(&format!(
"Projeto '{}' criado em {}",
args.nome.bold(),
dir_projeto.display()
));
if args.abrir {
let editor = &config.editor;
out.info(&format!("Abrindo com {editor}..."));
Command::new(editor)
.arg(&dir_projeto)
.spawn()
.with_context(|| format!("Falha ao abrir editor '{editor}'"))?;
}
Ok(())
}
// src/commands/list.rs
use crate::{cli::{ArgsList, OrdenarPor}, config::Config, output::Output};
use anyhow::Result;
use chrono::{DateTime, Local};
use serde::Serialize;
use std::path::PathBuf;
use tabled::Tabled;
#[derive(Debug, Serialize, Tabled)]
pub struct EntradaProjeto {
#[tabled(rename = "Nome")]
pub nome: String,
#[tabled(rename = "Caminho")]
pub caminho: String,
#[tabled(rename = "Modificado")]
pub modificado: String,
#[tabled(rename = "Tamanho")]
pub tamanho: String,
}
pub fn executar(args: ArgsList, config: &Config, out: &Output) -> Result<()> {
let dir = &config.dir_projetos;
if !dir.exists() {
out.aviso(&format!(
"Diretório de projetos não existe: {}",
dir.display()
));
out.info("Configure com: projctl config set dir_projetos /caminho");
return Ok(());
}
let spinner = out.spinner("Escaneando projetos...");
let mut projetos: Vec<EntradaProjeto> = std::fs::read_dir(dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir())
.filter(|e| {
// É um projeto Rust?
e.path().join("Cargo.toml").exists()
})
.filter(|e| {
// Aplica filtro de nome
if let Some(ref filtro) = args.filtro {
let nome = e.file_name().to_string_lossy().to_lowercase();
nome.contains(&filtro.to_lowercase())
} else {
true
}
})
.filter_map(|e| {
let path = e.path();
let nome = path.file_name()?.to_string_lossy().to_string();
let meta = std::fs::metadata(&path).ok()?;
let modificado = meta.modified().ok()
.map(|t| {
let dt: DateTime<Local> = t.into();
dt.format("%Y-%m-%d %H:%M").to_string()
})
.unwrap_or_else(|| "—".to_string());
let tamanho = tamanho_dir(&path);
Some(EntradaProjeto {
nome,
caminho: path.to_string_lossy().to_string(),
modificado,
tamanho,
})
})
.collect();
spinner.finish_and_clear();
// Ordena
match args.ordenar {
OrdenarPor::Nome => projetos.sort_by(|a, b| a.nome.cmp(&b.nome)),
OrdenarPor::Data => projetos.sort_by(|a, b| b.modificado.cmp(&a.modificado)),
OrdenarPor::Tamanho => projetos.sort_by(|a, b| b.tamanho.cmp(&a.tamanho)),
}
if args.reverso {
projetos.reverse();
}
if out.json {
println!("{}", serde_json::to_string_pretty(&projetos)?);
return Ok(());
}
if projetos.is_empty() {
out.aviso("Nenhum projeto encontrado.");
} else {
out.secao(&format!("Projetos ({})", projetos.len()));
out.tabela(&projetos);
}
Ok(())
}
fn tamanho_dir(path: &PathBuf) -> String {
let bytes: u64 = walkdir::WalkDir::new(path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_file())
.filter_map(|e| e.metadata().ok())
.map(|m| m.len())
.sum();
formatar_bytes(bytes)
}
fn formatar_bytes(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.1}GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.1}MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.1}KB", bytes as f64 / KB as f64)
} else {
format!("{bytes}B")
}
}
Ponto de entrada principal
// src/main.rs
mod cli;
mod commands;
mod config;
mod error;
mod output;
use anyhow::Result;
use clap::Parser;
use cli::{Cli, Comando};
use output::Output;
fn main() {
if let Err(e) = executar() {
// Formata erros anyhow com toda a cadeia de contexto
eprintln!("{}: {}", "erro".red().bold(), e);
// Em modo verbose, mostra o backtrace
let verbose = std::env::args().any(|a| a == "-v" || a == "--verbose");
if verbose {
for causa in e.chain().skip(1) {
eprintln!(" {} {}", "causado por:".dimmed(), causa);
}
}
std::process::exit(1);
}
}
fn executar() -> Result<()> {
let cli = Cli::parse();
let config = config::Config::carregar()
.unwrap_or_else(|e| {
eprintln!("Aviso: falha ao carregar config: {e}");
config::Config::default()
});
let out = Output::novo(cli.json, cli.verbose, cli.no_color);
match cli.comando {
Comando::Init(args) => {
commands::init::executar(args, &config, &out)
}
Comando::List(args) => {
commands::list::executar(args, &config, &out)
}
Comando::Status(args) => {
commands::status::executar(args, &config, &out)
}
Comando::Build(args) => {
commands::build::executar(args, &config, &out)
}
Comando::Config(args) => {
executar_config(args, config, &out)
}
Comando::Stats => {
commands::stats::executar(&config, &out)
}
}
}
fn executar_config(
args: cli::ArgsConfig,
mut config: config::Config,
out: &Output,
) -> Result<()> {
use cli::AcaoConfig;
match args.acao {
AcaoConfig::Show => {
let conteudo = toml::to_string_pretty(&config)?;
if out.json {
println!("{}", serde_json::to_string_pretty(&config)?);
} else {
println!("{}", conteudo);
}
}
AcaoConfig::Set { chave, valor } => {
config.set(&chave, &valor)?;
config.salvar()?;
out.sucesso(&format!("{chave} = {valor}"));
}
AcaoConfig::Unset { chave } => {
config.set(&chave, "")?;
config.salvar()?;
out.sucesso(&format!("{chave} removida"));
}
AcaoConfig::Edit => {
let caminho = config::Config::caminho();
if !caminho.exists() {
config.salvar()?;
}
std::process::Command::new(&config.editor)
.arg(&caminho)
.status()?;
}
}
Ok(())
}
Testes para CLIs
// tests/cli_integration.rs
use assert_cmd::Command;
use predicates::prelude::*;
#[test]
fn sem_argumentos_mostra_ajuda() {
let mut cmd = Command::cargo_bin("projctl").unwrap();
cmd.assert()
.failure()
.stderr(predicate::str::contains("Usage"));
}
#[test]
fn versao_disponivel() {
Command::cargo_bin("projctl").unwrap()
.arg("--version")
.assert()
.success()
.stdout(predicate::str::contains("projctl"));
}
#[test]
fn ajuda_disponivel() {
Command::cargo_bin("projctl").unwrap()
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Gerenciador"));
}
#[test]
fn list_json_output() {
Command::cargo_bin("projctl").unwrap()
.args(["--json", "list"])
.assert()
.success()
.stdout(predicate::str::starts_with("["));
}
#[test]
fn config_show() {
Command::cargo_bin("projctl").unwrap()
.args(["config", "show"])
.assert()
.success();
}
#[test]
fn subcomando_invalido() {
Command::cargo_bin("projctl").unwrap()
.arg("nao-existe")
.assert()
.failure()
.stderr(predicate::str::contains("error"));
}
Adicione ao Cargo.toml:
[dev-dependencies]
assert_cmd = "2"
predicates = "3"
tempfile = "3"
Shell completion
clap pode gerar scripts de completion para bash, zsh, fish, e PowerShell:
// src/bin/completions.rs — binário auxiliar para gerar completions
use clap::CommandFactory;
use clap_complete::{generate, Shell};
use std::io;
// Inclui a CLI do projeto principal
include!("../cli.rs");
fn main() {
let shell: Shell = std::env::args()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(Shell::Bash);
let mut cmd = Cli::command();
generate(shell, &mut cmd, "projctl", &mut io::stdout());
}
# Cargo.toml
[dependencies]
clap_complete = "4"
# Instalar completion no bash
projctl-completions bash >> ~/.bashrc
# Instalar no zsh
projctl-completions zsh >> ~/.zshrc
# Fish
projctl-completions fish > ~/.config/fish/completions/projctl.fish
Distribuição e instalação
# Instalar diretamente do crates.io
cargo install projctl
# Instalar de repositório git
cargo install --git https://github.com/usuario/projctl
# Instalar de path local (desenvolvimento)
cargo install --path .
# Compilar binário otimizado manualmente
RUSTFLAGS="-C target-cpu=native" cargo build --release
# Binário em: target/release/projctl
# Cross-compilação para Linux em macOS
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl
# Binário estático — roda em qualquer Linux sem dependências
GitHub Actions para releases automáticos
# .github/workflows/release.yml
name: Release
on:
push:
tags: ['v*']
jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
suffix: ""
- os: macos-latest
target: x86_64-apple-darwin
suffix: ""
- os: macos-latest
target: aarch64-apple-darwin
suffix: ""
- os: windows-latest
target: x86_64-pc-windows-msvc
suffix: ".exe"
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Build
run: cargo build --release --target ${{ matrix.target }}
- name: Renomear binário
shell: bash
run: |
cp target/${{ matrix.target }}/release/projctl${{ matrix.suffix }}
projctl-${{ matrix.target }}${{ matrix.suffix }}
- uses: softprops/action-gh-release@v1
with:
files: projctl-${{ matrix.target }}${{ matrix.suffix }}
Boas práticas para CLIs em Rust
Siga as convenções Unix. Exit code 0 para sucesso, não-zero para erro. Use stdout para dados, stderr para erros e diagnósticos. Respeite NO_COLOR. Suporte a pipes (stdin/stdout).
Mensagens de erro úteis. Não apenas "erro: operação falhou" — inclua o que falhou, por que falhou, e como corrigir. anyhow facilita muito isso com contexto encadeado.
Flags globais para comportamento universal. --verbose, --quiet, --json, --no-color devem funcionar em qualquer subcomando com global = true.
Progresso para operações longas. Qualquer operação que dure mais de 300ms deve ter um indicador de progresso. indicatif é excelente para isso.
Modo não-interativo para scripts. Se a CLI vai ser usada em scripts, --json para output parseable e evitar prompts são essenciais.
Fontes e leituras recomendadas
clapdocumentation — https://docs.rs/clapclapderive guide — https://docs.rs/clap/latest/clap/_derive/- "Command Line Interface Guidelines" — convenções de UX para CLIs — https://clig.dev
indicatifcrate — barras de progresso — https://docs.rs/indicatifdialoguercrate — prompts interativos — https://docs.rs/dialoguerassert_cmdcrate — testes de integração para CLIs — https://docs.rs/assert_cmd- "Awesome CLI Rust" — curadoria de ferramentas e bibliotecas — https://github.com/rust-unofficial/awesome-rust#command-line
- ripgrep source code — referência de CLI profissional em Rust — https://github.com/BurntSushi/ripgrep
Artigo #40 de 52 | Série: Dominando Rust em 1 Ano Próximo → Artigo #41: Rust para Data Science — Processamento de dados com Polars e ndarray