Script Linux de las Oposiciones 2015 al cuerpo FP SAI
Para retomar el capítulo de Scripting Linux, usando el Shell Bash, vamos a resolver uno de los ejercicios prácticos de las pasadas oposiciones a Profesor Técnico de Formación Profesional (PTFP), especialidad Aplicaciones y Sistemas Informáticos (SAI)
Script Linux de las Oposiciones 2015 al cuerpo FP SAI
El enunciado rezaba algo así:
Dado un fichero de datos con el siguiente contenido de ejemplo (digamos datos.txt):
Pepe 02:30:44 Marcos 23:56:33 Pepe 10:33:01 Marta 05:47:44 Pepe 12:22:33 José 11:55:00
Haced un Script Linux que devuelva un listado ordenado por tiempos (de uso de máquina por ejemplo) de forma ascendente. Teniendo en cuenta que si algún usuario se repite, solo produzca una línea de salida y sume los tiempos.
Al lío: Lo vamos a plantear mediante un bucle while … do … done al que le “entubamos” el fichero de datos ya ordenado de forma que los usuarios repetidos aparezcan de forma consecutiva al procesarlos.
Esto lo podemos conseguir así:
cat datos.txt|sort > datos2.txt
Con esto el fichero de datos inicial datos.txt lo transformamos en otro datos2.txt con este contenido:
José 11:55:00 Marcos 23:56:33 Marta 05:47:44 Pepe 02:30:44 Pepe 10:33:01 Pepe 12:22:33
Ahora como veis Pepe que aparecía desperdigado 3 veces, lo encontraremos de forma consecutiva.
Para leer el archivo de datos del que partimos datos2.txt ya ordenado por el primer campo (nombre) y al no decirle nada de forma ascendente (A-Z) usaremos un bucle de esta guisa:
while read persona tiempo do ... done < datos2.txt
Con este magnífico bucle, leemos, dado que el separador entre usuario y tiempos es el espacio en blanco, mediante el comando read persona tiempo, 2 variables una llamada persona, que al utilizarla referenciaremos como $persona (acordaos que para usar el contenido añadimos el símbolo del dolar), y otra tiempo, que referenciaremos como $tiempo.
Y digo magnífico bucle, porque fijaos como “chupa” el fichero de datos, al final del bucle, en el cierre del mismo, al done le “entubamos” el fichero datos2.txt, el ordenado, mediante el redirector de entrada estándar (el menor que <).
Si el fichero a leer, o los trozos/variables a extraer no utilizasen el separador “espacio en blanco” se usa esta sintaxis alternativa:
while IFS=";" read persona tiempo do ... done < datos2.txtCon el añadido IFS=”;” le decimos que los datos de cada línea del fichero están separados por un punto y coma.
Como dado el caso tendremos que sumar tiempos, en caso de usuario repetido, podemos trocear las horas, minutos y segundos mediante el comando cut, imaginemos que nos viene la línea:
José 11:55:00
Como estamos leyendo 2 variables en el while, persona y tiempo, podemos trocear la segunda así:
#aislo trozos de tiempo horas=`echo $tiempo|cut -f1 -d:` minutos=`echo $tiempo|cut -f2 -d:` segundos=`echo $tiempo|cut -f3 -d:`
Con el comando cut -f1 -d: estoy pidiendo del flujo de datos el primer trozo o campo (field) que especifico con la opción -f1 (f de field, 1 de primera posición) y también informo que el separador de campos es el carácter dos puntos, mediante -d: (delimiter :).
En el caso de que el carácter de separación fuese el espacio en blanco, se especificaría así -d” “. Es decir, habría que entrecomillar un espacio en blanco.
El siguiente hito de información es controlar si durante la evolución del bucle, la persona a procesar es la misma que la anterior. Por lo que, tanto para los trozos de tiempo, como para el nombre de usuario utilizaremos unas variables secundarias que inicializaremos antes de nada:
personaanterior=" " horasanterior=0 minutosanterior=0 segundosanterior=0
Además pensemos un momento, según los datos de ejemplo, me vendrá primero José, tenemos que averiguar si se trata de la misma persona que en la iteración anterior del bucle. Para preguntar esto último utilizaremos la estructura condicional if, más o menos así:
if [ "$persona" = "$personaanterior" ]; then ... else ... fi
Lo de encerrar las variables, que se supone tienen cadenas de texto dentro, entre comillas dobles, es uno de mis “por si …“, de forma que no de error de sintaxis en caso de que alguna de las dos esté vacía. Fijaos también en las separaciones que hay entre los corchetes, los operandos y el operador de comparación, de lo contrario ERROR.

