###### tags: `ASO-GISI` `Sistemas Operativos`
# Vectores (arrays) y funciones en la Bash
---

[TOC]
---
# Arrays
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, en la asignatura solo vamos a ver los arrays indexados.
## Declaración de un array
En los arrays indexados el índice es un número entero mayor o igual a 0.
Hay varias formas de declarar un array indexado, la primera explícita y las siguientes implícitas:
1) Con la orden `declare -a`. Nótese que no se indica el número de elementos, ya que el tamaño de los arrays en bash varía dinámicamente. Ej.:
```bash
declare -a vector
```
*Nota: La orden *`declare`* se utiliza para especificar que una variable de la shell tenga ciertos atributos, como en este caso para indicar que es un array.*
2) Asignando directamente varios elementos a una variable, lo que la convierte en un array. Los elementos se numeran (indexan) consecutivamente comenzando desde el 0. Ej.:
```bash
colores=(rojo verde azul)
operaciones=(+ - \* /) #escapado * (metacaráct. expansión nomb. fichero)
operaciones2=("+" "-" "*" "/") #con comillas así evitamos la expansión de nombres de ficheros
coloresES_EN_FR=("ES:rojo EN:red FR:rouge" "ES:verde EN:green FR:vert" "ES:azul EN:blue FR:bleu")
```
3) Asignando a una variable con el índice que ocupará el elemento. Ej.:
```bash
animales[0]=perro
animales[1]=gato
primo[2]=5
```
4) 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
```
Si se usa el índice de un elemento no existente, no se devuelve nada, de forma similar a cuando se intenta acceder a una variable que no existe. Esta es una de las formas de comprobar si un elemento existe.
Si no se especifica el elemento al que se quiere acceder, se sobreentiende que es el de índice 0.
También se puede acceder a los elementos numerándolos desde el final. En este caso se usan índices negativos, siendo -1 el último elemento del array. Por ejemplo:
```bash
$ estados=([0]=apagado [1]="en línea" [3]="en espera" [5]="fuera de línea")
$ echo ${estados[-1]} #el último, índice 5
fuera de línea
$ echo ${estados[-3]} #-1=>índ. 5 (el último); -2=>índ. 4; -3=> índ. 3
en espera
$ echo ${estados[-4]} #corresponde a índ. 2, que no está definido
$ echo ${estados[-5]}
en línea
```
*Nota: Los índices negativos podrían no funcionar en MacOS.*
## 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 acceso al 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[@]}` indistintamente 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
Como se vio, 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 elementos 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 (tendría como índice el del último elemento que había definido más uno):
```bash
$ colores+=(amarillo)
```
Con esta última opción, podemos añadir varios elementos simultáneamente:
```bash
$ colores+=(morado rosa)
```
Si se asigna un valor a un índice ya existente, se reemplaza por el nuevo valor. Por ejemplo:
```bash
$ colores[3]=púrpura
```
## 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
```
Las funciones también devuelven un exit status al que se puede acceder, como siempre, con `$?`. El exit status de una función es, por defecto, el exit status de la última instrucción ejecutada. Si se quiere que devuelva un exit status concreto se utiliza la orden `return` (que termina la función) acompañada del valor que se desee. La orden `return` también se puede utilizar sin argumento, en cuyo caso termina la función devolviendo como exit status el devuelto por la orden ejecutada previamente al `return`. Nótese que si en una función se usa `exit`, terminará todo el script.
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`.
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 aquellas 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)
# Uso: cabecera.sh texto [tamaño [carácter]]
# ExitStatus: 1=> Parámetros erróneos
# Autor: Javier Macías
# Fecha: 26/11/2019
# Constantes
readonly LONG_DEF=70 #tamaño por defecto de las líneas de la cabecera
readonly CHAR_DEF="=" #carácter por defecto para las líneas de la cabecera
cadena ()
#Escribe una cadena de longitud $1 con el carácter $2
{
local long=$1
local char="$2"
for((i=0; i<long; i++)) ; do
echo -n "$char" #-n evita el retorno de carro
done
}
cabecera ()
#Muestra la cabecera con el texto $1 centrado entre dos líneas de
#longitud $2 compuestas por el carácter $3
{
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]]"
exit 1
;;
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 también 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`.
- Las variables del ámbito más externo (esto es, fuera de una función) no pueden declararse como locales.
- Se pueden declarar constantes locales. Esto se puede hacer con una sentencia `readonly` tras su declaración con `local` o utilizando el parámetro `-r` en la declaración de `local`. Hay una limitación: una constante local no puede tener el mismo nombre que otra constante de un ámbito externo. Ejemplo:
```bash
ejemplo ()
{
local -r IVA=21
#alternativamente (no mezclar estilos en el mismo script):
#local IVA=21
#readonly IVA
#...
}
```
## Retorno de valores
- Como se dijo, las funciones no devuelven realmente un valor con `return`, sino el exit status. No se debería utilizar el exit status de una función para el retorno de valores, ya que estaríamos alterando la semántica del 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 como parámetros posicionales, esto es, `$1`, `$2`, etc.
- Se dispone también de `$*` y `$@` (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
1. Hacer un script llamado `Invertir.sh` que lea del teclado líneas de caracteres (hasta que se introduzca una vacía) y después muestre las líneas en orden invertido (es decir, empezando por la última línea introducida). Internamente, guardará las líneas introducidas en un array para luego poder mostrarlas.
2. Hacer un script llamado `Apariciones.sh` que reciba como parámetros una cadena y un carácter y escriba como resultado un mensaje indicando las veces que aparece el carácter en la cadena. Por ejemplo:
```bash
$ ./Apariciones.sh ampliación a
En "ampliación" aparece 2 veces la "a"
```
En caso de que el número de parámetros en la llamada sea distinto de dos escribirá un error e indicará la forma de uso y terminará. Asimismo escribirá un error y terminará si el segundo parámetro tiene más de un carácter.
Internamente usará una función que, dada una cadena y un carácter, "devuelva" el número de apariciones. Implemente esta función recorriendo uno a uno los caracteres de la cadena y comparando con el carácter a buscar.
Nota: Empiece primero probando como cadena con una palabra. Cuando funcione, pruebe con una frase. Pruebe también buscando algún carácter "especial", como *