763 - Migra de Bash a Rust. Procesa millones de líneas en segundos
Aprende a migrar de Bash a Rust con esta guía práctica: mejora el rendimiento, la gestión de errores y la concurrencia en tus scripts de Linux.
El objetivo de este episodio es ofrecer una guía práctica para migrar scripts de Bash a Rust-script, destacando las diferencias clave en la gestión de estados, argumentos, validación, comandos, rutas, variables de entorno, generación de logs, lectura de archivos y concurrencia. Soy consciente que explicar este tema en un episodio es algo realmente complicado y difícil de seguir, por lo que he todos los ejemplos, los he dejado en las notas del episodio. De cualquier forma, como sabes me gustan los retos, esto es un auténtico reto, y difícil de conseguir. Así que vamos a ello.

Migra de Bash a Rust. Procesa millones de líneas en segundos
Gestión de estados y errores (status)
En Bash, dependemos de la inspección manual de la variable $?. En Rust, el manejo de errores es parte del flujo natural del código.
- Bash (0_status.sh): Utiliza un bloque condicional if [ $? -ne 0 ] después de ejecutar mkdir para verificar si la operación falló.
- Rust (0_status.rsc): El operador ? sustituye por completo al condicional de Bash. Si la función fs::create_dir falla, el script termina inmediatamente y propaga el error de forma automática.
- Ventaja: El código es mucho más limpio y es imposible olvidar gestionar un error, ya que el compilador te obliga a manejar el tipo Result.
#!/bin/bash
# Intentamos crear un directorio
mkdir /root/test_dir 2>/dev/null
if [ $? -ne 0 ]; then
echo "Error: No se pudo crear el directorio."
exit 1
fi
echo "Directorio creado con éxito."
#!/usr/bin/env rust-script
use std::fs;
use std::io;
fn main() -> io::Result<()> {
// El operador '?' sustituye al 'if [ $? -eq 0 ]'
// Si mkdir falla, el script termina aquí y devuelve el error
fs::create_dir("/root/test_dir")?;
println!("Directorio creado con éxito.");
Ok(())
}
Paso de argumentos
Bash trata todo como cadenas de texto, lo que obliga a validaciones manuales complejas. Rust ofrece seguridad de tipos desde el primer momento.
- Bash (1_argumentos.sh): Requiere comprobar el número de argumentos ($#) y usar expresiones regulares para validar que un argumento sea un número antes de usarlo en un bucle.
- Rust (1_argumentos.rsc): Utiliza iteradores para extraer valores (args.next()). Al usar .parse()?, Rust realiza la conversión a un tipo numérico y la validación de errores en un solo paso.
- Ventaja: Evitas errores en tiempo de ejecución causados por datos mal formados, capturándolos en el momento del parseo.
#!/bin/bash
# Comprobar si hay suficientes argumentos
if [ "$#" -ne 2 ]; then
echo "Uso: $0 <nombre> <repeticiones>"
exit 1
fi
NOMBRE=$1
REPETICIONES=$2
# Validar si es un número (Bash no tiene tipos, hay que usar Regex o trucos)
if ! [[ "$REPETICIONES" =~ ^[0-9]+$ ]]; then
echo "Error: $REPETICIONES no es un número válido"
exit 1
fi
for i in $(seq 1 $REPETICIONES); do
echo "Hola $NOMBRE, esta es la vez $i"
done
#!/usr/bin/env rust-script
use std::env;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = env::args().skip(1); // Saltamos el nombre del script
// Extraemos los valores o lanzamos error si faltan
let nombre = args.next().ok_or("Falta el argumento: <nombre>")?;
let repeticiones: u32 = args.next()
.ok_or("Falta el argumento: <repeticiones>")?
.parse()?;
for i in 1..=repeticiones {
println!("{i}.- Hola {nombre}");
}
Ok(())
}
Validación
La validación en Rust es semántica y exhaustiva, a diferencia de los condicionales lógicos de Bash.
- Bash (2_validacion.sh): Combina Regex con operadores lógicos (&&) para validar rangos numéricos.
- Rust (2_validacion.rsc): Utiliza match para validar el parseo y el rango matemático de forma simultánea.
- Ventaja: El Pattern Matching permite cubrir todos los casos posibles de forma explícita, mejorando la legibilidad y seguridad del script.
#!/bin/bash
EDAD=$1
if [[ "$EDAD" =~ ^[0-9]+$ ]] && [ "$EDAD" -ge 0 ] && [ "$EDAD" -le 120 ]; then
echo "Edad válida"
else
echo "Entrada inválida"
fi
#!/usr/bin/env rust-script
fn main() {
let entrada = "25"; // Vendría de un argumento
// El parseo ya descarta texto no numérico.
// Luego validamos el rango de forma matemática.
match entrada.parse::<u8>() {
Ok(edad) if (0..=120).contains(&edad) => println!("Edad válida: {edad}"),
_ => eprintln!("Error: La edad debe ser un número entre 0 y 120"),
}
}
Comandos
Capturar la salida de un comando externo requiere transformar bytes en texto con sentido.
- Bash (3_comando.sh): La sintaxis $(…) captura la salida y elimina el salto de línea automáticamente.
- Rust (3_comando.rsc): Utiliza Command::new().output()? para ejecutar el proceso. Es necesario convertir explícitamente los bytes (u8) a String y usar .trim() para igualar el comportamiento de Bash.
- Ventaja: Rust es más seguro frente a inyecciones de comandos, ya que separa el ejecutable de los argumentos.
#!/bin/bash
# Captura la salida (quita el \n automáticamente)
FECHA=$(date +%Y-%m-%d)
# Imprime el resultado
echo "Hoy es: $FECHA"
#!/usr/bin/env rust-script
use std::process::Command;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Ejecutamos y capturamos (equivalente a $(...))
let salida = Command::new("date").arg("+%Y-%m-%d").output()?;
// 2. Convertimos bytes a String y limpiamos el \n final (como hace Bash)
let fecha = String::from_utf8(salida.stdout)?.trim().to_string();
// 3. Imprimimos (equivalente a echo)
println!("Hoy es: {fecha}");
Ok(())
}
Rutas
Bash trata las rutas como simples cadenas, lo que causa errores críticos si existen espacios. Rust utiliza objetos con conciencia estructural.
- Bash (5_rutas.sh): Requiere un uso meticuloso de comillas dobles («$RUTA») para evitar que el intérprete divida la ruta al encontrar espacios.
- Rust (5_rutas.rsc): Utiliza PathBuf, que gestiona la construcción de la ruta de forma interna y añade los separadores necesarios automáticamente.
- Ventaja: Eliminas errores accidentales por nombres de archivos «extraños» y aseguras que el script funcione correctamente en diferentes sistemas operativos.
#!/bin/bash
DIR="mi carpeta con espacios"
FICHERO="notas.txt"
# Si olvidas las comillas en "$DIR/$FICHERO", el script falla
RUTA="$DIR/$FICHERO"
touch "$RUTA"
#!/usr/bin/env rust-script
use std::path::PathBuf;
fn main() {
let mut ruta = PathBuf::from("mi carpeta con espacios");
ruta.push("notas.txt"); // Añade la barra / automáticamente
println!("Ruta lista para usar: {}", ruta.display());
}
Variables de entorno
La gestión de valores por defecto es más legible y menos propensa a errores sintácticos en Rust.
- Bash (6_variables_entorno.sh): Emplea la expansión de parámetros ${VARIABLE:-default}, una sintaxis potente pero críptica para usuarios noveles.
- Rust (6_variables_entorno.rsc): Usa env::var().unwrap_or_else(), un método que define claramente qué hacer si la variable no existe.
- Ventaja: El código se vuelve autodocumentado y facilita la depuración de configuraciones faltantes.
#!/bin/bash
# Sintaxis críptica para el usuario medio
LOG_LEVEL=${LOG_LEVEL:-"info"}
echo "Nivel de log: $LOG_LEVEL"
#!/usr/bin/env rust-script
use std::env;
fn main() {
// Intentamos leer la variable, si no existe o es inválida, usamos el default
let log_level = env::var("LOG_LEVEL").unwrap_or_else(|_| "info".into());
println!("Nivel de log: {log_level}");
}
Generar un log
import random
import time
from datetime import datetime
# Configuración
FILENAME = "access_log.txt"
LINES_COUNT = 2000000
# Datos para aleatoriedad
LOG_LEVELS = ["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"]
IPS = [f"192.168.1.{i}" for i in range(1, 255)]
MESSAGES = [
"User logged in successfully",
"Failed login attempt",
"Database connection established",
"Timeout waiting for response",
"File uploaded: /uploads/image.png",
"Disk usage above 80%",
"API request received: GET /v1/users",
"Session expired for user_id: 4502"
]
def generate_logs():
print(f"Generando {LINES_COUNT} líneas en '{FILENAME}'...")
start_time = time.time()
with open(FILENAME, "w", encoding="utf-8") as f:
for _ in range(LINES_COUNT):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
level = random.choice(LOG_LEVELS)
ip = random.choice(IPS)
msg = random.choice(MESSAGES)
# Formato: [TIMESTAMP] [LEVEL] IP: MESSAGE
f.write(f"[{timestamp}] [{level}] {ip}: {msg}\n")
end_time = time.time()
print(f"¡Listo! Tiempo transcurrido: {end_time - start_time:.2f} segundos.")
if __name__ == "__main__":
generate_logs()
#!/usr/bin/env rustscript
//! ``cargo
//! [dependencies]
//! rand = "0.8"
//! chrono = "0.4"
//! ``
use std::fs::File;
use std::io::{BufWriter, Write};
use std::time::Instant;
use rand::seq::SliceRandom; // Necesitas la crate 'rand'
fn main() -> std::io::Result<()> {
let path = "access_log.txt";
let num_lines = 2_000_000;
// Datos aleatorios
let levels = ["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"];
let ips = ["192.168.1.10", "10.0.0.5", "172.16.0.20", "192.168.1.50"];
let messages = [
"User logged in successfully",
"Failed login attempt",
"Database connection established",
"Timeout waiting for response",
"Disk usage above 80%",
];
let start = Instant::now();
let file = File::create(path)?;
let mut writer = BufWriter::new(file);
let mut rng = rand::thread_rng();
for _ in 0..num_lines {
let level = levels.choose(&mut rng).unwrap();
let ip = ips.choose(&mut rng).unwrap();
let msg = messages.choose(&mut rng).unwrap();
// Usamos writeln! para agregar el salto de línea automáticamente
writeln!(
writer,
"[{:?}] [{}] {}: {}",
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
level,
ip,
msg
)?;
}
// Asegurar que todo se escriba al disco
writer.flush()?;
let duration = start.elapsed();
println!("¡Listo! 100,000 líneas generadas en {:?}", duration);
Ok(())
}
Leer archivos
Cuando pasas de miles a millones de líneas, la velocidad y la gestión de memoria de Rust no tienen competencia.
- Bash (8_leer_archivo.sh): El bucle while read es extremadamente lento para procesar millones de líneas, ya que invoca procesos internos en cada iteración.
- Rust (8_leer_archivo.rsc): Utiliza BufReader, que lee el archivo en bloques (buffers), permitiendo un procesamiento casi instantáneo sin saturar la RAM.
- Ventaja: Una tarea que en Bash puede tardar minutos, en Rust se completa en segundos, permitiendo scripts de mantenimiento mucho más ambiciosos.
#!/bin/bash
while IFS= read -r linea; do
if [[ "$linea" == *"ERROR"* ]]; then
echo "Cuidado: $linea"
fi
done < access_log.txt
#!/usr/bin/env rust-script
use std::fs::File;
use std::io::{self, BufRead};
fn main() -> io::Result<()> {
let archivo = File::open("access_log.txt")?;
let lector = io::BufReader::new(archivo);
for linea in lector.lines() {
let l = linea?; // Manejo de error si la línea es corrupta
if l.contains("ERROR") {
println!("Cuidado: {l}");
}
}
Ok(())
}
Hilos y concurrencia
Paralelizar tareas es el punto donde Rust ofrece una arquitectura de ingeniería superior.
- Bash (9_hilos.sh): Lanza procesos al fondo con & y usa wait para sincronizar. El problema es que lanza 50 procesos ping pesados simultáneamente sin control de recursos.
- Rust (9_hilos.rsc): Crea hilos ligeros del sistema con thread::spawn y utiliza move para transferir la propiedad de los datos de forma segura. Finalmente, sincroniza todo con .join().
- Ventaja: Tienes un control total sobre el ciclo de vida de cada hilo y evitas la saturación de procesos que podría causar una «fork bomb» accidental en Bash si la lista de servidores creciera masivamente.
#!/bin/bash
SERVIDORES=(
"google.com" "github.com" "atareao.es" "rust-lang.org" "linux.org"
"debian.org" "archlinux.org" "ubuntu.com" "fedora.org" "kernel.org"
"duckduckgo.com" "bing.com" "yahoo.com" "wikipedia.org" "wikimedia.org"
"youtube.com" "vimeo.com" "twitch.tv" "netflix.com" "spotify.com"
"amazon.com" "ebay.com" "aliexpress.com" "microsoft.com" "apple.com"
"facebook.com" "instagram.com" "twitter.com" "linkedin.com" "reddit.com"
"cloudflare.com" "digitalocean.com" "linode.com" "hetzner.com" "ovh.com"
"docker.com" "kubernetes.io" "ansible.com" "terraform.io" "gitlab.com"
"stackoverflow.com" "medium.com" "dev.to" "hackernews.com" "slashdot.org"
"nytimes.com" "bbc.com" "elpais.com" "elmundo.es" "nasa.gov"
)
comprobar_ping() {
if ping -c 1 -W 1 "$1" > /dev/null 2>&1; then
echo "✅ $1 está vivo"
else
echo "❌ $1 está caído"
fi
}
echo "Lanzando ${#SERVIDORES[@]} comprobaciones al fondo..."
for ip in "${SERVIDORES[@]}"; do
# Lanzamos en segundo plano con &
comprobar_ping "$ip" &
done
# Esperamos a que todos los procesos hijos terminen
wait
echo "Monitorización finalizada."
#!/bin/bash
SERVIDORES=(
"google.com" "github.com" "atareao.es" "rust-lang.org" "linux.org"
"debian.org" "archlinux.org" "ubuntu.com" "fedora.org" "kernel.org"
"duckduckgo.com" "bing.com" "yahoo.com" "wikipedia.org" "wikimedia.org"
"youtube.com" "vimeo.com" "twitch.tv" "netflix.com" "spotify.com"
"amazon.com" "ebay.com" "aliexpress.com" "microsoft.com" "apple.com"
"facebook.com" "instagram.com" "twitter.com" "linkedin.com" "reddit.com"
"cloudflare.com" "digitalocean.com" "linode.com" "hetzner.com" "ovh.com"
"docker.com" "kubernetes.io" "ansible.com" "terraform.io" "gitlab.com"
"stackoverflow.com" "medium.com" "dev.to" "hackernews.com" "slashdot.org"
"nytimes.com" "bbc.com" "elpais.com" "elmundo.es" "nasa.gov"
)
# Definimos la lógica en una función
comprobar_ping() {
if ping -c 1 -W 1 "$1" > /dev/null 2>&1; then
echo "✅ $1 está vivo"
else
echo "❌ $1 está caído"
fi
}
# Exportamos la función para que parallel la reconozca
export -f comprobar_ping
# Ejecutamos en paralelo
# ::: pasa los elementos del array como argumentos
printf "%s\n" "${SERVIDORES[@]}" | parallel comprobar_ping
#!/usr/bin/env rust-script
use std::process::Command;
use std::thread;
fn main() {
let servidores = vec![
"google.com", "github.com", "atareao.es", "rust-lang.org", "linux.org",
"debian.org", "archlinux.org", "ubuntu.com", "fedora.org", "kernel.org",
"duckduckgo.com", "bing.com", "yahoo.com", "wikipedia.org", "wikimedia.org",
"youtube.com", "vimeo.com", "twitch.tv", "netflix.com", "spotify.com",
"amazon.com", "ebay.com", "aliexpress.com", "microsoft.com", "apple.com",
"facebook.com", "instagram.com", "twitter.com", "linkedin.com", "reddit.com",
"cloudflare.com", "digitalocean.com", "linode.com", "hetzner.com", "ovh.com",
"docker.com", "kubernetes.io", "ansible.com", "terraform.io", "gitlab.com",
"stackoverflow.com", "medium.com", "dev.to", "hackernews.com", "slashdot.org",
"nytimes.com", "bbc.com", "elpais.com", "elmundo.es", "nasa.gov"
];
let mut hilos = vec![];
for ip in servidores {
// Creamos un hilo por cada IP
let hilo = thread::spawn(move || {
let status = Command::new("ping")
.args(["-c", "1", "-W", "1", ip])
.stdout(std::process::Stdio::null()) // Silenciamos la salida
.stderr(std::process::Stdio::null())
.status();
match status {
Ok(s) if s.success() => println!("✅ {ip} está vivo"),
_ => println!("❌ {ip} está caído o no responde"),
}
});
hilos.push(hilo);
}
// El "colofón": esperar a que todos terminen antes de cerrar el script
for h in hilos {
let _ = h.join();
}
println!("Monitorización finalizada.");
}
#!/usr/bin/env rust-script
//! ``cargo
//! [dependencies]
//! rayon = "1.8"
//! ``
use rayon::prelude::*;
use std::process::Command;
use std::process::Stdio;
fn main() {
let servidores = vec![
"google.com", "github.com", "atareao.es", "rust-lang.org", "linux.org",
"debian.org", "archlinux.org", "ubuntu.com", "fedora.org", "kernel.org",
"duckduckgo.com", "bing.com", "yahoo.com", "wikipedia.org", "wikimedia.org",
"youtube.com", "vimeo.com", "twitch.tv", "netflix.com", "spotify.com",
"amazon.com", "ebay.com", "aliexpress.com", "microsoft.com", "apple.com",
"facebook.com", "instagram.com", "twitter.com", "linkedin.com", "reddit.com",
"cloudflare.com", "digitalocean.com", "linode.com", "hetzner.com", "ovh.com",
"docker.com", "kubernetes.io", "ansible.com", "terraform.io", "gitlab.com",
"stackoverflow.com", "medium.com", "dev.to", "hackernews.com", "slashdot.org",
"nytimes.com", "bbc.com", "elpais.com", "elmundo.es", "nasa.gov"
];
println!("Iniciando comprobación paralela con Rayon de {} servidores...", servidores.len());
// AQUÍ ESTÁ LA MAGIA: .into_par_iter() en lugar de .into_iter()
servidores.into_par_iter().for_each(|ip| {
let status = Command::new("ping")
.args(["-c", "1", "-W", "1", ip])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
match status {
Ok(s) if s.success() => println!("✅ {ip} está vivo"),
_ => println!("❌ {ip} está caído o no responde"),
}
});
println!("Monitorización finalizada en tiempo récord.");
}
Comparativa
| Característica | Bash | Rust-script | Por qué ganarás |
|---|---|---|---|
| Manejo de Errores | Inspección manual de la variable $? | Uso del operador ? y tipo Result | El script se detiene ante errores automáticamente, evitando fallos en cadena. |
| Paso de Argumentos | Validación manual con expresiones regulares | Tipado fuerte y método .parse() | Aseguras que los datos de entrada (como números) son válidos antes de procesarlos. |
| Lógica de Datos | Condicionales if/else y lógica de texto | match y Pattern Matching exhaustivo | Cubres todos los casos posibles (rangos, errores) de forma explícita y legible. |
| Captura de Salida | Captura implícita y limpieza con $(...) | Control manual de stdout y bytes | Control total sobre la codificación (UTF-8) y el tratamiento de la salida del sistema. |
| Consumo de APIs | Dependencia externa de herramientas como jq | Serialización integrada con serde_json | Script autocontenido; procesas datos JSON de forma segura sin fallos silenciosos. |
| Gestión de Rutas | Concatenación de cadenas de texto | Uso de objetos PathBuf | Evitas que un espacio en un nombre de archivo rompa la lógica del script. |
| Variables de Entorno | Expansión de parámetros ${VARIABLE:-default} | Métodos funcionales Result / Option | El código explica por sí mismo qué ocurre si la configuración no existe. |
| Escritura Masiva | Redirecciones de shell (>>) o echo | BufWriter para escritura en bloques | Generación de millones de líneas con rendimiento profesional y bajo consumo. |
| Lectura de Archivos | Bucle while read (lento en archivos grandes) | BufReader para lectura eficiente | Procesas archivos de logs de Gigabytes en segundos sin saturar la memoria RAM. |
| Concurrencia | Procesos al fondo mediante & y wait | Hilos nativos seguros con std::thread | Ejecución de tareas paralelas (como pings) de forma coordinada, rápida y segura. |
Que no se me olvide…. El primer episodio de La Era de las Distros