`tr`, `wc` y `sed`: procesamiento de texto en pipelines

Estas tres herramientas operan sobre flujos de texto de formas muy distintas. tr trabaja carácter por carácter sobre stdin —no entiende líneas ni palabras, solo bytes o caracteres—. wc cuenta: líneas, palabras, bytes o caracteres según el flag. sed es un editor de flujos (stream editor) que procesa el texto línea a línea aplicando expresiones regulares o sustituciones. Juntas cubren el 80% de las transformaciones rápidas que harás en un pipeline de shell.

El diseño de tr es intencionalmente primitivo: recibe dos conjuntos de caracteres y reemplaza cada carácter del primer conjunto por el correspondiente del segundo. No tiene flags de expresiones regulares, no procesa ficheros directamente (todo va por stdin/stdout), y eso lo hace extremadamente predecible. wc tampoco hace magia: lee bytes del flujo y aplica contadores. La distinción entre -c (bytes) y -m (caracteres) importa mucho en texto UTF-8, donde un solo carácter puede ocupar 2, 3 o 4 bytes.

Usas estas herramientas cuando necesitas transformaciones rápidas dentro de un pipeline: limpiar un CSV exportado desde Windows, contar registros en un log, normalizar espacios antes de pasarlo a awk, o tomar una muestra aleatoria de un fichero enorme sin cargarlo entero en memoria. Lo que rompes si los usas mal: perder datos silenciosamente con tr -d aplicado al conjunto equivocado, o confundir líneas con caracteres al usar wc -c en UTF-8 y obtener un número que no corresponde a lo que esperas contar.

Un caso concreto que lo ilustra todo: tienes un log de acceso exportado desde un servidor Windows, con saltos de línea \r\n, campos en minúsculas, y espacios múltiples como separadores irregulares. Quieres saber cuántas peticiones únicas hay, tomar una muestra de 5 para revisarlas, y extraer las líneas 3 a 7 de esa muestra.

# Fichero de ejemplo con problemas típicos de exportación Windows
printf 'get  /index.html  200\r\npost  /api/login  401\r\nget  /favicon.ico  304\r\nget  /index.html  200\r\npost  /api/data  200\r\nget  /robots.txt  200\r\npost  /api/login  200\r\nget  /style.css  200\r\nget  /app.js  200\r\ndelete  /api/session  200\r\n' > acceso_windows.log

# Paso 1: eliminar carriage returns (\r) que rompen cualquier procesamiento posterior
tr -d '\r' < acceso_windows.log > acceso_unix.log

# Verificar que ya no hay \r — si cat -A muestra $ al final sin ^M, estamos bien
cat -A acceso_unix.log | head -3

# Paso 2: comprimir espacios múltiples a uno solo y convertir a mayúsculas
# tr -s ' ' colapsa cualquier secuencia de espacios en un único espacio
# El pipeline encadena dos tr consecutivos: primero squeeze, luego transliteración
tr -s ' ' < acceso_unix.log | tr 'a-z' 'A-Z' > acceso_normalizado.log

cat acceso_normalizado.log

# Paso 3: contar líneas totales (peticiones), palabras y bytes
# -l líneas, -w palabras, -c bytes — en este fichero ASCII coinciden bytes y chars
wc -l acceso_normalizado.log
wc -w acceso_normalizado.log

# Con UTF-8 la diferencia importa: -c cuenta bytes, -m cuenta caracteres reales
echo "café" | wc -c   # devuelve 6: 4 letras + é en 2 bytes + newline
echo "café" | wc -m   # devuelve 5: 4 caracteres + newline

# Paso 4: muestra aleatoria de 5 líneas del fichero normalizado
# shuf -n evita cargar todo el fichero en memoria con head/tail combinados
shuf -n 5 acceso_normalizado.log > muestra.log
cat muestra.log

# Paso 5: de esa muestra de 5 líneas, extraer las líneas 3 a 5
# head -5 toma las primeras 5, tail -3 descarta las 2 primeras → líneas 3,4,5
head -5 muestra.log | tail -3

# Equivalente con sed para el mismo resultado (más legible en scripts)
sed -n '3,5p' muestra.log

Lo que está pasando en cada paso

tr -d '\r' en el paso 1 elimina literalmente todos los bytes 0x0D del flujo. No hay contexto, no hay condiciones: cualquier byte que coincida con el conjunto especificado desaparece. Por eso es peligroso aplicarlo sin pensar —si pones tr -d 'aeiou' en producción sobre un CSV, pierdes todas las vocales sin aviso—. La redirección < fichero > otro_fichero es la única forma de que tr procese un fichero, porque no acepta nombres de fichero como argumentos.

tr -s ' ' (squeeze) colapsa secuencias de caracteres repetidos. El argumento es el conjunto a comprimir, no un separador. Si pones tr -s ' \t' colapsarías también tabuladores, pero solo dentro de su propio tipo —no mezclaría espacios con tabuladores—.

La cadena tr -s ' ' < acceso_unix.log | tr 'a-z' 'A-Z' muestra el patrón central de estas herramientas: componer transformaciones simples en pipeline en lugar de escribir una sola herramienta compleja. Cada tr hace exactamente una cosa.

En el paso 3, la diferencia entre wc -c y wc -m es real en cualquier sistema con locale UTF-8. Debian Bookworm usa en_US.UTF-8 o es_ES.UTF-8 por defecto en instalaciones de escritorio; en servidores mínimos el locale puede ser C o POSIX, donde -m y -c devuelven el mismo número porque se asume ASCII. Comprueba con locale antes de confiar en -m para conteo de caracteres.

shuf -n 5 del paso 4 usa un algoritmo de reservoir sampling internamente: no carga todo el fichero en memoria para después seleccionar, lo que lo hace viable con ficheros de varios gigabytes. Es de coreutils, disponible en cualquier Debian. La alternativa clásica sort -R | head -n 5 sí carga y ordena todo, lo que en ficheros grandes es órdenes de magnitud más lento.

El truco head -N | tail -M del paso 5 para extraer un rango de líneas funciona así: head -5 emite las líneas 1-5, y tail -3 de esas 5 emite las últimas 3, que son las líneas 3, 4 y 5 del original. La versión con sed -n '3,5p' es más directa y más eficiente porque no procesa líneas innecesarias —sed para en la línea 5 sin leer el resto—. En ficheros pequeños no importa; en ficheros de millones de líneas, sí.

50

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio