lunes, 27 de febrero de 2017

Expresiones con enteros

Las siguientes expresiones se usan con enteros:

Tabla 27-3: Expresiones de test con enteros
Expresión Es verdadero si:
entero1 -eq entero2 entero1 es igual a entero2.
entero1 -ne entero2 entero1 no es igual a entero2.
entero1 -le entero2 entero1 es menor o igual a entero2.
entero1 -lt entero2 entero1 es menor que entero2.
entero1 -ge entero2 entero1 es mayor o igual a entero2.
entero1 -gt entero2 entero1 es mayor que entero2.

Aquí hay un script que lo demuestra:

#!/bin/bash

# test-integer: evaluate the value of an integer.

INT=-5

if [ -z "$INT" ]; then
     echo "INT is empty." >&2
     exit 1
fi

if [ $INT -eq 0 ]; then
     echo "INT is zero."
else
     if [ $INT -lt 0 ]; then
           echo "INT is negative."
     else
          echo "INT is positive."
     fi
     if [ $((INT % 2)) -eq 0 ]; then
          echo "INT is even."
     else
          echo "INT is odd."
     fi
fi

La parte interesante del script es cómo determina si un entero es par o impar. Realizando una operación de módulo 2 con el número, que divide el número entre dos y devuelve el resto, puede decirnos si es par o impar.

viernes, 24 de febrero de 2017

Expresiones para cadenas

Las siguientes expresiones se usan para evaluar cadenas:

Tabla 27-2: Expresiones para test de cadenas
Expresión Es verdadero si:
cadena cadena no es nula.
-n cadena La longitud de cadena es mayor que cero.
-z cadena La longitud de cadena es cero.
cadena1 = cadena2
cadena1 == cadena2
cadena1 y cadena2 son iguales. Pueden usarse signos igual simples o dobles, pero es preferible usar dobles signos igual.
cadena1 != cadena2 cadena1 y cadena2 no son iguales.
cadena1 > cadena2 cadena1 se ordena después de cadena2.
cadena1 < cadena2 cadena1 se ordena antes de cadena2.

Advertencia: Los operadores de expresión > y < deben ser entrecomillados (o escapados con una barra invertida) cuando se usan con test. Si no se hace esto, serán interpretados por el shell como operadores de redirección, con resultados potencialmente destructivos. Fíjate también que mientras que la documentación bash establece que el ordenado se hace según la secuencia de ordenado de la configuración regional actual, no lo hace así. Se usa el orden ASCII (POSIX) en versiones de bash hasta la 4.0 incluida.

Aquí tenemos un script que incorpora expresiones para cadenas:

#!/bin/bash

# test-string: evaluate the value of a string

ANSWER=maybe

if [ -z "$ANSWER" ]; then
     echo "There is no answer." >&2
     exit 1
fi

if [ "$ANSWER" = "yes" ]; then
     echo "The answer is YES."
elif [ "$ANSWER" = "no" ]; then
     echo "The answer is NO."
elif [ "$ANSWER" = "maybe" ]; then
     echo "The answer is MAYBE."
else
     echo "The answer is UNKNOWN."
fi

En este script, evaluamos la constante ANSWER. Primero determinamos si la cadena está vacía. Si es así, terminamos el script y establecemos el estado de salida a uno. Fíjate la redirección que se aplica al comando echo. Redirige el mensaje de error "No hay respuesta." al error estándar, que es lo "apropiado" que hay que hacer con los mensajes de error. Si la cadena no está vacía, evaluamos el valor de la cadena para ver si es igual a "sí", "no" o "quizás". Hacemos esto usando elif, que es la abreviatura para "else if" (y si no). Usando elif, podemos construir un test lógico más complejo.

miércoles, 22 de febrero de 2017

Expresiones para archivo

Las siguientes expresiones se usan para evaluar el estado de los archivos.

Tabla 27-1: Expresiones test para archivos
Expresión Es verdadero si:
archivo1 -ef archivo2 archivo1 y archivo2 tienen los mismos números de inodo (los dos nombres de archivo se refieren al mismo archivo mediante enlace duro).
archivo1 -nt archivo2 archivo1 es más nuevo que archivo2.
archivo1 -ot archivo2 archivo1 es más antiguo que archivo2.
-b archivo archivo existe y es un archivo (de dispositivo) de bloques.
-c archivo archivo existe y es un archivo (de dispositivo) de caracteres.
-d archivo archivo existe y es un directorio.
-e archivo archivo existe.
-f archivo archivo existe y es un archivo normal.
-g archivo archivo existe y tiene activado el bit de identificación de grupo (setgid).
-G archivo archivo existe y su propietario es el ID de grupo efectivo.
-k archivo archivo existe y tiene activado su "sticky bit".
-L archivo archivo existe y es un enlace simbólico.
-O archivo archivo existe y su propietario es el ID de usuario efectivo.
-p archivo archivo existe y es una tubería (pipe) con nombre.
-r archivo archivo existe y es legible (tiene permisos de lectura para el usuario efectivo).
-s archivo archivo existe y tiene una longitud mayor que cero.
-S archivo archivo existe y es archivo de socket.
-t fd fd es un descritor de archivo dirigido a/desde el terminal. Puede usarse para determinar si se está redirigiendo la entrada/salida/error estándar.
-u archivo archivo existe y tiene activado el bit de identificación de usuario (setuid).
-w archivo archivo existe y es editable (tiene permisos de escritura para el usuario efectivo).
-x archivo archivo existe y es ejecutable (tiene permisos de ejecución/búsqueda para el usuario efectivo).

Aquí tenemos un script que demuestra algunas de las expresiones para archivo:

#!/bin/bash

# test-file: Evaluate the status of a file

FILE=~/.bashrc

if [ -e "$FILE" ]; then
     if [ -f "$FILE" ]; then
          echo "$FILE is a regular file."
     fi
     if [ -d "$FILE" ]; then
          echo "$FILE is a directory."
     fi
     if [ -r "$FILE" ]; then
          echo "$FILE is readable."
     fi
     if [ -w "$FILE" ]; then
          echo "$FILE is writable."
     fi
     if [ -x "$FILE" ]; then
          echo "$FILE is executable/searchable."
     fi
else
     echo "$FILE does not exist"
     exit 1
fi

exit

El script evalúa el archivo asignado a la constante FILE y muestra sus resultados a medida que se realiza la evaluación. Hay dos cosas interesantes a tener en cuenta sobre este script. Primero, fíjate cómo el parámetro $FILE está entrecomillado dentro de las expresiones. Esto no es obligatorio, pero es una defensa contra un parámetro vacío. Si la expansión de parámetros de $FILE fuera a dar como resultado un valor vacío, podría causar un error (los operadores serían interpretados como cadenas no nulas en lugar de como operadores). Usando las comillas alrededor de los parámetros nos aseguramos que el operador siempre está seguido por una cadena, incluso si la cadena está vacía. Segundo, fíjate en la presencia del comando exit cerca del final del script. El comando exit acepta un único, y opcional, argumento que se convierte en el estado de salida del script. Cuando no se le pasa ningún argumento, el estado de salida por defecto es la salida del último comando ejecutado. Usar exit de esta forma permite que el script indique el fallo si $FILE se expande en el nombre de un archivo inexistente. El comando exit que aparece en la última línea del script es una formalidad. Cuando un script "alcanza el final" (llega al final del archivo), termina con un estado de salida del último comando ejecutado por defecto.

De forma similar, las funciones de shell pueden devolver un estado de salida incluyendo un argumento entero al comando return. Si fuéramos a convertir el script anterior en una función de shell para incluirlo en un programa mayor, podríamos reemplazar los comandos exit con sentencias return y obtener el comportamiento deseado:

test_file () {

     # test-file: Evaluate the status of a file

     FILE=~/.bashrc

     if [ -e "$FILE" ]; then
          if [ -f "$FILE" ]; then
                echo "$FILE is a regular file."
          fi
          if [ -d "$FILE" ]; then
               echo "$FILE is a directory."
          fi
          if [ -r "$FILE" ]; then
               echo "$FILE is readable."
          fi
          if [ -w "$FILE" ]; then
               echo "$FILE is writable."
          fi
          if [ -x "$FILE" ]; then
               echo "$FILE is executable/searchable."
          fi
     else
          echo "$FILE does not exist"
          return 1
     fi

}

