La Shell de Linux — Parte II: Programación Shell y control de flujo

📅 Actualizado en febrero 2026 ✍️ Ángel López 📊 Nivel: Intermedio ⏱️ 30 min de lectura

En la Parte I aprendiste a usar la shell de forma interactiva: variables, redirecciones, tuberías y comodines. Ahora vas a dar el salto a la programación. Un shell script es un archivo de texto con comandos que la shell ejecuta secuencialmente, y dominar su escritura convierte a un usuario de Linux en un verdadero administrador de sistemas. En esta segunda parte aprenderás condicionales, bucles, funciones, arrays, procesamiento de texto y técnicas de depuración profesional.

📜 Tu primer shell script

Un shell script no es más que un archivo de texto plano que contiene una secuencia de comandos que la shell ejecuta de arriba abajo. La diferencia con escribir comandos uno a uno en la terminal es que el script los ejecuta todos automáticamente, y puedes reutilizarlo tantas veces como necesites.

Todo script de Bash profesional comienza con una línea especial llamada shebang (o hashbang): #!/bin/bash. Esta línea le indica al sistema operativo qué intérprete debe usar para ejecutar el archivo. Sin ella, el sistema usará la shell por defecto, que podría no ser Bash.

mi_primer_script.sh
#!/bin/bash # Mi primer script de Bash # Autor: Curso de Linux — Ciberaula echo "¡Hola, mundo!" echo "Hoy es $(date '+%d de %B de %Y')" echo "Estás conectado como: $USER" echo "Tu directorio actual: $PWD"

Para ejecutar un script, necesitas darle permisos de ejecución con chmod y luego invocarlo con ./ (ruta relativa) o con su ruta absoluta:

terminal
# 1. Dar permisos de ejecución chmod +x mi_primer_script.sh # 2. Ejecutar ./mi_primer_script.sh # Alternativa: ejecutar con bash explícitamente (no necesita chmod) bash mi_primer_script.sh
✅ Buena práctica
Usa siempre #!/usr/bin/env bash en lugar de #!/bin/bash si quieres portabilidad. El comando env busca bash en el PATH del sistema, lo que funciona en más sistemas operativos (especialmente macOS, donde bash puede no estar en /bin/).
📝 1. Escribir nano script.sh + shebang + comandos 🔑 2. Permisos chmod +x script.sh rwxr-xr-x ▶️ 3. Ejecutar ./script.sh o bash script.sh 4. Resultado Salida en terminal $? = código salida

Ciclo de vida de un shell script: escritura, permisos, ejecución y resultado

📥 Argumentos y entrada de datos

Los scripts profesionales no tienen valores fijos «hardcodeados»: reciben datos del exterior. Bash ofrece dos mecanismos principales: argumentos posicionales (datos que se pasan al invocar el script) y lectura interactiva con el comando read.

argumentos.sh — Variables especiales
#!/bin/bash # Demostración de argumentos posicionales echo "Nombre del script: $0" echo "Primer argumento: $1" echo "Segundo argumento: $2" echo "Todos los args: $@" echo "Número de args: $#" echo "PID del script: $$" echo "Último exit code: $?" # Uso: ./argumentos.sh hola mundo
VariableContenidoEjemplo
$0Nombre del script./backup.sh
$1 a $9Argumentos posicionales$1 = primer argumento
${10}+Argumentos de dos cifras (con llaves)${12} = argumento 12
$#Número total de argumentos3 si pasas 3 args
$@Todos los argumentos (como lista)Ideal para iterar con for
$*Todos los argumentos (como cadena única)Se une con el primer carácter de IFS
$$PID del proceso actual12345
$?Código de salida del último comando0 = éxito, otro = error
lectura_interactiva.sh — Comando read
#!/bin/bash # Lectura básica read -p "¿Cómo te llamas? " nombre echo "Hola, $nombre" # Lectura con timeout (5 segundos) read -t 5 -p "Tienes 5 segundos: " respuesta # Lectura silenciosa (contraseñas) read -s -p "Contraseña: " password echo # Salto de línea tras input oculto # Lectura con valor por defecto read -p "Puerto [8080]: " puerto puerto="${puerto:-8080}" # Si vacío, usa 8080
Pantalla mostrando código de programación, representando la escritura de scripts de shell

