← Automatización con Scripts en Bash Bloque 7 · Texto y expresiones regulares · Tema 29 de 42

Tema 29

grep, sed y awk dentro de un script

El curso de Terminal presentó grep, sed y awk como herramientas para usar en la consola. Dentro de un script cambian dos cosas: los patrones y los valores ya no se escriben a mano, sino que vienen de variables con contenido arbitrario, y los resultados no se leen en la pantalla, sino que el script debe usarlos para decidir o calcular. Este tema muestra cómo hacer las dos cosas sin que un punto, una barra o un guion en los datos rompan el script.

1. Tres herramientas, tres trabajos

Las tres recorren un texto línea por línea, pero cada una está pensada para un trabajo distinto:

HerramientaTrabajoPregunta que responde
grepBuscar líneas.¿Qué líneas contienen esto? ¿Hay alguna? ¿Cuántas?
sedTransformar líneas.¿Cómo queda el texto si reemplazo esto por aquello, o borro estas líneas?
awkProcesar 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ónEfectoUso típico
-qNo muestra nada; solo devuelve el código.if grep -q ERROR app.log; then
-cMuestra cuántas líneas coinciden.errores=$(grep -c ERROR app.log || true)
-FEl patrón es texto literal, no una expresión.Buscar el valor de una variable.
-xDebe coincidir la línea entera.Comprobar si una línea exacta existe (tema 27).
-wDebe coincidir una palabra entera.grep -w ana no encuentra «mariana».
-EExpresiones regulares extendidas (tema 28).grep -E '^(INFO|AVISO)'
-oMuestra solo la parte que coincide, no la línea.Extraer datos (sección 4).
-vMuestra las líneas que no coinciden.Quitar comentarios: grep -v '^#'.
-lMuestra solo los nombres de los archivos con coincidencias.grep -rl TODO src/
-m NSe detiene después de N coincidencias.grep -m1: la primera.
-r, --includeBusca 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:

terminal · un archivo con «precio: 3.50», «precio: 3x50» y «-v opcion»
$ 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:

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:

extraer partes
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:

ProgramaEfecto
s/viejo/nuevo/Reemplaza la primera coincidencia de cada línea; con g al final, todas.
/patrón/dBorra 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.
usos típicos
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:

editar un archivo en su lugar
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

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:

terminal
$ 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 un valor para usarlo en el reemplazo
# 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:

la forma de un programa de awk
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
ElementoSignificado
-F:El separador de campos; por defecto, los espacios y tabuladores.
$1, $NF, $0El primer campo, el último, la línea completa.
NF, NRCantidad 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, ENDBloques 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:

FormaEjemploDetalle
-v nombre=valorawk -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.
terminal
$ 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:

sumar el tamaño de los archivos por extensión
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:

ResultadoForma
Un solo valortotal=$(awk '{ s += $3 } END { print s + 0 }' datos.txt)
Varios valoresread -r total maximo < <(awk '… END { print s, m }' datos.txt)
Una listamapfile -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

NecesitasUsa
Saber si algo aparece, o cuántas vecesgrep -q, grep -c
Filtrar líneas por un patróngrep
Extraer piezas con forma reconociblegrep -oE
Extraer lo que está junto a algosed -nE 's/…(…)…/\1/p', o awk
Reemplazar texto o borrar líneassed
Editar un archivo de configuraciónsed -i, con una edición idempotente
Trabajar con columnasawk (o cut para lo más simple)
Sumar, contar, agrupar, calcularawk
Procesar una sola línea que ya está en una variableBash: =~, 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.

una línea del registro
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
resumen-accesos
#!/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)
  }
'
terminal
$ 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

14. Errores frecuentes

EscribisteQué pasaCorrección
grep "$valor" archivoLos 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 -eSin 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 macOSCada 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 invertidasSe interpretan como secuencias de escape.ENVIRON["RUTA"].
awk '{ s += $1 } END { print s }' sobre un archivo vacíoEscribe una línea vacía, no un 0.print s + 0.
grep | sed | awk sobre un archivo enormeTres procesos y varias pasadas.Un solo awk, si sigue siendo claro.

15. Resumen

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

  1. Crea un archivo con valores que contengan puntos, asteriscos y guiones iniciales, y busca cada uno con grep con y sin -F --.
  2. Escribe con sed un comando que quite los comentarios y las líneas vacías de un archivo de configuración, incluidas las que solo tienen espacios.
  3. Escribe la función escapar_reemplazo y úsala para reemplazar un texto por una ruta con barras y un &.
  4. Reproduce la diferencia entre awk -v y ENVIRON con una ruta de Windows.
  5. 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.
  6. Si tienes acceso a un servidor web, prueba resumen-accesos con 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.