← Automatización con Scripts en Bash Bloque 5 · Funciones y argumentos · Tema 19 de 42

Tema 19

Devolver resultados de una función

En otros lenguajes, una función devuelve un valor con return y quien la llama lo usa directamente. En Bash, return solo devuelve un número entre 0 y 255, que es un código de salida, no un dato. Para devolver un texto, un número grande o una lista, hay que recurrir a otros mecanismos, cada uno con sus ventajas y sus trampas. Este tema los presenta todos y da un criterio para elegir.

1. Dos clases de resultado

Cuando una función termina, puede tener que comunicar dos cosas distintas, y Bash tiene un canal para cada una:

ResultadoEjemploCanal
Estado: ¿funcionó?, ¿se cumple?¿Hay espacio en el disco? ¿Se pudo copiar?El código de salida, con return.
Datos: un valor calculadoEl tamaño de un directorio, un nombre de archivo, una lista.La salida estándar, capturada con $( ), o una variable.

Confundir los dos canales es el origen de la mayoría de los errores con funciones en Bash. La regla general: el código de salida es para el éxito o el fracaso; los datos van por otro lado.

2. return: el estado

return termina la función y fija su código de salida, como exit lo hace para el script. Sin return, la función termina al llegar al final, con el código de su último comando.

FormaEfecto
return 0Termina indicando éxito.
return 1 (u otro valor)Termina indicando un fallo.
returnTermina con el código del último comando ejecutado.
Sin returnAl llegar al final, el código del último comando.

El valor está sujeto a los mismos límites que vimos en el tema 10: entre 0 y 255, tomado módulo 256. Por eso return no sirve para devolver datos numéricos:

terminal
$ tamano() { return 300; }
$ tamano; echo $?
44

Y aunque el número entrara en el rango, devolverlo por el código de salida sería un error de diseño: 0 significaría «éxito» para if y para set -e, y cualquier otro valor, un fallo. Una función que «devuelve» un tamaño de 0 bytes con return 0 y uno de 5 con return 5 estaría diciendo que el segundo falló.

3. Funciones que responden sí o no

El uso natural de return es el de las funciones que funcionan como preguntas, cuyo resultado se usa directamente en un if. A menudo ni siquiera necesitan return: basta con que la última línea sea la prueba, y su código pasa a ser el de la función.

funciones-pregunta
# es_entero TEXTO — éxito si TEXTO es un número entero no negativo.
es_entero() {
  [[ "$1" =~ ^[0-9]+$ ]]
}

# existe_usuario NOMBRE — éxito si el usuario existe en el sistema.
existe_usuario() {
  id -u "$1" > /dev/null 2>&1
}

# en_horario_laboral — éxito de lunes a viernes, de 9 a 18.
en_horario_laboral() {
  local dia hora
  dia=$(date +%u)
  hora=$(date +%-H)
  (( dia <= 5 && hora >= 9 && hora < 18 ))
}

if es_entero "$dias" && existe_usuario "$usuario"; then …
en_horario_laboral || { echo "Fuera de horario: no se despliega" >&2; exit 0; }

Cuando la función tiene varios pasos, return permite salir antes con la respuesta, del mismo modo que las cláusulas de guarda del tema 12:

salir antes con la respuesta
# respaldo_valido ARCHIVO — éxito si el archivo existe, no está vacío y tar puede leerlo.
respaldo_valido() {
  local archivo="$1"
  [[ -s "$archivo" ]] || return 1
  tar -tzf "$archivo" > /dev/null 2>&1 || return 1
  return 0
}

4. Datos por la salida: echo y $( )

La forma más común de devolver un dato es que la función lo escriba en su salida estándar, y que quien la llama lo capture con una sustitución de comandos, como haría con la salida de cualquier programa:

devolver un dato por la salida
# extension ARCHIVO — escribe la extensión en minúsculas, o nada si no tiene.
extension() {
  local nombre="${1##*/}"
  [[ "$nombre" == ?*.* ]] || return 0
  local ext="${nombre##*.}"
  printf '%s\n' "${ext,,}"
}