📸 Código en pantalla: la programación shell transforma comandos sueltos en automatizaciones potentes — Pexels (Licencia libre)

🔀 Condicionales: if, elif, else

Las estructuras condicionales permiten que tus scripts tomen decisiones. La sintaxis de if en Bash tiene una particularidad importante: la condición se evalúa con el comando test (equivalente a [ ]) o con el operador extendido [[ ]].

condicionales.sh
#!/bin/bash # ─── Comprobar si un archivo existe ─── if [[ -f "/etc/passwd" ]]; then echo "El archivo existe" else echo "No se encontró el archivo" fi # ─── Comparar números ─── edad=25 if [[ $edad -ge 18 ]]; then echo "Mayor de edad" elif [[ $edad -ge 16 ]]; then echo "Casi mayor de edad" else echo "Menor de edad" fi # ─── Comparar cadenas ─── read -p "¿Continuar? (s/n): " respuesta if [[ "$respuesta" == "s" || "$respuesta" == "S" ]]; then echo "Continuando..." fi
OperadorTipoSignificadoEjemplo
-eq, -neNuméricoIgual, no igual[[ $a -eq 5 ]]
-lt, -leNuméricoMenor, menor o igual[[ $a -lt 10 ]]
-gt, -geNuméricoMayor, mayor o igual[[ $a -ge 0 ]]
==, !=CadenaIgual, diferente[[ "$s" == "ok" ]]
-z, -nCadenaVacía, no vacía[[ -z "$var" ]]
-fArchivoExiste y es archivo regular[[ -f "/etc/hosts" ]]
-dArchivoExiste y es directorio[[ -d "/home" ]]
-r, -w, -xArchivoLegible, escribible, ejecutable[[ -x "$script" ]]
&&, ||LógicoY, O (dentro de [[ ]])[[ $a -gt 0 && $a -lt 100 ]]
⚠️ Error frecuente
Siempre entrecomilla tus variables dentro de [ ] (test clásico): [ "$var" = "valor" ]. Sin comillas, si $var está vacía, Bash verá [ = "valor" ] y dará error de sintaxis. Con [[ ]] esto no es necesario, pero es buena costumbre igualmente.
edad >= 18 ? echo "Mayor de edad" No edad >= 16 ? echo "Casi mayor" No echo "Menor de edad"

Diagrama de flujo de un condicional if/elif/else en Bash

🎯 El comando case (selección múltiple)

Cuando necesitas comparar una variable contra múltiples valores posibles, case es más legible que una cadena de if/elif. Es el equivalente al switch de otros lenguajes:

menu.sh — Ejemplo de case
#!/bin/bash echo "=== Gestor de servicios ===" echo "1) Iniciar Apache" echo "2) Detener Apache" echo "3) Reiniciar Apache" echo "4) Estado de Apache" read -p "Opción: " opcion case "$opcion" in 1) sudo systemctl start apache2 ;; 2) sudo systemctl stop apache2 ;; 3) sudo systemctl restart apache2;; 4) systemctl status apache2 ;; *) echo "Opción no válida" exit 1 ;; esac
💡 Patrones en case
Los patrones de case soportan comodines: [yYsS]) para capturar s/S/y/Y, *.txt) para archivos .txt, y ?) para un solo carácter. También puedes combinar patrones con |: start|iniciar).

🔁 Bucles: for, while y until

Los bucles permiten ejecutar un bloque de comandos repetidamente. Bash ofrece tres tipos, cada uno con su uso ideal:

Bucle for

Itera sobre una lista de elementos. Es el más versátil y el que usarás con más frecuencia:

