Ruby como herramienta de automatización
Un script de automatización en Ruby es un programa pequeño y enfocado que ejecutas desde la terminal para resolver tareas repetitivas: procesar carpetas, renombrar archivos, leer logs y generar reportes. La clave para que sea “eficiente” no es solo que corra rápido, sino que sea reutilizable, seguro y con salidas claras para que puedas integrarlo en tu flujo de trabajo (o en un cron/CI).
Principios prácticos para scripts reutilizables
- Interfaz consistente: acepta argumentos y opciones; si faltan, muestra ayuda y termina con un código de salida apropiado.
- Salida clara: separa “resultado” (STDOUT) de “diagnóstico” (STDERR). Muestra resúmenes y contadores.
- Modo seguro: evita sobrescribir archivos por defecto; ofrece
--dry-runpara simular cambios. - Casos borde: nombres con espacios, extensiones vacías, archivos duplicados, permisos insuficientes, rutas relativas/absolutas.
- Idempotencia: si lo ejecutas dos veces, no debería romper nada ni duplicar trabajo.
Argumentos de línea de comandos con ARGV
Ruby expone los argumentos posicionales en el arreglo ARGV. Es ideal para scripts simples: ruby mi_script.rb archivo.log. Para opciones tipo --dry-run o --out reporte.csv, Ruby incluye OptionParser (biblioteca estándar), que te permite definir banderas y generar mensajes de ayuda.
Plantilla mínima de script con ayuda y opciones
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'optparse'
options = {
dry_run: false,
verbose: false
}
parser = OptionParser.new do |opts|
opts.banner = "Uso: ruby script.rb [opciones] argumento1"
opts.on("--dry-run", "Simula sin hacer cambios") { options[:dry_run] = true }
opts.on("-v", "--verbose", "Muestra más detalle") { options[:verbose] = true }
opts.on("-h", "--help", "Muestra esta ayuda") do
puts opts
exit 0
end
end
begin
parser.parse!(ARGV)
rescue OptionParser::ParseError => e
warn "Error: #{e.message}"
warn parser
exit 2
end
if ARGV.empty?
warn "Falta el argumento requerido."
warn parser
exit 2
end
arg1 = ARGV[0]
puts "Argumento: #{arg1}" if options[:verbose]
Buenas prácticas rápidas: usa exit 0 para éxito, exit 2 para uso incorrecto; imprime errores con warn (va a STDERR); y considera #!/usr/bin/env ruby para ejecutar como ./script.rb tras chmod +x.
Automatización típica 1: procesamiento de carpetas y renombrado
Procesar carpetas suele implicar: (1) listar archivos, (2) filtrar por criterio, (3) calcular un nuevo nombre/ruta, (4) aplicar cambios con seguridad. Un patrón útil es “planificar” primero (mostrar qué se haría) y luego ejecutar.
Checklist de seguridad para renombrar/mover
- No sobrescribir por defecto: si el destino existe, decide una estrategia (sufijo incremental, saltar, o abortar).
- Limitar el alcance: trabaja dentro de un directorio base; evita seguir enlaces simbólicos si no lo necesitas.
- Validar rutas: normaliza con
File.expand_pathy verifica que el destino esté dentro del directorio base. - Dry-run: imprime operaciones antes de ejecutarlas.
Automatización típica 2: lectura de logs y generación de reportes
Un log es una fuente de datos “semi-estructurada”. Un script útil convierte líneas en métricas: conteos por nivel (INFO/WARN/ERROR), top de endpoints, latencias promedio, etc. Para mantenerlo robusto, evita asumir que todas las líneas cumplen el formato: cuenta “líneas inválidas” y sigue.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
Formato de ejemplo de log
Usaremos un formato simple (una línea por evento):
2026-01-10T12:00:01Z INFO user_id=42 action=login
2026-01-10T12:00:02Z WARN user_id=42 action=retry
2026-01-10T12:00:03Z ERROR user_id=99 action=payment_failed
Ejercicio 1: script que analiza un archivo de registro y produce métricas
Objetivo
- Entrada: ruta a un archivo
.log. - Salida: métricas en consola y, opcionalmente, un CSV.
- Métricas: total de líneas, líneas válidas/ inválidas, conteo por nivel, top 5 acciones.
- Opciones:
--out reporte.csv,--min-level WARN,--dry-run(solo para demostrar interfaz; aquí no modifica nada).
Paso a paso
- Parsear opciones y validar argumentos.
- Leer el archivo línea por línea (sin cargar todo en memoria).
- Parsear cada línea con una expresión regular tolerante.
- Acumular métricas en hashes.
- Imprimir resumen y, si se pidió, escribir CSV.
Código del script: log_metrics.rb
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'optparse'
require 'csv'
LEVEL_ORDER = { "DEBUG" => 0, "INFO" => 1, "WARN" => 2, "ERROR" => 3, "FATAL" => 4 }.freeze
options = {
out: nil,
min_level: "DEBUG",
verbose: false
}
parser = OptionParser.new do |opts|
opts.banner = "Uso: ruby log_metrics.rb [opciones] archivo.log"
opts.on("--out PATH", "Escribe un CSV con métricas (opcional)") { |v| options[:out] = v }
opts.on("--min-level LEVEL", "Nivel mínimo a contar (DEBUG, INFO, WARN, ERROR, FATAL)") { |v| options[:min_level] = v }
opts.on("-v", "--verbose", "Muestra detalle") { options[:verbose] = true }
opts.on("-h", "--help", "Ayuda") { puts opts; exit 0 }
end
begin
parser.parse!(ARGV)
rescue OptionParser::ParseError => e
warn "Error: #{e.message}"
warn parser
exit 2
end
path = ARGV[0]
if path.nil?
warn "Debes indicar un archivo .log"
warn parser
exit 2
end
unless File.file?(path)
warn "No existe el archivo: #{path}"
exit 1
end
min_level = options[:min_level].upcase
unless LEVEL_ORDER.key?(min_level)
warn "Nivel inválido: #{options[:min_level]}"
exit 2
end
min_rank = LEVEL_ORDER[min_level]
# Métricas
counts_by_level = Hash.new(0)
actions = Hash.new(0)
total = 0
valid = 0
invalid = 0
# Ejemplo de línea: 2026-01-10T12:00:01Z INFO user_id=42 action=login
line_re = /\A(?<ts>\S+)\s+(?<level>DEBUG|INFO|WARN|ERROR|FATAL)\s+(?<rest>.+)\z/
action_re = /\baction=(?<action>[A-Za-z0-9_\-]+)\b/
File.foreach(path, chomp: true) do |line|
total += 1
m = line_re.match(line)
unless m
invalid += 1
next
end
level = m[:level]
next if LEVEL_ORDER[level] < min_rank
valid += 1
counts_by_level[level] += 1
if (am = action_re.match(m[:rest]))
actions[am[:action]] += 1
end
end
puts "Archivo: #{path}"
puts "Total líneas: #{total}"
puts "Válidas (nivel >= #{min_level}): #{valid}"
puts "Inválidas: #{invalid}"
puts "\nConteo por nivel:"
LEVEL_ORDER.keys.each do |lvl|
next if LEVEL_ORDER[lvl] < min_rank
puts "- #{lvl}: #{counts_by_level[lvl]}"
end
top_actions = actions.sort_by { |_, c| -c }.first(5)
puts "\nTop acciones:"
if top_actions.empty?
puts "(sin acciones detectadas)"
else
top_actions.each { |a, c| puts "- #{a}: #{c}" }
end
if options[:out]
CSV.open(options[:out], "w") do |csv|
csv << ["metric", "value"]
csv << ["file", path]
csv << ["total_lines", total]
csv << ["valid_lines", valid]
csv << ["invalid_lines", invalid]
LEVEL_ORDER.keys.each do |lvl|
next if LEVEL_ORDER[lvl] < min_rank
csv << ["level_#{lvl.downcase}", counts_by_level[lvl]]
end
top_actions.each_with_index do |(a, c), idx|
csv << ["top_action_#{idx + 1}_name", a]
csv << ["top_action_#{idx + 1}_count", c]
end
end
warn "CSV escrito en: #{options[:out]}" if options[:verbose]
end
Pruebas rápidas
# Solo consola
ruby log_metrics.rb app.log
# Filtrar desde WARN y exportar CSV
ruby log_metrics.rb --min-level WARN --out reporte.csv app.log
Casos borde a considerar (y cómo los maneja)
- Líneas mal formateadas: se cuentan como inválidas y no rompen el script.
- Nivel desconocido: se rechaza en la opción
--min-level. - Archivo inexistente: termina con error y mensaje claro.
- Acción ausente: no suma al ranking, pero la línea sigue contando.
Ejercicio 2: organizar archivos por extensión en subcarpetas
Objetivo
- Entrada: un directorio.
- Acción: mover archivos a subcarpetas según extensión:
jpg,pdf,txt, etc. - Opciones:
--dry-run,--recursive,--unknown NAME(carpeta para archivos sin extensión),--conflict (skip|rename|abort). - Seguridad: no salir del directorio base; no seguir symlinks (por defecto); no sobrescribir sin estrategia.
Paso a paso
- Resolver directorio base a ruta absoluta y validar que exista.
- Recorrer archivos (solo archivos regulares) y decidir destino por extensión.
- Crear subcarpetas si no existen.
- Resolver conflictos si el destino ya existe.
- Ejecutar o simular según
--dry-run.
Código del script: organize_by_ext.rb
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'optparse'
require 'fileutils'
options = {
dry_run: false,
recursive: false,
unknown: "no_extension",
conflict: "rename", # skip|rename|abort
verbose: false
}
parser = OptionParser.new do |opts|
opts.banner = "Uso: ruby organize_by_ext.rb [opciones] directorio"
opts.on("--dry-run", "Simula movimientos") { options[:dry_run] = true }
opts.on("--recursive", "Incluye subcarpetas") { options[:recursive] = true }
opts.on("--unknown NAME", "Carpeta para archivos sin extensión") { |v| options[:unknown] = v }
opts.on("--conflict MODE", "Conflictos: skip, rename, abort") { |v| options[:conflict] = v }
opts.on("-v", "--verbose", "Detalle") { options[:verbose] = true }
opts.on("-h", "--help", "Ayuda") { puts opts; exit 0 }
end
begin
parser.parse!(ARGV)
rescue OptionParser::ParseError => e
warn "Error: #{e.message}"
warn parser
exit 2
end
dir = ARGV[0]
if dir.nil?
warn "Debes indicar un directorio"
warn parser
exit 2
end
base = File.expand_path(dir)
unless Dir.exist?(base)
warn "No existe el directorio: #{base}"
exit 1
end
unless %w[skip rename abort].include?(options[:conflict])
warn "--conflict inválido: #{options[:conflict]}"
exit 2
end
# Evita que un destino se salga del directorio base
# (protección básica ante rutas raras)
def within_base?(base, path)
b = File.expand_path(base)
p = File.expand_path(path)
p.start_with?(b + File::SEPARATOR)
end
# Genera un destino no existente: archivo(1).ext, archivo(2).ext...
def next_available_path(path)
return path unless File.exist?(path)
dir = File.dirname(path)
ext = File.extname(path)
name = File.basename(path, ext)
i = 1
loop do
candidate = File.join(dir, "#{name}(#{i})#{ext}")
return candidate unless File.exist?(candidate)
i += 1
end
end
pattern = options[:recursive] ? File.join(base, "**", "*") : File.join(base, "*")
moved = 0
skipped = 0
errors = 0
Dir.glob(pattern, File::FNM_DOTMATCH).each do |src|
next if [".", ".."].include?(File.basename(src))
# No tocar directorios
next if File.directory?(src)
# No seguir symlinks por seguridad básica
if File.symlink?(src)
skipped += 1
warn "Skip symlink: #{src}" if options[:verbose]
next
end
# Solo archivos regulares
unless File.file?(src)
skipped += 1
next
end
ext = File.extname(src).downcase
ext = ext.sub(/\A\./, "")
folder = ext.empty? ? options[:unknown] : ext
dest_dir = File.join(base, folder)
dest = File.join(dest_dir, File.basename(src))
unless within_base?(base, dest_dir) && within_base?(base, dest)
errors += 1
warn "Destino fuera del directorio base, abortando: #{dest}"
exit 1
end
begin
if File.exist?(dest)
case options[:conflict]
when "skip"
skipped += 1
warn "Existe, skip: #{dest}" if options[:verbose]
next
when "abort"
warn "Existe destino, abortando: #{dest}"
exit 1
when "rename"
dest = next_available_path(dest)
end
end
if options[:dry_run]
puts "MOVE #{src} -> #{dest}"
else
FileUtils.mkdir_p(dest_dir)
FileUtils.mv(src, dest)
puts "Moved: #{src} -> #{dest}" if options[:verbose]
end
moved += 1
rescue SystemCallError => e
errors += 1
warn "Error moviendo #{src}: #{e.class} #{e.message}"
end
end
puts "Base: #{base}"
puts "Movidos: #{moved}"
puts "Saltados: #{skipped}"
puts "Errores: #{errors}"
Pruebas rápidas
# Simular sin modificar nada
ruby organize_by_ext.rb --dry-run Descargas
# Organizar incluyendo subcarpetas y renombrar en conflictos
ruby organize_by_ext.rb --recursive --conflict rename Descargas
Casos borde y decisiones de diseño
| Caso | Riesgo | Estrategia del script |
|---|---|---|
| Archivos sin extensión | Quedan “sueltos” | Van a carpeta configurable --unknown |
| Extensiones en mayúsculas (.JPG) | Carpetas duplicadas | Normaliza a minúsculas |
| Conflicto de nombre | Sobrescritura | skip, abort o rename con sufijo |
| Symlinks | Escapar del directorio base | Se omiten por defecto |
| Archivos ocultos | Procesamiento inesperado | Se incluyen por glob; puedes filtrarlos si lo prefieres |
Patrones útiles para salidas claras y automatización en pipelines
Separar salida de datos vs. diagnósticos
- Usa
putspara resultados consumibles. - Usa
warnpara mensajes de error/estado (va a STDERR).
Códigos de salida recomendados
0: éxito.1: fallo operativo (archivo no existe, permisos, etc.).2: uso incorrecto (opciones inválidas, faltan argumentos).
Modo dry-run como estándar
En scripts que mueven/renombran, --dry-run reduce riesgos y facilita revisar el “plan” antes de ejecutar cambios reales.