Saltar al contenido
ES EN
Tema: automático

Ejercicio resuelto: contar, filtrar y resumir un fichero en Bash

Script Bash con getopts y awk que valida un registro de laboratorios, filtra por sala y umbral y resume minutos por usuario. Solución explicada.

Actualizado el

Código de Examen Bash Fundamentos de programación Nivel examen Código probado

Código de Examen: Ejercicio resuelto: contar, filtrar y resumir un fichero en Bash
Fundamentos de Programación · Examen de prácticas (shell)Ejercicio 2 · 2,5 puntos · 30 min

El servicio de laboratorios de la escuela guarda un registro de uso de los puestos. Cada línea útil tiene la forma usuario:sala:minutos. El usuario está formado solo por letras minúsculas y dígitos ([a-z0-9]+). La sala está formada por letras y dígitos ([A-Za-z0-9]+). Los minutos son un entero no negativo ([0-9]+), que puede llevar ceros a la izquierda. No se admiten espacios dentro de una línea de datos.

Escribe un script main.sh en Bash que lea el registro por la entrada estándar y acepte dos opciones, en cualquier orden: -s SALA, que restringe el resumen a los registros de esa sala (comparación exacta, distinguiendo mayúsculas), y -m MINUTOS, un entero no negativo que es el umbral mínimo de minutos totales para que un usuario aparezca en el listado. Por defecto no hay filtro de sala y el umbral es 0. Solo puedes usar Bash, coreutils, grep, sed y awk.

Las líneas vacías (o formadas solo por espacios y tabuladores) y las que empiezan por # se ignoran sin contarlas. Cualquier otra línea que no tenga exactamente tres campos con el formato descrito es inválida. Las inválidas se cuentan siempre, con independencia de la sala que se haya pedido con -s.

Salida: primero, una línea por usuario cuyo total de minutos (dentro del filtro de sala) sea mayor o igual que el umbral. La línea tiene el formato usuario total sesiones, separado por un espacio, y los usuarios aparecen ordenados por nombre en orden de bytes (el de LC_ALL=C). Después vienen exactamente cuatro líneas de resumen, en este orden: registros: R (registros válidos que pasan el filtro de sala), invalidas: I, total: T (suma de minutos de esos R registros, incluidos los de usuarios que el umbral oculta) y usuarios: U (usuarios listados).

Si la entrada está vacía, solo se imprimen las cuatro líneas de resumen, con ceros. Si la invocación es incorrecta (opción desconocida, opción sin valor, argumentos sobrantes o -m no numérico), el script escribe uso: main.sh [-s SALA] [-m MINUTOS] en la salida de error y termina con código 2, sin escribir nada en la salida estándar. Más abajo se muestra un ejemplo de invocación con el umbral 50.

Entrada de ejemplo

# registro semana 12
ana:L1:45
bruno:L2:30
ana:L2:50
carla:L1:20

bruno:L1:40
ana-lopez:L1:15
dani:L3:abc
carla:L1:25

Argumentos

-m 50

Salida esperada

ana 95 2
bruno 70 2
registros: 6
invalidas: 2
total: 210
usuarios: 2
Lectura de opciones con getopts, valores por defecto y rechazo de opciones desconocidas, valores ausentes, argumentos sobrantes o un mínimo no numérico
0,75
Clasificación de cada línea (ignorada, inválida o válida) en el orden correcto y recuento de inválidas independiente del filtro de sala
0,75
Acumulación por usuario de minutos y sesiones, aplicación del filtro de sala a los datos y del umbral solo a las líneas de usuario
0,5
Salida exacta: usuarios en orden de bytes, seguidos del resumen de cuatro líneas, también con entrada vacía
0,5

Pistas

Pista 1 · ¿Cómo separo las opciones de los datos?

Las opciones llegan como argumentos y los datos por la entrada estándar: son dos problemas distintos. Resuelve primero las opciones con el builtin getopts y una cadena de opciones que empiece por :. Así los errores llegan a tu case y no se imprime el mensaje por defecto. Al terminar, desplaza con shift $((OPTIND - 1)) y comprueba que no queda nada. Valida -m con una expresión regular antes de usarlo en una comparación numérica.

Pista 2 · ¿Bucle de Bash o una sola pasada de awk?