bucles_for.sh
#!/bin/bash # Iterar sobre una lista literal for fruta in manzana pera naranja; do echo "Fruta: $fruta" done # Iterar sobre un rango numérico for i in {1..10}; do echo "Número: $i" done # Iterar sobre archivos for archivo in /etc/*.conf; do echo "Config: $(basename "$archivo")" done # Bucle for estilo C (aritmético) for ((i=0; i<5; i++)); do echo "Iteración $i" done # Iterar sobre argumentos del script for arg in "$@"; do echo "Procesando: $arg" done

Bucles while y until

bucles_while_until.sh
#!/bin/bash # while: se ejecuta MIENTRAS la condición sea verdadera contador=1 while [[ $contador -le 5 ]]; do echo "Contador: $contador" ((contador++)) done # until: se ejecuta HASTA QUE la condición sea verdadera intentos=0 until [[ $intentos -ge 3 ]]; do echo "Intento $((intentos + 1)) de 3" ((intentos++)) done # Leer un archivo línea a línea while IFS= read -r linea; do echo "→ $linea" done < /etc/hostname # Bucle infinito con break while true; do read -p "Escribe 'salir' para terminar: " input [[ "$input" == "salir" ]] && break done

🧩 Funciones en Bash

Las funciones encapsulan bloques de código reutilizables. Son esenciales en cualquier script que supere las 30-40 líneas. En Bash, las funciones reciben argumentos posicionales ($1, $2...) igual que un script, pero dentro de la función estos se refieren a los argumentos de la función, no a los del script principal.

funciones.sh
#!/bin/bash # ─── Definir una función ─── saludar() { local nombre="$1" # Variable local local hora=$(date +%H) if [[ $hora -lt 12 ]]; then echo "Buenos días, $nombre" elif [[ $hora -lt 20 ]]; then echo "Buenas tardes, $nombre" else echo "Buenas noches, $nombre" fi } # ─── Función con valor de retorno ─── archivo_existe() { [[ -f "$1" ]] # Retorna 0 (true) o 1 (false) } # ─── Función que «devuelve» un valor vía echo ─── calcular_tamano() { local tam=$(du -sh "$1" 2>/dev/null | awk '{print $1}') echo "$tam" } # ─── Uso ─── saludar "Ana" if archivo_existe "/etc/passwd"; then tam=$(calcular_tamano "/etc/passwd") echo "/etc/passwd ocupa $tam" fi
✅ Buena práctica
Declara siempre las variables internas de tus funciones con local. Sin local, las variables son globales al script y pueden causar colisiones difíciles de depurar. Es el equivalente a declarar variables con let o const en JavaScript.

📊 Arrays y arrays asociativos

Bash soporta arrays indexados (desde Bash 2.0) y arrays asociativos (desde Bash 4.0, equivalentes a diccionarios o mapas). Son ideales para almacenar colecciones de datos:

arrays.sh
#!/bin/bash # ─── Array indexado ─── distros=("Ubuntu" "Fedora" "Debian" "Arch" "Mint") echo "Primera: ${distros[0]}" # Ubuntu echo "Todas: ${distros[@]}" # Todos echo "Cantidad: ${#distros[@]}" # 5 echo "Índices: ${!distros[@]}" # 0 1 2 3 4 # Añadir elemento distros+=("openSUSE") # Iterar for d in "${distros[@]}"; do echo "→ $d" done # ─── Array asociativo (Bash 4+) ─── declare -A servicios servicios[web]="Apache" servicios[db]="PostgreSQL" servicios[cache]="Redis" echo "Web: ${servicios[web]}" for clave in "${!servicios[@]}"; do echo "$clave → ${servicios[$clave]}" done

✂️ Procesamiento de texto: cut, awk, sed

El procesamiento de texto es el alma de la administración de sistemas en Linux. Tres herramientas dominan este campo: cut para extraer columnas, awk para procesamiento avanzado por campos, y sed para buscar y reemplazar.