ext=$(extension "/fotos/Playa.JPG")      # ext vale «jpg»

Es el mecanismo más natural en Bash, porque es el mismo que usan todos los comandos, y tiene ventajas reales: la función puede usarse en una tubería (extension "$f" | tr a-z A-Z), en una redirección o directamente en la terminal, y no necesita ninguna variable compartida. Su costo es que $( ) ejecuta la función en una subshell, un proceso aparte, con dos consecuencias que explican las dos secciones siguientes.

Para escribir el resultado se prefiere printf '%s\n' a echo, por la razón que vimos en el tema 4: con echo, un valor como -n se interpretaría como opción.

5. Los mensajes, a la salida de errores

Si la salida estándar es el canal de los datos, cualquier otra cosa que la función escriba allí se mezcla con el resultado. Un mensaje de progreso, inofensivo cuando se llama a la función directamente, contamina el valor capturado:

terminal
$ calcular() { echo "Calculando..."; echo 42; }
$ resultado=$(calcular); declare -p resultado
declare -- resultado=$'Calculando...\n42'
$ calcular() { echo "Calculando..." >&2; echo 42; }
$ resultado=$(calcular); declare -p resultado
Calculando...
declare -- resultado="42"

Con el mensaje en la salida de errores, el usuario lo sigue viendo en pantalla, pero la variable recibe solo el dato. Es la misma regla que venimos aplicando a los scripts, ahora en pequeño: en una función que devuelve datos por la salida, todo lo que no sea el dato va a >&2, y los comandos que escriben cosas que no interesan se silencian con > /dev/null.

6. Lo que se pierde dentro de $( )

La segunda consecuencia de la subshell es que los cambios que la función haga en variables globales no sobreviven. Es el mismo fenómeno que vimos con las tuberías en el tema 16:

terminal
$ contador=0
$ siguiente() { contador=$(( contador + 1 )); echo "$contador"; }
$ n=$(siguiente); echo "n=$n contador=$contador"
n=1 contador=0
$ siguiente > /dev/null; echo "contador=$contador"      # sin $( ): en la shell actual
contador=1

El dato se devolvió bien, pero el contador global no avanzó, porque se incrementó en la copia de la subshell. Una función pensada para usarse con $( ) no debe depender de modificar variables globales. Si necesita hacerlo, hay que usar uno de los mecanismos de las secciones 8 a 10.

La subshell tiene además un costo: crear un proceso por cada llamada. En una llamada suelta no importa; dentro de un bucle de miles de vueltas, sí, como vimos con basename en el tema 6.

7. Estado y datos a la vez

Una función puede devolver un dato por la salida y, a la vez, indicar con su código si lo consiguió. Como vimos en el tema 5, una asignación con $( ) toma el código de la función, y eso permite capturar y comprobar en la misma línea:

dato y estado en una sola llamada
# ip_de HOST — escribe la primera dirección IP del host; falla si no se resuelve.
ip_de() {
  local ip
  ip=$(getent hosts "$1" | awk '{ print $1; exit }')
  [[ -n "$ip" ]] || return 1
  printf '%s\n' "$ip"
}

if ip=$(ip_de servidor.example.com); then
  echo "La dirección es $ip"
else
  echo "No se pudo resolver el nombre" >&2
fi

Aquí vuelve la advertencia del tema 18: si la variable que recibe el dato se declara con local en la misma línea, se pierde el código. Dentro de una función, se escribe local ip y, en la línea siguiente, if ip=$(ip_de …); then.

8. Una variable global como resultado

La alternativa más simple a la subshell es que la función deje el resultado en una variable global acordada. Es rápida, porque no crea procesos, y permite devolver varios valores o modificar el estado del script:

