Ghost AcademyGhostLinux · Ghost Academy
GhostLinuxMódulo 105Examen 102-500

Shells y shell scripting

LPI 105.1 Entorno del shellLPI 105.2 Shell scripting

Arranca el Bloque 2 (examen 102-500). Solo tiene dos objetivos, pero son de los de más peso real en el día a día: personalizar tu entorno de trabajo y, sobre todo, automatizar tareas con scripts en vez de repetir comandos a mano. Si el módulo 103 te enseñó las piezas sueltas, este te enseña a encadenarlas con lógica — condiciones, bucles, variables.

Objetivos de la unidad

1. LPI 105.1 — Personalizar el entorno del shell

1.1 Ficheros de inicio: cuál se ejecuta y cuándo

Esta es la pregunta más mal entendida de todo LPIC-1: no hay un único fichero de configuración, hay varios, y cada uno se ejecuta en un momento distinto.

FicheroÁmbito¿Cuándo se ejecuta?
/etc/profileGlobal (todos los usuarios)Al iniciar un shell de login (login por consola, ssh usuario@host, su -)
/etc/profile.d/*.shGlobalCargados desde /etc/profile — así añaden paquetes su propia configuración sin tocar el fichero principal
~/.bash_profile, ~/.bash_login, ~/.profilePor usuarioShell de login — bash usa el primero que encuentre de los tres, en ese orden, nunca los tres a la vez
/etc/bash.bashrc (Debian/Ubuntu)GlobalShell interactivo no de login (abrir una terminal nueva en el escritorio)
~/.bashrcPor usuarioShell interactivo no de login — por convención, ~/.bash_profile casi siempre incluye una línea para cargar también ~/.bashrc, así que en la práctica se aplican los dos al conectar por SSH
~/.bash_logoutPor usuarioAl cerrar sesión de un shell de login — típico para limpiar la pantalla o borrar temporales
Un script que ejecutas con ./script.sh no lee ninguno de estos ficheros — no es ni login ni interactivo. Por eso un script no puede dar por hecho que existen los alias o funciones que tienes en tu .bashrc, a menos que los defina él mismo.
root@lpic1:~#
$ cat ~/.bashrc | tail -5
# Alias personalizados
alias ll='ls -lah'
export PATH="$PATH:/opt/herramientas/bin"

1.2 Variables de entorno de forma persistente

Ya viste export y $PATH en el módulo 103 — la diferencia ahora es hacerlo permanente: cualquier export escrito directamente en la terminal se pierde al cerrar la sesión. Para que sobreviva, va en ~/.bashrc (solo tu usuario) o en /etc/profile.d/algo.sh (todos los usuarios).

root@lpic1:~#
$ echo 'export PATH="$PATH:/opt/herramientas/bin"' >> ~/.bashrc
root@lpic1:~#
$ source ~/.bashrc
# recarga el fichero en la sesion actual, sin cerrar y volver a entrar
source fichero (o su atajo . fichero, con un punto) ejecuta el contenido en el shell actual, no en un subproceso — por eso las variables que define quedan disponibles después. Ejecutar el mismo fichero como ./fichero lo haría en un subshell aparte y las variables se perderían al terminar.

1.3 Funciones de shell

Un alias (módulo 103) solo sustituye texto; una función acepta lógica real y parámetros, como un mini script integrado en el shell:

root@lpic1:~#
$ cat >> ~/.bashrc << 'EOF'
backup_rapido() {
    tar -czf "$1_$(date +%F).tar.gz" "$1"
    echo "Backup de $1 creado."
}
EOF
root@lpic1:~#
$ source ~/.bashrc && backup_rapido /etc
Backup de /etc creado.
AliasFunción
Admite lógica (if, bucles)NoSí
Admite parámetros propios ($1, $2...)No — solo puede añadir texto al finalSí
Dónde se define de forma permanente~/.bashrc~/.bashrc

1.4 /etc/skel: la plantilla de los usuarios nuevos

root@lpic1:~#
$ ls -la /etc/skel
.bash_logout
.bashrc
.profile

Cuando se crea un usuario con useradd -m (módulo 107), todo el contenido de /etc/skel se copia automáticamente a su nuevo directorio personal. Si como administrador quieres que todos los usuarios que se creen a partir de ahora tengan un alias concreto o una carpeta proyectos/ ya creada, se añade aquí — los usuarios que ya existen no se ven afectados retroactivamente.

1.5 Operadores de lista: encadenar comandos por su resultado

OperadorEfecto
cmd1 && cmd2Ejecuta cmd2 solo si cmd1 terminó con éxito (código de salida 0)
cmd1 || cmd2Ejecuta cmd2 solo si cmd1 falló (código de salida distinto de 0)
cmd1 ; cmd2Ejecuta cmd2 siempre, haya ido bien o mal cmd1 — simplemente los separa
root@lpic1:~#
$ mkdir /mnt/datos && cd /mnt/datos && echo "Estoy en $PWD"
Estoy en /mnt/datos
root@lpic1:~#
$ ping -c1 servidor_que_no_existe || echo "Sin conectividad, abortando"
Sin conectividad, abortando
🧰 Recursos relacionados (Ghost Academy)

Practica más — labs ya publicados en Ghost Academy sobre este tema: 📜 Bash & shebang (lab)

2. LPI 105.2 — Personalizar o escribir scripts sencillos

2.1 El shebang y la ejecución

root@lpic1:~#
$ cat miscript.sh
#!/bin/bash
echo "Hola desde un script"
root@lpic1:~#
$ chmod +x miscript.sh
root@lpic1:~#
$ ./miscript.sh
Hola desde un script
Cómo se lanzaNecesita chmod +xShell que lo interpreta
./script.shSíEl indicado en el shebang (#!/bin/bash, #!/bin/sh...)
bash script.shNobash, siempre, ignorando el shebang
sh script.shNosh — en Debian/Ubuntu es dash, más estricto y sin algunas extensiones de bash
Un script escrito con sintaxis de bash (como [[ ]] o arrays) puede fallar al lanzarlo con sh script.sh en Debian/Ubuntu, porque sh ahí es dash, no bash. El shebang existe precisamente para evitar esta confusión: respeta siempre #!/bin/bash si usas sintaxis de bash, y ejecuta con ./script.sh, no con sh script.sh.

2.2 Variables y parámetros posicionales

root@lpic1:~#
$ cat saluda.sh
#!/bin/bash
echo "Script: $0"
echo "Primer argumento: $1"
echo "Segundo argumento: $2"
echo "Numero de argumentos: $#"
echo "Todos los argumentos: $@"
echo "PID de este script: $$"
root@lpic1:~#
$ ./saluda.sh Pedro Admin
Script: ./saluda.sh
Primer argumento: Pedro
Segundo argumento: Admin
Numero de argumentos: 2
Todos los argumentos: Pedro Admin
PID de este script: 4821
VariableSignificado
$0Nombre con el que se invocó el script
$1 .. $9Primer a noveno argumento posicional
${10}A partir del décimo argumento hace falta la llave
$#Número total de argumentos recibidos
$@Todos los argumentos, cada uno como palabra independiente (la forma normal de recorrerlos en un bucle)
$*Todos los argumentos como una única cadena — sutil diferencia con $@ que el examen pregunta
$$PID del proceso del script actual
$?Código de salida del último comando ejecutado (sección 2.8)

2.3 Sustitución de comandos y aritmética

root@lpic1:~#
$ echo "Hoy es $(date +%Y-%m-%d)"
Hoy es 2026-10-02
root@lpic1:~#
$ echo "Hoy es `date +%Y-%m-%d`"
Hoy es 2026-10-02
Las comillas invertidas `comando` hacen lo mismo que $(comando) pero son la sintaxis antigua: no se anidan bien (necesitan escapar las interiores) y la mayoría de guías de estilo modernas recomiendan $(comando) siempre. El examen puede preguntar por las dos formas.
root@lpic1:~#
$ echo $((5 + 3))
8
root@lpic1:~#
$ contador=0
echo $((contador + 1))
1

$((expresión)) evalúa aritmética de enteros directamente en bash — no hace falta expr (el comando histórico, más lento y con una sintaxis más incómoda, que el examen menciona como legado).

2.4 Condicionales: test, [ ] y [[ ]]

[ condición ] es literalmente el comando test con otro nombre — tan comando externo como ls. [[ condición ]] es una extensión de bash, más permisiva con los espacios y con más operadores, pero no portable a sh/dash.

Operador numéricoCompara
-eqIgual
-neDistinto
-lt / -leMenor que / menor o igual
-gt / -geMayor que / mayor o igual
Operador de cadenasCompara
= / !=Igual / distinto
-z cadenaVerdadero si la cadena está vacía
-n cadenaVerdadero si la cadena no está vacía
Operador de ficherosCompara
-f rutaExiste y es un fichero regular
-d rutaExiste y es un directorio
-e rutaExiste (de cualquier tipo)
-x ruta / -r ruta / -w rutaEs ejecutable / legible / escribible por quien lanza el script
root@lpic1:~#
$ cat comprueba.sh
#!/bin/bash
if [ -f "$1" ]; then
    echo "$1 es un fichero regular"
elif [ -d "$1" ]; then
    echo "$1 es un directorio"
else
    echo "$1 no existe"
fi
root@lpic1:~#
$ ./comprueba.sh /etc/passwd
/etc/passwd es un fichero regular
root@lpic1:~#
$ ./comprueba.sh /etc
/etc es un directorio
Ojo a los espacios dentro de [ ]: [ -f "$1" ] necesita espacio después del [ y antes del ] porque, de nuevo, [ no es sintaxis especial sino el nombre del comando test — sin esos espacios el shell no sabe separar los argumentos.

2.5 Bucles: for, while, until

root@lpic1:~#
$ cat recorre.sh
#!/bin/bash
for usuario in alicia bruno carla; do
    echo "Creando directorio para $usuario"
    mkdir -p "/home/$usuario/proyectos"
done
root@lpic1:~#
$ ./recorre.sh
Creando directorio para alicia
Creando directorio para bruno
Creando directorio para carla
root@lpic1:~#
$ cat espera.sh
#!/bin/bash
contador=5
while [ $contador -gt 0 ]; do
    echo "Quedan $contador"
    contador=$((contador - 1))
    sleep 1
done
echo "Despegue"
root@lpic1:~#
$ cat hasta.sh
#!/bin/bash
intentos=0
until ping -c1 -W1 servidor.local &>/dev/null; do
    intentos=$((intentos + 1))
    echo "Intento $intentos fallido, reintentando..."
    sleep 2
done
echo "Servidor disponible"
while repite mientras la condición sea verdadera; until repite mientras sea falsa (es decir, hasta que se cumpla) — son espejos el uno del otro, y el examen pregunta por convertir uno en el otro invirtiendo la condición.

2.6 case: alternativa legible a muchos elif

root@lpic1:~#
$ cat menu.sh
#!/bin/bash
case "$1" in
    start)
        echo "Arrancando el servicio..."
        ;;
    stop)
        echo "Deteniendo el servicio..."
        ;;
    restart|reload)
        echo "Reiniciando el servicio..."
        ;;
    *)
        echo "Uso: $0 {start|stop|restart}"
        exit 1
        ;;
esac
root@lpic1:~#
$ ./menu.sh restart
Reiniciando el servicio...
root@lpic1:~#
$ ./menu.sh borrar
Uso: ./menu.sh {start|stop|restart}

Nota el patrón restart|reload): un mismo bloque puede responder a varios valores separados por |, y *) actúa como el "en cualquier otro caso" por defecto, igual que el else final de un if.

2.7 Leer entrada del usuario: read

root@lpic1:~#
$ cat pregunta.sh
#!/bin/bash
read -p "¿Cómo te llamas? " nombre
echo "Encantado, $nombre"
root@lpic1:~#
$ ./pregunta.sh
¿Cómo te llamas? Jose
Encantado, Jose

2.8 Terminar correctamente: exit y $?

Por convención universal en Unix: 0 significa éxito, y cualquier valor del 1 al 255 significa algún tipo de fallo. Un script bien escrito siempre termina con el código de salida correcto, no solo cuando todo va bien.

root@lpic1:~#
$ cat verifica_backup.sh
#!/bin/bash
if tar -czf /tmp/backup.tar.gz /etc 2>/dev/null; then
    echo "Backup correcto"
    exit 0
else
    echo "El backup ha fallado" >&2
    exit 1
fi
root@lpic1:~#
$ ./verifica_backup.sh; echo "Codigo de salida: $?"
Backup correcto
Codigo de salida: 0
Por qué esto no es un detalle menor: si este script se lanza desde cron (módulo 107), el sistema solo manda un correo de aviso a root cuando el comando produce salida o termina con código distinto de 0, dependiendo de la configuración. Un script que nunca hace exit 1 en sus fallos puede estar fallando en silencio durante meses sin que nadie se entere — el objetivo oficial de LPI lo llama literalmente "terminar correctamente un script".

Dentro del propio script, puedes comprobar el resultado del comando anterior en cualquier punto con $?, no solo al final:

root@lpic1:~#
$ grep -q root /etc/passwd; echo $?
0
root@lpic1:~#
$ grep -q usuario_inventado /etc/passwd; echo $?
1

Práctica guiada

  1. Añade una función a tu ~/.bashrc que reciba un nombre de fichero y muestre su tamaño con du -h. Recárgalo con source y pruébala.
  2. Escribe un script disco.sh que reciba un punto de montaje como $1 y avise si el uso de disco (df) supera el 80% — usa if y un operador numérico.
  3. Escribe un script con un bucle for que recorra todos los ficheros .log del directorio actual y cuente cuántas líneas contienen la palabra "ERROR" en cada uno.
  4. Convierte ese mismo script para que use case en vez de if/elif cuando reciba un segundo argumento resumen/detalle/ninguno que cambie el formato de salida.
  5. Comprueba con echo $? el código de salida de un comando que sabes que va a fallar (por ejemplo ls /no/existe), y de uno que sabes que va a funcionar.
  6. Añade manejo de errores a uno de tus scripts: que termine con exit 1 y un mensaje por stderr (>&2) si falta el argumento obligatorio ($# -eq 0).

Resumen de comandos y sintaxis del módulo

BloqueElementos clave
105.1 Entorno del shell/etc/profile, ~/.bashrc, ~/.bash_profile, source, function, /etc/skel, &&/||
105.2 Scripting#!/bin/bash, $1.. $@ $# $?, $(...), $((...)), if/test/[ ], for/while/until, case, read, exit

Autoevaluación

  1. Te conectas por SSH a un servidor. ¿Qué ficheros de inicio del shell se ejecutan, por orden?
  2. ¿Por qué un script lanzado con ./script.sh no puede usar los alias definidos en tu ~/.bashrc a menos que el propio script los defina?
  3. ¿Qué diferencia práctica hay entre $@ y $*?
  4. Escribe la condición de un if que compruebe si el fichero pasado como $1 existe y es escribible.
  5. ¿Qué diferencia hay entre while y until?
  6. ¿Por qué un script que detecta un fallo pero no hace exit 1 es peligroso si lo lanza cron?
  7. Un script usa [[ ]] con un array de bash y lo lanzas con sh script.sh en una Debian. ¿Por qué puede fallar?
← 104 · Dispositivos y FHSÍndice del curso106 · Interfaces y X11 →