Durante o desenvolvimento, recompilar manualmente apos cada alteracao e tedioso e improdutivo. Ferramentas como nodemon, watchexec e cargo-watch resolvem isso monitorando o sistema de arquivos e executando acoes automaticamente. Neste projeto, vamos construir nosso proprio monitor de alteracoes em arquivos em Rust, com suporte a padroes glob, debouncing para evitar execucoes duplicadas e acoes customizaveis.
Este projeto ensina conceitos importantes de interacao com o sistema de arquivos, programacao orientada a eventos e controle fino de temporalidade.
O Que Vamos Construir
- Monitoramento recursivo de diretorios
- Filtragem por padroes glob (ex:
*.rs,*.toml) - Debouncing configuravel para evitar execucoes repetidas
- Execucao de comandos customizaveis ao detectar mudancas
- Ignorar diretorios especificos (ex:
target/,.git/) - Saida informativa com caminho do arquivo modificado
Estrutura do Projeto
file-watcher/
├── Cargo.toml
└── src/
├── main.rs
├── filtro.rs
└── executor.rs
Configurando o Projeto
cargo new file-watcher
cd file-watcher
Edite o Cargo.toml:
[package]
name = "file-watcher"
version = "0.1.0"
edition = "2021"
[dependencies]
notify = "6.1"
glob-match = "0.2"
clap = { version = "4.5", features = ["derive"] }
colored = "2.1"
Passo 1: Filtro de Arquivos
O modulo de filtro decide quais mudancas devem disparar acoes e quais devem ser ignoradas. Crie src/filtro.rs:
use std::path::Path;
/// Configuracao de filtragem para o monitor.
pub struct ConfigFiltro {
/// Padroes glob para incluir (ex: "*.rs")
pub padroes_incluir: Vec<String>,
/// Diretorios para ignorar (ex: "target", ".git")
pub diretorios_ignorar: Vec<String>,
}
impl ConfigFiltro {
pub fn new(padroes: Vec<String>, ignorar: Vec<String>) -> Self {
ConfigFiltro {
padroes_incluir: padroes,
diretorios_ignorar: ignorar,
}
}
/// Verifica se um caminho deve ser monitorado.
pub fn deve_processar(&self, caminho: &Path) -> bool {
// Verificar se esta em um diretorio ignorado
let caminho_str = caminho.to_string_lossy();
for dir_ignorado in &self.diretorios_ignorar {
if caminho_str.contains(&format!("/{}/", dir_ignorado))
|| caminho_str.contains(&format!("\\{}\\", dir_ignorado))
|| caminho_str.starts_with(&format!("{}/", dir_ignorado))
{
return false;
}
}
// Se nao ha padroes de inclusao, aceitar tudo
if self.padroes_incluir.is_empty() {
return true;
}
// Verificar se o arquivo corresponde a algum padrao
let nome = caminho
.file_name()
.unwrap_or_default()
.to_string_lossy();
for padrao in &self.padroes_incluir {
if corresponde_glob(&nome, padrao) {
return true;
}
}
false
}
}
/// Implementacao simples de matching de padroes glob.
/// Suporta '*' como curinga para qualquer sequencia de caracteres.
fn corresponde_glob(texto: &str, padrao: &str) -> bool {
if padrao == "*" {
return true;
}
// Padrao tipo "*.rs" -- verificar extensao
if let Some(extensao) = padrao.strip_prefix("*.") {
return texto.ends_with(&format!(".{}", extensao));
}
// Padrao tipo "Cargo.*" -- verificar prefixo
if let Some(prefixo) = padrao.strip_suffix(".*") {
return texto.starts_with(prefixo) && texto.contains('.');
}
// Correspondencia exata
texto == padrao
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn teste_filtro_extensao_rs() {
let filtro = ConfigFiltro::new(
vec!["*.rs".to_string()],
vec!["target".to_string()],
);
assert!(filtro.deve_processar(&PathBuf::from("src/main.rs")));
assert!(!filtro.deve_processar(&PathBuf::from("src/main.py")));
}
#[test]
fn teste_ignorar_diretorio() {
let filtro = ConfigFiltro::new(
vec!["*.rs".to_string()],
vec!["target".to_string()],
);
assert!(!filtro.deve_processar(&PathBuf::from("target/debug/main.rs")));
}
#[test]
fn teste_glob_simples() {
assert!(corresponde_glob("main.rs", "*.rs"));
assert!(!corresponde_glob("main.py", "*.rs"));
assert!(corresponde_glob("qualquer.txt", "*"));
assert!(corresponde_glob("Cargo.toml", "Cargo.*"));
}
}
O filtro usa padroes glob simplificados para decidir quais arquivos merecem atencao. Diretorios como target/ e .git/ sao ignorados por padrao para evitar loops infinitos de recompilacao.
Passo 2: Executor de Comandos
O executor roda o comando especificado pelo usuario ao detectar uma mudanca. Crie src/executor.rs:
use colored::*;
use std::process::Command;
use std::time::Instant;
/// Executa um comando no shell e exibe o resultado formatado.
pub fn executar(comando: &str) -> bool {
println!(
"\n{} {}",
">>>".green().bold(),
comando.white().bold()
);
println!("{}", "-".repeat(60).dimmed());
let inicio = Instant::now();
let resultado = Command::new("sh")
.arg("-c")
.arg(comando)
.status();
let duracao = inicio.elapsed();
println!("{}", "-".repeat(60).dimmed());
match resultado {
Ok(status) => {
if status.success() {
println!(
"{} Concluido com sucesso em {:.2}s\n",
"[OK]".green().bold(),
duracao.as_secs_f64()
);
true
} else {
println!(
"{} Falhou com codigo {} em {:.2}s\n",
"[ERRO]".red().bold(),
status.code().unwrap_or(-1),
duracao.as_secs_f64()
);
false
}
}
Err(e) => {
println!(
"{} Erro ao executar comando: {}\n",
"[ERRO]".red().bold(),
e
);
false
}
}
}
O executor invoca o comando via shell (sh -c) para suportar pipelines e redirecionamentos. A duracao da execucao e exibida para ajudar a identificar comandos lentos.
Passo 3: Juntando Tudo no main.rs
Agora vamos implementar o loop principal com o sistema de notificacao e debouncing. Crie src/main.rs:
mod executor;
mod filtro;
use clap::Parser;
use colored::*;
use filtro::ConfigFiltro;
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::PathBuf;
use std::sync::mpsc;
use std::time::{Duration, Instant};
/// Monitor de alteracoes em arquivos com execucao automatica de comandos
#[derive(Parser)]
#[command(name = "file-watcher")]
#[command(about = "Monitora arquivos e executa comandos ao detectar mudancas")]
struct Argumentos {
/// Diretorio para monitorar (padrao: diretorio atual)
#[arg(short, long, default_value = ".")]
diretorio: PathBuf,
/// Comando para executar ao detectar mudancas
#[arg(short, long)]
comando: String,
/// Padroes glob para filtrar arquivos (ex: "*.rs")
#[arg(short, long)]
padrao: Vec<String>,
/// Diretorios para ignorar
#[arg(short, long, default_values_t = vec![
"target".to_string(),
".git".to_string(),
"node_modules".to_string(),
])]
ignorar: Vec<String>,
/// Tempo de debounce em milissegundos
#[arg(long, default_value = "500")]
debounce: u64,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Argumentos::parse();
let filtro = ConfigFiltro::new(args.padrao.clone(), args.ignorar.clone());
println!("{}", "=== Monitor de Arquivos ===".green().bold());
println!("Diretorio: {}", args.diretorio.display());
println!("Comando: {}", args.comando.cyan());
if !args.padrao.is_empty() {
println!(
"Padroes: {}",
args.padrao.join(", ").yellow()
);
}
println!(
"Ignorando: {}",
args.ignorar.join(", ").dimmed()
);
println!(
"Debounce: {}ms",
args.debounce
);
println!(
"\n{}\n",
"Aguardando mudancas... (Ctrl+C para sair)".dimmed()
);
// Canal para receber eventos de modificacao
let (tx, rx) = mpsc::channel::<PathBuf>();
let mut watcher = RecommendedWatcher::new(
move |resultado: Result<Event, notify::Error>| {
if let Ok(evento) = resultado {
match evento.kind {
EventKind::Modify(_) | EventKind::Create(_) => {
for caminho in evento.paths {
let _ = tx.send(caminho);
}
}
_ => {}
}
}
},
Config::default(),
)?;
// Iniciar monitoramento recursivo
watcher.watch(&args.diretorio, RecursiveMode::Recursive)?;
let debounce = Duration::from_millis(args.debounce);
let mut ultima_execucao = Instant::now() - debounce; // Permitir execucao imediata
loop {
match rx.recv() {
Ok(caminho) => {
// Verificar se o arquivo passa no filtro
if !filtro.deve_processar(&caminho) {
continue;
}
// Debouncing: ignorar se executou ha pouco tempo
let agora = Instant::now();
if agora.duration_since(ultima_execucao) < debounce {
// Drenar eventos acumulados
while rx.try_recv().is_ok() {}
continue;
}
// Drenar eventos acumulados antes de executar
std::thread::sleep(Duration::from_millis(50));
while rx.try_recv().is_ok() {}
let nome_arquivo = caminho
.file_name()
.unwrap_or_default()
.to_string_lossy();
println!(
"{} Mudanca detectada: {}",
"[WATCH]".blue().bold(),
nome_arquivo.yellow()
);
executor::executar(&args.comando);
ultima_execucao = Instant::now();
}
Err(e) => {
eprintln!("Erro no canal de eventos: {}", e);
break;
}
}
}
Ok(())
}
O loop principal recebe notificacoes de mudancas, aplica o filtro e implementa debouncing para evitar que salvar o mesmo arquivo varias vezes rapidamente dispare multiplas execucoes. O pequeno atraso de 50ms antes de drenar eventos acumulados garante que mudancas simultaneas em multiplos arquivos sejam agrupadas em uma unica execucao.
Como Executar
# Compilar o projeto
cargo build --release
# Monitorar arquivos Rust e recompilar automaticamente
cargo run -- --diretorio . --comando "cargo build" --padrao "*.rs" --padrao "*.toml"
# Monitorar e rodar testes automaticamente
cargo run -- -d src/ -c "cargo test" -p "*.rs"
# Monitorar um projeto web e recarregar
cargo run -- -d ./static -c "echo 'Arquivos atualizados!'" -p "*.html" -p "*.css" -p "*.js"
# Com debounce customizado de 1 segundo
cargo run -- -d . -c "cargo build" -p "*.rs" --debounce 1000
Saida esperada:
=== Monitor de Arquivos ===
Diretorio: .
Comando: cargo build
Padroes: *.rs, *.toml
Ignorando: target, .git, node_modules
Debounce: 500ms
Aguardando mudancas... (Ctrl+C para sair)
[WATCH] Mudanca detectada: main.rs
>>> cargo build
------------------------------------------------------------
Compiling file-watcher v0.1.0 (/home/dev/file-watcher)
Finished dev [unoptimized + debuginfo] target(s) in 1.23s
------------------------------------------------------------
[OK] Concluido com sucesso em 1.25s
Desafios para Expandir
- Substituicao de variaveis: Permita usar
{{arquivo}}e{{diretorio}}no comando, substituindo pelo caminho do arquivo que mudou e seu diretorio pai. - Arquivo de configuracao: Crie suporte a um arquivo
.watcherrc.tomlna raiz do projeto com configuracoes predefinidas para evitar digitar argumentos longos. - Modo silencioso com notificacao: Adicione um modo que suprime a saida do comando e mostra apenas notificacoes de sucesso/falha usando
notify-rustpara notificacoes de desktop. - Restart automatico de processos: Implemente um modo
--restartque mantém o processo rodando e o reinicia quando detecta mudancas, util para servidores de desenvolvimento. - Historico de mudancas: Registre um log de todas as mudancas detectadas e execucoes realizadas em um arquivo, incluindo timestamps e resultados.