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