un resultado en una variable global
# medir_disco DIRECTORIO — deja en DISCO_USO y DISCO_LIBRE el porcentaje usado y los MB libres.
medir_disco() {
  local linea
  linea=$(df -BM --output=pcent,avail -- "$1" | tail -1)
  read -r DISCO_USO DISCO_LIBRE <<< "${linea//[%M]/}"
}

medir_disco /var
echo "Uso: $DISCO_USO %, libres: $DISCO_LIBRE MB"

El inconveniente es el de todas las variables globales: la función y quien la llama deben conocer el nombre, y cualquier otra parte del script puede pisarlo. Si se usa este mecanismo, conviene que los nombres sean inequívocos, por ejemplo con el nombre de la función como prefijo, como DISCO_USO, y documentarlo en el comentario de la función.

9. Referencias: declare -n

Bash ofrece una forma más limpia: que quien llama elija el nombre de la variable donde quiere el resultado, y se lo pase a la función como argumento. Dentro de la función, local -n (o declare -n) crea una referencia (nameref): una variable que es otro nombre para la variable indicada. Todo lo que se asigna a la referencia se asigna a la variable original.

terminal
$ obtener_fecha() { local -n _destino="$1"; _destino=$(date +%F); }
$ obtener_fecha hoy
$ echo "$hoy"
2026-10-14

La función no necesita conocer el nombre hoy: lo recibe. Y como no hay subshell, la asignación queda en la variable del llamador. Las referencias funcionan también con arrays, lo que las convierte en la forma más directa de devolver una lista:

llenar un array del llamador
# registros_viejos DIRECTORIO DIAS ARRAY — llena ARRAY con los .log de más de DIAS días.
registros_viejos() {
  local dir="$1" dias="$2"
  local -n _lista="$3"
  mapfile -t _lista < <(find "$dir" -name '*.log' -mtime +"$dias")
}

registros_viejos /var/log/app 30 viejos
echo "Hay ${#viejos[@]} registros para comprimir"
El choque de nombres

Si la variable que pasa quien llama tiene el mismo nombre que una variable local de la función, la referencia apunta a la local, y el resultado se pierde. Por ejemplo, si la función declara local valor y se la llama con funcion valor, la asignación va a la valor local. Por eso las referencias y las variables locales de estas funciones suelen llevar un prefijo poco probable, como el guion bajo de _destino y _lista. Las referencias existen desde Bash 4.3.

10. printf -v

Para el caso frecuente de dar formato a un valor y guardarlo, printf tiene una opción que escribe el resultado en una variable en lugar de en la salida, sin subshell:

dar formato y guardar, sin crear procesos
printf -v nombre 'respaldo-%s-%03d.tar.gz' "$equipo" "$numero"   # respaldo-web01-007.tar.gz
printf -v hoy '%(%Y-%m-%d)T' -1                                  # la fecha, sin llamar a date

El formato %(…)T da formato a una fecha con los mismos códigos que date; el argumento -1 significa «ahora». Dentro de una función, printf -v combinado con una referencia, printf -v "$1" …, es otra forma de devolver un texto formateado en la variable que elija el llamador.

11. Devolver una lista

Una función no puede devolver un array por la salida, porque la salida es texto. Hay dos formas de devolver una lista:

FormaCómoCuándo
Una referenciaLa función llena el array cuyo nombre recibe, como en la sección 9.Lo más directo, si la función es parte del script.
Por la salida, un elemento por líneaLa función escribe cada elemento en una línea, y quien llama los lee con mapfile -t.Si los elementos no pueden contener saltos de línea.
Por la salida, separados por nulosprintf '%s\0' … y mapfile -d '' -t.Si pueden contener cualquier carácter, como nombres de archivo.
una lista por la salida, separada por nulos
listar_respaldos() {
  find "$1" -maxdepth 1 -name '*.tar.gz' -print0 | sort -z
}

mapfile -d '' -t respaldos < <(listar_respaldos /respaldos)
echo "Hay ${#respaldos[@]} respaldos; el más reciente es ${respaldos[-1]}"

