💻 Bash scripting profesional
Scripts robustos de bash: shebang, variables, arrays, expansión, condiciones, loops, funciones, argumentos, set -euo pipefail, trap y ejemplos de backup y deploy.
Bash scripting profesional
Un script de bash no es «una lista de comandos pegados». Un script profesional es robusto: falla cuando debe fallar, no sigue ejecutando tras un error, limpia lo que ensucia y funciona igual en cualquier servidor. Esto es lo que separa un script.sh de un incidente a las 3 de la mañana.
Shebang y permisos
La primera línea dice al kernel con qué intérprete ejecutar el script:
#!/usr/bin/env bash
#!/usr/bin/env bashlocalizabashen elPATH(más portable).- El archivo debe tener permiso de ejecución:
chmod +x script.sh
./script.sh
Variables
NOMBRE="Ana" # sin espacios alrededor del =
echo "$NOMBRE" # Ana
readonly VERSION="1.0" # constante (no se puede reasignar)
# Para leer: usa llaves ${}
echo "${NOMBRE}_sufijo" # Ana_sufijo (sin {} haría NOMBRE_sufijo)
echo "$((2 + 3))" # aritmética: 5
Variables especiales:
| Variable | Contenido |
|---|---|
$0 |
Nombre del script |
$1…$9 |
Argumentos posicionales |
$@ |
Todos los argumentos |
$# |
Número de argumentos |
$? |
Código de salida del último comando |
$$ |
PID del script |
$HOME |
Directorio home del usuario |
⚠️ Siempre entre comillas:
"$var". Sin comillas, bash divide en palabras y expande globs, y"$@"(con comillas) preserva cada argumento entero aunque tenga espacios.
Comillas simples y dobles
| Comilla | Expande $var |
Expande $(cmd) |
Caracter |
|---|---|---|---|
'...' |
No | No | literal |
"..." |
Sí | Sí | expande |
\ |
— | — | escapa |
echo "$HOME" # /home/ana (expande)
echo '$HOME' # $HOME (literal)
echo "Hola \$HOME" # Hola $HOME (escapado)
Arrays y asociativos
frutas=("manzana" "pera" "uva")
echo "${frutas[0]}" # manzana
echo "${frutas[@]}" # manzana pera uva
echo "${#frutas[@]}" # 3 (tamaño)
frutas+=("mango") # añadir
for f in "${frutas[@]}"; do
echo "$f"
done
# Asociativo (como un dict/mapa)
declare -A puertos=([web]=80 [ssh]=22 [db]=5432)
echo "${puertos[web]}" # 80
💡
"${arr[@]}"es la forma correcta de iterar: preserva elementos con espacios."${arr[*]}"los une en una sola cadena.
Expansión: globbing, brace y sustitución de comando
# Globbing: el shell expande patrones de archivos
cp /etc/nginx/*.conf /backup/
echo *.log
# Brace expansion: genera combinaciones
echo {a,b,c}{1,2} # a1 a2 b1 b2 c1 c2
mkdir -p project/{src,docs,tests}
echo {1..10} # 1 2 3 ... 10
# Command substitution: el resultado de un comando es un valor
ahora=$(date +%F) # 2026-08-26
archivos=$(ls /etc | wc -l)
⚠️ Globbing sin coincidencias: por defecto, si
*.logno coincide, bash pasa el literal*.log. Connullglob(oset -f) cambia el comportamiento. Cuidado al borrar conrmusando globs.
Condiciones: [ ] vs [[ ]]
if [ -f "$archivo" ]; then # test POSIX (portable)
echo "existe"
fi
if [[ -f $archivo && -r $archivo ]]; then # bash nativo (recomendado)
echo "existe y se puede leer"
fi
[[ ]] es nativo de bash y mucho más potente:
[[ "hola" == h* ]] # true (glob)
[[ $n -gt 10 ]] # numérico mayor
[[ -z "$var" ]] # cadena vacía
[[ -n "$var" ]] # cadena no vacía
[[ -d $dir ]] # es directorio
[[ -f $file ]] # es archivo regular
[[ -x $file ]] # es ejecutable
[[ -e $path ]] # existe
[[ $a =~ ^[0-9]+$ ]] # regex
if / for / while / case
# if
if [[ $UID -eq 0 ]]; then
echo "root"
elif [[ $UID -gt 1000 ]]; then
echo "usuario normal"
else
echo "sistema"
fi
# for sobre una lista
for i in {1..5}; do
echo "iter $i"
done
# while
contador=0
while (( contador < 3 )); do
echo "$contador"
((contador++))
done
# case
case "$1" in
start) echo "arrancando" ;;
stop) echo "parando" ;;
restart) echo "reiniciando" ;;
*) echo "uso: $0 start|stop|restart"; exit 1 ;;
esac
Funciones
# Definir
saludar() {
local nombre="$1" # 'local': variable de la función
echo "Hola, $nombre"
}
# Llamar
saludar "Ana" # Hola, Ana
💡 Las funciones reciben argumentos igual que los scripts (
$1,$@,$#). Devuelven códigos de salida (0 = éxito), no valores: usaechopara «devolver» un valor y captúralo con$(...).
Argumentos y errores
# Argumentos
echo "Script: $0, args: $#, primero: $1"
for arg in "$@"; do echo "arg: $arg"; done
# Salida de error y salida del script
if [[ ! -f "$1" ]]; then
echo "Error: archivo $1 no existe" >&2 # a stderr
exit 1 # código de error
fi
set -euo pipefail y trap
El estándar de facto para scripts robustos:
#!/usr/bin/env bash
set -euo pipefail
| Flag | Efecto |
|---|---|
set -e |
Sale del script al primer comando que falle |
set -u |
Error si usas una variable sin definir |
set -o pipefail |
Un pipe falla si falla cualquier comando de la tubería |
Y trap para limpiar en cualquier salida (Ctrl+C, error, fin):
trap 'echo "Error en línea $LINENO"; cleanup' ERR
trap cleanup EXIT # siempre al salir
trap 'echo "Ctrl+C"; exit 130' INT
cleanup() {
rm -f /tmp/temp_file_$$
}
⚠️
set -econ trampas: un comando en una condiciónif, o el último de una tubería, no dispara la salida. Conoces el flag para no sorprenderte cuando un error no detiene el script.
Script de backup robusto
#!/usr/bin/env bash
set -euo pipefail
ORIGEN="/var/www/midominio"
DESTINO="/backups"
FECHA=$(date +%Y%m%d_%H%M%S)
ARCHIVO="backup_${FECHA}.tar.gz"
if [[ ! -d "$ORIGEN" ]]; then
echo "Error: $ORIGEN no existe" >&2
exit 1
fi
mkdir -p "$DESTINO"
if tar -czf "$DESTINO/$ARCHIVO" -C "$ORIGEN" .; then
echo "Backup OK: $DESTINO/$ARCHIVO"
else
echo "Error en el backup" >&2
exit 1
fi
# Borra backups de más de 7 días
find "$DESTINO" -name "backup_*.tar.gz" -mtime +7 -delete
Script de deploy
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="/opt/miapp"
REPO="git@github.com:usuario/miapp.git"
BRANCH="${1:-main}"
# Comprobar que las herramientas existen
for cmd in git npm systemctl; do
if ! command -v "$cmd" &>/dev/null; then
echo "Falta: $cmd" >&2
exit 1
fi
done
cd "$APP_DIR"
git fetch origin
git checkout "$BRANCH"
git pull --ff-only origin "$BRANCH"
npm ci --production
npm run build
systemctl restart miapp
echo "Deploy completado en $BRANCH"
printf y read
# printf: formato de salida controlado (mejor que echo)
printf "Nombre: %s, Edad: %d\n" "Ana" 30
printf "%-10s | %5s\n" "clave" "valor"
# read: leer entrada (o desde un archivo/pipe)
read -rp "¿Seguro? (s/n): " confirmacion
[[ "$confirmacion" == "s" ]] || { echo "cancelado"; exit 1; }
# leer un archivo línea a línea
while IFS= read -r linea; do
echo "> $linea"
done < config.txt
Cheatsheet
| Necesito… | Bash |
|---|---|
| Script robusto | set -euo pipefail |
| Limpiar al salir | trap ... EXIT |
| Condición avanzada | [[ ... ]] |
| Iterar array | for x in "${arr[@]}" |
| Aritmética | $(( ... )) |
| Resultado de comando | $( ... ) |
| Valor de función | echo + $( ... ) |
| Leer línea | while IFS= read -r l; do ... done < f |
| Formatear | printf |
Para profundizar
- Bash Guide (Greg’s Wiki): la referencia avanzada definitiva.
- Google Shell Style Guide: estándares de estilo y robustez.
- ShellCheck: linter de bash; úsalo siempre.
- The Bash Hackers Wiki: expansiones y sintaxis a fondo.
- Bash Reference Manual (GNU): la documentación oficial.
- Ruta completa: Linux y terminal.