3.5. Funciones

La shell permite definir funciones con la siguiente sintaxis:

nombre_funcion() {
     # Bloque de órdenes que la componen
}

Nota

Dado que el código es interpretado línea a línea, la definición de la función debe encontrarse antes de su primera invocación.

Como puede verse, sólo se declara la función y no los argumentos que recibe. El bloque de órdenes tiene las siguientes particularidades:

  1. Las funciones se invocan de la misma manera que las órdenes:

    nombre_funcion uno dos tres
    

    de manera que «uno», «dos» y «tres» son los tres argumentos que se le proporcionan.

  2. Los argumentos proporcionados a la función son accesibles mediante las variables $1, $2, etc., es decir, exactamente igual que son accesibles los argumentos del script en el código principal. Todo lo dicho para estos argumentos es aplicable a los argumentos de la función.

  3. Dentro de la función son accesibles las variables globales, esto es, las existentes en el código principal del script (excepto las referentes a los argumentos del script, porque se sobreescriben con los valores de los argumentos de la propia función). Si se asigna valor a alguna de estas variables, el valor se propagará fuera de la función.

  4. Si se define una variable dentro de la función tal como hemos ya explicado, la variable será global. Para que sea local a la función hay que definirla usando la palabra local:

    local soy_local
    

    Nota

    Con local pueden usarse las mismas opciones que con declare. Por ejemplo, -a para definir un vector.

    Advertencia

    local no forma parte del estándar POSIX[1], pero la mayoría de las shells (con la excepción de ksh[2], pero no ide su derivada mksh) lo implementan.

  5. Vista desde fuera (o sea, vista desde el código que la invoca), una función podemos considerarla casi a todos los efectos como una orden [interna][3], por lo que:

    • Puede participar en tuberías:

      toupper() {
          tr '[:lower:]' '[:upper:]'
      }
      
      echo "Pása a mayúsculas" | toupper
      
    • Envía datos al resto del código imprimiendo en pantalla. El ejemplo escrito arriba lo ilustra: el resultado, simplemente, se manda a pantalla. Si hubiéramos querido capturarlo, habríamos hecho lo mismo que cuando capturamos salidas de órdenes:

      traduccion=$(echo "Pása a mayúsculas" | toupper)
      
    • Es capaz de devolver el buen o mal suceso de la tarea que desarrolla mediante un byte. Esta devolución se hace con la palabra return, en vez de exit, ya que esta última provocaría la salida inmediata del proceso, no de la función. Como en el caso del programa, si explícitamente no se usa return, el código de salida es el código de salida de la última instrucción que se ejecutó.

      En el código principal, su código de salida se asigna a la variable $?, exactamente igual que cuando se ejecuta una orden.

      Advertencia

      Es importante tener presente esto: las funciones de la shell no son capaces de devolver datos: sólo código de sálida, por lo que cualquier dato que se desee exportar se ha de hacer a través de la pantalla (o de un fichero, claro).

  6. Si se definen dos funciones con un mismo nombre, prevalece la última definición.

  7. Podemos anidar funciones, esto es, definir una función dentro de otra función. Si hacemos así, la función interna sólo será accesible desde la externa.

Cuando tratamos la orden interna alias, vimos que podía usarse la barra invertida o la orden interna command para evitar la ejecución del alias. Esto permitía modificar el comportamiento normal de una órden escribiendo un alias homónimo:

alias ls='ls --color=auto -F'

Pues bien, ambas alternativas también evitan la ejecución de funciones, por lo que es posible escribir wrappers también con funciones:

ls() {
   command ls -F --color=auto "$@"
}

Nota

Con independencia de que en este caso tan simple es mejor hacer el wrapper con alias, observe una sutil diferencia entre ambos: la definición de alias entiende ls como el comando a secas, mientras que al ejecutarse la función, ls se tomará como su definición en ese momento, que es la propia función, por lo que se obtendría una recursión infinita si no usáramos command.

Ejemplo

Definir una función que calcule la clase a la que pertenece una IP determinada

#!/bin/sh

# Determina si una IP es correcta
#  $1: La IP
es_ip() {
   # En realidad sólo comprobamos si hay
   # cuatro números separados por 3 puntos.
   echo "$1" | grep -Eq '^[0-9]+(\.[0-9]){3}$'
}

# Calcula la clase de red.
# Parámetros:
#   $1: La IP.
# Devuelve error si la IP es incorrecta.
netclass() {
   es_ip $1 || return 1

   local octeto=${1%%.*}

   if [ $octeto -lt 128 ]; then
      echo "A"
   elif [ $octeto -lt 192 ]; then
      echo "B"
   elif [ $octeto -lt 224 ]; then
      echo "C"
   elif [ $octeto -lt 240 ]; then
      echo "D"
   else
      echo "E"
   fi
}

clase=$(netclass $1)
res=$?

if [ $res -ne 0 ]; then
   echo "Imposible obtener la clase de $1" >&2
   exit $res
fi

echo "La clase de $1 es $clase."

El código de ejemplo no tiene mucho misterio: nos quedamos sólo con el primer byte , comprobamos que es un número y, después, dependiendo de cuál sea, imprimimos por pantalla su clase. En un lenguaje de programación normal, la letra que identifica la clase sería el valor de retorno, pero en la shell esto no puede ser así: el valor de retorno tiene forzosamente que ser un byte y sólo sirve para indicar al código invocante si se ha podido realizar el cálculo o no. Por esa razón, devolvemos valores distintos de cero al producirse errores. Cuando estos no se producen, se devuelve la valor de retorno de la última orden, que es echo y no falla nunca.

Obsérvese también cómo podemos usar la función para retener la clase: con una subshell que captura la salida. Después no tenemos más que comprobar el valor de retorno para ver si se produjo o no error; y en caso de que no, continuar nuestro programa sabiendo que tenemos el cálculo almacenado en la variable clase.

Notas al pie