lunes, 20 de febrero de 2017

test

De lejos, el comando usado más frecuentemente con if es test. El comando test realiza una variedad de comprobaciones y comparaciones. Tiene dos formas equivalentes:

test expresión

y la más popular:

[ expresión ]

donde expresión es una expresión que se evalúa como verdadera o falsa. El comando test devuelve un estado de salida de cero cuando la expresión es verdadera y de uno cuando la expresión es falsa.

viernes, 17 de febrero de 2017

Estado de salida

Los comandos (incluyendo los scripts y las funciones de shell que escribamos) envían un valor al sistema cuando terminan, llamado estado de salida. Este valor, que es un entero dentro del rango de 0 a 255, indica el éxito o fracaso de la ejecución del comando. Por convención, un valor de cero indica éxito y cualquier otro valor indica fracaso. El shell proporciona un parámetro que puede usarse para examinar el estado de salida. Aquí lo vemos en acción:

[me@linuxbox ~]$ ls -d /usr/bin
/usr/bin
[me@linuxbox ~]$ echo $?
0
[me@linuxbox ~]$ ls -d /bin/usr
ls: cannot access /bin/usr: No such file or directory
[me@linuxbox ~]$ echo $?
2

En este ejemplo, ejecutamos el comando ls dos veces. La primera vez, el comando se ejecuta con éxito. Si mostramos el valor del parámetro $?, vemos que es un cero. Ejecutamos el comando ls una segunda vez, produciendo un error, y examinamos el parámetro $? otra vez. Esta vez contiene un 2, indicando que el comando ha encontrado un error. Algunos comandos usan estados de salida diferentes para proporcionar diagnósticos de error, mientras que muchos comandos simplemente salen con el valor uno cuando fallan. Las man pages incluyen a menudo una sección llamada "Estado de salida" describiendo qué códigos se usan. Sin embargo, un cero siempre indica éxito.

El shell proporciona dos comandos extremadamente simples que no hacen nada excepto terminar con un cero o un uno en el estado de salida. El comando true siempre se ejecuta con éxito y el comando false siempre se ejecuta sin éxito:

[me@linuxbox ~]$ true
[me@linuxbox ~]$ echo $?
0
[me@linuxbox ~]$ false
[me@linuxbox ~]$ echo $?
1

Podemos usar estos comandos para ver cómo funcionan las sentencias if. Lo que la sentencia if hace en realidad es evaluar el éxito o el fracaso de los comandos:

[me@linuxbox ~]$ if true; then echo "It's true."; fi
It's true.
[me@linuxbox ~]$ if false; then echo "It's true."; fi
[me@linuxbox ~]$

El comando echo "It's true." se ejecuta cuando el comando que sigue a if se ejecuta con éxito, y no se ejecuta cuando el comando que sigue a if no se ejecuta con éxito. Si es una lista de comandos lo que sigue a if, el que se evalua es el último comando de la lista:

[me@linuxbox ~]$ if false; true; then echo "It's true."; fi
It's true.
[me@linuxbox ~]$ if true; false; then echo "It's true."; fi
[me@linuxbox ~]$

miércoles, 15 de febrero de 2017

if

Usando el shell, podemos codificar la lógica anterior tal como sigue:

x=5

if [ $x -eq 5 ]; then
     echo "x equals 5."
else
     echo "x does not equal 5."
fi

o podemos introducirlo directamente en la línea de comandos (lígeramente simplificada):

[me@linuxbox ~]$ x=5
[me@linuxbox ~]$ if [ $x -eq 5 ]; then echo "equals 5"; else echo "does not equal 5"; fi
equals 5
[me@linuxbox ~]$ x=0
[me@linuxbox ~]$ if [ $x -eq 5 ]; then echo "equals 5"; else echo "does not equal 5"; fi
does not equal 5

En este ejemplo, ejecutamos el comando dos veces. La primera, con el valor de x establecido en 5, que da como resultado la cadena "equals 5" como salida, y la segunda vez con el valor de x establecido en 0, que da como resultado la cadena "does not equal 5" como salida.

La sentencia if tiene la siguiente sintaxis:

if comandos; then
     comandos
[elif comandos; then
     comandos...]
