.. highlight:: bash Trucos y consejos ================= El epígrafe está dedicado a exponer algunas prácticas de escritura que particularmente al autor le parecen recomendables. Gestión de errores ------------------ Es recomendable, cuando se produce un error que interrumpe la ejecución, escribir un mensaje de error en la salida de errores y acabar con un código distinto a 0. Algo así:: echo "ERROR. Se ha producido bla, bla, bla" >&2 exit 1 Lo habitual es que estos errores puedan producirse en distintos puntos del programa y que tengamos que repetir la estructura. Una buena práctica es crear una función para tratar los errores:: # # Trata los errores. # $1: Codigo de salida. Si es 0, se considera que el mensaje es sólo una # advertencia y no se interrumpe la ejecución. # $*: Mensaje de información que se mostrará por la salida de errores. # error() { local EXITCODE=$1 shift if [ $EXITCODE -eq 0 ]; then echo "¡ATENCIÓN! "$* >&2 else echo "ERROR. "$* >&2 exit $EXITCODE fi } De este modo, para generar errores basta con hacer:: error 2 Opción desconocida o bien, si se desea escribir un mensaje de advertencia, sin llegar a interrumpir la ejecucuón:: error 0 Comportamiento indefinido. Puede que no obtenga lo que espera. .. note:: Si en el *script* se decide :ref:`escribir mensajes en el registro `, lo lógico es que sustituyamos los :program:`echo` de la función anterior por llamadas a la función *log*. El primer mensaje puede ser de gravedad *ERROR* y el segundo debe ser de gravedad *CRIT*, puesto que provoca la salida inmediata del programa. Ayuda en línea -------------- En un *script* que use :ref:`opciones en línea ` es indispensable que preparemos una función de ayuda que se ejecute al usar el *script* con las opciones ``-h`` o ``--help``. De hecho, es conveniente no usar jamás estas opciones para otra labor que no sea mostrar ayuda, ya que más de un usuario tendrá la tentación de ejecutarlo por primera vez con una de estas dos opciones para conocer cómo se usa y, si el *script* realiza alguna operación que supone un efecto irreversible, las consecuencias pueden ser desastrosas. Lo más conveniente es crear una función de ayuda que se invoque luego desde el código principal e imprima por pantalla la ayuda pertinente:: help() { echo "$(basename $0) [opciones] fichero: Programa que hace tal cosa... (lo describimos brevemente). Opciones: -h, --help Muestra esta misma ayuda. -o, --output Fichero donde almacenar la salida en vez de mostrarla por pantalla. " } .. note:: Conviene que nos aseguremos de que estas líneas no tendrá un ancho mayor a 80 caracteres. Valores predeterminados ----------------------- Cuando un *script* define valores predeterminados que se usan en caso de que el usuario no ios defina (a través de opciones en línea, por ejemplo), lo aconsejable es colocar tales valores al comienzo del *script*, en previsión de que por alguna razón algún usuario estime oportuno modificar tales valores:: #!/bin/sh # # Valores predeterminados. # INI=1 FIN=100 LAPSO=.5 # Aquí comenzamos el script... for x in $(seq $INI $FIN); do echo $x sleep $LAPSO done De este modo, se le deja muy fácil hacer los cambios oportunos. Ahora bien, si queremos ser aún más elegantes, podemos permitir que cambie estos valores a través de variables de ambiente:: #!/bin/sh # # Valores predeterminados. # INI=${CTD_INI:-1} FIN=${CTD_FIN:-100} LAPSO=${CTD_LAPSO:-.5} # Aquí comenzamos el script... etc... En este caso, es conveniente evitar que el nombre de alguna de nuestras variables pueda coincidir con el nombre de una variable de ambiente ya existente. Por ese motivo conviene construir el nombre con un prefijo que puede ser el nombre del programa o un apócope formado a partir de él. En el ejemplo, se ha supuesto que el nombre del programa es *contador* y con él se ha construido el prefijo *CTD*. Obrando así, ejecutar el programa con un *lapso* diferente a medio segundo es tan sencillo como: .. code-block:: console $ LAPSO=2 ,/mi_programa.sh .. _sh-join: Función *join* -------------- La mayoría de los lenguajes disponen de una función (o un método) que permite concatenar los elementos de un *array* (de cadenas) usando como separador un carácter. La *shell* carece de ella, pero podemos implementarla haciendo uso de la propiedad de la variable *$\** al encerrarse entre comillas dobles:: join() { local IFS="$1" shift echo "$*" } De esta forma, componer una dirección |IP| si se tienen sus cuatro octetos es tan fácil como: .. code-block:: console $ join . 192 168 1 14 192.168.1.14 Lamentablemente, la función anterior sólo es útil si el elemento de unión es un único carácter. Si consta de varios caracteres, es preciso echar mano de :ref:`printf `, aprovechando que éste comando repite el patrón si este no es capaz de consumir el resto de argumentos suministrados\ [#]_:: join() { local glue="$1" shift printf -- "$1" shift [ $# -gt 0 ] && printf -- "$glue%s" "$@" } Esta función permite lo siguiente: .. code-block:: console $ join " -- " aa bb cc aa -- bb -- cc .. _sh-split: Función *split* --------------- Esta es la función que hace la tarea inversa: dada una cadena, descomponerla según un carácter que se considera separador. Por ejemplo, descomponer la cadena "a_b_c" para obtener los componentes por separado "a", "b" y "c". Con :program:`bash` si tiene sentido crear una función para esta tarea, ya que :program:`bash` soporta *arrays*:: # # Descompone una cadena según un carácter separador # $1: nombre del array en que se almacenará el resultado. # $2: caracter separador. # $3: Cadena a descomponer # !!ATENCiÓN!! Sólo para BASH. No vale para POSIX sh. # split() { local sep="$2" arr="$1" shift 2 IFS="$sep" read -ra $arr <<<"$*" } Definida esta función, nos bastaría para descomponer la cadena y almacenar los elementos en un array llamado *arr* lo siguiente:: split arr _ "a_b_c" En el estándar *POSIX* en cambio, no hay forma de almacenar los componentes en un *array*, por lo que tenemos dos alternativas, aunque ninguna de las dos se puede implementar como función: * Utilizar el único *array* que existe: el que almacena los argumentos posicionales:: cadena="a_b_c" IFS=_ set -- $cadena unset IFS * Utilizar :ref:`read `:: a="a_b_c" echo "$a" | { IFS=_ read -r x y z echo x echo y echo z } Esta solución tiene el inconveniente de que sólo es válida cuando sabemos de antemanos cuántos serán los elementos en los que se descompondrá la cadena. Consulta de usuarios -------------------- Debe tenerse presente que la consulta del fichero :file:`/etc/passwd` sólo da información de los usuarios locales, pero no de usuarios de red que pueden estar definidos a través de :ref:`OpenLDAP ` o :ref:`Samba `. Lo más correcto cuando en un *script* queremos consultar o comprobar algún dato de usuario es hacer uso de la orden :ref:`getent `:: $ getent passwd bartolo .. warning:: Caso distinto es que, además, pretendamos modificar con el script los datos. En ese caso, la herramienta de modificación dependerá del soporte que defina al usuario y tendremos que implementar un método distinto para cada soporte. En ese caso, lo más conveniente es aislar en una o varias funciones las tareas de modificación, de manera que si se desea cambiar el "*backend*", baste con reimplementar el código de estas funciones. Además, cuando nuestro *script* requiere que guardemos en variables los datos de los usuarios, es muy pertinente una construcción de este tipo: .. code-block:: bash getent passwd nombre_usuario | { IFS=: read -r user _ uid gid gecos home shell # ¡Atención! Recuerde que ni estas variables ni otras que defina dentro de # este bloque existirán fuera, porque se ejecuta en una subshell. # Aquí procederemos a usar esas variables como mejor convenga. Por ejemplo: echo "El usuario se llama: $user" } y si lo que queremos es tratar a todos los usuarios .. code-block:: bash getent passwd | while IFS=: read -r user _ uid gid gecos home shell; do # Lea lo expuesto en el código anterior. done que es la aplicación a este caso particular de lo expuesto al hablar de la :ref:`función split `. .. _sh-verbose-simulate: Simulación de acciones ---------------------- En ocasiones puede interesarnos que nuestro *script* no llegue a ejecutar las acciones, pero que presente las órdenes que habría ejecutado. Un buen ejemplo es el de aquellos *script* cuya misión es facilitarnos la tarea de construir una orden compleja con muchos argumentos. Para ello, podemos construir la siguiente función:: execute() { [ -n "$VERBOSE" ] && echo "$@" > /dev/tty [ -z "$SIMULATE" ] && "$@" } que permite ejecutar cualquier orden anteponiendo la palabra *execute*. Por ejemplo:: execute ls / En ausencia de las variables *VERBOSE* y *SIMULATE*, la orden se ejecuta normalmente. Si se le da algún valor a la variable *VERBOSE*, se mostrará por pantalla cuál es la orden ejecutada; y si se le da valor a la variable *SIMULATE*, no se ejecutará. .. _func-ext: Funciones ejecutadas por órdenes externas ----------------------------------------- Hay órdenes como :ref:`xargs ` o :ref:`find ` que toman como argumento otras órdenes con el fin de ejecutarlas. Ahora bien, puede ocurrir que la segunda orden sea algo compleja y requiramos hacer un pequeño *script* para llevarla a cabo. Por ejemplo, tenemos este código para pasar a mayúsculas:: cat "$1" | tr '[:lower:]' [':upper:'] y, por otro lado, este otro que busca ficheros y los muestra en mayúsculas:: find -type f -exec toupper.sh '{}' \; Nuestro problema es que el pequeño código para convertir en mayúsculas debemos colocarlo en un *script* aparte, ya que :command:`find` requiere una orden externa. Si la segunda línea ya se encontraba dentro de un *script*, la consecuencia es que tendremos que trocear en dos *script* independientes nuestro *script* para poder llevarlo a cabo. La pregunta es ¿no hay forma de incluir la línea de código dentro de una función y hacer que :command:`find` sea capaz de ejecutarla? La respuesta inmediata es que no, pero podemos buscarnos argucias para lograrlo. Apegándonos estrictamente al estándar la solución está en añadir un argumento al *script* de manera que, cuando se incluya en su invocación, se limite a ejecutar la función:: #!/bin/sh toupper() { cat "$1" | tr '[:lower:]' '[:upper:]' } # El tratamiento será más complejo # si el propio script requiere otros argumentos. if [ "$1" = "-x" ]; then toupper "$2" exit 0 fi find -maxdepth 1 -type f -name "*.sh" -exec "$0" -x "{}" \; En :program:`bash`, se pueden exportar también funciones, así que eso podemos hacer y usarla en una subshell:: #!/bin/bash toupper() { cat "$1" | tr '[:lower:]' '[:upper:]' } export -f toupper find -maxdepth 1 -type f -name "*.sh" -exec bash -c "toupper '{}'" \; .. rubric:: Notas al pie .. [#] O dicho con un ejemplo, si hacemos: .. code-block:: console $ printf "%.2f\n" 1 2 3 4 1.00 2.00 3.00 4.00 El patrón sólo incluye cómo tratar un número, así que el resto de números usan ese mismo patrón.