martes, septiembre 19, 2017

Tutorial de uso de funciones en Bash

Una de las funcionalidades que tiene la shell Bash es la posibilidad de usar funciones las cuales no son más que una agrupación de comandos con un nombre, que se ejecutará cuando se utilize dicho nombre para involcarlas. Esto permite construir librerías de funciones que realizen operaciones que interesan.

Definición de funciones

En Bash se pueden declarar funciones de varias maneras:

#!/bin/bash

function func1(){
    echo "Function 1"
}
func2(){
    echo "Function 2"
}
# Debe haber un espacio entre el nombre
# y la llave
function func3 {
    echo "Function 3"
}

La manera más portable - y la que recoge POSIX - es la segunda, definir el cuerpo de la función usando los paréntesis.

A la hora de definir los bloques de comandos que forman el cuerpo de la función se puede hacer usando las llaves { ... body ... } o los paréntesis (... body ...). La diferencia es que en el segundo caso la función se ejecutará en una subshell. Si se usa la versión 4 de bash se puede ver con en este ejemplo el comportamiento de cada manera definir la función: Si se hacen dos ejecuciones del mismo: La llamada a func1 siempre devolverá el mismo PID que corresponde con el del shell desde donde se está llamando, mientras que en el segundo caso variará, ya que el comando se ejecuta en otra subshell.

#!/bin/bash
function func1() {
    /bin/echo "FUNC1 $BASHPID"
}
function func2() (
    /bin/echo "FUNC2 $BASHPID"
)
func1
func2

La salida de ejecutar un script con ese contenido usando source es la siguiente:.

$ source script.sh
func 1 31238
func 2 31329
$ source script.sh 
func 1 31238
func 2 31338

Se ve como func1 tiene el mismo PID (el del shell desde donde se ejecuta el source), mientras que cambia el de la subshell. Recordar que pare que bash defina la variable $BASHPID debe ser la versión cuatro.

Alcance de variables

Una cosa particular de las funciones es que las variables que se definan en ellas, tienen alcance global por defecto del script que se está ejecutando, a menos que explicitamente se les añada el modificador local. Por ejemplo, el siguiente script la llamada a func1 asignará un valor a TEMPFUNCVAR1 que se verá tras la ejecución, cosa que no pasará con TEMPFUNCVAR2 de la segunfa función.

#!/bin/bash
func1() {
    echo "Func 1"
    FUNCVAR1="You can see me"
}
func2() {
    echo "Func 2"
    local FUNCVAR2="You can't see me"
}

func1
echo "FUNCVAR1=$FUNCVAR1"
func2
echo "FUNCVAR2=$FUNCVAR2"

Si se ejecuta el script anterior, se ve que la variable que ha sido declarada con local no muestra el contenido tras ejecutar la función:

$ script.sh
Func 1
FUNCVAR1=You can see me
Func 2
FUNCVAR2=

Parámetros de la función

Cuando se llama a una función con parámetros, estos pasan a ocupar los parámetros posicionales de la shell:

  • $* se expande a todos los parámetros posiciones ($1,$2,...), con la particularidad que sigue unas reglas de expansión si está entre dobles comillas.
  • $@ Exactamente igual que el anterior, pero los parámetros se expanden cada uno a una palabra distinta.
  • $# Es el número de parámetros de la función.
  • $1,$2,...$9 son los 9 primeros parámetros de la función. Si se necesitan más, estos se numera con ayuda de las llaves, es decir ${10}, ${11}, etc
  • .

El siguiente ejemplo de código coge todo los parámetros que se le pasa a la función, imprime el número de parámetros y luego cada parámetro en una línea distinta, con usando el comando interno shift para desplazar la lista de parámetros a la izquierda al irlos consumiendo.

#!/bin/bash
func1() {
    echo "Number of parameters: $#"
    while [ ! -z "$1" ]; do
        echo "$1"
        shift
    done
}
func1 You can see five parameters
func1 two parameters
func1

Al ejecutar el script anterior la salida es la siguiente:

$ ./script.sh
Number of parameters: 5
You
can
see
five
parameters
Number of parameters: 2
two
parameters
Number of parameters: 0

Si aprovechamos la expansión del parámetro $@ la anterior función tb se puede implementar de la siguiente manera. La ejecución de este script da el mismo resultado que el anterior:

#!/bin/bash
func1() {
    echo "Number of parameters: $#"
    for param in $@;do
        echo "$param"
    done
}
func1 You can see five parameters
func1 two parameters
func1

Valores de retorno de la función

El valor de retorno de la función se devuelve por defecto en la variable $?. Las reglas que definen el valor que tomará dicha variable a la salida de la función son las siguientes:

  • Si no existe ninguna llamada a return, el valor de retorno de la función será el valor del último comando que se ha ejecutado.
  • Si se usa return sin argumento numérico, ocurre igual que en caso anterior, el valor de retorno es el de último comando ejecutado.
  • Si se usa un valor numérico, ese es el valor de retorno de la función.

Hacer notar los siguientes dos puntos:

  • Por convención en Unix, los comandos suelen devolver un valor de cero en caso de que la ejecución del mismo haya tenido éxito.
  • El mecanismo de retorno de valores de ejecución de Bash nada más que permite valores numéricos. Para devolver cadenas, hay que usar otras construcciones.

Veamos un ejemplo de todo lo anterior

#!/bin/bash
RANDOM_NAME="/tmp/$(cat /dev/urandom | LC_ALL="C" tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"
func1() {
    cp -f  "$RANDOM_NAME" "$RANDOM_NAME_1" 2> /dev/null
}
func2() {
    cp -f  "$RANDOM_NAME" "$RANDOM_NAME_1" 2> /dev/null
    return
}
func3() {
    return 10
}
func1
echo $?
func2
echo $?
func3
echo $?

El script anterior va a generar un nombre aleatorio y se definen tres funciones:

  • func1 va a realizar una operación que va a fallar, copiar un fichero que no existe. Como es el último comando que se ejecuta en la función, su valor de retorno será lo que devuelva la función a través de $?.
  • func2 Utiliza un return sin parámetros, en este caso el valor de retorno será el del comando anterior, que vuelve a ser el cp que falla y es lo que retorna.
  • func3 Por último, la función en este caso, retorna directamente el valor 10.

Resumen

Este es un tutorial muy básico sobre el uso de funciones en bash que permite empezar a usar las mismas en nuestros scripts. He dejado algunos puntos sin tocar, para los que tengáis curiosidad, se puede consultar el manual de bash y el enlace sobre shell scripting de la sección referencia. Para una próxima entrada me gustaría tratar de:

  • Como definir las funciones con una sola línea.
  • Como devolver cadenas desde una función.
  • Cuál es la mejor manera de agrupar funciones para usarlas como una librería.
  • Redirección de la salida de una función

Referencias

No hay comentarios: