Administración de Linux
grep, sed y awk: pipelines de procesamiento de texto que responden a preguntas reales
Intermedio10 min de lecturaActualizado
Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés
Tres herramientas cubren casi todo el procesamiento de texto en la línea de comandos: grep selecciona líneas, sed las edita y awk hace cálculos sobre campos. Esta guía asienta la base común de expresiones regulares, dedica a cada herramienta la profundidad que merece, añade las pequeñas utilidades que sirven de pegamento en los pipelines y, por último, resuelve preguntas realistas sobre el log de un servidor web, desde la primera coincidencia hasta un informe ordenado.
Lo que aprenderás
- Escribir expresiones regulares básicas y extendidas, y saber qué dialecto acepta cada herramienta
- Seleccionar, contar y extraer texto con grep y sus opciones más útiles
- Editar flujos con sed usando direcciones, modificadores de sustitución y edición in situ
- Calcular resúmenes por campo con awk mediante patrones, acciones, arrays y bloques BEGIN/END
- Combinar cut, sort, uniq, tr y join en pipelines que respondan a preguntas sobre ficheros de log
El procesamiento de texto en la línea de comandos devuelve con creces una pequeña inversión. En un sistema Linux, cada fichero de log, cada exportación CSV, cada fichero de configuración y cada salida de un comando es texto, y tres programas —grep, sed y awk— cubren casi todo lo que querrás hacer con él. Llevan décadas estables, están en todas las máquinas, incluido el contenedor más recortado, y en cuanto sabes combinarlos dejas de escribir scripts de usar y tirar para preguntas que responde un pipeline de una sola línea.
Conviene dejar claro desde el principio el reparto de tareas, porque elegir la herramienta equivocada es lo que vuelve feos estos pipelines. grep decide qué líneas. sed cambia lo que dice una línea. awk trata una línea como campos y sabe contar, sumar y recordar. Esta guía construye la base de expresiones regulares que comparten, las recorre una por una y termina respondiendo preguntas realistas sobre el log de accesos de un servidor web.
Las expresiones regulares que hay debajo
Una expresión regular describe un conjunto de cadenas. La sintaxis es en gran parte común, con una bifurcación importante.
| Construcción | Coincide con |
|---|---|
. | cualquier carácter individual |
[abc], [a-z], [^0-9] | un carácter que pertenece, o no, a un conjunto |
[[:digit:]], [[:space:]], [[:alpha:]] | clases de caracteres POSIX, dependientes del locale |
^, $ | inicio y fin de línea |
* | cero o más repeticiones del elemento anterior |
\+, \? (BRE) o +, ? (ERE) | una o más; cero o una |
\{2,5\} (BRE) o {2,5} (ERE) | entre dos y cinco repeticiones |
\(…\) (BRE) o (…) (ERE) | un grupo de captura, al que luego se hace referencia con \1 |
| `\ | (BRE) o|` (ERE) |
\b, \<, \> | límites de palabra (extensión de GNU) |
Las expresiones regulares básicas (BRE) son las predeterminadas en grep y sed; las extendidas (ERE) se activan con -E y son las que awk usa de forma nativa. La única diferencia está en qué caracteres necesitan barra invertida, y el dialecto que conviene adoptar como estándar es ERE, donde (a|b)+ significa exactamente lo que parece. Las expresiones compatibles con Perl, con lookahead y cuantificadores no codiciosos, están disponibles como grep -P en sistemas GNU, pero no existen en BSD ni en macOS, así que evítalas en scripts portables.
Dos hábitos evitan la mayor parte de las confusiones. Pon siempre el patrón entre comillas simples para que el shell no expanda *, $ o \ antes de que la herramienta los vea. Y recuerda que estos motores son codiciosos (greedy): .*" llega hasta la última comilla de la línea, no hasta la primera. Cuando eso te dé problemas, usa una clase negada: [^"]*" se detiene en la primera. Construir una expresión de forma interactiva contra un texto de ejemplo, en el probador de regex o con grep --color, es mucho más rápido que razonarla de cabeza.
grep: elegir líneas
grep -i 'timeout' app.log # case-insensitive
grep -n 'ERROR' app.log # prefix each match with its line number
grep -c 'ERROR' app.log # count matching lines, not matches
grep -v '^#' /etc/ssh/sshd_config # invert: everything that is not a comment
grep -w 'id' schema.sql # whole word only: not "uuid" or "identity"
grep -F 'a.b.c' hosts.txt # fixed string: no regex interpretation
grep -r --include='*.java' 'TODO' src/ # recursive, filtered by name
grep -l 'password' /etc/myapp/* # list only the file names that match
grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+' access.log # print only the matched part
Cuatro opciones pesan más que el resto. -E te da el dialecto legible. -o imprime solo el texto que coincide, lo que convierte a grep en un extractor en vez de un filtro. -C 3 (o -A/-B) muestra líneas de contexto alrededor de cada coincidencia, que es como se lee una stack trace localizada por el nombre de su excepción. Y -q no imprime nada y solo fija el código de salida, que es lo que necesita un script:
if grep -q '^PasswordAuthentication no' /etc/ssh/sshd_config; then
echo 'password login is disabled'
fi
grep -f patterns.txt data.txt lee muchos patrones desde un fichero, una forma rápida de filtrar una lista con otra. Y en un fichero grande, anteponer LC_ALL=C puede hacerlo varias veces más rápido, porque el motor pasa a trabajar con bytes en lugar de caracteres multibyte; es seguro siempre que tus datos sean ASCII.
sed: editar un flujo
sed lee la entrada línea a línea, aplica un script a cada una e imprime el resultado. El comando más habitual con diferencia es la sustitución, s/patrón/reemplazo/modificadores.
sed 's/localhost/db.internal/' app.conf # first occurrence per line
sed 's/localhost/db.internal/g' app.conf # every occurrence
sed 's/DEBUG/TRACE/gi' app.log # g and i combine
sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/' dates.txt # reorder with groups
sed 's#/var/www/old#/var/www/new#g' nginx.conf # any delimiter: # avoids escaping slashes
sed -E 's/(password=)[^ ]+/\1REDACTED/' app.log # mask a secret before sharing a log
En el reemplazo, & representa la coincidencia completa y \1 … \9 los grupos de captura. GNU sed añade \U, \L y \E para cambiar mayúsculas y minúsculas.
Cualquier comando puede limitarse a una dirección: un número de línea, un rango o un patrón.
sed -n '10,20p' large.log # -n suppresses output; p prints: lines 10-20
sed '1d' data.csv # delete the header row
sed '/^$/d' notes.txt # delete blank lines
sed '/^#/d; /^$/d' /etc/nginx/nginx.conf # comments and blanks: two commands, semicolon
sed -n '/BEGIN CERT/,/END CERT/p' bundle.pem # everything between two patterns
sed '/\[database\]/,/^\[/ s/^port =.*/port = 5433/' app.ini # substitute only inside a section
sed '$a\ExtraLine' file.txt # append after the last line
sed -n '/exception/I{=;p}' app.log # print line numbers and lines, case-insensitive
Ese último grupo, un rango de direcciones con una sustitución, es el patrón que hace que sed sea realmente útil para ficheros de configuración, porque permite cambiar una clave solo dentro de la sección a la que pertenece.
La edición in situ se hace con -i, y la forma segura indica un sufijo para la copia de seguridad:
sed -i.bak -E 's/^#?(PermitRootLogin) .*/\1 no/' /etc/ssh/sshd_config
diff /etc/ssh/sshd_config.bak /etc/ssh/sshd_config
Ejecuta siempre el comando primero sin -i y mira la salida. Y ten presente que sed -i escribe un fichero nuevo y lo renombra sobre el antiguo, de modo que el inode cambia: el propietario puede restablecerse, los enlaces duros se rompen y un proceso que tenga abierto el fichero antiguo sigue leyendo el contenido antiguo. Para ficheros que lee un servicio en ejecución, normalmente no pasa nada; para cualquier cosa más delicada, edita una copia e instálala de forma explícita.
sed tiene un repertorio de comandos más amplio: y para transliterar, N para traer la línea siguiente, y los comandos del hold space h, H, x, G para trabajar con varias líneas. Existen y alguna vez te salvan, pero un programa sed multilínea es código de solo escritura. En cuanto necesitas el hold space, casi seguro que lo que quieres es awk.
awk: campos, patrones y aritmética
awk divide cada línea de entrada en campos por los espacios en blanco (o por el separador que elijas) y le aplica una secuencia de reglas pattern { action }. $1 es el primer campo, $NF el último, $0 la línea completa, NR el número de registro y NF el número de campos.
$ awk '{ print $1, $9 }' access.log | head -3
203.0.113.7 200
198.51.100.44 404
203.0.113.7 200
$ awk -F: '$3 >= 1000 { print $1, $6 }' /etc/passwd
alice /home/alice
bob /home/bob
Si omites el patrón, la acción se ejecuta en todas las líneas; si omites la acción, la predeterminada es print $0, lo que convierte awk '/error/' en un sustituto funcional de grep.
BEGIN se ejecuta antes de leer ninguna entrada y END después de leerla toda: el lugar natural para cabeceras y totales.
awk -F, 'NR > 1 { total += $3; n++ }
END { printf "%d rows, average %.2f\n", n, total / n }' sales.csv
Los arrays de awk son asociativos, indexados por cadenas, y aquí es donde deja atrás a las demás herramientas. Contar las apariciones de cualquier cosa es un programa de dos líneas:
awk '{ count[$1]++ } END { for (ip in count) print count[ip], ip }' access.log | sort -rn | head
Unas cuantas construcciones más cubren la mayoría de los usos reales:
# Sum bytes served per status code
awk '{ bytes[$9] += $10 } END { for (s in bytes) printf "%s %.1f MB\n", s, bytes[s]/1048576 }' access.log
# Requests slower than one second, with their path
awk '$NF > 1.0 { printf "%6.2fs %s\n", $NF, $7 }' timing.log
# Field separator that is a regular expression, and output separator
awk -F'[[:space:]]+' -v OFS='\t' '{ print $2, $5 }' report.txt
# Multi-line records: a blank line separates each block
awk -v RS='' -v FS='\n' '/state: failed/ { print $1 }' units.txt
# Pass a shell variable in properly, rather than splicing it into the program text
threshold=500
awk -v t="$threshold" '$3 > t { print }' metrics.tsv
-v es la forma correcta de pasar valores del shell a un programa awk: incrustarlos en una cadena entre comillas simples invita a errores de entrecomillado y, si el valor viene de fuera, a la inyección de código. Fíjate también en que awk tiene funciones de cadena de verdad —length(), substr(), split(), gsub(), match(), sprintf()— y que gawk añade strftime() y systime() para manejar fechas. Un programa awk lo bastante largo como para merecer su propio fichero se ejecuta con awk -f report.awk data.txt.
Los actores secundarios
| Herramienta | Qué hace |
|---|---|
cut | selecciona campos o rangos de bytes: cut -d: -f1,6 /etc/passwd, cut -c1-8 |
sort | ordena líneas: -n numérico, -r inverso, -k2,2 por un campo, -u únicas, -h tamaños legibles, -t separador |
uniq | colapsa duplicados adyacentes: -c cuenta, -d solo los duplicados, -u solo los únicos |
tr | traduce o elimina caracteres: tr 'A-Z' 'a-z', tr -d '\r', tr -s ' ' |
wc | cuenta líneas, palabras y bytes: wc -l |
head / tail | primeras o últimas líneas; tail -f sigue un fichero que crece |
paste | une ficheros lado a lado en columnas |
join | join relacional de dos ficheros ordenados por un campo común |
comm | compara dos ficheros ordenados: líneas solo en el primero, solo en el segundo, en ambos |
column -t | alinea la salida separada por espacios en columnas legibles |
nl, rev, fold, expand | numeran, invierten, ajustan y destabulan líneas |
El que hace tropezar a todo el mundo es uniq: solo compara líneas contiguas, así que la fórmula habitual es sort | uniq -c, y uniq a secas sobre una entrada sin ordenar no hace casi nada sin avisar. La secuencia sort | uniq -c | sort -rn —agrupar, contar, ordenar— es el caballo de batalla del análisis de logs improvisado.
join merece más uso del que recibe. Con dos ficheros ordenados por la misma clave, realiza la misma operación que un join en una base de datos, algo muy práctico para cruzar una exportación con una tabla de consulta:
sort -t, -k1,1 users.csv > /tmp/u.csv
sort -t, -k1,1 orders.csv > /tmp/o.csv
join -t, -1 1 -2 1 /tmp/u.csv /tmp/o.csv
Si recurres a join con frecuencia sobre ficheros grandes, es señal de que esos datos deberían estar en una base de datos, donde las ideas de las buenas prácticas de SQL se aplican como es debido.
Pipelines que responden a preguntas
Toma un log de accesos estándar en formato combined, donde el campo 1 es la dirección del cliente, el 7 la ruta, el 9 el código de estado y el 10 el tamaño de la respuesta.
¿Qué clientes generaron más peticiones?
awk '{ print $1 }' access.log | sort | uniq -c | sort -rn | head -20
¿Cuántas peticiones hay por código de estado y qué porcentaje del total suponen?
awk '{ c[$9]++; n++ } END { for (s in c) printf "%-5s %6d %5.1f%%\n", s, c[s], 100*c[s]/n }' \
access.log | sort -k2 -rn
¿Qué URL devuelven 500, y con qué frecuencia?
awk '$9 == 500 { print $7 }' access.log | sort | uniq -c | sort -rn | head
¿Cómo varía el tráfico según la hora? El campo de la marca de tiempo tiene este aspecto: [07/Sep/2026:14:32:01, así que se corta por los dos puntos:
awk '{ split($4, t, ":"); hour[t[2]]++ }
END { for (h = 0; h < 24; h++) printf "%02d %s\n", h, hour[sprintf("%02d", h)] + 0 }' access.log
Total de bytes servidos por cliente, de mayor a menor, en megabytes:
awk '{ b[$1] += $10 } END { for (ip in b) printf "%10.2f MB %s\n", b[ip]/1048576, ip }' \
access.log | sort -rn | head
¿Qué errores han aparecido hoy que ayer no estaban?
grep -o 'ERROR .*' today.log | sort -u > /tmp/today
grep -o 'ERROR .*' yesterday.log | sort -u > /tmp/yesterday
comm -23 /tmp/today /tmp/yesterday
Seguir un log en directo, quedarse solo con las peticiones lentas y ocultar los tokens:
tail -F /var/log/app/access.log \
| awk '$NF > 2.0' \
| sed -E 's/(token=)[A-Za-z0-9._-]+/\1REDACTED/g'
Fíjate en tail -F en lugar de -f: la mayúscula vuelve a abrir el fichero tras la rotación de logs, que es justo el momento en que menos quieres que tu monitorización se quede muda. La misma distinción importa en la parte sobre rotación de logs y resolución de problemas.
Construye estos pipelines poco a poco. Ejecuta la primera etapa, mira diez líneas, añade la siguiente etapa y vuelve a mirar. Depurar un pipeline de cinco etapas escrito de una sentada es mucho más difícil que hacerlo crecer etapa a etapa, y añadir | head -5 mientras trabajas mantiene la respuesta instantánea incluso con un fichero grande.
Elegir la herramienta adecuada
Una regla de decisión aproximada:
- ¿Solo necesitas saber si algo está, o ver las líneas? grep.
- ¿Necesitas cambiar el texto de cada línea, o borrar o imprimir un rango? sed.
- ¿Necesitas columnas, aritmética, agrupaciones, formato o estado entre líneas? awk.
- ¿Tiene que ir rápido con un fichero enorme? Pon primero el
grepmás selectivo para que las etapas siguientes reciban menos datos, y antepónLC_ALL=C. - ¿Son datos estructurados, como JSON, YAML o XML? Usa un parser.
jq,yqyxmlstarletexisten precisamente porque las expresiones regulares no pueden analizar de forma fiable formatos anidados, y un pipeline que hace grep sobre JSON se romperá el día que cambie el orden de los campos. Lo mismo vale para HTML.
Errores comunes
- Patrones sin comillas. El shell expande primero
*y$; pon cada patrón entre comillas simples. uniqsinsort. Solo se colapsan los duplicados adyacentes, así que los recuentos salen mal.- Un
.*codicioso que se traga la línea. Usa una clase negada como[^"]*para detenerte en el primer delimitador. grep | awk '{print $1}'. Elgrepsobra:awk '/pattern/ { print $1 }'hace las dos cosas en una sola pasada.sed -isin copia de seguridad ni prueba en seco. Imprime primero el resultado y después edita con-i.bak.- Incrustar variables del shell en un programa awk. Pásalas con
-v. - Analizar JSON o XML con expresiones regulares. Usa
jqo un parser de verdad; el anidamiento derrota al motor de regex. - Olvidar que
cut -fnecesita un delimitador de un solo carácter. Para secuencias de espacios usaawk, o normaliza antes contr -s ' '.
Ideas clave
grepselecciona líneas,sedlas transforma yawkcalcula sobre campos: ajustar la herramienta a la pregunta mantiene cortos los pipelines.- Aprende las expresiones regulares extendidas (
grep -E,sed -E, awk de forma nativa) y pon siempre los patrones entre comillas simples. grep -o,-c,-q,-vy-Cconvierten un filtro en un extractor, un contador, una comprobación y un visor de contexto.- Las direcciones de
sed(números de línea, rangos y rangos de patrones) permiten editar exactamente una sección de un fichero de configuración;-i.baktras una prueba en seco lo mantiene seguro. - Los arrays asociativos de awk, junto con los bloques
BEGIN/END, responden en una sola pasada a preguntas de recuento, suma y agrupación; pasa los valores del shell con-v. sort | uniq -c | sort -rnes la fórmula clásica para hacer rankings;commyjoincomparan y cruzan ficheros ordenados.- Construye los pipelines etapa a etapa, pon primero el filtro más selectivo y pasa a un parser de verdad para JSON, XML y HTML.
Fuentes y lecturas recomendadas
Preguntas frecuentes
- ¿Cuándo debo usar grep, cuándo sed y cuándo awk?
- Usa grep para decidir qué líneas te interesan, sed para transformar líneas con sustituciones y awk cuando la respuesta dependa de dividir una línea en campos, hacer operaciones aritméticas o acumular estado entre líneas. Si te ves encadenando varios comandos sed con cut y un contador, normalmente todo eso cabe en un programa awk corto, más fácil de leer y más rápido.
- ¿Por qué mi expresión regular necesita barras invertidas en una herramienta y en otra no?
- Las expresiones regulares básicas, las predeterminadas en grep y sed, tratan el signo más, el signo de interrogación, las llaves, los paréntesis y la barra vertical como caracteres literales, y exigen una barra invertida para darles su significado especial. Las expresiones regulares extendidas, que se activan con grep -E y sed -E y que awk usa de forma nativa, hacen justo lo contrario. Usa siempre el dialecto extendido y tus expresiones serán mucho más legibles.
- ¿Es seguro ejecutar sed -i sobre un fichero de configuración en producción?
- Lo es si pruebas antes y guardas una copia de seguridad. Ejecuta el comando sin -i y lee la salida; después usa -i.bak para que el original se conserve como fichero.bak. Ten en cuenta que GNU sed sustituye el fichero renombrando una copia temporal, así que el propietario y los posibles enlaces duros pueden cambiar. En un fichero crítico, edita una copia, compárala con diff e instala el resultado con el propietario y los permisos correctos.
- ¿Cómo busco un tabulador o trato un fichero con finales de línea de Windows?
- Las herramientas GNU aceptan las secuencias de escape \t y \r dentro de las expresiones regulares, y awk también las entiende en cadenas. Los ficheros que vienen de Windows llevan un retorno de carro al final que hace que el último campo de cada línea no coincida. Elimínalos de una vez con sed -i 's/\r$//' fichero, o ajusta el separador de campos para que awk los ignore, en lugar de pasarte una hora depurando caracteres invisibles.