# Arrays, funciones en la Bash e introducción a cron --- [TOC] --- El uso de arrays puede resultar muy útil, en particular en scripts. Los arrays en bash tienen limitaciones (por ejemplo son de una sola dimensión, esto es, son vectores), pero también disponen de alguna caracteristica más avanzada, como los arrays asociativos. En cualquier caso, aquí solo se verán arrays indexados. ## Arrays ### Declaración de un array Hay varias formas de declarar un array. Aquí veremos tres de ellas, que son implícitas: 1) Asignando directamente varios elementos a una variable. Ej.: ```bash colores=(rojo verde azul) operaciones=(+ - \* /) #escapado * (metacaráct. expansión nomb. fichero) ```` 2) Asignando a una variable con el índice que ocupará el elemento. Ej.: ```bash animales[0]=perro #los índices empiezan por 0 primo[2]=5 ``` 3) Combinando las dos formas anteriores (se asignan varios elementos, pero diciendo las posiciones dentro del array, que no tienen por qué ser consecutivas): ```bash errores=([0]=sintactico [1]=semantico [9]=indefinido) ``` ### Acceso a un elemento Para acceder a un elemento concreto de un array, se usa como como siempre `$` precediendo la variable y el índice deseado entre corchetes, pero es obligatorio el uso de llaves de forma que se delimite sin ambigüedad que lo que se quiere es acceder a un elemento del array. Ej.: ```bash $ echo ${colores[2]} azul ``` Si se usa una variable para el índice, no es necesario usar `$` (sí para referirse a los parámetros de entrada, por ejemplo $1, para que pueda diferenciar si se quiere acceder a la posición cuyo valor está en $1 o a la posición 1 del array). Por ejemplo: ```bash $ for ((i=0; i<3; i++)) ; do echo "Me gusta el color ${colores[i]}"; done Me gusta el color rojo Me gusta el color verde Me gusta el color azul ``` ```bash $ for ((i=0; i<4; i++)) ; do echo 10 "${operaciones[i]}" 2 = $((10 ${operaciones[i]} 2)); done 10 + 2 = 12 10 - 2 = 8 10 * 2 = 20 10 / 2 = 5 ``` ### Acceso a todos los elementos Si se usa como índice `*` o `@` se accede a todos los elementos del array. Por ejemplo: ```bash $ echo ${colores[*]} rojo verde azul $ echo ${colores[@]} rojo verde azul ``` Es muy típico su uso en bucles `for in`: ```bash for color in ${colores[*]}; do echo "Me gusta el color $color"; done Me gusta el color rojo Me gusta el color verde Me gusta el color azul for color in ${colores[@]}; do echo "Me gusta el color $color"; done Me gusta el color rojo Me gusta el color verde Me gusta el color azul ``` Sin embargo, `*` y `@` difieren cuando el array está entrecomillado (con `@` diferencia cada elemento del array y con `*` no): ```bash $ for color in "${colores[*]}"; do echo "Me gusta el color $color"; done Me gusta el color rojo verde azul $ for color in "${colores[@]}"; do echo "Me gusta el color $color"; done Me gusta el color rojo Me gusta el color verde Me gusta el color azul ``` La diferencia se aprecia todavía más si el array contiene elementos formados por más de una palabra. Por ejemplo: ```bash $ colours=("rojo (red)" "verde (green)" "azul (blue)") $ for color in ${colours[*]}; do echo "Me gusta el color $color"; done Me gusta el color rojo Me gusta el color (red) Me gusta el color verde Me gusta el color (green) Me gusta el color azul Me gusta el color (blue) $ for color in ${colours[@]}; do echo "Me gusta el color $color"; done Me gusta el color rojo Me gusta el color (red) Me gusta el color verde Me gusta el color (green) Me gusta el color azul Me gusta el color (blue) $ for color in "${colours[*]}"; do echo "Me gusta el color $color"; done Me gusta el color rojo (red) verde (green) azul (blue) $ for color in "${colours[@]}"; do echo "Me gusta el color $color"; done Me gusta el color rojo (red) Me gusta el color verde (green) Me gusta el color azul (blue) ``` Si retomamos el ejemplo de operaciones usando ahora un `for in`, se hace necesario entrecomillarlo para que no ocurra la sustitución del tercer elemento del array (el *) por los ficheros del directorio actual. Pero para que cada elemento del array se trate individualmente en un entrecomillado, solo se puede utilizar @ como índice (con * se accedería a todo el array a la vez, que no es lo que se necesita): ```bash $ for o in "${operaciones[@]}"; do echo 10 "$o" 2 = $((10 "$o" 2)); done 10 + 2 = 12 10 - 2 = 8 10 * 2 = 20 10 / 2 = 5 ``` ### Acceso a los índices de los elementos existentes Dado que como se ha visto los arrays pueden no tener definidos todos sus elementos, puede ser de utilidad saber cuáles existen. Para ello se usa la sintaxis `${!array[*]}` o `${!array[@]}` como se puede ver en el siguiente ejemplo: ```bash $ echo ${!errores[*]} 0 1 9 $ for i in "${!errores[@]}" ; do echo $i; done 0 1 9 ``` ### Obtener el tamaño de un array En bash se puede utilizar `${#var}` para obtener el número de caracteres (longitud) de `var`. Por ejemplo: ```bash $ miVar=pepe ; echo ${#miVar} 4 ``` Una sintaxis parecida se utiliza para conocer el número de elementos de un array: ```bash $ echo ${#colours[*]} 3 $ echo ${#colours[@]} 3 ``` ### Añadir un elemento a un array Se puede hacer especificando un elemento concreto, como por ejemplo: ```bash $ colores[3]=naranja ``` O añadiendo al final, sin necesidad de especificar el índice: ```bash $ colores+=(amarillo) ``` Con esta última opción, podemos añadir varios elementos simultáneamente: ```bash $ colores+=(morado rosa) ``` ### Eliminar un array o un elemento Como con otras variables, para eliminar un array se utiliza unset. Ej.: ```bash unset errores ``` También se puede usar para borrar elementos determinados por su índice, por ejemplo: ```bash unset "colores[1]" ``` Es importante usar las comillas para que no se realicen expansiones de nombre de fichero a causa de los corchetes. ## Funciones Las funciones facilitan la modularización del código. Generalmente disminuyen el tamaño de los programas ya que se escriben una sola vez pero pueden ser llamadas desde diversos puntos. Son útiles para la optimización, las pruebas y la reutilización del código. Además en muchos lenguajes (y la propia bash) permiten llamarse a sí mismas (funciones recursivas). ### Sintaxis Una función se declara (formato POSIX) de la siguiente forma: ```bash nombreFuncion () { órdenes } ``` Una función se puede declarar directamente en la línea de órdenes: ```bash $ holaMundo () > { > echo "Hola, mundo" > } $ ``` Para llamar a una función, se escribe directamente su nombre (sin los paréntesis; estos se usan solo en la declaración): ```bash $ holaMundo Hola, mundo ``` El valor de `$?` tras la ejecución de una función es, por defecto, el de la última instrucción ejecutada. Si se quiere que devuelva otro valor, se utiliza la instrucción return (que termina la función) acompañada del exit status que se desea. Para ver las funciones que hay declaradas, se utiliza: ```bash $ declare -F declare -f __expand_tilde_by_ref declare -f __get_cword_at_cursor_by_ref declare -f __load_completion declare -f __ltrim_colon_completions declare -f __parse_options declare -f __reassemble_comp_words_by_ref declare -f _allowed_groups declare -f _allowed_users declare -f _apport-bug ... declare -f _variables declare -f _xfunc declare -f _xinetd_services declare -f command_not_found_handle declare -f dequote declare -f holaMundo declare -f quote declare -f quote_readline ```` Como se puede ver, además de la función que hemos definido aparecen muchas otras. Estas funciones están declaradas en del fichero `/usr/share/bash-completion/bash_completion` que se carga desde `/etc/bash.bashrc` (archivo genérico de inicio de la bash para shell interactivos). Para ver cómo está declarada una función se usa declare `-f` o `type`: ```bash $ declare -f quote quote () { local quoted=${1//\'/\'\\\'\'}; printf "'%s'" "$quoted" } $ type quote quote is a function quote () { local quoted=${1//\'/\'\\\'\'}; printf "'%s'" "$quoted" } ```` Una función definida se puede borrar con `unset -f`: ```bash $ unset -f holaMundo $ type holaMundo bash: type: holaMundo: not found ``` En el resto de la sección dedicada a funciones nos centraremos en las definidas en scripts. ### Funciones en un script Sea el siguiente ejemplo: ```bash #!/bin/bash # Programa: cabecera.sh # Propósito: Muestra una pequeña cabecera (texto centrado entre dos líneas) # Autor: Javier Macías # Fecha: 26/11/2019 # "Constantes" (prececer cada una con readonly para que realmente lo sean) LONG_DEF=70 #tamaño por defecto de las líneas de la cabecera CHAR_DEF="=" #carácter por defecto para las líneas de la cabecera #Escribe una cadena de longitud $1 con el carácter $2 cadena () { local long=$1 local char="$2" for((i=0; i<long; i++)) ; do echo -n "$char" done } #Muestra la cabecera con el texto $1 centrado entre dos líneas de #longitud $2 compuestas por el carácter $3 cabecera () { local texto="$1" local long=$2 local char="$3" local longTex=${#texto} cadena $long "$char" #línea superior echo cadena $(( ( long - longTex ) / 2)) " " #espacios en blanco línea media echo "$texto" #texto línea media cadena $long "$char" #línea inferior echo } ##PROGRAMA PRINCIPAL## #comprobamos los parámetros pasados case $# in 1) texto="$1" long=$LONG_DEF char="$CHAR_DEF" ;; 2) texto="$1" long=$2 char="$CHAR_DEF" ;; 3) texto="$1" long=$2 char="$3" ;; *) echo "Parámetros erróneos. Uso: $0 texto [tamaño [carácter]]" ;; esac #Escribimos cabecera cabecera "$texto" $long "$char" ```` Vamos a ejecutar el script: ```bash $ bash cabecera.sh "hola, mundo" 40 "*" **************************************** hola, mundo **************************************** ```` Apoyándonos en el ejemplo, tenemos: - Las funciones deben declararse antes de su llamada, por lo que aparecen antes que el “programa principal”. Una función puede llamar a otra, como es el caso de las llamadas a cadena desde cabecera. Ámbito de las variables: - En bash por defecto todas globales, incluso las declaradas dentro de una función. - Para que una variable declarada en una función sea local debe precederse de local. Una variable local es tambien accesible a todas las funciones llamadas desde la función donde esta declarada la variable local (por ejemplo, cuando cabecera llama a cadena, `$texto` sería accesible desde cadena). - Las variables declaradas con un mismo nombre en un ámbito interno que en uno externo, ocultan a las declaradas en ámbitos externos. Por ejemplo, cuando se está ejecutando la función `cadena` (nótese que se la llama siempre desde la función `cabecera`), su variable `$long` oculta a la variable `$long` de la función `cabecera`. ### Retorno de valores - Como se dijo, las funciones no devuelven realmente un valor con return , sino el exit status - Una forma de “retornar” valores, pero poco recomendada, es modificar variables globales o locales de la función llamante. - Una forma más recomendable es usar echo en la función llamada y “capturar” con `$()`. Ej.: ```bash miFuncion () { echo “Esto es todo” } resultado=$(miFuncion) ``` ### Paso argumentos. - Poniéndolos a la derecha en la llamada a la función. Se pasan en `$1`, `$2`. - Se dispone también de `$*` (todos los argumentos) y `$#` (números de argumentos). Lo que no se altera es `$0` que mantiene el valor original (nombre del script). - No se puede pasar un array directamente: hay que pasar sus elementos y luego reconstruir: ```bash miFuncion() { local nuevoArray=("$@") } array=(1 2 3) miFuncion "${array[@]}" ```` ### Sobreescritura de órdenes - Una función puede tener el mismo nombre que una orden que habitualmente se use desde de línea de comandos. Esto permite crear un envoltorio (wrapper). Se puede utilizar la palabra reservada `command` para acceder a la orden original. Por ejemplo: ```bash # Create a wrapper around the command ls ls () { command ls -lh } ls ``` ### Declaración de librerías Si tenemos funciones en un script (librería) que deseamos usar directamente desde la shell interactiva de la `bash` se puede ejecutar el script con `source`. Si queremos tenerlo siempre disponible, podemos añadirlo en `~/.bashrc` (para el usuario actual) o `/etc/bash.bashrc` (para todos lo usuarios). ## Ejercicios ### Script de comprobación de proof of work Se necesita diseñar un script que verifique el minado de un bloque de datos por parte de un tercero. El bloque consiste en una única línea que contiene tres campos separados por el carácter de dos puntos: `payload:nonce:hash` El primer campo `payload` contiene la información del bloque, que puede ser una cadena de texto arbitraria. El campo `nonce` es un contador que varía con objeto de hacer cambiar el resultado del `hash`, y por último el campo `hash` es el resultado de calcular el sha-512 de `payload:nonce` (incluyendo el caracter de dos puntos :) Por ejemplo, sea el siguiente bloque: `Hola caracola:1017:00e72927fc19dc3df6611252b6f27a779e42645f7360d033080b2402758864c731a21e8550e538c01bdeb09421fb0e1357e152ee58d932cdc5cfabccaececde6` Para verificar la prueba utilizaremos la orden `shasum -a 512` (en algunas distribuciones de Linux, esta orden aparece con el nombre de `sha512sum`) que recibe por stdin una cadena de texto a calcular. Obtenemos los dos primeros campos y los concatenamos con el carácter dos puntos: `Hola caracola:1017`. A continuación calculamos el SHA-512 de esa cadena y obtenemos `00e72927fc19dc3df6611252b6f27a779e42645f7360d033080b2402758864c731a21e8550e538c01bdeb09421fb0e1357e152ee58d932cdc5cfabccaececde6` y finalmente comparamos si dicha cadena coincide con el tercer campo del bloque. De ser así, solo queda una prueba más para dar por válida la prueba: el hash generado debe comenzar por dos ceros. :::warning Cuidado con el caracter de new line si emplea `echo` Si se emplea echo para generar cadenas, no olvide usar el flag -n para evitar que echointroduzca un carácter de avance de línea al final de la cadena generada, ya que este es su comportamiento por defecto. ::: Realice un script bash que reciba como argumento un archivo que contenga el bloque a verificar y que muestre por pantalla si el archivo contiene una prueba correcta. ### Shell script de minado de un bloque En nuestro sistema de criptomonedas, minar un bloque consiste en concatenar al bloque a minar (payload), usando como delimitador el carácter de dos puntos, un campo nonce con un contenido arbitrario, de forma que el hash SHA-512 de dicha concatenación comience por un determinado número de ceros. El número de ceros por el que debe comenzar el hash se conoce como grado de dificultad del minado y en nuestro ejemplo estará fijo a 2. Realice un script que reciba como argumento un bloque (una cadena de texto arbitraria) y genere un bloque con el formato: `payload:nonce:hash` Tal que el SHA-512 de `payload:nonce` sea igual a hash, y que dicho hash comience por la secuencia `00`. ### Ejemplo de función: un script de minado de bloques :::spoiler Posible solución para el script de minado ```bash function minar { BLOCK=$1 NONCE=$RANDOM RESULT=999 while echo -n "$RESULT" | grep ^0000 -q -v do RESULT=$(echo $BLOCK:$NONCE | sha512sum ) NONCE=$(expr $NONCE + 1) done echo $(echo $RESULT | cut -d'-' -f1) } ``` ::: ## Introducción a cron Es habitual que en un sistema haya que realizar tareas que se repitan con cierta periodicidad, como hacer copias de seguridad todos los días, enviar un informe del uso de las impresoras semanalmente, etc. Para automatizarlas se suele utilizar cron. Dada la importancia que para el administrador del sistema tiene cron, se presenta aquí una pequeña introducción. Cron es un demonio que se ejecuta cada minuto y comprueba si hay que realizar alguna tarea. Estas tareas vienen especificadas en ficheros crontab. Si no se limita (para esto se utilizan los ficheros `/etc/cron.deny` y `/etc/cron.allow`^[Estos ficheros contienen los id de los usuarios a los que se les deniega el acceso (en el primer caso) o se les permite (en el segundo). Si existen los dos ficheros, solo se utiliza el más restrictivo: `\etc\cron.allow`]), cualquier usuario puede crear su propio fichero crontab para automatizar sus tareas repetitivas. Un usuario puede ver su fichero crontab utilizando `crontab -l`. Si no existiera o lo deseara modificar, utilizaría la orden `crontab -e`. Es importante utilizar este método ya que aunque los ficheros crontab son ficheros de texto cuyo nombre es igual el del usuario y que se encuentran en `/var/spool/cron/crontabs`, si se modifican externamente el demonio (cron) no los relee y no se van a aplicar los cambios hasta que no sea reiniciado. Supongamos que somos el usuario root y ejecutamos `crontab -l`, que nos va a mostrar el contenido de nuestro fichero crontab, esto es, `/var/spool/cron/crontabs/root`: ```bash #Fichero crontab ejemplo SHELL=/bin/bash # m h dom mon dow command #Borrado ficheros en /tmp de más de 10 días, todos los días a las 02:00 0 2 * * * find /tmp -type f -atime +10 -delete #Copia seguridad de /home todos los lunes a las 04:30 30 4 * * 1 tar -zcf /var/backups/home.tgz /home/ ``` El significado de este fichero es el siguiente: En primer lugar, las líneas en blanco y aquellas cuyo primer carácter no blanco es `#` , que representan comentarios, se ignoran. Por otro lado, en los ficheros crontab se pueden especificar ciertas variables de entorno propias de los crontab (aunque tengan nombres iguales a las de la shell), como la variable `PATH`, que por defecto tiene el valor `"/usr/bin:/bin"`. Otra variable de entorno de los crontab es `SHELL`, que indica la shell que ejecutará las tareas (por defecto es `/bin/sh`) y como se ve en el ejemplo se ha cambiado a `/bin/bash`. Por último, se encuentran las líneas que indican las tareas a ejecutar. Los cinco primeros campos indican el momento en que se ejecutarán, y el resto la orden a ejecutar (que en muchas ocasiones será un script). Cada línea va separada por una nueva línea, **incluida la última**. En este caso, hay planificadas dos tareas (una copia de seguridad y un borrado del temporal). El significado de los cinco primeros campos (abreviados como *m h dom mon dow*) es: m: minute (0-59) h: hour (0-23) dom: day of month (1-31) mon: month (1-12) dow: day of week (0-7; el domingo se representa como 0 y 7). Cada uno de estos campos se puede especificar como rangos separando el valor inicial del final con un guión (por ejemplo `1-5` en el *dow* para especificar de lunes a viernes), como listas separando los valores por comas (por ejemplo, en el campo *h* `8-10,15,23` significa a las 8,9,10,15 y 23 horas) y todo el rango con `*` (por ejemplo, en el campo *m* un `*` significa lo mismo que 0-59). A su vez, en cada rango se puede especificar el "salto" entre valores añadiendo una barra (`/`) seguida del valor del salto. Por ejemplo, si en el campo *h* se pone `8-15/2` es lo mismo que especificar las horas 8,10,12,14; y si en el campo *mon* se escribe `*/3` es lo mismo que especificar 1, 4, 7 y 10. Cuando se especifica simultáneamente el día del mes y el día de la semana, se activa cuando se cumple cualquiera de ellos. Por ejemplo, si tenemos esta línea: ```bash 0 3 1 * 6 x.sh ``` el comando x.sh se ejecutará a las 03:00 todos los meses los días 1 y los sábados. Si la orden ejecutada generara alguna salida por la salida estándar, se redireccionaría automáticamente al correo del usuario. Para más información: `man crontab`, `man 5 crontab` y `man cron`.