procesamiento_texto.sh
#!/bin/bash # ─── cut: extraer campos por delimitador ─── # Obtener solo los nombres de usuario de /etc/passwd cut -d: -f1 /etc/passwd | head -5 # ─── awk: el «navaja suiza» del texto ─── # Procesos que consumen más de 1% de CPU ps aux | awk '$3 > 1.0 {printf "%-15s %s%%\n", $11, $3}' # Sumar una columna de números echo -e "10\n20\n30\n40" | awk '{sum+=$1} END {print "Total:", sum}' # ─── sed: buscar y reemplazar ─── # Reemplazar texto en un archivo (in-place) sed -i 's/localhost/192.168.1.100/g' config.conf # Eliminar líneas vacías sed '/^$/d' archivo.txt # Eliminar comentarios (líneas que empiezan por #) sed '/^#/d' /etc/ssh/sshd_config # ─── tr: transformar caracteres ─── echo "hola mundo" | tr 'a-z' 'A-Z' # HOLA MUNDO echo "a::b::c" | tr -s ':' # a:b:c (squeeze)
✂️ cut Extraer columnas por posición o delimitador cut -d: -f1,3 Rápido y simple Solo extracción 🔧 awk Procesamiento avanzado por campos y patrones awk '$3>1 {print}' Lenguaje completo Variables, cálculos 🔄 sed Stream Editor: buscar, reemplazar, eliminar líneas sed 's/old/new/g' Edición in-place Regex POSIX 🔤 tr Transformar caracteres: mayúsc., eliminar tr 'a-z' 'A-Z' Solo caracteres No regex

Las cuatro herramientas fundamentales de procesamiento de texto en Linux

🔢 Aritmética y sustitución de comandos

Bash no es un lenguaje numérico, pero ofrece varias formas de realizar cálculos enteros y de capturar la salida de un comando dentro de una variable:

aritmetica.sh
#!/bin/bash # ─── Aritmética con $(( )) ─── a=15; b=4 echo "Suma: $((a + b))" # 19 echo "Resta: $((a - b))" # 11 echo "Producto: $((a * b))" # 60 echo "División: $((a / b))" # 3 (entera) echo "Módulo: $((a % b))" # 3 echo "Potencia: $((2 ** 10))" # 1024 # Incremento/decremento ((a++)) ((b-=2)) # ─── Para decimales: usar bc ─── resultado=$(echo "scale=2; 22/7" | bc) echo "Pi aprox: $resultado" # 3.14 # ─── Sustitución de comandos ─── fecha_actual=$(date '+%Y-%m-%d') num_usuarios=$(wc -l < /etc/passwd) kernel=$(uname -r) echo "Fecha: $fecha_actual | Usuarios: $num_usuarios | Kernel: $kernel"

🚨 Señales y traps

Las señales son mecanismos que el kernel usa para notificar a los procesos sobre eventos. Un script profesional debe manejar señales para limpiar archivos temporales, restaurar configuraciones o informar al usuario antes de terminar:

traps.sh — Manejo de señales
#!/bin/bash # Archivo temporal que debe eliminarse al terminar TMPFILE=$(mktemp /tmp/mi_script.XXXXXX) # Definir función de limpieza limpiar() { echo -e "\n⚠️ Señal recibida. Limpiando..." rm -f "$TMPFILE" echo "✅ Temporal eliminado: $TMPFILE" exit 0 } # Capturar señales: SIGINT (Ctrl+C), SIGTERM (kill), EXIT trap limpiar SIGINT SIGTERM EXIT # Simulación de trabajo largo echo "Trabajando con $TMPFILE..." echo "Pulsa Ctrl+C para interrumpir" for i in {1..30}; do echo "dato_$i" >> "$TMPFILE" sleep 1 done
SeñalNúmeroCuándo se envíaAcción por defecto
SIGHUP1Se cierra la terminalTerminar proceso
SIGINT2Ctrl+CTerminar proceso
SIGTERM15kill (por defecto)Terminar proceso
SIGKILL9kill -9Terminar inmediatamente (no capturable)
EXITScript termina (normal o error)Pseudoseñal de Bash
Desarrollador revisando código en pantalla, representando la depuración de scripts de shell

