1. Por qué funciones
Un script de cincuenta líneas sin funciones se puede leer de arriba abajo. Uno de doscientas ya no: las mismas tres líneas para mostrar un error aparecen en diez lugares, y para entender qué hace hay que leerlo entero. Las funciones resuelven tres problemas a la vez:
| Problema | Cómo lo resuelve una función |
|---|---|
| Repetición | El código se escribe una vez y se llama desde donde haga falta. Si hay que cambiarlo, se cambia en un lugar. |
| Legibilidad | Un nombre como comprobar_espacio explica qué hace un bloque sin tener que leerlo. El programa principal se lee como una lista de pasos. |
| Aislamiento | Cada función recibe sus datos como argumentos y usa sus propias variables locales, sin interferir con el resto. |
2. Definir y llamar
Una función se define con su nombre, un par de paréntesis vacíos y un bloque entre llaves:
saludar() {
echo "Hola, $USER"
echo "Hoy es $(date +%A)"
}
Se llama escribiendo su nombre, como cualquier comando: saludar. Los paréntesis de la definición no llevan nada dentro ni se escriben al llamarla; solo indican que lo que sigue es el cuerpo de una función. Bash admite también la forma function saludar { … }, heredada de otra shell, pero la de paréntesis es la estándar y la que usa este curso, como establecimos en el tema 4.
Bash lee y ejecuta un script de arriba abajo, y una función debe estar definida antes de que se ejecute la línea que la llama:
$ bash -c 'saludar; saludar() { echo hola; }'
bash: line 1: saludar: command not found
Por eso las funciones se escriben al comienzo del script, después de la configuración y antes del programa principal, como en la estructura del tema 2. Una función sí puede llamar a otra definida más abajo, siempre que la llamada ocurra después de que ambas existan.
3. Los argumentos de una función
Una función recibe argumentos igual que un script: se escriben después del nombre al llamarla, y dentro de ella se leen con $1, $2 y los demás. $# es la cantidad, y "$@", todos juntos. Mientras se ejecuta la función, esas variables son las suyas; al volver, se recuperan las del script.
$ mostrar() { echo "Recibí $# argumentos; el primero es «$1»"; }
$ mostrar "informe final.pdf" otro
Recibí 2 argumentos; el primero es «informe final.pdf»
Hay una excepción: $0 sigue siendo el nombre del script, no el de la función. El nombre de la función que se está ejecutando está en ${FUNCNAME[0]}, útil para mensajes de error.
Una buena costumbre es copiar los argumentos a variables locales con nombre en la primera línea de la función. $1 y $2 no dicen qué contienen; origen y destino, sí:
copiar_seguro() {
local origen="$1" destino="$2"
if [[ -e "$destino" ]]; then
echo "No sobrescribo $destino" >&2
return 1
fi
cp -- "$origen" "$destino"
}
copiar_seguro informe.pdf /respaldos/informe.pdf
return 1 termina la función con código 1, de la misma forma que exit 1 terminaría el script. Lo veremos en detalle en el tema 19.
4. Todas las variables son globales
En la mayoría de los lenguajes, las variables creadas dentro de una función existen solo dentro de ella. En Bash no: toda variable es global, se cree donde se cree, salvo que se declare como local. Una función que usa una variable con el mismo nombre que el script la pisa sin aviso:
$ contar() { for i in 1 2 3; do :; done; }
$ for i in a b; do contar; echo "i vale $i"; done
i vale 3
i vale 3
El bucle exterior esperaba a y b, pero la función usó también una variable llamada i, que es la misma. Errores así son difíciles de encontrar, porque cada parte, mirada por separado, es correcta.
5. local
La palabra local, dentro de una función, declara variables que existen solo mientras se ejecuta la función. Si fuera de ella había una variable con el mismo nombre, queda intacta, y se recupera al volver:
$ contar() { local i; for i in 1 2 3; do :; done; }
$ for i in a b; do contar; echo "i vale $i"; done
i vale a
i vale b
La regla práctica es sencilla: toda variable que se usa dentro de una función se declara con local, incluidas las de los bucles. Solo quedan globales las que la función debe compartir a propósito con el resto del script, y conviene que eso sea la excepción y esté comentado.
| Forma | Efecto |
|---|---|
local x | Variable local, sin valor. |
local x="valor" y=2 | Varias variables locales con valor. |
local -r MAX=10 | Local y de solo lectura. |
local -a lista=() | Un array local. |
local -A tabla=() | Un array asociativo local. |
declare x dentro de una función | También crea una variable local. |
local con una sustitución de comandos
Como vimos en el tema 10, en local salida=$(comando) el código de salida es el de local, que siempre tiene éxito: si comando falla, nadie se entera, ni siquiera set -e. Se declara en una línea y se asigna en la siguiente: local salida y luego salida=$(comando). ShellCheck lo señala con SC2155.
6. El ámbito dinámico
Las variables locales de Bash tienen una particularidad: una función que llama a otra le deja ver sus variables locales. Esto se llama ámbito dinámico, y es distinto de lo que hacen casi todos los lenguajes modernos:
$ x=global
$ mostrar() { echo "mostrar ve x=$x"; }
$ externa() { local x=interna; mostrar; }
$ externa
mostrar ve x=interna
$ mostrar
mostrar ve x=global
mostrar ve una x u otra según quién la llamó. Se puede aprovechar, pero hace que el comportamiento de una función dependa de su contexto, y eso la vuelve difícil de entender. La recomendación es no depender de él: una función debe recibir lo que necesita como argumentos, y no leer variables locales de quien la llama.
7. Las funciones son comandos
Para Bash, una función es un comando más. Eso significa que todo lo que se puede hacer con un comando se puede hacer con una función: usarla como condición, redirigir su salida, conectarla a una tubería o ejecutarla en segundo plano.
if hay_espacio "$destino" 500; then … # como condición, por su código
generar_informe > informe.txt # redirigir toda su salida
listar_respaldos | sort -r | head -5 # en una tubería
comprimir_registros & # en segundo plano
copiar_seguro a b || echo "No se copió" >&2 # con || y &&
Como en todo comando, el código de salida de una función es el de su último comando, salvo que use return. Y su salida es todo lo que escriben los comandos de su interior. Estas dos propiedades son las que se usan para devolver resultados, el tema siguiente.
Cuando una función tiene el mismo nombre que un comando, la función gana, como vimos en el tema 3. Si dentro de ella hay que llamar al comando original, se usa command: una función rm() { command rm -i "$@"; } agrega una confirmación y luego llama al rm verdadero, sin llamarse a sí misma infinitamente.
8. La función main
Una estructura muy extendida pone todo el programa principal también dentro de una función, llamada por convención main, y la última línea del script la llama pasándole los argumentos:
#!/usr/bin/env bash
set -euo pipefail
readonly CONFIGURACION=…
funcion_auxiliar() {
…
}
main() {
local argumento="${1:?falta el argumento}"
funcion_auxiliar "$argumento"
}
main "$@"
Tiene tres ventajas sobre dejar el programa principal suelto al final:
- Las variables del programa principal también pueden ser locales, y no quedan globales por accidente.
- El orden de las funciones deja de importar: como todo se ejecuta recién en la última línea, cualquier función puede estar definida en cualquier parte del archivo, incluso después de
main. - Un script incompleto no se ejecuta a medias. Si el archivo se trunca mientras se descarga o se copia, la línea
main "$@"no llega a existir, y no se ejecuta nada. Sinmain, se ejecutarían las primeras líneas y no el resto.
Para scripts breves no hace falta. A partir de unas cincuenta líneas, o cuando el script tiene varias funciones, conviene adoptarla.
9. Funciones de utilidad
Hay funciones que aparecen en casi todos los scripts, porque resuelven necesidades comunes. Estas son las que usará el curso de aquí en adelante:
# Mensaje de error, con el nombre del script, en la salida de errores.
error() {
printf '%s: error: %s\n' "${0##*/}" "$*" >&2
}
# Error y fin del script.
morir() {
error "$@"
exit 1
}
# Comprueba que estén instalados los programas indicados.
requerir() {
local programa
for programa in "$@"; do
command -v "$programa" > /dev/null || morir "falta el programa $programa"
done
}
# Pregunta sí o no; éxito solo si la respuesta empieza con «s».
confirmar() {
local respuesta
read -rp "$1 [s/N] " respuesta
[[ "${respuesta,,}" == s* ]]
}
"$*", a diferencia de "$@", une todos los argumentos en un solo texto, separados por espacios: es lo que conviene para armar un mensaje, de modo que error "no existe" "$archivo" muestre una sola línea. Copiar estas funciones en cada script funciona, pero no es ideal: si se mejora una, hay que mejorarla en todos. En el tema 22 veremos cómo reunirlas en una biblioteca que los scripts cargan con source.
10. Nombres y documentación
Como dijimos en el tema 4, los nombres de función van en minúsculas, con guion bajo, y empiezan con un verbo que diga qué hace: crear_respaldo, comprobar_espacio, enviar_aviso. Las funciones que responden sí o no se nombran como una pregunta: hay_espacio, es_numero, existe_usuario, de modo que if hay_espacio … se lea como una frase.
Una función que no es evidente lleva un comentario breve encima, que diga qué hace, qué argumentos recibe y qué devuelve:
# hay_espacio DIRECTORIO MEGABYTES
# Éxito si el sistema de archivos de DIRECTORIO tiene al menos MEGABYTES libres.
hay_espacio() {
local dir="$1" necesarios="$2"
local libres
libres=$(df -BM --output=avail -- "$dir" | tail -1 | tr -dc '0-9')
(( libres >= necesarios ))
}
Una función debería hacer una sola cosa. Si su nombre necesita un «y», como comprobar_y_copiar, probablemente sean dos funciones. Y si es tan larga que no se ve entera en la pantalla, conviene partirla.
11. Inspeccionar y exportar funciones
Algunos comandos permiten ver y manejar las funciones definidas:
| Comando | Efecto |
|---|---|
type nombre | Dice si nombre es una función, y la muestra. |
declare -f nombre | Muestra la definición de la función. |
declare -F | Lista los nombres de todas las funciones definidas. |
unset -f nombre | Elimina la función. |
export -f nombre | Hace que la función esté disponible en los procesos Bash hijos. |
Como las variables, las funciones no pasan a los procesos hijos salvo que se exporten. export -f es útil en un caso concreto: cuando un comando externo debe ejecutar una función del script, como xargs o find -exec, que lanzan un Bash nuevo con bash -c:
procesar() { echo "Procesando $1"; }
export -f procesar
find . -name '*.log' -exec bash -c 'procesar "$1"' _ {} \;
12. Un script completo
Este script crea un respaldo comprimido de un directorio, con la fecha y la hora en el nombre, y borra los respaldos de ese mismo directorio con más de un número de días. Está organizado en funciones: cada una hace una cosa, y main se lee como una descripción del procedimiento.
#!/usr/bin/env bash
#
# respaldar — crea un respaldo comprimido de un directorio y borra los antiguos.
#
# Uso: respaldar ORIGEN [DESTINO] (DESTINO por defecto: ~/respaldos)
# Entorno: DIAS días que se conservan los respaldos (14)
# Salida: 0 si se creó el respaldo; 1 ante un error; 2 si el uso es incorrecto.
#
set -euo pipefail
readonly DIAS="${DIAS:-14}"
error() {
printf '%s: error: %s\n' "${0##*/}" "$*" >&2
}
morir() {
error "$@"
exit 1
}
uso() {
echo "Uso: ${0##*/} ORIGEN [DESTINO]" >&2
exit 2
}
requerir() {
local programa
for programa in "$@"; do
command -v "$programa" > /dev/null || morir "falta el programa $programa"
done
}
# crear_respaldo ORIGEN DESTINO — empaqueta ORIGEN en DESTINO, con fecha y hora.
crear_respaldo() {
local origen="$1" destino="$2"
local nombre
nombre="$(basename "$origen")-$(date +%Y%m%d-%H%M%S).tar.gz"
tar -czf "$destino/$nombre" -C "$(dirname "$origen")" "$(basename "$origen")"
echo "Creado $destino/$nombre"
}
# borrar_antiguos DESTINO PREFIJO — borra los respaldos de PREFIJO con más de DIAS días.
borrar_antiguos() {
local destino="$1" prefijo="$2"
local borrados
borrados=$(find "$destino" -maxdepth 1 -name "$prefijo-*.tar.gz" -mtime +"$DIAS" -print -delete | wc -l)
echo "Respaldos de más de $DIAS días borrados: $borrados"
}
main() {
(( $# >= 1 && $# <= 2 )) || uso
local origen="${1%/}" # sin la barra final, si la tiene
local destino="${2:-$HOME/respaldos}"
[[ -d "$origen" ]] || morir "no existe el directorio $origen"
requerir tar find
mkdir -p -- "$destino"
crear_respaldo "$origen" "$destino"
borrar_antiguos "$destino" "$(basename "$origen")"
}
main "$@"
$ respaldar ~/proyecto/
Creado /home/ana/respaldos/proyecto-20261014-121811.tar.gz
Respaldos de más de 14 días borrados: 1
$ respaldar; echo "código $?"
Uso: respaldar ORIGEN [DESTINO]
código 2
$ respaldar ~/no-existe
respaldar: error: no existe el directorio /home/ana/no-existe
maincuenta la historia: validar, comprobar requisitos, preparar el destino, crear el respaldo, borrar los viejos. Los detalles de cada paso están en su función, y quien lee puede detenerse en el nivel que le interese.- Cada función recibe lo que necesita como argumentos y declara sus variables con
local. La única global que usan esDIAS, una constante de configuración de solo lectura. local nombrey la asignación están separadas, para no ocultar un fallo de la sustitución de comandos, por la advertencia de la sección 5.-C "$(dirname "$origen")"hace quetarse sitúe en el directorio padre, de modo que dentro del archivo las rutas empiecen con el nombre del directorio respaldado y no con la ruta completa. Al extraerlo, se creaproyecto/y nohome/ana/proyecto/.- El patrón de
findincluye el prefijo, de modo que en un mismo destino pueden convivir respaldos de varios directorios sin que la limpieza de uno borre los de otro.
13. Errores frecuentes
| Escribiste | Qué pasa | Corrección |
|---|---|---|
| Llamar a la función antes de definirla | command not found | Definir antes, o usar la estructura con main. |
saludar() al llamarla | Bash lo toma como el comienzo de una nueva definición. | saludar, sin paréntesis. |
Variables sin local dentro de una función | Pisan variables del script con el mismo nombre. | local para todas las variables de la función. |
local x=$(comando) | El fallo de comando pasa inadvertido. | local x y luego x=$(comando). |
exit dentro de una función para terminarla | Termina el script entero. | return. |
Usar $0 para el nombre de la función | Da el nombre del script. | ${FUNCNAME[0]}. |
ls() { ls -l "$@"; } | La función se llama a sí misma sin fin. | command ls -l "$@". |
| Una función que lee variables locales de quien la llama | Su comportamiento depende del contexto. | Pasarle los datos como argumentos. |
14. Resumen
- Una función es un bloque de comandos con nombre:
nombre() { …; }. Se llama como cualquier comando, sin paréntesis. - Debe estar definida antes de que se ejecute la llamada; por eso las funciones van al comienzo, o se usa una función
main. - Recibe argumentos en
$1,$2,$#y"$@";$0sigue siendo el script y${FUNCNAME[0]}es la función. - En Bash, toda variable es global salvo que se declare con
local; dentro de una función, todas sus variables deberían serlo. local xyx=$(cmd)van en líneas separadas para no ocultar el código de salida.- Con el ámbito dinámico, una función ve las variables locales de quien la llama; conviene no depender de eso.
- Una función es un comando: se usa en condiciones, redirecciones, tuberías y en segundo plano. Su código de salida es el de su último comando.
- La estructura con
mainal final ymain "$@"vuelve locales las variables del programa principal y evita ejecutar un script incompleto. - Funciones como
error,morir,requeriryconfirmarse reutilizan en casi todos los scripts. - Los nombres empiezan con un verbo, o son una pregunta si la función responde sí o no; cada función hace una sola cosa.
15. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué tres problemas resuelven las funciones?
La repetición, porque el código se escribe una vez; la legibilidad, porque un nombre explica un bloque; y el aislamiento, porque cada función recibe sus datos y usa sus propias variables.
2. ¿Por qué falla saludar; saludar() { echo hola; }?
Porque Bash ejecuta de arriba abajo, y cuando llega a la llamada la función todavía no está definida.
3. Dentro de una función, ¿qué contienen $1 y $0?
$1 es el primer argumento que recibió la función. $0 sigue siendo el nombre del script; el de la función está en ${FUNCNAME[0]}.
4. ¿Qué ocurre si una función usa una variable i sin declararla con local, y el script también usa i?
Es la misma variable: la función modifica la del script. Si el script la usaba como variable de un bucle, su valor queda alterado.
5. ¿Por qué se recomienda escribir local x y x=$(cmd) en dos líneas?
Porque en local x=$(cmd) el código de salida es el de local, que siempre tiene éxito, y el fallo de cmd queda oculto.
6. ¿Qué es el ámbito dinámico?
Que una función ve las variables locales de la función que la llamó. Así, el valor que ve una función depende de quién la llama, no solo de dónde está escrita.
7. ¿Cuál es el código de salida de una función sin return?
El de su último comando ejecutado.
8. Menciona dos ventajas de terminar un script con main "$@".
Las variables del programa principal pueden ser locales; el orden de definición de las funciones deja de importar; y un script truncado no se ejecuta a medias, porque la llamada a main está al final.
9. ¿Por qué error usa "$*" y no "$@"?
Porque "$*" une todos los argumentos en un solo texto, que es lo que necesita printf para el %s del mensaje. Con "$@", cada argumento sería un dato separado y printf repetiría el formato.
10. ¿Cómo se escribe una función ls que llame al ls real con una opción agregada?
ls() { command ls -l "$@"; }. command hace que se ejecute el comando y no la función, que de otro modo se llamaría a sí misma indefinidamente.
11. ¿Para qué sirve export -f?
Para que una función esté disponible en los procesos Bash hijos, por ejemplo los que lanzan find -exec bash -c o xargs.
Para practicar
- Escribe una función
lineaque reciba un carácter y una cantidad y dibuje una línea con ese carácter repetido, por ejemplolinea = 40. - Reproduce el problema de la variable
icompartida y corrígelo conlocal. - Escribe la función
hay_espaciode la sección 10 y úsala en unifpara comprobar si tu directorio personal tiene al menos 1000 MB libres. - Toma uno de los scripts de temas anteriores, por ejemplo
comprobar-respaldodel tema 11, y reorganízalo con funciones y una funciónmain. - Crea un archivo con las funciones de utilidad de la sección 9 y cárgalo con
sourcedesde un script de prueba (adelanto del tema 22). - Prueba
respaldarcon un directorio propio, y comprueba contar -tzfque las rutas dentro del archivo empiezan con el nombre del directorio.