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í.
N° 50