📸 Depuración de código: un buen script requiere tanto escritura como revisión — Pexels (Licencia libre)

🔍 Depuración profesional de scripts

Todo programador necesita herramientas de depuración. Bash ofrece varias opciones que, combinadas, convierten la caza de errores en un proceso sistemático en lugar de una búsqueda a ciegas.

cabecera_segura.sh — Plantilla profesional
#!/bin/bash # ─── Cabecera de seguridad (recomendada en TODO script) ─── set -e # Salir inmediatamente si un comando falla set -u # Error si se usa una variable no definida set -o pipefail # Error si falla cualquier comando en una tubería # Equivalente compacto: # set -euo pipefail # ─── Activar modo depuración ─── # set -x # Muestra cada comando antes de ejecutarlo # ─── Modo depuración desde fuera ─── # bash -x mi_script.sh # ─── Depuración selectiva (solo una sección) ─── echo "Inicio normal" set -x # Activar traza resultado=$((2 + 3)) archivo="/etc/hosts" set +x # Desactivar traza echo "Continúa sin traza"
OpciónEfectoCuándo usarla
set -eSale si cualquier comando retorna error (no 0)Siempre en scripts de producción
set -uError al usar variables no definidasSiempre. Evita errores silenciosos
set -o pipefailLa tubería falla si falla cualquier comando, no solo el últimoSiempre con tuberías
set -xImprime cada comando antes de ejecutarlo (traza)Desarrollo y depuración
bash -n script.shVerifica sintaxis sin ejecutar nadaAntes del primer test
1 Sintaxis bash -n script.sh Verifica sin ejecutar Detecta errores de paréntesis, comillas... 2 Traza set -x / bash -x Muestra cada línea antes de ejecutarla con variables expandidas 3 Modo estricto set -euo pipefail Sale ante errores vars no definidas y fallos en pipes 4 Linting shellcheck script Análisis estático Detecta malas prácticas y bugs

Los cuatro niveles de depuración en Bash: de la verificación sintáctica al análisis estático

✅ Herramienta imprescindible: ShellCheck
ShellCheck es un analizador estático gratuito para scripts de Bash. Detecta errores comunes como variables sin comillas, uso incorrecto de test, y cientos de malas prácticas. Instálalo con sudo apt install shellcheck y ejecútalo con shellcheck mi_script.sh. También puedes usarlo online.

📝 Ejercicios prácticos

📋 Ejercicio 1: Script de backup

Crea un script backup.sh que reciba un directorio como argumento, compruebe que existe, y cree un archivo .tar.gz con la fecha en el nombre. Si no se pasa argumento, debe mostrar un mensaje de uso.

💡 Ver solución
backup.sh
#!/bin/bash set -euo pipefail if [[ $# -eq 0 ]]; then echo "Uso: $0 <directorio>" exit 1 fi directorio="$1" if [[ ! -d "$directorio" ]]; then echo "Error: '$directorio' no es un directorio" exit 1 fi fecha=$(date +%Y%m%d_%H%M%S) nombre_backup="backup_$(basename "$directorio")_${fecha}.tar.gz" tar -czf "$nombre_backup" "$directorio" echo "✅ Backup creado: $nombre_backup ($(du -h "$nombre_backup" | awk '{print $1}'))"
📋 Ejercicio 2: Menú interactivo con case

Crea un script que muestre un menú con 4 opciones de información del sistema (hostname, kernel, memoria, disco) y ejecute la opción elegida usando case.

💡 Ver solución
sysinfo.sh
#!/bin/bash echo "=== Información del sistema ===" echo "1) Hostname y usuario" echo "2) Información del kernel" echo "3) Uso de memoria" echo "4) Uso de disco" read -p "Elige (1-4): " op case "$op" in 1) echo "Host: $(hostname) | User: $USER" ;; 2) uname -a ;; 3) free -h ;; 4) df -h / ;; *) echo "Opción no válida"; exit 1 ;; esac
📋 Ejercicio 3: Renombrado masivo con bucle