[else
     comandos]
fi

donde comandos es una lista de comandos. Esto es un poco confuso a primera vista. Pero antes de que podamos aclararlo, tenemos que ver cómo evalúa el shell el éxito o fracaso de un comando.

lunes, 13 de febrero de 2017

Control de flujo: Ramificando con if

En el último capítulo, nos hemos encontrado con un problema. ¿Cómo podemos hacer que nuestro script generador de informes se adapte a los privilegios del usuario que está ejecutando el script? La solución a este problema requerirá que encontremos una forma de "cambio de dirección" dentro de nuestro script, basándose en el resultado de un test. En términos de programación, necesitamos que el programa se ramifique.

Consideremos un ejemplo simple de lógica expresada en pseudocódigo, un lenguaje de ordenador simulado pensado para el consumo humano:

X = 5
Si X = 5, entonces:
          Di “X es igual a 5.”
Si no:
          Di “X no es igual a 5.”

Este es un ejemplo de ramificación. Basándose en la condición "¿Es X = 5?" haz una cosa "Di X es igual a 5", si no haz otra cosa, "Di X no es igual a 5."

viernes, 10 de febrero de 2017

Para saber más

miércoles, 8 de febrero de 2017

Resumiendo

En este capítulo, hemos presentado un método común de diseño de programas llamado diseño de arriba a abajo, y hemos visto cómo las funciones de shell se usan para construir el refinamiento gradual que requiere. También hemos visto cómo las variables locales pueden usarse para hacer a las funciones de shell independientes unas de otras y del programa en el que se encuentran. Esto hace posible que las funciones de shell sean escritas de una forma portable y que sean reutilizables permitiendo que se coloquen en múltiples programas; un gran ahorro de tiempo.

lunes, 6 de febrero de 2017

Funciones shell en tu archivo .bashrc

Las funciones de shell son excelentes reemplazos de los alias, y en realidad son el método preferido de crear pequeños comandos para uso personal. Los alias están muy limitados en cuanto al tipo de comandos y características de shell que soportan, mientras que las funciones de shell permiten cualquier cosa que pueda ser incluida en un script. Por ejemplo, si nos gusta la función shell report_disk_space que hemos desarrollado en nuestro script, podríamos crear una función similar llamada ds para nuestro archivo .bashrc:

ds () {
   echo “Disk Space Utilization For $HOSTNAME”
   df -h
}

viernes, 3 de febrero de 2017

Mantener los scripts ejecutándose

Mientras que desarrollamos nuestro programa, es útil mantener el programa en estado ejecutable. Haciendo esto, y probándolo frecuentemente, podemos detectar errores pronto en el proceso de desarrollo. Esto hará que los problemas de depuración sean mucho más fáciles. Por ejemplo, si ejecutamos el programa, hacemos un pequeño cambio, luego ejecutamos el programa nuevamente y encontramos un problema, es muy probable que el cambio más reciente sea el origen del problema. Añadiendo las funciones vacías, llamadas stubs en la jerga de los programadores, podemos verificar el flujo lógico de nuestro programa en una fase temprana. Cuando construimos un stub, es una buena idea incluir algo que proporcione retroalimentación al programador, que muestre que el flujo lógico se está realizando. Si miramos la salida de nuestro script ahora:

[me@linuxbox ~]$ sys_info_page
<HTML>
   <HEAD>
       <TITLE>System Information Report For twin2</TITLE>
   </HEAD>
   <BODY>
       <H1>System Information Report For linuxbox</H1>
       <P>Generated 03/19/2009 04:02:10 PM EDT, by me</P>



   </BODY>
</HTML>

vemos que hay algunas líneas en blanco en nuestra salida tras la marca de tiempo, pero no podemos estar seguros de cual es la causa. Si cambiamos las funciones para incluir algo de retroalimentación:

report_uptime () {
        echo "Function report_uptime executed."
        return
}

report_disk_space () {
        echo "Function report_disk_space executed."
        return
}

report_home_space () {
        echo "Function report_home_space executed."
        return
}

y ejecutamos el script de nuevo:

