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:
| Resultado | Ejemplo | Canal |
|---|---|---|
| Estado: ¿funcionó?, ¿se cumple? | ¿Hay espacio en el disco? ¿Se pudo copiar? | El código de salida, con return. |
| Datos: un valor calculado | El 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.
| Forma | Efecto |
|---|---|
return 0 | Termina indicando éxito. |
return 1 (u otro valor) | Termina indicando un fallo. |
return | Termina con el código del último comando ejecutado. |
Sin return | Al 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:
$ 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.
# 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:
# 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:
# 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:
$ 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:
$ 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:
# 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:
# 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.
$ 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:
# 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"
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:
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:
| Forma | Cómo | Cuándo |
|---|---|---|
| Una referencia | La 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ínea | La 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 nulos | printf '%s\0' … y mapfile -d '' -t. | Si pueden contener cualquier carácter, como nombres de archivo. |
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 devolver | Usa |
|---|---|
| Si algo funcionó o se cumple | return, o el código del último comando. |
| Un dato, en una función de uso general o fuera de bucles intensivos | La salida y $( ), con los mensajes a >&2. |
| Un dato, en un bucle de miles de vueltas | Una referencia, o printf -v. |
| Varios datos a la vez | Varias referencias, o variables globales con nombres claros. |
| Una lista | Una referencia a un array, o la salida con mapfile. |
| Un dato y si se obtuvo | La 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.
#!/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 "$@"
$ 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
es_directorio_legibledevuelve solo un estado, y se usa conif !. Su única línea es la prueba, y su código es el de la función.a_legibledevuelve un texto por la salida, y se usa con$( )directamente como argumento deprintf. Trabaja en décimas para conservar un decimal con aritmética entera, la técnica del tema 7, y como no escribe nada más, la captura recibe solo el dato.medirdevuelve dos valores a la vez, cosa que la salida no permite con comodidad, y lo hace por referencia:mainle pasa los nombresbytesyarchivos, y la función los llena. Sus referencias llevan guion bajo para no chocar con esas variables, que justamente se llaman casi igual.maindevuelve el estado del script con su última línea, como en los temas anteriores.
14. Errores frecuentes
| Escribiste | Qué pasa | Corrección |
|---|---|---|
return "$total" para devolver un número | Se 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 datos | Se 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 dato | Falla con valores como -n o -e. | printf '%s\n' "$resultado". |
| Referencia con el mismo nombre que la variable del llamador | El resultado queda en la variable local. | Prefijo en los nombres internos: _destino. |
$(f) dentro de un bucle de miles de vueltas | Un proceso por vuelta: lento. | Referencia o printf -v. |
15. Resumen
- Una función puede devolver un estado, por su código de salida, o datos, por otro canal. No hay que mezclarlos.
return ntermina la función con el códigon, entre 0 y 255; no sirve para devolver datos.- Las funciones-pregunta terminan con una prueba, y se usan directamente en
if,&&y||. - Un dato se devuelve escribiéndolo con
printfy se recibe convar=$(función). - En esas funciones, todo lo que no sea el dato va a la salida de errores.
$( )ejecuta la función en una subshell: los cambios en variables globales se pierden, y cada llamada crea un proceso.if x=$(f); thenrecibe el dato y comprueba el estado a la vez.- Una variable global acordada es rápida, pero acopla la función con el resto del script.
local -n ref="$1"crea una referencia a la variable que elige el llamador; sirve para textos, varios valores y arrays.printf -v var formato …guarda un texto formateado sin subshell;%(…)Tda formato a fechas.- Una lista se devuelve por referencia, o por la salida, línea por línea o separada por nulos, para leerla con
mapfile.
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
- Escribe
es_par,es_enteroyes_archivo_vaciocomo funciones-pregunta y úsalas en condiciones. - Escribe una función
sin_extensionque devuelva por la salida un nombre de archivo sin su extensión, y úsala en un bucle sobre*.txt. - Reproduce el problema del contador dentro de
$( ), y resuélvelo con una referencia. - Escribe una función
dividirque reciba dos números y los nombres de dos variables, y deje en ellas el cociente y el resto. - Escribe una función que llene, por referencia, un array con los usuarios del sistema con UID mayor o igual que 1000.
- Mide con
timela diferencia entre llamar diez mil veces a una función con$( )y conprintf -vo una referencia. - Prueba
uso-directorioscon varios directorios, y agrega una columna con el tamaño medio por archivo, usandoa_legible.