Escribe un script que recorra todos los archivos .txt del directorio actual y les añada un prefijo con la fecha de hoy (formato 20260227_archivo.txt).

💡 Ver solución
renombrar.sh
#!/bin/bash set -euo pipefail fecha=$(date +%Y%m%d) contador=0 for archivo in *.txt; do [[ -f "$archivo" ]] || continue nuevo="${fecha}_${archivo}" mv "$archivo" "$nuevo" echo "$archivo → $nuevo" ((contador++)) done echo "✅ $contador archivos renombrados"
📋 Ejercicio 4: Función de validación

Crea una función validar_email que reciba una cadena y compruebe si tiene formato de email válido (contiene @ y al menos un . después de la arroba). Úsala en un script que pida emails hasta que el usuario escriba «fin».

💡 Ver solución
validar_emails.sh
#!/bin/bash validar_email() { local email="$1" if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then return 0 else return 1 fi } while true; do read -p "Email (o 'fin'): " input [[ "$input" == "fin" ]] && break if validar_email "$input"; then echo "✅ Válido: $input" else echo "❌ Inválido: $input" fi done
📋 Ejercicio 5: Análisis de logs con awk

Escribe un script que analice /var/log/syslog (o /var/log/auth.log) y muestre: el número total de líneas, las 5 palabras más frecuentes en la tercera columna, y cuántas líneas contienen la palabra «error» (sin distinguir mayúsculas).

💡 Ver solución
analizar_log.sh
#!/bin/bash set -euo pipefail LOG="${1:-/var/log/syslog}" if [[ ! -r "$LOG" ]]; then echo "No se puede leer: $LOG" exit 1 fi total=$(wc -l < "$LOG") errores=$(grep -ic "error" "$LOG" || true) echo "=== Análisis de $LOG ===" echo "Total líneas: $total" echo "Líneas con 'error': $errores" echo "" echo "Top 5 procesos (columna 5):" awk '{print $5}' "$LOG" | sort | uniq -c | sort -rn | head -5

❓ Preguntas frecuentes sobre La Shell de Linux — Parte II: Programación Shell y control de flujo

Las dudas más comunes respondidas de forma clara y directa.

sh (Bourne Shell) es el estándar POSIX, compatible con casi cualquier sistema Unix. Bash extiende sh con arrays, aritmética avanzada, [[ ]] y más. Si necesitas portabilidad máxima, usa sh. Si trabajas en Linux, bash ofrece más herramientas.
Dos pasos: primero añade la línea shebang (#!/bin/bash) al inicio del archivo, y luego dale permisos de ejecución con chmod +x mi_script.sh. Después puedes ejecutarlo con ./mi_script.sh.
Cuando repitas lógica más de una vez, cuando el script supere las 50 líneas, o cuando quieras organizar el código en bloques reutilizables. Las funciones mejoran la legibilidad y facilitan la depuración.
[ ] es el comando test clásico compatible con sh. [[ ]] es una extensión de Bash que soporta operadores como =~, &&, || dentro de la expresión, y no requiere entrecomillar variables. Para scripts Bash, se recomienda [[ ]].
Usa set -x al inicio del script para ver cada comando antes de ejecutarse. Combínalo con set -e (salir ante errores) y set -u (error si usas variable no definida). También puedes ejecutar bash -x mi_script.sh desde fuera.
Sí, y es muy común. Por ejemplo: cat archivo.csv | sed "1d" | awk -F"," "{print $2}" elimina la cabecera con sed y extrae la segunda columna con awk. Combinar herramientas de texto es la esencia de la filosofía Unix.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre La Shell de Linux — Parte II: Programación Shell y control de flujo? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!

🚀 ¿Quieres dominar Linux profesionalmente?
Cursos bonificados por FUNDAE para empresas — formación 100% subvencionada
Ver cursos de Linux →