Try   HackMD

Arrays, funciones en la Bash e introducción a cron



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.:
colores=(rojo verde azul)
operaciones=(+ - \* /)    #escapado * (metacaráct. expansión nomb. fichero)
  1. Asignando a una variable con el índice que ocupará el elemento. Ej.:
animales[0]=perro #los índices empiezan por 0
primo[2]=5
  1. Combinando las dos formas anteriores (se asignan varios elementos, pero diciendo las posiciones dentro del array, que no tienen por qué ser consecutivas):
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.:

$ 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:

$ 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
$ 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:

$ echo ${colores[*]}
rojo verde azul
$ echo ${colores[@]}
rojo verde azul

Es muy típico su uso en bucles for in:

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):

$ 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:

$ 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):

$ 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:

$ 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:

$ miVar=pepe ; echo ${#miVar}
4

Una sintaxis parecida se utiliza para conocer el número de elementos de un array:

$ echo ${#colours[*]}
3
$ echo ${#colours[@]}
3

Añadir un elemento a un array

Se puede hacer especificando un elemento concreto, como por ejemplo:

$ colores[3]=naranja

O añadiendo al final, sin necesidad de especificar el índice:

$ colores+=(amarillo)

Con esta última opción, podemos añadir varios elementos simultáneamente:

$ colores+=(morado rosa)

Eliminar un array o un elemento

Como con otras variables, para eliminar un array se utiliza unset. Ej.:

unset errores

También se puede usar para borrar elementos determinados por su índice, por ejemplo:

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:

nombreFuncion () {
    órdenes
}

Una función se puede declarar directamente en la línea de órdenes:

$ 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):

$ 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:

$ 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:

$ 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:

$ 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:

#!/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 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.:

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:
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:
# 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.

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

Posible solución para el script de minado
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[1]), 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:

#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:

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.