Saltar al contenido

Fundamentos de shell scripting para desarrolladores modernos

Patrones prácticos de shell scripting para automatizar el desarrollo, desde construcciones básicas de Bash hasta scripts robustos de producción.

4 min de lectura
Ventana de terminal mostrando un script de shell bien estructurado con resaltado de sintaxis

El shell scripting ocupa un terreno incómodo. Es demasiado importante como para ignorarlo: todo pipeline de despliegue, toda configuración de desarrollo y todo flujo de automatización lo atraviesa. Pero también está lleno de trampas que hacen que scripts «simples» fallen en silencio en producción.

La diferencia entre un script que funciona en tu máquina y uno que funciona en cualquier entorno se reduce a un puñado de prácticas fundamentales que la mayoría de los desarrolladores se saltan.

Empieza bien cada script

Las primeras tres líneas de cualquier script de Bash determinan si falla de forma ruidosa o corrompe datos en silencio.

shbash
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

Qué hace cada línea:

  • set -e — Sale inmediatamente ante cualquier fallo de comando (en lugar de continuar con un estado inconsistente)
  • set -u — Trata las variables no definidas como errores (en lugar de expandirlas silenciosamente a una cadena vacía)
  • set -o pipefail — Un pipeline falla si falla cualquier comando dentro de él (en lugar de comprobar solo el último)
  • IFS=$'\n\t' — Evita la división de palabras por espacios en nombres de archivo
shbash
# ❌ Without set -euo pipefail — silently uses wrong directory
cd /tmp/deploy
rm -rf *
# If cd fails, this runs rm -rf in whatever directory you're in
 
# ✅ With set -euo pipefail — stops immediately
set -euo pipefail
cd /tmp/deploy
rm -rf ./*
# If cd fails, the script stops. Crisis averted.

Ese escenario de fallo de cd ha provocado caídas reales en producción. El flag set -e no es opcional — es el cinturón de seguridad.

Variables y comillas

Las variables sin comillas son la causa número uno de errores en scripts de shell. Una variable que contiene espacios o caracteres de globbing se expandirá de maneras que no esperabas.

shbash
# ❌ Unquoted variable — breaks on spaces and special chars
file_path=/tmp/my project/data.csv
cp $file_path /backup/
# Actually runs: cp /tmp/my project/data.csv /backup/
# Shell sees three arguments: /tmp/my, project/data.csv, /backup/
 
# ✅ Always quote variables
file_path="/tmp/my project/data.csv"
cp "$file_path" /backup/

Usa "${variable}" para interpolación de cadenas y "$@" para pasar argumentos a otros comandos:

shbash
# Pass all script arguments to another command
run_tests() {
  local test_dir="${1:-.}"
  local flags=("${@:2}")
 
  echo "Running tests in ${test_dir}..."
  pytest "$test_dir" "${flags[@]}"
}
 
run_tests "$@"

La palabra clave local limita el alcance de la variable a la función. Sin ella, toda variable es global — una receta para sobrescrituras accidentales en scripts más largos.

Funciones y manejo de errores

Estructura los scripts con funciones, un punto de entrada main y manejo explícito de errores. Este patrón escala desde utilidades de 10 líneas hasta scripts de despliegue de 500 líneas.

shbash
#!/usr/bin/env bash
set -euo pipefail
 
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly LOG_FILE="/tmp/deploy-$(date +%Y%m%d-%H%M%S).log"
 
log() {
  local level="$1"
  shift
  echo "[$(date +'%H:%M:%S')] [${level}] $*" | tee -a "$LOG_FILE"
}
 
cleanup() {
  local exit_code=$?
  if [[ $exit_code -ne 0 ]]; then
    log "ERROR" "Script failed with exit code ${exit_code}"
    log "ERROR" "Check log: ${LOG_FILE}"
  fi
  # Remove temp files, restore state, etc.
  rm -f /tmp/deploy-lock
}
 
check_prerequisites() {
  local missing=()
  for cmd in docker kubectl jq; do
    if ! command -v "$cmd" &>/dev/null; then
      missing+=("$cmd")
    fi
  done
 
  if [[ ${#missing[@]} -gt 0 ]]; then
    log "ERROR" "Missing required tools: ${missing[*]}"
    exit 1
  fi
}
 
main() {
  trap cleanup EXIT
  log "INFO" "Starting deployment..."
 
  check_prerequisites
  # ... rest of deployment logic
  log "INFO" "Deployment complete"
}
 
main "$@"

El trap cleanup EXIT garantiza que la limpieza se ejecute tanto si el script tiene éxito como si falla. La palabra clave readonly evita la reasignación accidental de constantes.

Lógica condicional y comparaciones

Bash tiene dos sintaxis de comparación: [ ] (POSIX) y [[ ]] (extendida de Bash). Usa [[ ]] — maneja espacios, coincidencia de patrones y operadores lógicos sin sorpresas.

shbash
# ❌ Single brackets — breaks on empty variables, needs escaping
if [ $status = "active" -a $count -gt 0 ]; then
 
# ✅ Double brackets — safe with empty vars, supports && ||
if [[ "$status" == "active" && "$count" -gt 0 ]]; then

Patrones condicionales habituales:

shbash
# File checks
[[ -f "$file" ]]     # File exists and is a regular file
[[ -d "$dir" ]]      # Directory exists
[[ -x "$script" ]]   # File is executable
[[ -s "$file" ]]     # File exists and is not empty
 
# String checks
[[ -z "$var" ]]      # String is empty
[[ -n "$var" ]]      # String is not empty
[[ "$var" == *.log ]] # Glob pattern match
 
# Default values
name="${1:-anonymous}"          # Default if unset
db_host="${DB_HOST:?'DB_HOST must be set'}"  # Error if unset

La sintaxis :? es muy valiosa para variables de entorno obligatorias. En lugar de usar silenciosamente una cadena vacía, el script termina con un mensaje de error claro.

Procesar datos de forma segura

Evita analizar la salida de ls o depender de la división de palabras para iterar sobre archivos. Usa globs y find con un manejo adecuado del delimitador nulo.

shbash
# ❌ Parsing ls — breaks on spaces, special characters, symlinks
for file in $(ls /data/*.csv); do
  process "$file"
done
 
# ✅ Glob pattern — handles all filenames correctly
for file in /data/*.csv; do
  [[ -f "$file" ]] || continue
  process "$file"
done
 
# ✅ find with null delimiter — recursive, handles everything
while IFS= read -r -d '' file; do
  process "$file"
done < <(find /data -name '*.csv' -type f -print0)

Para procesar JSON, usa jq. Para texto estructurado, usa awk. Evita encadenar grep | sed | cut para cualquier cosa que un solo comando jq o awk pueda resolver.

shbash
# ❌ Fragile pipeline — breaks if JSON format changes
curl -s "$API_URL" | grep '"name"' | sed 's/.*: "//;s/".*//'
 