Contar y sumar por clave es exactamente lo que hacen los arrays asociativos de awk. Pasa la sala y el umbral con -v y usa -F:. El orden de las reglas importa: primero descarta lo ignorable, después detecta lo inválido (y cuéntalo) y solo al final aplica el filtro de sala. Si filtras antes de validar, el número de inválidas dependerá de -s, y el enunciado lo prohíbe.

Pista 3 · ¿Cómo ordeno los usuarios sin desordenar el resumen?

El bucle for (u in ...) de awk no garantiza ningún orden. Puedes delegar en sort, pero las cuatro líneas de resumen deben quedar al final y en su orden. Un truco clásico es decorar, ordenar y desdecorar: antepón a cada línea una clave y un tabulador que fuercen la posición deseada, ordena la salida entera y elimina la clave con cut.

Solución

Solución explicada

La solución se divide en dos partes bien separadas. Bash se ocupa de la interfaz: lee las opciones, valida y decide si la invocación es correcta. Una única pasada de awk se ocupa de los datos: clasifica cada línea, acumula por usuario y emite el resultado. Un sort seguido de un cut pone las líneas en su sitio.

Fijar LC_ALL=C al principio no es un adorno. Hace que los rangos [a-z] de las expresiones regulares y el orden de sort se comporten igual en cualquier máquina, que es lo que exige el enunciado al hablar de orden de bytes.

#!/usr/bin/env bash
# Resumen de uso de laboratorios: main.sh [-s SALA] [-m MINUTOS] < registro
set -u
export LC_ALL=C

uso() {
    echo "uso: main.sh [-s SALA] [-m MINUTOS]" >&2
    exit 2
}

sala=""
minimo=0
while getopts ":s:m:" opt; do
    case "$opt" in
        s) sala="$OPTARG" ;;
        m) minimo="$OPTARG" ;;
        *) uso ;;
    esac
done
shift $((OPTIND - 1))

[ "$#" -eq 0 ] || uso
[[ "$minimo" =~ ^[0-9]+$ ]] || uso

awk -F: -v sala="$sala" -v minimo="$minimo" '
BEGIN { umbral = minimo + 0 }
/^[ \t]*$/ || /^#/ { next }
NF != 3 || $1 !~ /^[a-z0-9]+$/ || $2 !~ /^[A-Za-z0-9]+$/ || $3 !~ /^[0-9]+$/ {
    invalidas++
    next
}
sala != "" && $2 != sala { next }
{
    registros++
    total += $3
    minutos[$1] += $3
    sesiones[$1]++
}
END {
    for (u in minutos) {
        if (minutos[u] >= umbral) {
            printf "1\t%s %d %d\n", u, minutos[u], sesiones[u]
            mostrados++
        }
    }
    printf "2a\tregistros: %d\n", registros
    printf "2b\tinvalidas: %d\n", invalidas
    printf "2c\ttotal: %d\n", total
    printf "2d\tusuarios: %d\n", mostrados
}' | sort | cut -f2-

Opciones. La cadena ":s:m:" declara que -s y -m llevan valor. Los dos puntos iniciales activan el modo silencioso: ante una opción desconocida, getopts deja ? en opt, y ante una opción sin valor deja :. Ambos casos caen en la rama *) y llaman a la función uso, que escribe en la salida de error y termina con código 2. Como getopts no exige un orden, la invocación con -m 45 -s L1 del cuarto test funciona igual que en el orden inverso.

Validación previa. Tras shift $((OPTIND - 1)), en $@ solo quedan los argumentos que no son opciones. Este script no admite ninguno, porque los datos llegan por la entrada estándar, así que [ "$#" -eq 0 ] rechaza los sobrantes. La prueba [[ "$minimo" =~ ^[0-9]+$ ]] evita que un valor como -m diez llegue a awk. Allí se convertiría silenciosamente en 0 y el script daría un resultado erróneo sin avisar.

Clasificación de líneas. En awk, cada línea recorre las reglas de arriba abajo y next corta el recorrido. La primera regla descarta las líneas en blanco y los comentarios. La segunda comprueba que hay exactamente tres campos y que cada uno encaja con su patrón. Si algo falla, incrementa invalidas. En el ejemplo, ana-lopez falla por el guion y dani:L3:abc falla por los minutos, de ahí invalidas: 2. Solo entonces se aplica el filtro de sala. Por eso los tests con -s L1 siguen mostrando 2 inválidas.