La versión por la salida tiene la ventaja de que la función también sirve en una tubería, por ejemplo listar_respaldos /respaldos | xargs -0 ls -l, y de que puede escribirse en cualquier lenguaje si algún día se convierte en un programa aparte.

12. Cuál usar

Necesitas devolverUsa
Si algo funcionó o se cumplereturn, o el código del último comando.
Un dato, en una función de uso general o fuera de bucles intensivosLa salida y $( ), con los mensajes a >&2.
Un dato, en un bucle de miles de vueltasUna referencia, o printf -v.
Varios datos a la vezVarias referencias, o variables globales con nombres claros.
Una listaUna referencia a un array, o la salida con mapfile.
Un dato y si se obtuvoLa salida y el código juntos: if x=$(f); then.

13. Un script completo

Este script muestra, para cada directorio que recibe, cuánto ocupa y cuántos archivos contiene, en unidades legibles, y el total al final. Usa los tres mecanismos del tema: una función que responde sí o no, una que devuelve un texto por la salida, y una que llena dos variables del llamador por referencia.

uso-directorios
#!/usr/bin/env bash
#
# uso-directorios — cuánto ocupa cada directorio y cuántos archivos contiene.
#
# Uso:    uso-directorios DIRECTORIO...
# Salida: 0 si se midieron todos; 1 si alguno no se pudo leer; 2 sin argumentos.
#
set -euo pipefail

# es_directorio_legible DIR — estado: éxito si DIR es un directorio que se puede recorrer.
es_directorio_legible() {
  [[ -d "$1" && -r "$1" && -x "$1" ]]
}