# ✅ Structured JSON parsing with jq
curl -s "$API_URL" | jq -r '.items[].name'

Patrones de scripts portables

Cuando un script necesita ejecutarse en macOS, Linux y entornos de CI, evita las suposiciones específicas de cada plataforma.

shbash
# ❌ GNU-specific flags — fails on macOS
date -d "2020-01-01" +%s
sed -i 's/old/new/g' file.txt
 
# ✅ Cross-platform alternatives
# Date parsing — use Python for portability
python3 -c "from datetime import datetime; print(int(datetime(2020,1,1).timestamp()))"
 
# In-place sed — macOS requires backup extension
if [[ "$(uname)" == "Darwin" ]]; then
  sed -i '' 's/old/new/g' file.txt
else
  sed -i 's/old/new/g' file.txt
fi

Cuando la compatibilidad multiplataforma se vuelve dolorosa, es una señal para cambiar a Python o Node.js. El shell scripting sobresale orquestando otros comandos. Le cuesta la manipulación de cadenas, las estructuras de datos y la lógica compleja.

Ideas clave

  1. Usa siempre set -euo pipefail — evita fallos silenciosos que provocan caídas reales
  2. Pon comillas a cada variable — "$var" es correcto, $var es un error esperando a suceder
  3. Usa [[ ]], no [ ] — los corchetes dobles manejan casos límite que los simples no cubren
  4. Estructura con funciones y main — hasta los scripts pequeños se benefician de una organización clara
  5. Usa jq para JSON y globs para archivos — evita pipelines frágiles de grep | sed | cut
  6. Sabe cuándo parar — si el script necesita estructuras de datos o manejo de errores más allá de try/catch, cambia a un lenguaje de programación real
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX