1. Tres herramientas, tres trabajos
Las tres recorren un texto línea por línea, pero cada una está pensada para un trabajo distinto:
| Herramienta | Trabajo | Pregunta que responde |
|---|---|---|
grep | Buscar líneas. | ¿Qué líneas contienen esto? ¿Hay alguna? ¿Cuántas? |
sed | Transformar líneas. | ¿Cómo queda el texto si reemplazo esto por aquello, o borro estas líneas? |
awk | Procesar campos y calcular. | ¿Cuánto suma esta columna? ¿Cuántas veces aparece cada valor? |
Como dijimos en el tema 16, procesan un archivo entero en una sola pasada, mucho más rápido que un bucle while read de Bash. La regla general del curso, Bash coordina y las herramientas hacen el trabajo pesado, se aplica aquí más que en ningún otro lado.
2. grep en un script
En la terminal, grep se usa para mirar. En un script, casi siempre se usa para preguntar, y lo que importa es su código de salida, como vimos en el tema 10: 0 si encontró algo, 1 si no, 2 si hubo un error. Estas opciones son las más útiles en scripts:
| Opción | Efecto | Uso típico |
|---|---|---|
-q | No muestra nada; solo devuelve el código. | if grep -q ERROR app.log; then |
-c | Muestra cuántas líneas coinciden. | errores=$(grep -c ERROR app.log || true) |
-F | El patrón es texto literal, no una expresión. | Buscar el valor de una variable. |
-x | Debe coincidir la línea entera. | Comprobar si una línea exacta existe (tema 27). |
-w | Debe coincidir una palabra entera. | grep -w ana no encuentra «mariana». |
-E | Expresiones regulares extendidas (tema 28). | grep -E '^(INFO|AVISO)' |
-o | Muestra solo la parte que coincide, no la línea. | Extraer datos (sección 4). |
-v | Muestra las líneas que no coinciden. | Quitar comentarios: grep -v '^#'. |
-l | Muestra solo los nombres de los archivos con coincidencias. | grep -rl TODO src/ |
-m N | Se detiene después de N coincidencias. | grep -m1: la primera. |
-r, --include | Busca en directorios, con un filtro de nombres. | grep -r --include='*.sh' TODO . |
Recuerda la trampa del tema 23: con set -e, un grep que no encuentra nada detiene el script si se usa fuera de una condición. Cuando «no encontrar» es un resultado aceptable, se escribe en un if, o con || true.
3. Buscar el valor de una variable
El primer argumento de grep es una expresión regular. Si el script busca el contenido de una variable, cualquier punto, asterisco o corchete en ese contenido se interpreta como parte de la expresión:
$ buscado="3.50"
$ grep "$buscado" precios.txt # el punto es «cualquier carácter»
precio: 3.50
precio: 3x50
$ grep -F -- "$buscado" precios.txt # texto literal
precio: 3.50
$ buscado="-v"; grep -F -- "$buscado" precios.txt
-v opcion
Dos protecciones, y conviene usar las dos siempre que el patrón venga de una variable con datos:
-Fhace que el patrón se tome como texto literal, con todos sus caracteres.--marca el fin de las opciones, de modo que un valor que empiece con guion, como-v, no se tome como opción degrep. También puede usarse-e "$buscado", que indica explícitamente cuál es el patrón.
Si lo que hay en la variable es, a propósito, una expresión regular, se omite -F y se usa -E, como cuando el script recibe un patrón de búsqueda del usuario.
4. Extraer con grep -o
Con -o, grep muestra solo la parte de cada línea que coincide, cada una en una línea propia. Es la forma más simple de extraer datos con un formato reconocible, como números, direcciones o fechas:
grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' acceso.log | sort -u # todas las IP distintas
grep -oE 'ERROR \[[a-z]+\]' app.log | sort | uniq -c # errores por módulo
version=$(grep -oE '[0-9]+\.[0-9]+\.[0-9]+' <<< "$(programa --version)" | head -1)
Para extraer una parte que está junto a otra, por ejemplo el valor que sigue a version= pero sin version=, grep -o no alcanza: devuelve todo lo que coincide. Ese es el terreno de sed, de awk, o de =~ con BASH_REMATCH si se trata de una sola línea.
5. sed en un script
sed aplica un programa de edición a cada línea. En scripts se usan, sobre todo, cuatro operaciones:
| Programa | Efecto |
|---|---|
s/viejo/nuevo/ | Reemplaza la primera coincidencia de cada línea; con g al final, todas. |
/patrón/d | Borra las líneas que coinciden. |
-n '/patrón/p' | Muestra solo las líneas que coinciden (como grep). |
-n 's/…(…)…/\1/p' | Muestra solo una parte capturada de las líneas que coinciden: extraer. |
sed -E '/^[[:space:]]*(#|$)/d' config.conf # quitar comentarios y líneas vacías
sed -E 's/[[:space:]]+$//' archivo # quitar espacios al final de las líneas
sed -nE 's/^version=(.*)$/\1/p' proyecto.conf # el valor de «version», sin la clave
sed -nE 's/.*"[A-Z]+ ([^ ]+) HTTP.*/\1/p' acceso.log # las rutas pedidas a un servidor web
Las dos últimas líneas son la forma de extraer «lo que está junto a» algo: el grupo entre paréntesis captura la parte que interesa, la sustitución reemplaza toda la línea por ese grupo (\1), y -n con p muestra solo las líneas donde hubo sustitución. Como en el tema 28, -E activa las expresiones extendidas, de modo que se escriben igual que en =~.
El separador de s no tiene por qué ser la barra. Cuando el texto tiene barras, como una ruta, cualquier otro carácter evita escaparlas: s|/var/www|/srv/web|.
6. Editar archivos con sed -i
Con -i, sed modifica el archivo directamente, en lugar de escribir el resultado en la salida. Es cómodo para editar configuraciones desde un script, y tiene dos detalles que conviene conocer:
sed -i 's/^Puerto .*/Puerto 2222/' /etc/ssh/sshd_config.d/local.conf
sed -i.bak 's/^Puerto .*/Puerto 2222/' config.conf # guarda una copia: config.conf.bak
- Portabilidad.
sed -ies una extensión, y las versiones difieren: elsedde GNU, en Linux, acepta-isolo; el de macOS y BSD exige un sufijo, aunque sea vacío,-i ''. La forma-i.bak, con el sufijo pegado, funciona en ambos. - No es atómico en todos los sentidos. GNU
sed -iescribe un archivo temporal y lo renombra, de modo que el archivo nunca queda a medias. Pero crea un archivo nuevo: si el original era un enlace simbólico, lo reemplaza por un archivo común, salvo con--follow-symlinks; y los permisos especiales pueden no conservarse.
Y como toda edición, conviene que sea idempotente, como vimos en el tema 27: reemplazar «la línea que empieza con Puerto» se puede repetir sin problemas; agregar una línea al final con sed '$a …', no.
7. Variables dentro de sed
Insertar una variable en el programa de sed es fácil, y frágil: el programa va entre comillas dobles para que la variable se expanda, y su contenido pasa a ser parte del programa. Si contiene el separador o un carácter especial, el resultado es un error o, peor, un reemplazo distinto del esperado:
$ v='a/b&c'
$ echo "ruta=X" | sed "s/X/$v/"
sed: -e expression #1, char 7: unknown option to `s'
$ echo "ruta=X" | sed "s|X|$v|" # otro separador: ya no falla, pero…
ruta=a/bXc
En el segundo caso, el & de la variable se interpretó como «el texto que coincidió», y se reemplazó por X. En el reemplazo de sed son especiales el separador, & y la barra invertida; en el patrón, además, todos los caracteres de las expresiones regulares. Hay dos formas de manejarlo:
# escapar_reemplazo TEXTO — escribe TEXTO listo para el reemplazo de s/…/…/
escapar_reemplazo() {
printf '%s' "$1" | sed 's/[\/&]/\\&/g'
}
v='a/b&c'
echo "ruta=X" | sed "s/X/$(escapar_reemplazo "$v")/" # ruta=a/b&c
La función antepone una barra invertida a cada barra, & o barra invertida del valor, de modo que sed los toma literalmente. Funciona, pero agrega complejidad. La alternativa, a menudo más simple, es no meter datos dentro del programa: usar awk, que recibe los datos como variables separadas del código (sección 9), o, para una sola línea, las expansiones de Bash del tema 6, que no interpretan nada: ${linea//X/$v}.
8. awk en un script
awk divide cada línea en campos, $1, $2 y así, y ejecuta un pequeño programa sobre ellos. Es un lenguaje de programación completo, con variables, condiciones, bucles y arrays, pensado para procesar texto en columnas:
awk -F: '
BEGIN { print "Usuarios con shell:" } # antes de leer la primera línea
$3 >= 1000 && $7 !~ /nologin/ { # condición: para qué líneas
printf " %-12s %s\n", $1, $7 # acción: qué hacer con ellas
n++
}
END { print n " usuarios" } # después de la última línea
' /etc/passwd
| Elemento | Significado |
|---|---|
-F: | El separador de campos; por defecto, los espacios y tabuladores. |
$1, $NF, $0 | El primer campo, el último, la línea completa. |
NF, NR | Cantidad de campos de la línea; número de línea. |
condición { acción } | La acción se ejecuta para las líneas que cumplen la condición; sin condición, para todas. |
~ y !~ | Coincide, o no, con una expresión regular extendida. |
BEGIN, END | Bloques que se ejecutan antes de la primera línea y después de la última. |
El programa va entre comillas simples, siempre, para que Bash no expanda los $1 de awk como si fueran argumentos del script. Por la misma razón, las variables del script nunca se insertan en el texto del programa: se le pasan aparte.
9. Pasar datos a awk
Hay dos formas seguras de pasar valores del script a awk:
| Forma | Ejemplo | Detalle |
|---|---|---|
-v nombre=valor | awk -v limite="$limite" '$3 > limite' | Lo habitual. awk interpreta las barras invertidas del valor: C:\temp llega con un tabulador en lugar de \t. |
ENVIRON["NOMBRE"] | RUTA="$ruta" awk 'BEGIN { print ENVIRON["RUTA"] }' | Lee una variable de entorno sin interpretar nada: el valor llega exacto. |
$ v='C:\temp'
$ awk -v ruta="$v" 'BEGIN { print ruta }'
C: emp
$ RUTA="$v" awk 'BEGIN { print ENVIRON["RUTA"] }'
C:\temp
Para números y textos sin barras invertidas, -v es perfectamente adecuado; para valores arbitrarios, como rutas de Windows o textos escritos por el usuario, ENVIRON es la forma segura. Lo que nunca debe hacerse es armar el programa con la variable dentro, como en awk "\$3 > $limite": un valor inesperado se convertiría en código de awk, el mismo problema que vimos con la aritmética en el tema 7.
10. Contar y agrupar con awk
La capacidad más valiosa de awk en scripts son sus arrays asociativos, que permiten acumular por categoría en una sola pasada, lo que en Bash requería el bucle del tema 8:
find . -type f -printf '%s %f\n' | awk '
{
n = split($2, partes, ".")
ext = (n > 1) ? tolower(partes[n]) : "(sin extensión)"
total[ext] += $1
cuantos[ext]++
}
END {
for (e in total) printf "%10d %6d %s\n", total[e], cuantos[e], e
}
' | sort -rn
Como en Bash, los arrays de awk no tienen orden, y el resultado se ordena después con sort. awk también puede ordenar por sí mismo enviando su salida a un comando con print … | "sort -rn", como hace el script de la sección 13.
11. Llevar los resultados al script
Cuando el script necesita usar lo que calcularon estas herramientas, hay tres formas, según la cantidad de datos:
| Resultado | Forma |
|---|---|
| Un solo valor | total=$(awk '{ s += $3 } END { print s + 0 }' datos.txt) |
| Varios valores | read -r total maximo < <(awk '… END { print s, m }' datos.txt) |
| Una lista | mapfile -t ips < <(grep -oE '…' acceso.log | sort -u) |
El s + 0 de la primera línea asegura que awk escriba 0 y no una línea vacía cuando el archivo no tiene datos, de modo que la variable siempre tenga un número para la aritmética de Bash. Y la segunda forma, con read y una sustitución de procesos, obtiene varios valores con una sola pasada por el archivo, en lugar de una pasada por cada valor.
12. Cuál usar
| Necesitas | Usa |
|---|---|
| Saber si algo aparece, o cuántas veces | grep -q, grep -c |
| Filtrar líneas por un patrón | grep |
| Extraer piezas con forma reconocible | grep -oE |
| Extraer lo que está junto a algo | sed -nE 's/…(…)…/\1/p', o awk |
| Reemplazar texto o borrar líneas | sed |
| Editar un archivo de configuración | sed -i, con una edición idempotente |
| Trabajar con columnas | awk (o cut para lo más simple) |
| Sumar, contar, agrupar, calcular | awk |
| Procesar una sola línea que ya está en una variable | Bash: =~, expansiones, read |
Una cadena larga de herramientas, como grep | sed | awk | sort | uniq, suele poder reducirse a un solo awk, que hace todo en una pasada. No siempre vale la pena: una tubería de pasos simples se lee mejor que un programa de awk complejo. El criterio es la claridad, y la velocidad solo cuando los archivos son grandes.
13. Un script completo
Los servidores web registran cada petición en una línea con un formato estándar, llamado combinado: dirección IP, fecha, método y ruta, código de respuesta, bytes enviados y más. Este script resume un registro así: total de peticiones, errores, volumen enviado, y las direcciones y rutas más frecuentes. Usa grep -F para filtrar por fecha y awk para todo el cálculo, en una sola pasada.
203.0.113.5 - - [14/Oct/2026:10:00:01 -0300] "GET /index.html HTTP/1.1" 200 5120 "-" "Mozilla/5.0"
# $1 = IP · $4 = [fecha · $7 = ruta · $9 = código · $10 = bytes
#!/usr/bin/env bash
#
# resumen-accesos — resume un registro de accesos de un servidor web (formato combinado).
#
# Uso: resumen-accesos [-n NÚMERO] [-f FECHA] [ARCHIVO...]
# -n NÚMERO cuántas entradas mostrar en cada lista (5)
# -f FECHA solo las peticiones de esa fecha, escrita como en el registro: 14/Oct/2026
# Sin archivos, lee la entrada estándar.
#
set -euo pipefail
cantidad=5
fecha=""
while getopts ":n:f:" opcion; do
case "$opcion" in
n) cantidad="$OPTARG" ;;
f) fecha="$OPTARG" ;;
*) echo "Uso: ${0##*/} [-n NÚMERO] [-f FECHA] [ARCHIVO...]" >&2; exit 2 ;;
esac
done
shift $(( OPTIND - 1 ))
[[ "$cantidad" =~ ^[1-9][0-9]*$ ]] || { echo "${0##*/}: -n debe ser un número positivo" >&2; exit 2; }
# Las líneas a analizar: todas, o solo las de una fecha.
# grep -F toma la fecha como texto literal; que no haya ninguna no es un error.
lineas() {
if [[ -n "$fecha" ]]; then
grep -F -- "[$fecha:" "$@" || true
else
cat -- "$@"
fi
}
lineas "$@" | awk -v n="$cantidad" '
{
total++
ip[$1]++
ruta[$7]++
clase[substr($9, 1, 1) "xx"]++
if ($9 >= 500) errores++
if ($10 ~ /^[0-9]+$/) bytes += $10
}
END {
if (total == 0) { print "Sin peticiones."; exit }
printf "Peticiones: %d Errores 5xx: %d Datos enviados: %.1f MiB\n", total, errores, bytes / 1048576
print "\nPor clase de respuesta:"; fflush()
for (c in clase) printf " %s %6d %5.1f %%\n", c, clase[c], 100 * clase[c] / total | "sort"
close("sort")
orden = "sort -rn | head -n " n
print "\nIP con más peticiones:"; fflush()
for (i in ip) printf " %6d %s\n", ip[i], i | orden
close(orden)
print "\nRutas más pedidas:"; fflush()
for (r in ruta) printf " %6d %s\n", ruta[r], r | orden
close(orden)
}
'
$ resumen-accesos -n 3 /var/log/nginx/access.log
Peticiones: 48213 Errores 5xx: 37 Datos enviados: 1843.6 MiB
Por clase de respuesta:
2xx 41022 85.1 %
3xx 4310 8.9 %
4xx 2844 5.9 %
5xx 37 0.1 %
IP con más peticiones:
3120 203.0.113.5
1874 198.51.100.7
1022 192.0.2.44
Rutas más pedidas:
12044 /index.html
6380 /css/estilo.css
2210 /api/estado
$ zcat /var/log/nginx/access.log.2.gz | resumen-accesos -f 12/Oct/2026
- Una sola pasada:
awklee cada línea una vez y actualiza a la vez el total, tres arrays asociativos y dos acumuladores. Congrep,sortyuniqpor separado, habría que leer el archivo una vez por cada lista. - La fecha se filtra con
grep -F, porque contiene barras y es un texto literal, no una expresión. El corchete inicial y los dos puntos finales,[14/Oct/2026:, evitan coincidencias en otras partes de la línea. Y el|| truehace que un día sin peticiones no detenga el script, por la regla degrepyset -e. - La cantidad se pasa con
-v, sin insertarla en el programa. Al ser un número ya validado, no hay riesgo con las barras invertidas. - Los bytes se suman solo si son un número: algunas respuestas, como los 304, registran un
-. - Las listas se ordenan enviando la salida de
awkasort, conclose()después de cada una para que cada lista se complete antes de imprimir el título de la siguiente;fflush()asegura que el título salga antes que la lista. - Sin archivos, lee la entrada estándar, lo que permite analizar registros comprimidos con
zcat, como en el último ejemplo.
14. Errores frecuentes
| Escribiste | Qué pasa | Corrección |
|---|---|---|
grep "$valor" archivo | Los caracteres especiales del valor se interpretan; un valor con guion inicial, como opción. | grep -F -- "$valor" archivo |
n=$(grep -c x f) con set -e | Sin coincidencias, el script se detiene. | || true. |
sed "s/X/$v/" | Una barra o un & en el valor rompen o alteran el reemplazo. | Escapar el valor, o usar awk o una expansión de Bash. |
sed -i '' en Linux, o sed -i en macOS | Cada uno falla en el otro sistema. | sed -i.bak. |
awk "{ print \$$col }" | Datos convertidos en código; comillas y barras difíciles de seguir. | awk -v c="$col" '{ print $c }' |
awk -v r="$ruta" con barras invertidas | Se interpretan como secuencias de escape. | ENVIRON["RUTA"]. |
awk '{ s += $1 } END { print s }' sobre un archivo vacío | Escribe una línea vacía, no un 0. | print s + 0. |
grep | sed | awk sobre un archivo enorme | Tres procesos y varias pasadas. | Un solo awk, si sigue siendo claro. |
15. Resumen
grepbusca,sedtransforma yawkprocesa campos y calcula; los tres procesan un archivo en una sola pasada.- En un script,
grepse usa sobre todo como pregunta, con-qy su código de salida;-ccuenta y-oextrae. - Para buscar el valor de una variable:
grep -F -- "$valor", literal y protegido de los guiones. sed -nE 's/…(…)…/\1/p'extrae lo que está junto a algo;/patrón/dborra líneas.sed -iedita en su lugar;-i.bakes la forma portable, y la edición debe ser idempotente.- Una variable dentro del programa de
seddebe escaparse; a menudo es más simple usarawko una expansión de Bash. - El programa de
awkva entre comillas simples; los datos se pasan con-v, o conENVIRONsi pueden tener barras invertidas. - Los arrays asociativos de
awkcuentan y agrupan en una pasada; el orden se obtiene consort. - Los resultados vuelven al script con
$( ),read … < <( )omapfile;s + 0garantiza un número.
16. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Por qué grep "$v" puede encontrar líneas que no contienen el valor de v?
Porque el valor se interpreta como expresión regular: un punto coincide con cualquier carácter, un asterisco repite lo anterior. Con -F se toma como texto literal.
2. ¿Para qué sirve -- en grep -F -- "$v" archivo?
Para que un valor que empiece con guion, como -v, se tome como patrón y no como opción de grep.
3. ¿Qué diferencia hay entre grep -c y grep -o … | wc -l?
grep -c cuenta las líneas que contienen al menos una coincidencia. grep -o … | wc -l cuenta las coincidencias, aunque haya varias en una misma línea.
4. ¿Qué hace sed -nE 's/^version=(.*)$/\1/p'?
Muestra solo el valor que sigue a version=, en las líneas que empiezan así. -n evita mostrar las demás líneas, el grupo captura el valor, la sustitución reemplaza la línea por él y p la muestra.
5. ¿Por qué sed -i.bak es más portable que sed -i?
Porque el sed de GNU acepta -i solo y el de macOS exige un sufijo. Con el sufijo pegado, -i.bak, la misma línea funciona en los dos.
6. ¿Qué pasa con sed "s/X/$v/" si v contiene &?
En el reemplazo, & significa «el texto que coincidió», y se sustituye por él. El resultado no contiene el & literal. Hay que escaparlo como \&.
7. ¿Por qué el programa de awk va siempre entre comillas simples?
Para que Bash no expanda $1, $NF y los demás como si fueran variables del script. Esos símbolos son de awk.
8. ¿Qué diferencia hay entre pasar un valor con -v y con ENVIRON?
Con -v, awk interpreta las secuencias de escape del valor, como \t. Con ENVIRON, lee la variable de entorno exactamente como está.
9. ¿Cómo se cuenta con awk cuántas veces aparece cada valor de la primera columna?
Con un array asociativo: awk '{ n[$1]++ } END { for (v in n) print n[v], v }' archivo, y luego sort -rn para ordenar.
10. ¿Cómo se obtienen dos valores calculados por awk en dos variables de Bash, con una sola pasada?
Con read -r a b < <(awk '… END { print x, y }' archivo).
Para practicar
- Crea un archivo con valores que contengan puntos, asteriscos y guiones iniciales, y busca cada uno con
grepcon y sin-F --. - Escribe con
sedun comando que quite los comentarios y las líneas vacías de un archivo de configuración, incluidas las que solo tienen espacios. - Escribe la función
escapar_reemplazoy úsala para reemplazar un texto por una ruta con barras y un&. - Reproduce la diferencia entre
awk -vyENVIRONcon una ruta de Windows. - Con
awk, calcula en una sola pasada el total y el máximo de la tercera columna de un archivo, y llévalos a dos variables de Bash. - Si tienes acceso a un servidor web, prueba
resumen-accesoscon su registro. Si no, crea un registro de prueba con una decena de líneas. Agrega una lista con los agentes de usuario más frecuentes, el último campo entre comillas.