Acumulación. Cada registro que sobrevive suma a los contadores globales registros y total y a los arrays asociativos minutos y sesiones, indexados por usuario. La expresión $3 se trata como número al sumarla, de modo que 007 cuenta como 7, como se ve en el último test. El umbral se convierte a número en BEGIN (umbral = minimo + 0) para que la comparación sea numérica y no de cadenas.

Salida ordenada. En END se recorren los usuarios y se imprimen los que cumplen minutos[u] >= umbral, contándolos en mostrados. Cada línea va decorada con la clave 1 y un tabulador. Las del resumen llevan las claves 2a a 2d. El sort ordena la salida completa: las líneas que empiezan por 1 quedan delante de las que empiezan por 2, y dentro de ellas manda el nombre. En C, el espacio precede a cualquier dígito, así que ana sale antes que ana2. Finalmente, cut -f2- elimina la clave.

Casos límite. Con la entrada vacía, awk no procesa ninguna línea, pero sí ejecuta END. Las variables no inicializadas valen 0 con %d, así que salen las cuatro líneas a cero, como pide el test correspondiente. Fíjate también en que total no depende del umbral: en el ejemplo, carla no aparece porque solo acumula 45 minutos, pero sus 45 minutos sí están en total: 210.

Casos de prueba

CasoEntradaSalida esperadaSalida realResultado
Ejemplo del enunciado con umbral 50-m 50 # registro semana 12 ana:L1:45 bruno:L2:30 ana:L2:50 carla:L1:20 bruno:L1:40 ana-lopez:L1:15 dani:L3:abc carla:L1:25ana 95 2 bruno 70 2 registros: 6 invalidas: 2 total: 210 usuarios: 2ana 95 2 bruno 70 2 registros: 6 invalidas: 2 total: 210 usuarios: 2OK
Sin opciones# registro semana 12 ana:L1:45 bruno:L2:30 ana:L2:50 carla:L1:20 bruno:L1:40 ana-lopez:L1:15 dani:L3:abc carla:L1:25ana 95 2 bruno 70 2 carla 45 2 registros: 6 invalidas: 2 total: 210 usuarios: 3ana 95 2 bruno 70 2 carla 45 2 registros: 6 invalidas: 2 total: 210 usuarios: 3OK
Filtro de sala L1-s L1 # registro semana 12 ana:L1:45 bruno:L2:30 ana:L2:50 carla:L1:20 bruno:L1:40 ana-lopez:L1:15 dani:L3:abc carla:L1:25ana 45 1 bruno 40 1 carla 45 2 registros: 4 invalidas: 2 total: 130 usuarios: 3ana 45 1 bruno 40 1 carla 45 2 registros: 4 invalidas: 2 total: 130 usuarios: 3OK
Opciones combinadas en otro orden-m 45 -s L1 # registro semana 12 ana:L1:45 bruno:L2:30 ana:L2:50 carla:L1:20 bruno:L1:40 ana-lopez:L1:15 dani:L3:abc carla:L1:25ana 45 1 carla 45 2 registros: 4 invalidas: 2 total: 130 usuarios: 2ana 45 1 carla 45 2 registros: 4 invalidas: 2 total: 130 usuarios: 2OK
Entrada vacía(vacía)registros: 0 invalidas: 0 total: 0 usuarios: 0registros: 0 invalidas: 0 total: 0 usuarios: 0OK
Ceros a la izquierda, prefijos y línea incompletazoe:B7:007 ana2:B7:5 ana:B7:0 zoe:B7ana 0 1 ana2 5 1 zoe 7 1 registros: 3 invalidas: 1 total: 12 usuarios: 3ana 0 1 ana2 5 1 zoe 7 1 registros: 3 invalidas: 1 total: 12 usuarios: 3OK

Salidas reales: código compilado con Bash 5.2 y ejecutado en un contenedor aislado el 2 de noviembre de 2026.

Complejidad

