🔎 Buscar

💻 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.

Wiki / Apuntes📖 Contenido

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 bash localiza bash en el PATH (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
"..." 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 *.log no coincide, bash pasa el literal *.log. Con nullglob (o set -f) cambia el comportamiento. Cuidado al borrar con rm usando 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: usa echo para «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 -e con trampas: un comando en una condición if, 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

Estudio · Recursos de todo el mundo (inglés, chino, japonés, español, francés, ruso…) curados y traducidos al español.