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

Script Linux de las Oposiciones 2015 al cuerpo FP 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.txt

Con 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

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
Posible error en el comando expr si no separamos operandos y operador por un espacio en blanco

Posible error en el comando expr si no separamos operandos y operador por un espacio en blanco

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.

6 comments

  • antonio

    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

  • antonio

    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

  • Enrique

    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

  • Germán

    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

  • Pep

    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 😉

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.