Tiempo. Sea N el número de líneas y U el de usuarios distintos. La pasada de awk es O(N) en promedio, porque cada línea hace un número constante de comparaciones con expresiones regulares y accesos a tabla hash. El sort trabaja sobre U + 4 líneas, es decir, O(U log U). El coste total es O(N + U log U), dominado en la práctica por la lectura cuando hay muchos registros por usuario.

Memoria. No se guarda el fichero: awk solo conserva dos entradas por usuario en minutos y sesiones, lo que da O(U). Un registro de millones de líneas con unas pocas decenas de usuarios cabe holgadamente en el límite de memoria. Una solución que hiciera datos=$(cat) y recorriera la variable varias veces gastaría O(N) sin necesidad.

Errores típicos

  • Aplicar el filtro de sala antes de validar la línea. El recuento de inválidas pasa a depender de -s y el resumen deja de cumplir el enunciado.
  • Olvidar shift $((OPTIND - 1)) o no comprobar los argumentos sobrantes. Una llamada como main.sh -s L1 L2 se acepta en silencio y el usuario cree que ha filtrado por dos salas.
  • Leer con cat | while read y acumular en variables de Bash. El bucle corre en una subshell, los contadores se pierden al salir de la tubería y el resumen sale a cero.
  • Suponer que for (u in minutos) recorre las claves en orden alfabético. Funciona por casualidad con pocos datos y falla con otros o en otra implementación de awk.
  • Ordenar con sort toda la salida sin decorarla. Las líneas registros: o total: se mezclan con los nombres de usuario y el resumen queda desordenado.
  • Aplicar el umbral también a registros y total, o comparar con -m sin validarlo, de modo que un valor no numérico se convierte en 0 sin ningún aviso.

Variantes

Resumen por sala en lugar de por usuario

Añade una opción -p que cambie la clave de agrupación. Basta con calcular en awk clave = (porsala ? $2 : $1) y usar clave como índice de los arrays. El resto del script, incluida la decoración para sort, no cambia. Ten cuidado de que -s y -p juntos tengan sentido: con ambos saldría una única línea.

Listado ordenado por minutos, de mayor a menor

Mantén la clave 1 para los usuarios, pero ordena por campos: sort -t $'\t' -k1,1 -k2,2 ya no basta. Lo más limpio es decorar con los minutos rellenados con ceros, por ejemplo con printf "1\t%010d\t%s %d %d\n", y ordenar con sort -k1,1 -k2,2r. Para desempatar por nombre, añade -k3,3. Después hay que desdecorar con cut -f3- en las líneas de usuario y con cut -f2- en las de resumen, o bien decorar el resumen con un campo vacío extra.

Leer de fichero o de la entrada estándar

Si el profesor pide que el fichero sea opcional, admite como máximo un argumento tras las opciones. Comprueba que es legible con [ -r "$1" ] y pásalo a awk como operando. Sin argumento, awk lee de la entrada estándar sin cambiar nada más. Hay que relajar la comprobación [ "$#" -eq 0 ] a [ "$#" -le 1 ].

En los temarios

El manejo del intérprete de comandos forma parte del temario de CC3301 Programación de Software de Sistemas de la Universidad de Chile, cuyo programa incluye la shell junto a procesos, tuberías y la API de archivos de Linux. Es el contexto natural de este ejercicio, en el que una tubería awk | sort | cut hace el trabajo y la subshell explica la pregunta del profesor. Por su parte, el programa de Fundamentos de Programación del Instituto Politécnico Nacional incluye sentencias de control, funciones y archivos, que aquí se ven desde el lado del script.

La parte de agrupar por clave conecta con los datos estructurados del temario de Programación (1236) de la UNAM y con la unidad de arrays de Algoritmos y Programación I (95.11) de la Universidad de Buenos Aires. El array asociativo de awk es la versión de alto nivel de esas estructuras.

Historial de cambios

  1. Reescrito con la plantilla de Código de Examen: enunciado de examen, pistas, solución explicada, casos de prueba y código compilado y ejecutado; versión en inglés enlazada.
  2. Publicación original.

Artículo generado mediante AI.larebelion · Revisado por Kernel

Kernel

· Dirección de tecnología · España

«Un script que acepta basura en sus argumentos sin quejarse es una avería que alguien encontrará en producción, no tú en la terminal.»