Posibles errores al utilizar if..then..else..fi si no separamos los corchetes, operandos y operadores
Siguiendo con lo nuestro, en el caso de que la persona sea la misma que la anterior, lo que debemos hacer es sumar los tiempos. Cosa que haremos en el primer bloque, el bloque VERDADERO, en los primeros “…” de la estructura condicional usada. Pero si no se cumple la condición, OJO puede que se trate de la primera iteración, y que “José” sea distinto de ” “, que es como hemos inicializado la variable personaanterior, justo antes de entrar al bucle. En ese caso, no escribimos aun en el fichero resultado.txt, sino que tenemos que contemplar este caso especial de “1ª ITERACIÓN“, para ello podríamos dentro del bucle, ir incrementando un contador, que inicialmente fuera del mismo hayamos inicializado a cero.
... contador=0 while read persona tiempo do contador=`expr $contador + 1` ...
Mediante el comando expr podemos hacer operaciones aritméticas sencillas, en este trozo anterior al contenido de la variable contador ($contador) le sumamos 1. Utilizamos el operador grave `…` para que se ejecute la orden y el resultado se lo asigne a la variable de la izquierda del igual, el mismo contador PERO sin el símbolo dolar. Fijaos además de la separación que hay entre los operandos y la operación suma, de lo contrario dará ERROR. Por ejemplo esto fallaría
expr $contador +1
Volviendo a nuestro if … then … else … fi:
if [ "$persona" = "$personaanterior" ]; then echo "La misma $persona sumo y espero" #sumamos tiempos ... else echo "distinta $persona" #primero escribo la anterior a no ser que sea 1era iteración if [ $contador -ne 1 ]; then echo "$personaanterior $horasanterior:$minutosanterior:$segundosanterior" >> resultado.txt fi #reasigno personaanterior=$persona horasanterior=$horas minutosanterior=$minutos segundosanterior=$segundos fi
Como sumamos los tiempos? Teniendo en cuenta que si sumamos los segundos actuales con los anteriores y llegan o pasan de 60 habría que añadir a los minutos, y mismo cuento para los minutos-horas, con algo así:
if [ "$persona" = "$personaanterior" ]; then echo "La misma $persona sumo y espero" #sumamos tiempos horasanterior=`expr $horasanterior + $horas` minutosanterior=`expr $minutosanterior + $minutos` if [ $minutosanterior -ge 60 ]; then horasanterior=`expr $horasanterior + 1` minutosanterior=`expr $minutosanterior - 60` fi segundosanterior=`expr $segundosanterior + $segundos` if [ $segundosanterior -ge 60 ]; then minutosanterior=`expr $minutosanterior + 1` segundosanterior=`expr $segundosanterior - 60` fi else echo "distinta $persona" #primero escribo la anterior a no ser que sea 1era iteración if [ $contador -ne 1 ]; then echo "$personaanterior $horasanterior:$minutosanterior:$segundosanterior" >> resultado.txt fi #reasigno personaanterior=$persona horasanterior=$horas minutosanterior=$minutos segundosanterior=$segundos fi
Y ya casi lo tenemos, falta ensamblar las partes y tener en cuenta que la última persona según nuestro algoritmo no se imprimiría.
Una solución rápida sería imprimirla fuera del bucle, ya que si era la misma que la anterior sumábamos tiempos y esperábamos (caso de Pepe que aparece 3 veces), y si era distinta imprimíamos la anterior y reasignábamos a la espera de la siguiente iteración, así pues se cumplen todos los hitos, y la última persona, sea o no sea repetida en el fichero original, la imprimiremos justo después de terminar el bucle.
Y un detalle más, el enunciado decía “Ordenados de forma ascendente por tiempo“, eso lo solucionamos rápidamente mediante la opción -kN del comando sort, así:
sort -k2 resultado.txt
El comando anterior ordena por el segundo campo o clave (key), el primero seria el nombre, y el segundo el tiempo.
¿Que pasa en caso de empate de horas? Solucionalo tu y envianos la solución.
Aquí va el código completo:
#!/bin/bash rm resultado.txt cat datos.txt|sort > datos2.txt personaanterior=" " horasanterior=0 minutosanterior=0 segundosanterior=0 contador=0 while read persona tiempo do contador=`expr $contador + 1` #aislo trozos de tiempo horas=`echo $tiempo|cut -f1 -d:` minutos=`echo $tiempo|cut -f2 -d:` segundos=`echo $tiempo|cut -f3 -d:` echo "Actual: $persona - $horas - $minutos - $segundos" if [ "$persona" = "$personaanterior" ]; then echo "La misma $persona sumo y espero" #sumamos tiempos horasanterior=`expr $horasanterior + $horas` minutosanterior=`expr $minutosanterior + $minutos` if [ $minutosanterior -ge 60 ]; then horasanterior=`expr $horasanterior + 1` minutosanterior=`expr $minutosanterior - 60` fi segundosanterior=`expr $segundosanterior + $segundos` if [ $segundosanterior -ge 60 ]; then minutosanterior=`expr $minutosanterior + 1` segundosanterior=`expr $segundosanterior - 60` fi else echo "distinta $persona" #primero escribo la anterior a no ser que sea 1era iteración if [ $contador -ne 1 ]; then echo "$personaanterior $horasanterior:$minutosanterior:$segundosanterior" >> resultado.txt fi #reasigno personaanterior=$persona horasanterior=$horas minutosanterior=$minutos segundosanterior=$segundos fi done < datos2.txt #escribo la ultima echo "$personaanterior $horasanterior:$minutosanterior:$segundosanterior" >> resultado.txt sort -k2 resultado.txt
Espero que os haya gustado, hasta la próxima.
Compañero. Gracias por compartir. Yo lo he hecho sin mirar el tuyo y más o menos he utilizado el mismo sistema. Sólo que a la hora de convertir el tiempo, he preferido utilizar divisiones y restos.
Además, he utilizado un bucle for. En fin, casi igual.
Gracias
Te dejo mi solución. Espero que os sirva
#!/bin/bash
clear
anterior=””
let valor=0;
IFSOLD=$IFS
IFS=$’\n’;
let entra=0;
for i in $(sort datos.txt);do
nombre=$(echo $i | cut -d’ ‘ -f1);
let horas=$(echo $i | cut -d’ ‘ -f2 | cut -d’:’ -f1);
let minutos=$(echo $i | cut -d’ ‘ -f2 | cut -d’:’ -f2);
let segundos=$(echo $i | cut -d’ ‘ -f2 | cut -d’:’ -f3);
let tiempo=$segundos+$minutos\*60+$horas\*3600;
if [ “$anterior” = “$nombre” ];then
let valor=$valor+$tiempo;
let entra=$entra+1;
else
echo $valor $anterior >> temporal.txt;
let valor=$tiempo;
anterior=$nombre;
let entra=0;
fi
done
if [ $entra -ne 0 ];then
echo $valor $anterior >> temporal.txt;
fi
for i in $(sort temporal.txt);do
tiempo=$(echo $i | cut -d’ ‘ -f1);
if [ “$tiempo” != “0” ];then
nombre=$(echo $i | cut -d’ ‘ -f2);
let horas=$tiempo/3600;
let resto=$tiempo%3600;
let minutos=$resto/60;
let segundos=$resto%60;
echo $nombre $horas:$minutos:$segundos;
fi
done
IFS=$IFSOLD
rm temporal.txt
fichero=”datos.txt”
#Obtenemos el nombre de todos los usuarios
personas=$(cat $fichero | cut -d” ” -f1 | sort | uniq )
#Bucle para sumar tiempos en segundos
for p in $personas
do
#Podría ser en una línea pero así se ve más claro.
hor=$(cat $fichero | grep $p | cut -d” ” -f2 | awk -F: ‘BEGIN{total=0}{total=total+$1} END{print total}’)
min=$(cat $fichero | grep $p | cut -d” ” -f2 | awk -F: ‘BEGIN{total=0}{total=total+$2} END{print total}’)
seg=$(cat $fichero | grep $p | cut -d” ” -f2 | awk -F: ‘BEGIN{total=0}{total=total+$3} END{print total}’)
echo $p $(($hor*3600+$min*60+$seg)) >> temp.txt
done
#Ordenamos por tiempo y volvemos a pasar a horas, minutos y segundos
cat “temp.txt” | sort -rnk 2 | awk ‘{print $1″ “int($2/3600)”:”int($2%3600/60)”:”int($2%3600%60)}’
rm temp.txt
Hola José Blanco. En primer lugar, gracias por el script, me ha sido de gran ayuda para entender el ejercicio.
En segundo lugar, he de decir que he encontrado un fallo y un “warning” en tu script:
El fallo es que a la hora de sumar los tiempos se deben sumar primero los segundos, luego los minutos y por último las horas y no al revés.
El “warning” sería algo meramente estético y es que cuando sumamos los minutos y segundos y el resultado es un valor de una única cifra los tiempos nos quedan con el formato 21:5:3, cuando estéticamente lo suyo es que aparecieran como 21:05:03.
Una solución rápida sería cambiar en el echo el separador “:” por “h “,”m ” y “s”, de forma que el tiempo final quede como 21h 5m 3s.
Por último, me gustaría saber si tienes algún enunciado más de la parte práctica de las opos de SAI y si puedes compartirlos.
Un saludo.
Pingback: Bonus Pack 2018: Recopilación de artículos de sospedia.net – Jose Blanco Vega
Muy buen aporte Jose Blanco. He estado repasando algunos ejercicios de exámenes pasados, así que os dejo otra forma de trabajar con las horas y con el tiempo en general, para tener otra visión del problema.
El comando para trabajar con horas es el date
$date -u -d “1 jan 1970 15:10:25” +%s
# convierte la hora:min:sec a segundos desde el inicio de los tiempos informáticos 1-1-1970
-u -> UTC Coordinated Universal Time – convierte a una fecha estandarizada
-d -> Es la fecha u hora que deseamos convertir a segundos
+%s -> Se convierte a segundos
$date -d “0 15609 sec” +%H:%M:%S # convierte estos segundos al formato hora:min:sec
El script quedaría utilizando un array asociativo (declare -A vector):
declare -A usuaris
while read line; do
us=`echo $line | cut -d” ” -f1`
te=`echo $line | cut -d” ” -f2`
if [ -z ${usuaris[$us]} ]; then
usuaris[$us]=$te
else
t1=`date -u -d”1 jan 1970 ${usuaris[$us]}” +%s`
t2=`date -u -d”1 jan 1970 $te” +%s `
ttotal=$(($t1+$t2))
t3=`date -u -d”0 $ttotal sec” +%H%:M:%S`
usuaris[$us]=$t3
fi
done < datos.txt
#solo faltaría mostrar por pantalla o mandar a un archivo el array ordenado por las horas (columna 2)
for usu in ${!usuaris[@]}; do
echo $usu ${usuaris[$usu]}
done | sort -r -k 2
Espero que sirva 😉