En el capítulo anterior construiste un analizador de logs con regex, BufReader y conteos con BTreeMap. Procesabas archivos línea a línea, filtrabas por nivel de severidad y mostrabas una tabla con barras de progreso. Mola, pero seguías dentro de los límites de Rust puro. Ahora toca salir al mundo real. Los scripts no viven aislados, llaman a df, free, ps, uptime, grep, systemctl, docker, y un largo etcétera. En Bash es tan sencillo como escribir el comando entre backticks o con $(...). En Rust tienes std::process::Command, que te da un control total sobre cómo ejecutas comandos externos. Pero hay un problema: los comandos fallan. El binario no está instalado, el usuario no tiene permisos, el disco está lleno, el proceso lleva demasiado tiempo… y tu programa tiene que manejar todo eso sin crashear.
Ejecutando comandos
Como te indicaba en la introducción, una cosa es ejecutar comandos en bash, y otra ligeramente distinta es hacerlo desde Rust, pero, como vas a ver a continuación, todo tiene sus ventajas e inconvenientes. Así en este capítulo vamos a ver como hacer determinadas acciones como,
- Ejecutar comandos del sistema con
std::process::Command - Capturar stdout, stderr y el código de salida
- Gestionar errores de verdad con
anyhowythiserror - Crear tus propios tipos de error personalizados
- Encadenar comandos (pipe) como en Bash
- Construir
crustaceo-sysinfo, un monitor del sistema que ejecutadf,free,psyuptime, y lo presenta todo con colores
std::process::Command: ejecutar comandos desde Rust
std::process::Command es la puerta de entrada a los comandos externos. Construyes un comando, le pasas argumentos, y lo ejecutas. La forma más básica:
use std::process::Command;
fn main() {
let mut cmd = Command::new("whoami");
let salida = cmd.output().expect("fallo al ejecutar whoami");
println!("stdout: {}", String::from_utf8_lossy(&salida.stdout));
println!("stderr: {}", String::from_utf8_lossy(&salida.stderr));
println!("código salida: {:?}", salida.status.code());
}
Compila y ejecuta:
cargo new crustaceo-cmd --name crustaceo-cmd
cd crustaceo-cmd
cargo run
# stdout: lorenzo
# stderr:
# código salida: Some(0)
Command::new("whoami") crea el comando. .output() lo ejecuta y devuelve un Result<Output, std::io::Error>. Output contiene tres campos:
stdout: Vec<u8>— la salida estándar como bytesstderr: Vec<u8>— el error estándar como bytesstatus: ExitStatus— el código de salida y si terminó por señal
String::from_utf8_lossy() convierte los bytes a String. Si los bytes no son UTF-8 válidos, los reemplaza con « en lugar de petar.
Pasar argumentos
Los comandos rara vez se ejecutan sin argumentos. Se pasan con .arg() o .args():
use std::process::Command;
fn main() {
// Con .arg() uno a uno
let salida = Command::new("ls")
.arg("-la")
.arg("/home")
.output()
.expect("fallo al ejecutar ls");
println!("{}", String::from_utf8_lossy(&salida.stdout));
}
O con .args() y un slice:
let salida = Command::new("df")
.args(["-h", "--total"])
.output()
.expect("fallo al ejecutar df");
Regla de oro: cada argumento va en su propio .arg(). NO metas varios argumentos en un solo string como harías en Bash ("df -h --total"). Eso le pasa a df un único argumento que es literalmente "-h --total", y df no sabe qué hacer con eso.
En Bash haces:
df -h --total
En Rust haces:
Command::new("df").args(["-h", "--total"]);
Cada elemento del array es un argumento separado, exactamente como el shell separa por espacios.
Capturar stdout como String
Es tan común que merece la pena tener una función helper:
use std::process::Command;
fn ejecutar(comando: &str, args: &[&str]) -> Result<String, std::io::Error> {
let salida = Command::new(comando)
.args(args)
.output()?;
if salida.status.success() {
Ok(String::from_utf8_lossy(&salida.stdout).to_string())
} else {
let stderr = String::from_utf8_lossy(&salida.stderr);
Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("{} falló: {}", comando, stderr.trim()),
))
}
}
fn main() -> std::io::Result<()> {
let resultado = ejecutar("df", &["-h", "/"])?;
println!("{}", resultado);
Ok(())
}
Esta función ya maneja algo fundamental: si el comando devuelve un código de salida distinto de cero, lo trata como error. En Bash harías algo parecido comprobando $?.
Capturar stderr por separado
A veces necesitas stdout y stderr por separado. Por ejemplo, ffprobe (de FFmpeg) escribe la información del video a stderr, no a stdout. Tenerlos separados te da control total:
use std::process::Command;
use std::str;
fn main() {
let salida = Command::new("sh")
.args(["-c", "echo 'stdout: hola' && echo 'stderr: errorcito' >&2"])
.output()
.expect("fallo al ejecutar");
println!("=== STDOUT ===");
print!("{}", str::from_utf8(&salida.stdout).unwrap_or("(no utf-8)"));
println!("=== STDERR ===");
print!("{}", str::from_utf8(&salida.stderr).unwrap_or("(no utf-8)"));
println!("=== CÓDIGO ===");
println!("{:?}", salida.status.code());
}
Compila y ejecuta:
cargo run
# === STDOUT ===
# stdout: hola
# === STDERR ===
# stderr: errorcito
# === CÓDIGO ===
# Some(0)
Usar str::from_utf8 en lugar de from_utf8_lossy te permite detectar si la salida no es UTF-8 válido. Para comandos del sistema, from_utf8_lossy suele ser suficiente, pero si necesitas rigor, usa from_utf8 y maneja el error.
Ejecutar sin capturar salida: .status()
A veces no te interesa capturar la salida del comando, solo ejecutarlo y saber si funcionó. Por ejemplo, al crear un directorio con mkdir:
use std::process::Command;
fn main() {
let status = Command::new("mkdir")
.args(["-p", "/tmp/prueba-rust"])
.status()
.expect("fallo al ejecutar mkdir");
if status.success() {
println!("Directorio creado correctamente");
} else {
eprintln!("Error al crear directorio: código {:?}", status.code());
std::process::exit(1);
}
}
.status() ejecuta el comando y devuelve el ExitStatus sin capturar stdout/stderr. Son heredados del proceso padre, así que el comando escribe directamente a tu terminal.
Spawn: ejecutar y olvidar
Para lanzar un proceso en segundo plano sin esperar a que termine (como poner un & al final en Bash):
use std::process::Command;
fn main() {
let mut child = Command::new("sleep")
.arg("30")
.spawn()
.expect("fallo al lanzar sleep");
// Hacemos otras cosas mientras sleep corre...
println!("sleep lanzado en segundo plano, PID: {}", child.id());
// Esperamos a que termine
let status = child.wait().expect("fallo al esperar sleep");
println!("sleep terminó con código: {:?}", status.code());
}
Manejo de errores REAL: qué pasa cuando el comando no existe
Vale, hasta aquí todo correcto si los comandos existen y funcionan. Pero esto es el mundo real. ¿Qué pasa si el usuario no tiene instalado df? ¿O si ejecuta tu herramienta en un contenedor Alpine mínimo? Vamos a verlo.
Comando no encontrado
use std::process::Command;
fn main() {
match Command::new("comando-inexistente").output() {
Ok(salida) => {
println!("stdout: {}", String::from_utf8_lossy(&salida.stdout));
println!("stderr: {}", String::from_utf8_lossy(&salida.stderr));
println!("código: {:?}", salida.status.code());
}
Err(e) => {
eprintln!("Error al ejecutar el comando: {}", e);
}
}
}
Compila y ejecuta:
cargo run
# Error al ejecutar comando: No such file or directory (os error 2)
Fíjate: el Err no viene del ExitStatus del comando, sino que Command::new("comando-inexistente").output() devuelve directamente un error de E/S porque el binario no se encuentra en el PATH. Esto es diferente a cuando el comando existe pero devuelve un código de salida distinto de cero — en ese caso output() devuelve Ok(Output) con status.code() = Some(1) o similar.
La distinción es importante:
| Situación | output() devuelve | Cómo se detecta |
|---|---|---|
| Comando no existe en PATH | Err(io::Error) con NotFound | match o ? |
| Comando existe pero falla | Ok(Output) con status.code() != 0 | salida.status.success() |
| Comando existe y funciona | Ok(Output) con status.code() == 0 | salida.status.success() |
Manejo robusto: combinando ambos casos
Una función que maneje ambos escenarios correctamente:
use std::process::Command;
use std::io;
fn ejecutar_comando(comando: &str, args: &[&str]) -> Result<String, String> {
let salida = match Command::new(comando).args(args).output() {
Ok(o) => o,
Err(e) => {
if e.kind() == io::ErrorKind::NotFound {
return Err(format!("'{}' no está instalado. Instálalo con tu gestor de paquetes.", comando));
}
return Err(format!("Error al ejecutar '{}': {}", comando, e));
}
};
if salida.status.success() {
Ok(String::from_utf8_lossy(&salida.stdout).to_string())
} else {
let stderr = String::from_utf8_lossy(&salida.stderr);
Err(format!("'{}' falló (código {:?}): {}",
comando,
salida.status.code(),
stderr.trim()))
}
}
fn main() {
match ejecutar_comando("df", &["-h", "/"]) {
Ok(salida) => print!("{}", salida),
Err(e) => eprintln!("Error: {}", e),
}
match ejecutar_comando("comando-inexistente", &[]) {
Ok(salida) => print!("{}", salida),
Err(e) => eprintln!("Error: {}", e),
}
}
Esto ya es un manejo de errores de nivel profesional. Y sin crates externas.
Error handling profesional con anyhow y thiserror
Lo que acabamos de hacer con Result<String, String> funciona, pero tiene limitaciones. El tipo String como error no lleva contexto: no puedes añadir causas, no puedes definir errores específicos, no puedes hacer matching por tipo de error.
Aquí entran anyhow y thiserror, dos crates del mismo ecosistema que se complementan:
anyhow: para errores en aplicaciones y scripts. Te permite usaranyhow::Result<T>como tipo de retorno y propagar cualquier error con?. Ideal paramain()y funciones de alto nivel donde no necesitas saber el tipo exacto de error.thiserror: para definir tipos de error personalizados con#[derive(Error)]. Ideal para librerías o cuando necesitas que los errores tengan nombres y campos específicos.
Ya usaste anyhow en el capítulo anterior. Ahora vamos a profundizar y añadir thiserror a la mezcla.
Crea el proyecto para este capítulo:
cargo new crustaceo-sysinfo
cd crustaceo-sysinfo
Edita Cargo.toml:
[package]
name = "crustaceo-sysinfo"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = «1» thiserror = «2» colored = «2» chrono = «0.4»
Vamos a ver cada crate:
| Crate | Para qué |
|---|---|
anyhow | Errores genéricos con anyhow::Result<T>, propagar cualquier error con ? |
thiserror | Definir errores personalizados con derive macro |
colored | Colores en terminal (la conoces del capítulo 01) |
chrono | Formatear timestamps con zona horaria |
Primeros pasos con thiserror
thiserror te permite definir errores con una macro derive. Mira qué sencillo:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum SysError {
#[error("Comando '{0}' no encontrado. Instálalo con tu gestor de paquetes.")]
ComandoNoEncontrado(String),
#[error("El comando '{0}' falló con código {1}: {2}")]
ComandoFallido(String, i32, String),
#[error("Error de E/S: {0}")]
IoError(#[from] std::io::Error),
}
Cada variante del enum lleva un atributo #[error("...")] con un mensaje que puede usar {0}, {1}, etc., para referirse a los campos. El trait Display se implementa automáticamente.
El #[from] en IoError hace que puedas convertir un std::io::Error a SysError automáticamente con el operador ?. Es una de las características más potentes de thiserror.
Usar errores personalizados con comandos
Juntémoslo todo con un ejemplo completo que ejecuta df y maneja errores reales:
use anyhow::{Context, Result};
use colored::*;
use std::process::Command;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum SysError {
#[error("'{0}' no está instalado. Ejecuta: sudo apt install {0}")]
ComandoNoEncontrado(String),
#[error("'{0}' falló (código {1}): {2}")]
ComandoFallido(String, i32, String),
#[error("Error de E/S ejecutando '{0}': {1}")]
IoError(String, #[source] std::io::Error),
}
/// Ejecuta un comando y devuelve stdout como String, con manejo de errores profesional
fn ejecutar(comando: &str, args: &[&str]) -> Result<String> {
let salida = Command::new(comando)
.args(args)
.output()
.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
SysError::ComandoNoEncontrado(comando.to_string())
} else {
SysError::IoError(comando.to_string(), e)
}
})?;
if salida.status.success() {
Ok(String::from_utf8_lossy(&salida.stdout).to_string())
} else {
let stderr = String::from_utf8_lossy(&salida.stderr).trim().to_string();
let codigo = salida.status.code().unwrap_or(-1);
Err(SysError::ComandoFallido(comando.to_string(), codigo, stderr).into())
}
}
fn main() -> Result<()> {
match ejecutar("df", &["-h", "/"]) {
Ok(salida) => {
println!("{}", "=== df ===".bold().cyan());
println!("{}", salida);
}
Err(e) => {
eprintln!("{} {}", "Error:".red().bold(), e);
// En este caso, al ser el main, podríamos devolver Err(e)
// pero queremos mostrar el error y continuar
}
}
Ok(())
}
Compila:
cargo build
./target/debug/crustaceo-sysinfo
# === df ===
# Filesystem Size Used Avail Use% Mounted on
# /dev/sda1 50G 20G 28G 42% /
Ahora prueba qué pasa si el comando no existe:
// En main(), sustituye temporalmente:
match ejecutar("comando-inexistente", &[]) {
Ok(salida) => println!("{}", salida),
Err(e) => eprintln!("{} {}", "Error:".red().bold(), e),
}
cargo run
# Error: 'comando-inexistente' no está instalado. Ejecuta: sudo apt install comando-inexistente
El error es descriptivo, dice exactamente qué falta y cómo solucionarlo. Eso es un buen error.
El poder de #[from] en thiserror
Fíjate en el detalle de IoError:
#[error("Error de E/S ejecutando '{0}': {1}")]
IoError(String, #[source] std::io::Error),
El #[source] indica que el segundo campo es la causa del error. Esto permite a anyhow mostrar la cadena completa de errores cuando haces {:#}:
Err(SysError::IoError("df".into(), e)).context("al ejecutar df")?
Si usas {:?} o {:#} en la presentación, ves la causa raíz. Esto es clave para depuración.
Contexto con anyhow
Una de las funciones más útiles de anyhow es el método .context() (del trait Context). Te permite añadir información adicional a un error cuando lo propagas:
use anyhow::{Context, Result};
use std::fs::File;
fn leer_config() -> Result<String> {
let ruta = "/etc/crustaceo/config.toml";
let contenido = std::fs::read_to_string(ruta)
.with_context(|| format!("No se pudo leer el archivo de configuración '{}'", ruta))?;
Ok(contenido)
}
fn main() -> Result<()> {
let cfg = leer_config()?;
println!("Config: {}", cfg);
Ok(())
}
Si el archivo no existe, verás:
Error: No se pudo leer el archivo de configuración '/etc/crustaceo/config.toml'
Caused by:
No such file or directory (os error 2)
Sin .context(), solo verías «No such file or directory», y tendrías que adivinar qué archivo faltaba. Con .context(), sabes exactamente qué operación falló y por qué.
En los comandos del sistema esto es oro puro:
fn obtener_uptime() -> Result<String> {
let salida = Command::new("uptime")
.arg("-p")
.output()
.context("No se pudo ejecutar 'uptime' — ¿estás en un sistema Linux?")?
.stdout;
Ok(String::from_utf8_lossy(&salida).trim().to_string())
}
Pipe entre comandos en Rust
En Bash, el pipe es la operación más básica:
ps aux | grep rust | head -5 | awk '{print $2, $11}'
En Rust no tienes operador | para procesos, pero tienes varias formas de encadenar comandos. La más directa: capturar la salida de uno y pasársela como stdin al siguiente.
Pipe manual: stdout → stdin
use std::process::{Command, Stdio};
fn main() -> anyhow::Result<()> {
// Primer comando: ps aux
let ps = Command::new("ps")
.args(["aux"])
.stdout(Stdio::piped())
.spawn()?;
// Segundo comando: grep rust — recibe stdin del pipe
let grep = Command::new("grep")
.arg("rust")
.stdin(Stdio::from(ps.stdout.unwrap())) // pipe!
.stdout(Stdio::piped())
.spawn()?;
// Tercer comando: head -5
let head = Command::new("head")
.args(["-5"])
.stdin(Stdio::from(grep.stdout.unwrap()))
.stdout(Stdio::piped())
.spawn()?;
// Cuarto comando: awk
let awk = Command::new("awk")
.args(["'{print $2, $11}'"])
.stdin(Stdio::from(head.stdout.unwrap()))
.stdout(Stdio::piped())
.spawn()?;
let salida = awk.wait_with_output()?;
println!("{}", String::from_utf8_lossy(&salida.stdout));
Ok(())
}
El truco está en Stdio::piped() para crear el pipe y Stdio::from(child.stdout.unwrap()) para conectar la salida de un proceso a la entrada del siguiente.
Pero esto es verboso. Vamos a crear una función helper que haga pipes como en Bash:
use std::process::{Command, Output, Stdio};
/// Encadena comandos como un pipe de Bash: pipe(&["ps aux", "grep rust", "head -5"])
fn pipe(comandos: &[&str]) -> anyhow::Result<Output> {
anyhow::ensure!(!comandos.is_empty(), "No hay comandos para ejecutar");
// Dividir cada comando en programa y argumentos
let mut partes: Vec<Vec<&str>> = comandos
.iter()
.map(|c| c.split_whitespace().collect())
.collect();
let programa = partes.remove(0);
let (cmd, args) = (programa[0], &programa[1..]);
// Primer comando
let mut child = Command::new(cmd)
.args(args)
.stdout(Stdio::piped())
.spawn()
.map_err(|e| anyhow::anyhow!("Error al ejecutar '{}': {}", cmd, e))?;
let mut prev_stdout = child.stdout.take();
// Comandos intermedios
for comando in &partes {
let (cmd, args) = (comando[0], &comando[1..]);
child = Command::new(cmd)
.args(args)
.stdin(Stdio::from(prev_stdout.unwrap()))
.stdout(Stdio::piped())
.spawn()
.map_err(|e| anyhow::anyhow!("Error al ejecutar '{}': {}", cmd, e))?;
prev_stdout = child.stdout.take();
}
// Último comando: capturar salida
let salida = child.wait_with_output()?;
if !salida.status.success() {
let stderr = String::from_utf8_lossy(&salida.stderr);
anyhow::bail!("El pipe falló (código {:?}): {}",
salida.status.code(), stderr.trim());
}
Ok(salida)
}
fn main() -> anyhow::Result<()> {
let salida = pipe(&[
"ps aux",
"grep rust",
"head -5",
])?;
println!("{}", String::from_utf8_lossy(&salida.stdout));
Ok(())
}
Esta función pipe() recibe un array de strings, donde cada string es un comando con sus argumentos. Internamente los divide por espacios y los encadena como pipes de Bash.
Capturar la salida completa del pipe
A veces quieres capturar todo el resultado del pipe sin imprimirlo directamente:
let resultado = pipe(&[
"df -h",
"grep /dev/sd",
"awk '{print $5, $6}'",
])?;
for linea in String::from_utf8_lossy(&resultado.stdout).lines() {
if let Some((pct, punto)) = linea.split_once(' ') {
println!("Disco {}: {} usado", punto, pct);
}
}
Proyecto: crustaceo-sysinfo
Vale, basta de fragmentos. Vamos a construir la herramienta completa del capítulo: crustaceo-sysinfo, un monitor de sistema que ejecuta comandos reales de Linux y presenta la información formateada con colores.
¿Qué hace?
- Uso de disco (
df -h) — particiones montadas, uso en porcentaje - Memoria RAM (
free -h) — total, usada, libre, swap - Procesos principales (
ps aux --sort=-%mem | head -6) — top 5 por consumo de memoria - Tiempo de actividad (
uptime -p) — desde cuándo está funcionando el sistema
Todo con una salida limpia, coloreada, y con manejo de errores profesional.
El Cargo.toml
Ya lo tienes de antes, pero confirma:
[package]
name = "crustaceo-sysinfo"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = «1» thiserror = «2» colored = «2» chrono = «0.4»
El código completo de src/main.rs
use anyhow::{Context, Result};
use chrono::Local;
use colored::*;
use std::process::{Command, Stdio};
use thiserror::Error;
// ═══════════════════════════════════════════════
// Error personalizado con thiserror
// ═══════════════════════════════════════════════
#[derive(Error, Debug)]
pub enum SysError {
#[error("'{0}' no está instalado.\n → Instálalo con: sudo apt install {0}")]
ComandoNoEncontrado(String),
#[error("'{0}' falló (código {1}):\n → {2}")]
ComandoFallido(String, i32, String),
#[error("Error al ejecutar '{0}': {1}")]
Ejecucion(String, #[source] std::io::Error),
}
// ═══════════════════════════════════════════════
// Ejecutor de comandos con errores reales
// ═══════════════════════════════════════════════
/// Ejecuta un comando, captura stdout y gestiona errores de forma profesional
fn ejecutar(comando: &str, args: &[&str]) -> Result<String> {
let salida = Command::new(comando)
.args(args)
.output()
.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
SysError::ComandoNoEncontrado(comando.to_string())
} else {
SysError::Ejecucion(comando.to_string(), e)
}
})
.context(format!("Ejecutando '{}' con argumentos {:?}", comando, args))?;
if salida.status.success() {
Ok(String::from_utf8_lossy(&salida.stdout).to_string())
} else {
let stderr = String::from_utf8_lossy(&salida.stderr).trim().to_string();
let codigo = salida.status.code().unwrap_or(-1);
Err(SysError::ComandoFallido(comando.to_string(), codigo, stderr))
.context(format!("Procesando salida de '{}'", comando))
}
}
/// Ejecuta un pipe de comandos (como `cmd1 | cmd2 | cmd3`)
fn pipe(comandos: &[&str]) -> Result<Vec<String>> {
anyhow::ensure!(!comandos.is_empty(), "No se especificaron comandos para el pipe");
let mut partes: Vec<Vec<&str>> = comandos
.iter()
.map(|c| c.split_whitespace().collect())
.collect();
let programa = partes.remove(0);
let (cmd, args) = (programa[0], &programa[1..]);
let mut child = Command::new(cmd)
.args(args)
.stdout(Stdio::piped())
.spawn()
.with_context(|| format!("Iniciando pipe: '{}'", comandos[0]))?;
let mut prev_stdout = child.stdout.take();
for comando in &partes {
let (cmd, args) = (comando[0], &comando[1..]);
child = Command::new(cmd)
.args(args)
.stdin(Stdio::from(prev_stdout.unwrap()))
.stdout(Stdio::piped())
.spawn()
.with_context(|| format!("Pipe intermedio: '{}'", comando.join(" ")))?;
prev_stdout = child.stdout.take();
}
let salida = child.wait_with_output()?;
let stdout = String::from_utf8_lossy(&salida.stdout);
Ok(stdout.lines().map(|l| l.to_string()).collect())
}
// ═══════════════════════════════════════════════
// Funciones de obtención de datos del sistema
// ═══════════════════════════════════════════════
fn obtener_uptime() -> Result<String> {
let salida = ejecutar("uptime", &["-p"])
.context("No se pudo obtener el uptime")?;
Ok(salida.trim().trim_start_matches("up ").to_string())
}
fn obtener_memoria() -> Result<Vec<String>> {
let lineas = ejecutar("free", &["-h"])
.context("No se pudo obtener la información de memoria")?;
Ok(lineas.lines().map(|l| l.to_string()).collect())
}
fn obtener_disco() -> Result<Vec<String>> {
// Mostramos solo sistemas de archivos reales (ext4, btrfs, xfs, zfs)
let lineas = pipe(&[
"df -h",
"grep -E '^(/dev/|overlay)'",
]).context("No se pudo obtener la información de disco")?;
if lineas.is_empty() {
// Fallback: mostrar todos los sistemas de archivos si grep no encuentra nada
ejecutar("df", &["-h"])
.context("No se pudo obtener df")
.map(|s| s.lines().map(|l| l.to_string()).collect())
} else {
Ok(lineas)
}
}
fn obtener_procesos() -> Result<Vec<String>> {
let lineas = pipe(&[
"ps aux",
"sort -k4 -r -n", // ordenar por %mem descendente
"head -6", // cabecera + top 5
]).context("No se pudo obtener la lista de procesos")?;
Ok(lineas)
}
// ═══════════════════════════════════════════════
// Formateo de la salida
// ═══════════════════════════════════════════════
fn mostrar_encabezado(titulo: &str, color: &dyn colored::Colorize) {
println!();
println!("{}", "┌──────────────────────────────────────────────┐".cyan());
println!("{} {:<42} {}", "│".cyan(), titulo.bold(), "│".cyan());
println!("{}", "└──────────────────────────────────────────────┘".cyan());
println!();
}
fn mostrar_uptime(uptime: &str) {
mostrar_encabezado("⏱ UPTIME", &colored::Cyan);
println!(" El sistema lleva funcionando: {}\n", uptime.green().bold());
}
fn mostrar_memoria(lineas: &[String]) {
mostrar_encabezado("💾 MEMORIA RAM", &colored::Cyan);
for (i, linea) in lineas.iter().enumerate() {
if i == 0 {
// Cabecera
println!("{}", linea.cyan().bold());
} else if linea.contains("Swap") {
println!("{}", linea.yellow());
} else {
println!("{}", linea.green());
}
}
println!();
}
fn mostrar_disco(lineas: &[String]) {
mostrar_encabezado("💿 USO DE DISCO", &colored::Cyan);
for (i, linea) in lineas.iter().enumerate() {
if i == 0 {
println!("{}", linea.cyan().bold());
continue;
}
// Detectar si el uso supera el 80% para colorear en rojo
let campos: Vec<&str> = linea.split_whitespace().collect();
if let Some(pct_str) = campos.get(4) {
let pct = pct_str.trim_end_matches('%');
if let Ok(n) = pct.parse::<u8>() {
if n >= 90 {
println!("{}", linea.red());
} else if n >= 75 {
println!("{}", linea.yellow());
} else {
println!("{}", linea.green());
}
} else {
println!("{}", linea);
}
} else {
println!("{}", linea);
}
}
println!();
}
fn mostrar_procesos(lineas: &[String]) {
mostrar_encabezado("⚙️ TOP 5 PROCESOS (por memoria)", &colored::Cyan);
for (i, linea) in lineas.iter().enumerate() {
if i == 0 {
println!("{}", linea.cyan().bold());
} else {
// Extraer usuario, %mem, %cpu y comando
let partes: Vec<&str> = linea.split_whitespace().collect();
if partes.len() >= 11 {
let usuario = partes[0];
let cpu = partes[2];
let mem = partes[3];
let comando = partes[10..].join(" ");
println!(
" {:>2}. {:<10} CPU: {:>5} MEM: {:>5} {}",
i,
usuario.cyan(),
cpu.yellow(),
mem.red().bold(),
comando.dimmed(),
);
} else {
println!(" {}", linea);
}
}
}
println!();
}
fn mostrar_error(e: &anyhow::Error, contexto: &str) {
eprintln!("{} {}: {}", "❌".red().bold(), contexto.red().bold(), e);
}
// ═══════════════════════════════════════════════
// Main
// ═══════════════════════════════════════════════
fn main() -> Result<()> {
// Preámbulo
println!();
println!("{}", "🦀 crustaceo-sysinfo v0.1.0".cyan().bold());
println!("{}", "═".repeat(50).cyan());
let ahora = Local::now().format("%Y-%m-%d %H:%M:%S");
println!(" Informe generado: {}", ahora.to_string().cyan());
println!("{}", "═".repeat(50).cyan());
// 1. Uptime
match obtener_uptime() {
Ok(u) => mostrar_uptime(&u),
Err(e) => mostrar_error(&e, "uptime"),
}
// 2. Memoria
match obtener_memoria() {
Ok(m) => mostrar_memoria(&m),
Err(e) => mostrar_error(&e, "memoria"),
}
// 3. Disco
match obtener_disco() {
Ok(d) => mostrar_disco(&d),
Err(e) => mostrar_error(&e, "disco"),
}
// 4. Procesos
match obtener_procesos() {
Ok(p) => mostrar_procesos(&p),
Err(e) => mostrar_error(&e, "procesos"),
}
// Pie
println!("{}", "═".repeat(50).cyan());
println!("{}", " Información del sistema recopilada con éxito ✅".green().bold());
println!();
Ok(())
}
Desglose del código
Errores personalizados (SysError) :
#[derive(Error, Debug)]
pub enum SysError {
#[error("'{0}' no está instalado.\n → Instálalo con: sudo apt install {0}")]
ComandoNoEncontrado(String),
// ...
}
Cada variante lleva su mensaje. Cuando el error se muestre con {}, aparece el mensaje formateado con los valores de los campos. El #[source] en Ejecucion permite que anyhow encadene la causa raíz.
Función ejecutar() :
Maneja tres escenarios:
- El comando no existe en el PATH →
Err(...)con mensaje de cómo instalarlo - El comando existe pero falla →
Err(...)con código de salida y stderr - El comando funciona →
Ok(stdout)con la salida capturada
El .context() de anyhow añade información adicional sobre dónde falló exactamente.
Función pipe() :
Implementa pipes entre comandos usando Stdio::piped() y Stdio::from(child.stdout.take()). Cada comando recibe como stdin la stdout del anterior. El último comando captura toda la salida.
Funciones obtener_*() :
Cada una ejecuta comandos diferentes. obtener_disco() usa pipe para filtrar solo dispositivos reales (/dev/*). Si el grep no encuentra nada (sistema sin esos dispositivos), cae en un fallback que muestra todo df -h.
Formateo :
Cada función mostrar_*() recibe las líneas de texto y las colorea:
- Uptime en verde brillante
- Cabeceras en cian negrita
- Uso de disco >90% en rojo, >75% en amarillo, resto en verde
- Procesos con colores por campo: usuario en cian, CPU en amarillo, memoria en rojo
- Errores en rojo con el icono ❌
Verificación
# 1. El proyecto compila sin warnings
cd ~/crustaceo-sysinfo
cargo build 2>&1 | grep -i warning || echo "✅ Sin warnings"
# 2. Ejecutar la herramienta completa
./target/debug/crustaceo-sysinfo
# Debe mostrar: uptime, memoria, disco y procesos con colores
# 3. Probar que los comandos individuales funcionan
./target/debug/crustaceo-sysinfo 2>&1 | head -20
# Debe mostrar el encabezado y al menos uptime
# 4. Probar con un comando que no existe (simular)
# Edita temporalmente el código y cambia "uptime" por "uptime-no-existe"
# luego compila y ejecuta:
# Debe mostrar: ❌ uptime: 'uptime-no-existe' no está instalado.
# → Instálalo con: sudo apt install uptime-no-existe
# 5. Probar pipe
./target/debug/crustaceo-sysinfo 2>&1 | grep -c "PROCESOS"
# Debe devolver 1 (la sección de procesos aparece una vez)
# 6. Release build
cargo build --release
ls -lh target/release/crustaceo-sysinfo
# ~800KB-1MB, un solo binario estático
# 7. Copiar a ~/bin y ejecutar desde cualquier lugar
cp target/release/crustaceo-sysinfo ~/bin/
crustaceo-sysinfo
# Debe funcionar igual
Salida esperada
Cuando ejecutes la herramienta, verás algo como:
🦀 crustaceo-sysinfo v0.1.0
══════════════════════════════════════════════
Informe generado: 2026-06-24 15:30:42
══════════════════════════════════════════════
┌──────────────────────────────────────────────┐
│ ⏱ UPTIME │
└──────────────────────────────────────────────┘
El sistema lleva funcionando: 3 days, 12 hours, 42 minutes
┌──────────────────────────────────────────────┐
│ 💾 MEMORIA RAM │
└──────────────────────────────────────────────┘
total used free shared buff/cache available
Mem: 7.6Gi 3.2Gi 1.1Gi 245Mi 3.3Gi 3.9Gi
Swap: 2.0Gi 256Mi 1.8Gi
┌──────────────────────────────────────────────┐
│ 💿 USO DE DISCO │
└──────────────────────────────────────────────┘
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 50G 20G 28G 42% /
/dev/sda2 100G 85G 11G 89% /home
┌──────────────────────────────────────────────┐
│ ⚙️ TOP 5 PROCESOS (por memoria) │
└──────────────────────────────────────────────┘
USER PID %CPU %MEM COMMAND
1. lorenzo 1234 2.3 12.5 /usr/bin/firefox ...
2. root 567 0.1 8.2 /usr/bin/mysqld ...
3. lorenzo 789 1.5 6.3 /usr/bin/code ...
4. lorenzo 2345 0.8 4.1 /usr/bin/slack ...
5. root 345 0.2 3.8 /usr/sbin/sshd ...
══════════════════════════════════════════════
Información del sistema recopilada con éxito ✅
Los valores variarán según tu sistema, pero la estructura y los colores deben ser los mismos.
Prueba de manejo de errores real
Vamos a hacer una prueba específica de errores. Crea este archivo test-errores.sh:
#!/bin/bash
# test-errores.sh - Prueba el manejo de errores de crustaceo-sysinfo
echo "🔍 Prueba 1: Comando no encontrado"
cd ~/crustaceo-sysinfo
cat > /tmp/test-no-existe.rs << 'EOF'
use std::process::Command;
fn main() {
match Command::new("comando-inexistente").output() {
Ok(salida) => {
println!("stdout: {}", String::from_utf8_lossy(&salida.stdout));
println!("código: {:?}", salida.status.code());
}
Err(e) => {
// Esto es lo que queremos probar: que se detecta correctamente
if e.kind() == std::io::ErrorKind::NotFound {
println!("✅ Error detectado correctamente: comando no encontrado");
} else {
println!("❌ Error inesperado: {}", e);
}
}
}
}
EOF
cargo build --release 2>/dev/null
echo ""
echo "🔍 Prueba 2: Ejecutar crustaceo-sysinfo completo"
./target/release/crustaceo-sysinfo
echo ""
echo "🔍 Prueba 3: Verificar que el binario es autónomo"
ldd target/release/crustaceo-sysinfo 2>&1 | grep "not a dynamic executable"
echo "✅ Binario estático, sin dependencias dinámicas"
Ejecuta:
chmod +x test-errores.sh
./test-errores.sh
Resumen
| Concepto | Rust | Bash equivalente |
|---|---|---|
| Ejecutar comando simple | Command::new("cmd").output() | cmd o $(cmd) |
| Pasar argumentos | .args(["-a", "-b"]) | cmd -a -b |
| Capturar stdout | String::from_utf8_lossy(&output.stdout) | salida=$(cmd) |
| Capturar stderr | String::from_utf8_lossy(&output.stderr) | salida=$(cmd 2>&1) |
| Pipe entre comandos | Stdio::from(child.stdout.unwrap()) | cmd1 | cmd2 |
| Ejecutar y olvidar | .spawn() | cmd & |
| Error: comando no existe | io::ErrorKind::NotFound | command not found |
| Error: código de salida != 0 | salida.status.success() | $? -ne 0 |
| Error personalizado | derive thiserror::Error | echo "Error: ..."; exit 1 |
| Añadir contexto al error | .context("mensaje") | No hay equivalente directo |
En el próximo capítulo veremos cómo leer y escribir archivos de configuración en formatos estándar (TOML, YAML, JSON), cómo persistir datos de tus herramientas y cómo crear scripts configurables sin hardcodear rutas. Todo lo necesario para que tus herramientas parezcan programas de verdad.
- Más información,
- Tutorial Scripts en Bash en atareao.es — el punto de partida: en Bash los pipes son nativos, en Rust los construyes con Stdio
- Tutorial de Monitoreo en atareao.es — monitorización de sistemas, el contexto de uso de crustaceo-sysinfo
- std::process::Command — documentación oficial
- std::process::Stdio — documentación oficial
- Crate anyhow en crates.io — manejo de errores para aplicaciones
- Crate thiserror en crates.io — derive macro para errores personalizados
- Crate colored en crates.io — coloración de terminal
- Crate chrono en crates.io — fecha y hora con zona horaria
- Manejo de errores en Rust (The Book)
- Rust by Example — Command
- Rust by Example — Pipe
- Error handling patterns en Rust