[me@linuxbox ~]$ sys_info_page
<HTML>
   <HEAD>
      <TITLE>System Information Report For linuxbox</TITLE>
   </HEAD>
   <BODY>
      <H1>System Information Report For linuxbox</H1>
      <P>Generated 03/20/2009 05:17:26 AM EDT, by me</P>
      Function report_uptime executed.
      Function report_disk_space executed.
      Function report_home_space executed.
   </BODY>
</HTML>

ahora vemos, de hecho, que nuestras tres funciones se están ejecutando.

Con nuestra estrucutra de funciones en su sitio y funcionando, es hora de desarrollar el código de nuestras funciones. Primero, la función report_uptime:

report_uptime () {
     cat <<- _EOF_
          <H2>System Uptime</H2>
          <PRE>$(uptime)</PRE>
          _EOF_
     return
}

Es bastante sencilla. Usamos un documento-aquí para mostrar un encabezado de sección y la salida del comando uptime, rodeándolo de etiquetas <PRE> para preservar el formato del comando. La función report_disk_space es similar:

report_disk_space () {
     cat <<- _EOF_
          <H2>Disk Space Utilization</H2>
          <PRE>$(df -h)</PRE>
          _EOF_
     return
}

Esta función usa el comando df -h para determinar la cantidad de espacio en disco. Finalmente, construiremos la función report_home_space:

report_home_space () {
     cat <<- _EOF_
          <H2>Home Space Utilization</H2>
          <PRE>$(du -sh /home/*)</PRE>
          _EOF_
     return
}

Usamos el comando du con las opciones -sh para realizar esta tarea. Esto, sin embargo, no es una solución completa al problema. Aunque funcionará en algunos sitemas (Ubuntu, por ejemplo), no funcionará en otros. La razón es que muchos sistemas establecen los permisos de los directorios home para prevenir que sean legibles por todo el mundo, lo que es una medida de seguridad razonable. En estos sistemas, la función report_home_space, tal como está escrita, sólo funcionará si nuestro script se ejecuta con privilegios de superusuario. Un solución mejor sería hacer que el script ajuste su comportamiento según los privilegios del usuario. Lo veremos en el próximo capítulo.

miércoles, 1 de febrero de 2017

Variables locales

En los scripts que hemos escrito hasta ahora, todas las variables (incluyendo las constantes) han sido variables globales. Las variables globales siguen existiendo a lo largo del programa. Esto es bueno para muchas cosas, pero, a veces, puede complicar el uso de las funciones de shell. Dentro de las funciones de shell, a menudo es preferible tener variables locales. Las variables locales sólo son accesibles dentro de la función de shell en la que han sido definidas y dejan de existir una vez que la función de shell termina.

Tener variables locales permite al programador usar variables con nombres que pueden existir anteriormente, tanto en el script globalmente como en otras funciones de shell, si tener que preocuparnos por potenciales conflictos de nombres.

Aquí hay un ejemplo de script que demuestra como se definen y usan las variables locales:

#!/bin/bash

# local-vars: script to demonstrate local variables

foo=0 # global variable foo

funct_1 () {

    local foo # variable foo local to funct_1

    foo=1
    echo "funct_1: foo = $foo"
}

funct_2 () {

    local foo # variable foo local to funct_2

    foo=2
    echo "funct_2: foo = $foo"
}

echo "global: foo = $foo"
funct_1
echo "global: foo = $foo"
funct_2
echo "global: foo = $foo"

Como podemos ver, las variables locales se definen precediendo al nombre de la variable con la palabra local. Esto crea una variable que es local para la función de shell en la que se define. Una vez fuera de la función de shell, la variable deja de existir. Cuando ejecutamos este script, vemos los resultados:

[me@linuxbox ~]$ local-vars
global:  foo = 0
funct_1: foo = 1
global:  foo = 0
funct_2: foo = 2
global:  foo = 0

Vemos que la asignación de valores a la variable local foo dentro de ambas funciones de shell no tiene efecto en el valor de foo definido fuera de las funciones.

Esta característica permite que las funciones de shell sean escritas de forma que se mantengan independientes entre sí y del script en el que aparecen. Esto es muy valioso, ya que ayuda a prevenir que una parte del programa interfiera en otra. También permite que las funciones de shell sean escritas de forma que sean portables. Es decir, pueden cortarse y pegarse de un script a otro, según sea necesario.