PRÁCTICAS DE SISTEMAS OPERATIVOS I

QUINTA SESIÓN


  1. Orden if

    Concluimos estas sesiones dedicadas a la programación de la shell con las estructuras de control que, como buen lenguaje de programación, nos ofrece la programación de la shell.

    La estructura de control más simple y, a la vez, más denostada, es el equivalente al goto de C. La shell no dispone de una orden equivalente directa.

    La siguiente estructura de control en simplicidad es aquella que permite establecer saltos condicionales. Su nombre es igual que en C, no así su sintaxis:
    if orden
    then
      [[bloque de operaciones si orden se ejecuta con éxito]]
    else
      [[bloque de operaciones si orden fracasa]]
    fi
    
    Para saber si una orden se ha ejecutado correctamente, en realidad lo que hace if es mirar su código de retorno. Si es cero, supone que se ha ejecutado bien y si es distinto de cero, lo contrario. Este es el convenio habitual. Veamos un ejemplo:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ if ls -l ModestiaAparte-COmoTeMueves.mp3
    > then
    >   echo El fichero existe
    > else
    >   echo Desafortunadamene, el fichero no existe
    > fi
    ModestiaAparte-COmoTeMueves.mp3: No such file or directory
    Desafortunadamene, el fichero no existe
    
    El primer mensaje que aparece, lo imprime el propio ls. De hecho, está en inglés. La orden ls devuelve 2 a la shell. La orden if supone que se ha ejecutado con error y va al else, donde se imprime el mensaje.

    Se pueden encadenar varias órdenes seguidas dentro del mismo bloque mediante elif, por lo que la estructura más completa es:
    if orden
    then
      [[bloque de operaciones si orden se ejecuta con éxito]]
    elif orden2
    then
      [[bloque de operaciones si orden2 se ejecuta con éxito]]
    elif orden3
    then
      [[bloque de operaciones si orden3 se ejecuta con éxito]]
    [[...]]
    else
      [[bloque de operaciones si todas las órdenes fracasan]]
    fi
    
    Tanto los elifs como el else son opcionales.

  2. Expresiones condicionales

    Aunque interesante, el bloque if anterior parece insuficiente. Lo suyo es que se nos permita hacer comparaciones con variables, como en cualquier otro lenguaje de programación. De hecho, esto es posible, pero a través de órdenes nuevas.

    Las más sencillas de estas órdenes son true y false. Ninguna de las dos hace nada, pero devuelven un valor equivalente a verdadero (0) y falso (distinto de 0):
    <gyermo@ENCINA>/usuarios/profes/gyermo$ true
    <gyermo@ENCINA>/usuarios/profes/gyermo$ echo $?
    0
    <gyermo@ENCINA>/usuarios/profes/gyermo$ false 
    <gyermo@ENCINA>/usuarios/profes/gyermo$ echo $?
    1
    
    La más útil es, sin duda, la orden test. Acepta como parámetros una expresión y devuelve verdadero (0) o falso (distinto de 0), dependiendo de la evaluación de dicha expresión.

    Las expresiones pueden versar sobre lo más variopinto. El primer conjunto trata acerca de los ficheros. Algunas de las opciones (para ver el resto, consúltese la página de manual de test) son:
    Algunos ejemplos:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ ls -ld PRUEBA
    d---r-xrwx   2 gyermo   profes       512 ago 18 02:24 PRUEBA
    <gyermo@ENCINA>/usuarios/profes/gyermo$ test -r PRUEBA
    <gyermo@ENCINA>/usuarios/profes/gyermo$ echo $?
    1
    <gyermo@ENCINA>/usuarios/profes/gyermo$ ls -l CARTAS/*
    -rw-------   1 gyermo   profes       186 ago 21 13:31 CARTAS/ReyesMagos.txt
    -rw-------   1 gyermo   profes       186 ago 19 18:57 CARTAS/SantaClaus.txt
    <gyermo@ENCINA>/usuarios/profes/gyermo$ test CARTAS/ReyesMagos.txt -nt CARTAS/SantaClaus.txt 
    <gyermo@ENCINA>/usuarios/profes/gyermo$ echo $?
    0
    
    En el primer ejemplo, comprobamos que no tenemos acceso de lectura la subdirectorio PRUEBA, mientras que en el segundo, test nos dice que la carta a los Reyes Magos es es posterior a la carta a Santa Claus.

    El segundo grupo trata acerca de las cadenas de caracteres y, por ende, de las variables:
    Un ejemplo, mezclado ya con un bloque if:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ if test "$HOSTNAME" = encina
    > then
    >   echo Estoy en encina
    > fi
    Estoy en encina
    
    Es buena idea poner el nombre de la variable entre comillas, pues en el caso de que la variable no exista, de no haber comillas, la shell no le pasa nada en esa posición a test y da error. Por ejemplo:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ test $PRESI = "Juan Cuesta"
    bash: test: =: unary operator expected
    
    El tercer grupo lo forman las comparaciones con números. Las opciones no nos deparan ninguna sorpresa:
    Cuidado con usar cuando se debe las comparaciones numéricas:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ test 007 = 7
    <gyermo@ENCINA>/usuarios/profes/gyermo$ echo $?
    1
    <gyermo@ENCINA>/usuarios/profes/gyermo$ test 007 -eq 7
    <gyermo@ENCINA>/usuarios/profes/gyermo$ echo $?
    0
    
    En el primer caso, como se han comparado cadenas de caracteres, el resultado es falso. Mientras que en el segundo, 007 representa el mismo número que 7 y, por consiguiente, el resultado es verdadero.

    ¿Cuál será el resultado de ejecutar:
    test $((007)) -eq 7
    
    ? ¿Y el de ejecutar:
    test $((009)) -eq 7
    
    ?

    El último grupo de expresiones trata acerca de las propias expresiones relacionadas mediante operadores lógicos:
    Probemos lo aprendido con una condición que refleja la propiedad de que todo número par mayor que dos no es primo:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ NUMERO=8
    <gyermo@ENCINA>/usuarios/profes/gyermo$ if test $(($NUMERO % 2)) -eq 0 -a $NUMERO -gt 2
    > then 
    >   echo $NUMERO no es primo
    > else
    >   echo $NUMERO habría que mirar si es primo
    > fi
    8 no es primo
    
    Existe precedencia entre los operadores lógicos, pero lo más práctico es usar paréntesis en el caso de que se combinen varias expresiones. Generalicemos el caso anterior para reflejar la propiedad de que todo número par o múltiplo de tres, mayor que tres, no es primo:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ NUMERO=9
    <gyermo@ENCINA>/usuarios/profes/gyermo$ if test \( $(($NUMERO % 2)) -eq 0 -o \
    >                                                  $(($NUMERO % 3)) -eq 0 \) \
    >                                               -a $NUMERO -gt 3
    > then
    >   echo $NUMERO no es primo
    > else
    >   echo $NUMERO habría que mirar si es primo
    > fi
    9 no es primo
    
    Como podéis observar en el ejemplo, hay que anteponer el carácter \ a los paréntesis para privarlos del significado especial que les otorga la shell. También debéis reparar en que se le ha dado formato a la expresión para que resulte más legible. Para ello se ha necesitado continuar una orden en la línea siguiente un par de veces. Esto se logra, de nuevo, tecleando el carácter \ como último carácter de la línea, justo antes de pulsar Intro. Se han escrito en rojo estos caracteres de continuación para que los localicéis mejor.
  3. Ejercicio

    Programar un guion de la shell al que se le debe proporcionar una única ruta. Si no se le proporciona o si se le proporciona más de una, lo debe indicar y acabar. Cuando se le proporciona justo una ruta, debe indicar mediante un mensaje, la opción correcta de las siguientes:
    1. la ruta no existe
    2. la ruta es un fichero normal
    3. la ruta es un enlace simbólico
    4. la ruta es un directorio
    5. la ruta existe, pero es de tipo desconocido
  4. Órdenes while y until

    Ambas órdenes son equivalentes, con una pequeña diferencia, y su comportamiento es parecido a la orden while de C. Al igual que esta orden, las órdenes de la shell ejecutan una y otra vez las instrucciones de un bloque, dependiendo del resultado de una condición. También como ocurre en la orden de C, puede ser que el cuerpo del bucle no se ejecute ni siquiera una vez.

    La diferencia entre las dos es que la primera continúa ejecutando las órdenes mientras la condición sea verdadera. La orden until, sin embargo, continúa efectuando el bucle mientras la condición sea falsa. Veamos cómo haríamos una cuenta atrás de 10 a 0, primero con una de ellas y, luego, con la otra:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ CUENTA=10
    <gyermo@ENCINA>/usuarios/profes/gyermo$ echo $CUENTA ; while test $CUENTA -gt 0
    > do
    >   sleep 1
    >   CUENTA=$(($CUENTA-1))
    >   echo $CUENTA
    > done
    10
    9
    8
    7
    6
    5
    4
    3
    2
    1
    0
    <gyermo@ENCINA>/usuarios/profes/gyermo$ CUENTA=10
    <gyermo@ENCINA>/usuarios/profes/gyermo$ echo $CUENTA ; until test $CUENTA -eq 0
    > do
    >   sleep 1
    >   CUENTA=$(($CUENTA-1))
    >   echo $CUENTA
    > done
    10
    9
    8
    7
    6
    5
    4
    3
    2
    1
    0
    
  5. Ejercicio

    Programar un guion de la shell que pida un número al usuario y, a continuación, imprima la tabla de multiplicar de dicho número. Las salida debe ser como se muestra en el siguiente ejemplo:
    ¿Qué tabla de multiplicar desea?
    8
    
      TABLA DE MULTIPLICAR DEL 8
             8x0 = 0
             8x1 = 8
             8x2 = 16
             8x3 = 24
             8x4 = 32
             8x5 = 40
             8x6 = 48
             8x7 = 56
             8x8 = 64
             8x9 = 72
             8x10 = 80
    
  6. Ejercicio

    Hacer un guion de la shell que admina un número arbitrario de parámetros, pero al menos dos. El programa debe sumar los parámetros y ofrecer la salida justo con el formato de este ejemplo:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ suma 1 2 3 4 5
    1+2+3+4+5=15
    
  7. Orden case

    Similar a la orden switch del C, pero con capacidad para usar expresiones regulares, como se ve en este rudimentario ejemplo de guion que trata de determinar el tipo de un fichero:
    #!/bin/bash
    case $1 in
      *.gif | *.jpg | *.png )
        echo El fichero \"$1\" es un fichero gráfico
        ;;
      *.txt )
        echo El fichero \"$1\" es un fichero de texto
        ;;
      * )
        echo No sé de qué tipo es el fichero \"$1\"
        ;;
    esac
    
    Y esta es una muestra de su ejecución:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ tipo.sh Quijote.txt
    El fichero "Quijote.txt" es un fichero de texto
    <gyermo@ENCINA>/usuarios/profes/gyermo$ tipo.sh ElGrito.gif
    El fichero "ElGrito.gif" es un fichero gráfico
    <gyermo@ENCINA>/usuarios/profes/gyermo$ tipo.sh MammaMIa.mp3
    No sé de qué tipo es el fichero "MammaMIa.mp3"
    
  8. Orden for

    La orden for de la shell no se parece mucho a su homónima de C. Esta última no es más que un bucle while escrito de otra forma. En cambio a la orden de la shell le hemos de proporcionar nosotros una lista y ella iterará sobre los elementos de esa lista, como en el siguiente ejemplo:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ for FRUTAS in limones plátanos melones  
    > do
    >   echo Me gustan mucho los $FRUTAS
    > done
    Me gustan mucho los limones
    Me gustan mucho los plátanos
    Me gustan mucho los melones
    
    A veces es la shell la que proporciona la lista expandiendo una expresión regular, a veces la lista es el resultado de usar una orden entre acentos graves (`) o, como en el ejemplo, la proporcionamos nosotros directamente.
  9. Ejercicio

    Modificar este ejercicio situado justo antes de la sección dedicada a la orden read para que el usuario confirme si desea cambiar los permisos de cada fichero que se le ha pasado. Podéis hacerlo con shift o con la orden for recién vista.
  10. Funciones

    La shell también dispone de funciones. Su invocación es similar a la de los guiones de la shell, pero no se almacenan en un fichero y no sobreviven, por tanto, a una nueva conexión. La función recibe sus argumentos en las mismas variables de entorno que los guiones y su valor devuelto es el de la última orden que se ejecuta, aunque también existe la orden return para acabar. Veamos un primer ejemplo de función:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ function nargumentos
    >  {
    >   return $#
    >   }
    <gyermo@ENCINA>/usuarios/profes/gyermo$ nargumentos uno dos y tres
    <gyermo@ENCINA>/usuarios/profes/gyermo$ echo $?
    4
    
    Y ahora un segundo ejemplo de una función sencilla que calcula el factorial de un número:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ function factorial
    >  {
    >   RESULTADO=1
    >   N="$1"
    >   while test $N -gt 1
    >   do
    >     RESULTADO=$(($RESULTADO*$N))
    >     N=$(($N-1))
    >   done
    >   echo $RESULTADO
    >   true
    >   }
    <gyermo@ENCINA>/usuarios/profes/gyermo$ factorial 5
    120
    
    Las funciones definidas de la shell, al igual que las variables, aparecen al dar la orden set:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ set
    [[...]]
    TZ=Europe/Madrid
    UID=108
    USER=gyermo
    _=4
    factorial () 
    { 
        RESULTADO=1;
        N="$1";
        while test $N -gt 1; do
            RESULTADO=$(($RESULTADO*$N));
            N=$(($N-1));
        done;
        echo $RESULTADO;
        true
    }
    nargumentos () 
    { 
        return $#
    }
    
    Las funciones operan de una forma muy similar a los ejecutables normales. Admiten, por ejemplo, redirección:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ function genera20
    >  {
    >   i=0
    >   while test $i -lt 20
    >   do
    >     i=$(($i+1))
    >     echo $i
    >   done
    >   }
    <gyermo@ENCINA>/usuarios/profes/gyermo$ genera20 | grep 1
    1
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    Y no solo eso, las propias estructuras de control de la shell, también admiten redirigirse, como se ve en este ejemplo que escribe un asterisco por cada línea que se recibe:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ cat > prueba.txt
    Uno
    Dos 
    Tres
    Cuatro
    <gyermo@ENCINA>/usuarios/profes/gyermo$ while read
    > do
    >   echo "*"
    > done < prueba.txt
    *
    *
    *
    *
    
    Si la redirección se hace en la definición de una función, esta se realiza cuando la función se ejecuta:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ function zutana
    >  {
    >   echo -n Comienza la ejecuciOn: 
    >   date
    >   sleep 7
    >   echo -n Fin de la ejecuciOn:
    >   date
    >   } > log.txt
    <gyermo@ENCINA>/usuarios/profes/gyermo$ zutana
     [[Siete segundos depués...]]
    <gyermo@ENCINA>/usuarios/profes/gyermo$ cat log.txt
    Comienza la ejecuciOn:lunes 27 de octubre de 2014 14H05'14" CET
    Fin de la ejecuciOn:lunes 27 de octubre de 2014 14H05'22" CET
    
  11. Ejercicio

    Programar una función de la shell que imprima los n primeros términos de la sucesión de Fibonacci, siendo n un número pasado como único argumento. Recordemos que dicha sucesión se construye con las siguientes reglas:
    1. El primer término es 0
    2. El segundo término es 1
    3. Cada término adicional se calcula como la suma de los dos términos anteriores

    0, 1, 1, 2, 3, 5, 8, 13, 21, 33, 54, 87, ...
  12. Redirecciones avanzadas.

    Con las tuberías y redirecciones hemos logrado que los procesos puedan fácilmente tratar los datos procedentes de una fuente (otro proceso o fichero) y dirigir la salida de datos hacia un destino (fichero o proceso). Pero, ¿qué ocurre cuando las fuentes o los destinos son varias? En ese caso, la técnica estándar se denomina sustitución de procesos.

    Veámoslo con un ejemplo: se quiere hacer una función que genere los números del 1 al 10 y le pase los pares a otro proceso que los imprima tal cual y le pase los impares a otro proceso que los imprima al revés (de más alto a más bajo).

    Primero definimos la función:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ function unoAlDiez
    > {
    >  echo "Descriptor de la primera salida: $1"
    >  echo "Descriptor de la segunda salida: $2"
    > 
    >  for I in 1 2 3 4 5 6 7 8 9 10
    >  do
    >    if test $(($I%2)) -eq 0
    >    then
    >      echo $I >>$1  # Los pares por la primera salida
    >    else
    >      echo $I >>$2  # Los impares por la segunda salida
    >    fi
    >  done
    >  }
    
    Es una función que, con el permiso de sus familiares más queridos, es de lo más vulgar. De hecho, si la invocamos con el nombre de dos ficheros, en el primero almacena los pares y en el segundo los impares:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ unoAlDiez pares.txt impares.txt
    Descriptor de la primera salida: pares.txt
    Descriptor de la segunda salida: impares.txt
    <gyermo@ENCINA>/usuarios/profes/gyermo$ cat pares.txt
    2
    4
    6
    8
    10
    <gyermo@ENCINA>/usuarios/profes/gyermo$ sort -r impares.txt
    9
    7
    5
    3
    1
    
    Solo nos queda, para conseguir nuestro objetivo, poder prescindir de los ficheros intermedios. La notación es la siguiente:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ unoAlDiez >(cat) >(sort -r)
    Descriptor de la primera salida: /dev/fd/63
    Descriptor de la segunda salida: /dev/fd/62
    9
    7
    5
    3
    1
    2
    4
    6
    8
    10
    
    Lo que ocurre es que la shell arranca la orden cat y crea una tubería que la une con unoAlDiez, justo como si de una tubería normal se tratase. La salida de la tubería conecta con la entrada del cat, de modo normal. Pero la entrada de la tubería se sustituye y se pasa como parámetro a unoAlDiez, para que pueda escribir lo que quiera fácilmente. Lo mismo se hace con el segundo proceso (sort).

    Todo esto permite una gran flexibilidad e incluso capacidades para hacer demostraciones en fiestas familiares o ferias itinerantes y lograr un dinerillo extra, que nunca viene mal. Por ejemplo, veamos las diferencias que existen entre los números impares menores que 15 y los números de la sucesión de Fibonacci menores que 15. Lo haremos generando todo al vuelo, sin mancharnos las manos con ningún fichero temporal. Primero, los impares menores que 15:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ I=1;while test $I -lt 15; do echo $I;I=$(($I+2));done
    1
    3
    5
    7
    9
    11
    13
    
    Ahora, la sucesión de Fibonacci:
    <gyermo@ENCINA>/usuarios/profes/gyermo$ I=0;J=1;while test $(($I+$J)) -lt 15; do J=$(($I+$J));I=$(($J-$I));echo $J;done
    1
    2
    3
    5
    8
    13
    
    Finalmente, ¡Tachán!, la solución (se ha partido la línea de la órden en tres partes con ayuda de la barra invertida, por claridad):
    <gyermo@ENCINA>/usuarios/profes/gyermo$ diff \
    >    <(I=1;while test $I -lt 15; do echo $I;I=$(($I+2));done) \
    >    <(I=0;J=1;while test $(($I+$J)) -lt 15; do J=$(($I+$J));I=$(($J-$I));echo $J;done)
    1a2
    > 2
    4,6c5
    < 7
    < 9
    < 11
    ---
    > 8
    
    Traduciendo el resultado a la lengua del pueblo llano, se nos viene a decir que para conseguir la lista de los números de la sucesión de Fibonacci menores que 15, tomemos los números impares menores que 15, añadamos el 2 y sustituyamos el 7,9 y 11 por el 8.

  13. Órdenes de la shell relacionadas.

    if then elif else fi
    bloque condicional
    true
    se ejecuta con éxito
    false
    fracasa
    test
    permite efectuar comparaciones
    while do done
    bloque de bucle while
    until do done
    bloque de bucle until
    case in esac
    bloque case
    for in do done
    bloque for
    function { }
    permite definir funciones
    return
    permite volver de una función y devolver un valor


  14. Funciones de biblioteca relacionadas.



  15. LPEs.


© 2011 Guillermo González Talaván.