# a_legible BYTES — dato por la salida: «1.5 KiB», «2.7 MiB»…
a_legible() {
  local bytes="$1"
  local -a unidades=(B KiB MiB GiB TiB)
  local i=0 decimas=$(( bytes * 10 ))
  while (( decimas >= 10240 && i < ${#unidades[@]} - 1 )); do
    decimas=$(( decimas / 1024 ))
    i=$(( i + 1 ))
  done
  if (( i == 0 )); then
    printf '%d B\n' "$bytes"
  else
    printf '%d.%d %s\n' $(( decimas / 10 )) $(( decimas % 10 )) "${unidades[i]}"
  fi
}

# medir DIR VAR_BYTES VAR_ARCHIVOS — datos por referencia: tamaño y cantidad de archivos.
medir() {
  local dir="$1"
  local -n _bytes="$2" _archivos="$3"
  _bytes=$(du -sb -- "$dir" | cut -f1)
  _archivos=$(find "$dir" -type f | wc -l)
}

main() {
  if (( $# == 0 )); then
    echo "Uso: ${0##*/} DIRECTORIO..." >&2
    exit 2
  fi

  local dir bytes archivos total=0 errores=0
  for dir in "$@"; do
    if ! es_directorio_legible "$dir"; then
      echo "${0##*/}: no es un directorio legible: $dir" >&2
      errores=$(( errores + 1 ))
      continue
    fi
    medir "$dir" bytes archivos
    total=$(( total + bytes ))
    printf '%10s  %6d archivos  %s\n' "$(a_legible "$bytes")" "$archivos" "$dir"
  done
  printf '%10s  en total\n' "$(a_legible "$total")"

  (( errores == 0 ))
}

main "$@"
terminal
$ uso-directorios ~/documentos ~/fotos ~/no-existe; echo "código $?"
  48.3 MiB     412 archivos  /home/ana/documentos
   1.2 GiB    2210 archivos  /home/ana/fotos
uso-directorios: no es un directorio legible: /home/ana/no-existe
   1.2 GiB  en total
código 1

14. Errores frecuentes

EscribisteQué pasaCorrección
return "$total" para devolver un númeroSe trunca módulo 256 y se interpreta como éxito o fallo.Escribirlo por la salida, o una referencia.
Mensajes con echo en una función que devuelve datosSe mezclan con el resultado capturado.Los mensajes a >&2.
Modificar una global en una función llamada con $( )El cambio se pierde.Llamarla sin $( ) y devolver por referencia.
local x=$(f)El código de f se pierde.local x y luego x=$(f).
echo "$resultado" para devolver un datoFalla con valores como -n o -e.printf '%s\n' "$resultado".
Referencia con el mismo nombre que la variable del llamadorEl resultado queda en la variable local.Prefijo en los nombres internos: _destino.
$(f) dentro de un bucle de miles de vueltasUn proceso por vuelta: lento.Referencia o printf -v.

15. Resumen

16. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Por qué return no sirve para devolver el tamaño de un archivo?

Porque solo admite valores entre 0 y 255, tomados módulo 256, y porque el valor se interpreta como éxito (0) o fallo (cualquier otro), no como un dato.

2. ¿Qué código devuelve una función que no tiene return?

El de su último comando ejecutado.

3. ¿Cómo se escribe una función es_par que se pueda usar en if es_par "$n"; then?

es_par() { (( $1 % 2 == 0 )); }. La comparación aritmética es la última línea, y su código es el de la función.

4. ¿Cómo devuelve un texto una función, y cómo lo recibe quien la llama?

La función lo escribe en su salida estándar, preferentemente con printf '%s\n', y quien la llama lo captura con variable=$(función argumentos).

5. ¿Por qué un mensaje de progreso dentro de una función que devuelve datos debe ir a >&2?

Porque todo lo que la función escriba en la salida estándar queda capturado por $( ) junto con el dato. En la salida de errores, el mensaje se sigue viendo en pantalla pero no contamina el resultado.

6. Una función incrementa un contador global y se la llama con x=$(f). ¿Por qué el contador no cambia?

Porque $( ) ejecuta la función en una subshell, con una copia de las variables. El incremento ocurre en la copia, que desaparece al terminar la sustitución.

7. ¿Qué hace if ip=$(ip_de "$host"); then?

Ejecuta la función, guarda su salida en ip, y usa su código de salida como condición: entra en el then solo si la función tuvo éxito.

8. ¿Qué es una referencia creada con local -n?

Una variable que es otro nombre para la variable indicada. Todo lo que se le asigna se asigna a la original, de modo que una función puede llenar una variable cuyo nombre recibe como argumento.

9. ¿Qué pasa si una función con local -n ref="$1" también declara local valor, y se la llama con f valor?

La referencia apunta a la variable valor local de la función, no a la del llamador, y el resultado se pierde al terminar la función. Se evita usando nombres internos poco probables, como _valor.

10. ¿Qué ventaja tiene printf -v sobre var=$(printf …)?

No crea una subshell: guarda el resultado directamente en la variable, lo que es mucho más rápido dentro de bucles.

11. ¿Cómo devuelve una función una lista de nombres de archivo que pueden contener cualquier carácter?

Escribiéndolos separados por el carácter nulo, con printf '%s\0' o find -print0, y leyéndolos con mapfile -d '' -t array < <(función). O llenando un array del llamador mediante una referencia.

Para practicar

  1. Escribe es_par, es_entero y es_archivo_vacio como funciones-pregunta y úsalas en condiciones.
  2. Escribe una función sin_extension que devuelva por la salida un nombre de archivo sin su extensión, y úsala en un bucle sobre *.txt.
  3. Reproduce el problema del contador dentro de $( ), y resuélvelo con una referencia.
  4. Escribe una función dividir que reciba dos números y los nombres de dos variables, y deje en ellas el cociente y el resto.
  5. Escribe una función que llene, por referencia, un array con los usuarios del sistema con UID mayor o igual que 1000.
  6. Mide con time la diferencia entre llamar diez mil veces a una función con $( ) y con printf -v o una referencia.
  7. Prueba uso-directorios con varios directorios, y agrega una columna con el tamaño medio por archivo, usando a_legible.