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.
- Saber qué fichero de inicio del shell se ejecuta en cada situación (login, interactivo, script).
- Añadir variables y rutas de forma persistente al entorno.
- Escribir funciones de shell reutilizables.
- Conocer
/etc/skely los operadores de lista&&/||. - Escribir scripts con variables, parámetros posicionales, condicionales, bucles y códigos de salida correctos.
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/profile | Global (todos los usuarios) | Al iniciar un shell de login (login por consola, ssh usuario@host, su -) |
/etc/profile.d/*.sh | Global | Cargados desde /etc/profile — así añaden paquetes su propia configuración sin tocar el fichero principal |
~/.bash_profile, ~/.bash_login, ~/.profile | Por usuario | Shell 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) | Global | Shell interactivo no de login (abrir una terminal nueva en el escritorio) |
~/.bashrc | Por usuario | Shell 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_logout | Por usuario | Al cerrar sesión de un shell de login — típico para limpiar la pantalla o borrar temporales |
./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.$ 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).
$ echo 'export PATH="$PATH:/opt/herramientas/bin"' >> ~/.bashrc$ source ~/.bashrc
# recarga el fichero en la sesion actual, sin cerrar y volver a entrarsource 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:
$ cat >> ~/.bashrc << 'EOF'
backup_rapido() {
tar -czf "$1_$(date +%F).tar.gz" "$1"
echo "Backup de $1 creado."
}
EOF$ source ~/.bashrc && backup_rapido /etc
Backup de /etc creado.| Alias | Función | |
|---|---|---|
| Admite lógica (if, bucles) | No | Sí |
Admite parámetros propios ($1, $2...) | No — solo puede añadir texto al final | Sí |
| Dónde se define de forma permanente | ~/.bashrc | ~/.bashrc |
1.4 /etc/skel: la plantilla de los usuarios nuevos
$ ls -la /etc/skel
.bash_logout
.bashrc
.profileCuando 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
| Operador | Efecto |
|---|---|
cmd1 && cmd2 | Ejecuta cmd2 solo si cmd1 terminó con éxito (código de salida 0) |
cmd1 || cmd2 | Ejecuta cmd2 solo si cmd1 falló (código de salida distinto de 0) |
cmd1 ; cmd2 | Ejecuta cmd2 siempre, haya ido bien o mal cmd1 — simplemente los separa |
$ mkdir /mnt/datos && cd /mnt/datos && echo "Estoy en $PWD"
Estoy en /mnt/datos$ ping -c1 servidor_que_no_existe || echo "Sin conectividad, abortando"
Sin conectividad, abortandoPractica 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
$ cat miscript.sh
#!/bin/bash
echo "Hola desde un script"
$ chmod +x miscript.sh$ ./miscript.sh
Hola desde un script| Cómo se lanza | Necesita chmod +x | Shell que lo interpreta |
|---|---|---|
./script.sh | Sí | El indicado en el shebang (#!/bin/bash, #!/bin/sh...) |
bash script.sh | No | bash, siempre, ignorando el shebang |
sh script.sh | No | sh — en Debian/Ubuntu es dash, más estricto y sin algunas extensiones de bash |
[[ ]] 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
$ 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: $$"
$ ./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| Variable | Significado |
|---|---|
$0 | Nombre con el que se invocó el script |
$1 .. $9 | Primer 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
$ echo "Hoy es $(date +%Y-%m-%d)"
Hoy es 2026-10-02$ echo "Hoy es `date +%Y-%m-%d`"
Hoy es 2026-10-02`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.$ echo $((5 + 3))
8$ 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érico | Compara |
|---|---|
-eq | Igual |
-ne | Distinto |
-lt / -le | Menor que / menor o igual |
-gt / -ge | Mayor que / mayor o igual |
| Operador de cadenas | Compara |
|---|---|
= / != | Igual / distinto |
-z cadena | Verdadero si la cadena está vacía |
-n cadena | Verdadero si la cadena no está vacía |
| Operador de ficheros | Compara |
|---|---|
-f ruta | Existe y es un fichero regular |
-d ruta | Existe y es un directorio |
-e ruta | Existe (de cualquier tipo) |
-x ruta / -r ruta / -w ruta | Es ejecutable / legible / escribible por quien lanza el script |
$ 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
$ ./comprueba.sh /etc/passwd
/etc/passwd es un fichero regular$ ./comprueba.sh /etc
/etc es un directorio[ ]: [ -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
$ cat recorre.sh
#!/bin/bash
for usuario in alicia bruno carla; do
echo "Creando directorio para $usuario"
mkdir -p "/home/$usuario/proyectos"
done
$ ./recorre.sh
Creando directorio para alicia
Creando directorio para bruno
Creando directorio para carla$ cat espera.sh
#!/bin/bash
contador=5
while [ $contador -gt 0 ]; do
echo "Quedan $contador"
contador=$((contador - 1))
sleep 1
done
echo "Despegue"
$ 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
$ 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
$ ./menu.sh restart
Reiniciando el servicio...$ ./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
$ cat pregunta.sh
#!/bin/bash
read -p "¿Cómo te llamas? " nombre
echo "Encantado, $nombre"
$ ./pregunta.sh
¿Cómo te llamas? Jose
Encantado, Jose2.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.
$ 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
$ ./verifica_backup.sh; echo "Codigo de salida: $?"
Backup correcto
Codigo de salida: 0cron (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:
$ grep -q root /etc/passwd; echo $?
0$ grep -q usuario_inventado /etc/passwd; echo $?
1Práctica guiada
- Añade una función a tu
~/.bashrcque reciba un nombre de fichero y muestre su tamaño condu -h. Recárgalo consourcey pruébala. - Escribe un script
disco.shque reciba un punto de montaje como$1y avise si el uso de disco (df) supera el 80% — usaify un operador numérico. - Escribe un script con un bucle
forque recorra todos los ficheros.logdel directorio actual y cuente cuántas líneas contienen la palabra "ERROR" en cada uno. - Convierte ese mismo script para que use
caseen vez deif/elifcuando reciba un segundo argumentoresumen/detalle/ningunoque cambie el formato de salida. - Comprueba con
echo $?el código de salida de un comando que sabes que va a fallar (por ejemplols /no/existe), y de uno que sabes que va a funcionar. - Añade manejo de errores a uno de tus scripts: que termine con
exit 1y un mensaje porstderr(>&2) si falta el argumento obligatorio ($# -eq 0).
Resumen de comandos y sintaxis del módulo
| Bloque | Elementos 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
- Te conectas por SSH a un servidor. ¿Qué ficheros de inicio del shell se ejecutan, por orden?
- ¿Por qué un script lanzado con
./script.shno puede usar los alias definidos en tu~/.bashrca menos que el propio script los defina? - ¿Qué diferencia práctica hay entre
$@y$*? - Escribe la condición de un
ifque compruebe si el fichero pasado como$1existe y es escribible. - ¿Qué diferencia hay entre
whileyuntil? - ¿Por qué un script que detecta un fallo pero no hace
exit 1es peligroso si lo lanzacron? - Un script usa
[[ ]]con un array de bash y lo lanzas consh script.shen una Debian. ¿Por qué puede fallar?