Lo que haces a mano todos los días se te acaba olvidando algún día. Por eso, en un servidor, lo que se repite se escribe una vez en un script y se programa para que se haga solo. Así se hacen las copias de seguridad, la limpieza de lo viejo y casi todo el mantenimiento.
En este tema aprendes a escribir scripts de bash (con variables, argumentos, decisiones y
bucles), a hacer copias con tar, a borrar las viejas con find, a instalar lo que falta con
apt y a dejarlo todo programado con cron. Los ejemplos son de srv-obrador, el servidor de
un obrador de pan: cada día guarda los pedidos en /srv/pedidos y sus copias en /srv/copias.
Tu primer script
Un script es un archivo de texto con órdenes, una por línea, que se ejecutan en orden. Aquí no hay
editor, así que se escribe con cat y un here-doc: todo lo que escribas hasta la línea
EOF va al archivo. Mientras falte el EOF, la terminal te pide más con >:
tecnico@srv-obrador:~$ cat > pedidos.sh <<'EOF' > #!/bin/bash > # Los pedidos de hoy > echo "Pedidos en /srv/pedidos:" > ls /srv/pedidos > EOF
La primera línea, #!/bin/bash (el shebang), dice con qué programa se ejecuta el archivo.
Las demás que empiezan por # son comentarios. Las comillas de 'EOF' hacen que lo de dentro se
guarde tal cual, sin sustituir nada: es lo que quieres casi siempre en un script.
Un archivo nuevo no se puede ejecutar: le falta el permiso.
tecnico@srv-obrador:~$ ./pedidos.sh bash: ./pedidos.sh: Permission denied
tecnico@srv-obrador:~$ chmod +x pedidos.sh
tecnico@srv-obrador:~$ ./pedidos.sh Pedidos en /srv/pedidos: pedido-0410.txt pedido-0416.txt pedido-0424.txt pedido-0428.txt pedido-0413.txt pedido-0418.txt pedido-0425.txt
El ./ delante dice “el de aquí”: los programas sin ruta se buscan en las carpetas del PATH
(/usr/bin, /usr/local/bin…), y tu carpeta no es una de ellas. Los scripts de todo el servidor
se suelen dejar en /usr/local/bin.
Ejercicio
Tu primer script
En el obrador repiten cada mañana las mismas órdenes para ver los pedidos. Guárdalas en un script: un archivo con órdenes que se ejecuta de una vez.
Preparando la máquina…
Variables y $(…)
Una variable se crea con NOMBRE=valor, sin espacios alrededor del =, y se usa con $
delante. Con espacios, bash cree que FECHA es una orden:
tecnico@srv-obrador:~$ FECHA = hoy FECHA: command not found
$(…) ejecuta una orden y se sustituye por lo que escribe. Así se guarda la fecha, o la cuenta de
algo:
tecnico@srv-obrador:~$ HOY=$(date +%F) tecnico@srv-obrador:~$ echo "Hoy es $HOY" Hoy es 2026-11-10
tecnico@srv-obrador:~$ N=$(ls /srv/pedidos | wc -l) tecnico@srv-obrador:~$ echo "Hay $N pedidos" Hay 7 pedidos
Entre comillas dobles, $N se sustituye; entre comillas simples, no (echo 'Hay $N pedidos'
escribe $N tal cual). date +%F es la fecha como AAAA-MM-DD: ordenada, sirve para poner nombre
a las copias.
Un script recibe argumentos: $1 es el primero, $2 el segundo, $# cuántos hay y "$@"
todos:
tecnico@srv-obrador:~$ cat > hola.sh <<'EOF' > #!/bin/bash > echo "Hola, $1. Me has dado $# argumentos." > EOF
tecnico@srv-obrador:~$ bash hola.sh Marta Luis Hola, Marta. Me has dado 2 argumentos.
Con bash hola.sh no hace falta el permiso de ejecución: el que se ejecuta es bash, que lee el
archivo.
Ejercicio
Variables y $(…)
Un script necesita datos que cambian: la fecha de hoy, cuántos pedidos hay, lo que le pasas al lanzarlo. Guárdalos en variables.
Preparando la máquina…
Decidir: if y [ ]
Toda orden acaba con un código de salida: 0 si fue bien, otro número si no. Está en $?
justo después. La orden [ … ] (también llamada test) no escribe nada: solo comprueba algo y
acaba con 0 (verdad) o 1 (mentira):
tecnico@srv-obrador:~$ [ -d /srv/archivo ]; echo $? 1
-d es “existe y es un directorio”; -f, un archivo normal; -e, que existe. Los números se
comparan con -eq, -ne, -gt (mayor), -lt (menor), -ge y -le; los textos, con = y
!=. Los espacios dentro de los corchetes son obligatorios.
if ejecuta un bloque según el código de salida de la condición. ! le da la vuelta:
tecnico@srv-obrador:~$ if [ ! -d /srv/archivo ]; then sudo mkdir /srv/archivo; echo "creada"; fi creada
tecnico@srv-obrador:~$ if [ ! -d /srv/archivo ]; then sudo mkdir /srv/archivo; echo "creada"; else echo "ya estaba"; fi ya estaba
tecnico@srv-obrador:~$ if [ $N -gt 5 ]; then echo "día fuerte"; else echo "día tranquilo"; fi día fuerte
En un script, cada parte suele ir en su línea (if …, then, las órdenes, fi); en la
terminal, en una sola, separadas por ;.
Ejercicio
Decidir: if y test
Un script tiene que comprobar antes de actuar: ¿existe la carpeta?, ¿hay pedidos? Con if y [ ] decide qué hacer.
Preparando la máquina…
Repetir: for y while
for da una vuelta por cada elemento de una lista, con él en la variable. Con un comodín, la lista
son los archivos:
tecnico@srv-obrador:~$ for f in /srv/pedidos/*.txt; do echo "$f"; done /srv/pedidos/pedido-0410.txt /srv/pedidos/pedido-0413.txt /srv/pedidos/pedido-0416.txt /srv/pedidos/pedido-0418.txt /srv/pedidos/pedido-0424.txt /srv/pedidos/pedido-0425.txt /srv/pedidos/pedido-0428.txt
Si pulsas Intro antes del done, la terminal te pide el resto con >, como en los here-docs:
tecnico@srv-obrador:~$ for f in /srv/pedidos/*.txt; do > echo "== $f" > head -1 "$f" > done == /srv/pedidos/pedido-0410.txt cliente: elena == /srv/pedidos/pedido-0413.txt cliente: elena == /srv/pedidos/pedido-0416.txt cliente: pablo …
"$f" va entre comillas por si un nombre tiene espacios. while repite mientras la condición
sea verdad; $((…)) hace cuentas:
tecnico@srv-obrador:~$ i=1; while [ $i -le 5 ]; do echo $i; i=$((i+1)); done 1 2 3 4 5
Ejercicio
Repetir: for y while
Lo mismo para cada pedido: con un for se hace una vez por archivo, sin escribirlo a mano.
Preparando la máquina…
Copias: tar y find
tar junta una carpeta entera en un solo archivo: -c crea, -z comprime (gzip) y -f dice el
nombre. Con la fecha en el nombre, cada día tiene su copia:
tecnico@srv-obrador:~$ sudo tar -czf /srv/copias/pedidos-$(date +%F).tar.gz /srv/pedidos tar: Removing leading `/' from member names
El aviso es normal: dentro de la copia, las rutas se guardan sin la / del principio, para que al
sacarlas no pisen nada. -t enseña lo que hay dentro sin sacarlo (y -x, en otra carpeta, lo
saca):
tecnico@srv-obrador:~$ tar -tzf /srv/copias/pedidos-2026-11-10.tar.gz srv/pedidos/ srv/pedidos/pedido-0410.txt srv/pedidos/pedido-0413.txt srv/pedidos/pedido-0416.txt …
Para eso están las copias: si un archivo se pierde o se estropea, se devuelve de una de antes.
sudo tar -xzf /srv/copias/pedidos-2026-11-10.tar.gz -C / srv/pedidos/pedido-0413.txt saca solo
ese (nombrado tal cual lo enseña -t) y, con -C /, a su sitio de siempre; sin nombrar ninguno,
los sacaría todos y pisaría los de ahora.
Una copia al día llena el disco. Las viejas se borran con find y -mtime, que cuenta días
completos desde el último cambio: +7 es “más de 7”.
tecnico@srv-obrador:~$ ls -l /srv/copias total 36 -rw-r--r-- 1 root root 3251 Oct 18 11:20 pedidos-2026-10-18.tar.gz -rw-r--r-- 1 root root 2992 Oct 25 11:20 pedidos-2026-10-25.tar.gz -rw-r--r-- 1 root root 2844 Oct 29 11:20 pedidos-2026-10-29.tar.gz -rw-r--r-- 1 root root 2733 Nov 1 11:20 pedidos-2026-11-01.tar.gz -rw-r--r-- 1 root root 2585 Nov 5 11:20 pedidos-2026-11-05.tar.gz -rw-r--r-- 1 root root 2511 Nov 7 11:20 pedidos-2026-11-07.tar.gz -rw-r--r-- 1 root root 2474 Nov 8 11:20 pedidos-2026-11-08.tar.gz -rw-r--r-- 1 root root 2437 Nov 9 11:20 pedidos-2026-11-09.tar.gz -rw-r--r-- 1 root root 499 Nov 10 11:20 pedidos-2026-11-10.tar.gz
tecnico@srv-obrador:~$ find /srv/copias -name '*.tar.gz' -mtime +7 /srv/copias/pedidos-2026-11-01.tar.gz /srv/copias/pedidos-2026-10-29.tar.gz /srv/copias/pedidos-2026-10-25.tar.gz /srv/copias/pedidos-2026-10-18.tar.gz
Ejercicio
Copias: tar y find -mtime
La copia de los pedidos va a /srv/copias, con la fecha en el nombre. Y como se acumulan, las de más de una semana sobran.
Preparando la máquina…
Para quien usa Windows: zip y unzip
Un .tar.gz es lo normal entre servidores, pero en Windows no se abre con doble clic. Para
mandar algo a una gestoría, o a cualquiera con Windows, se usa zip: -r mete las carpetas
con todo lo que llevan dentro (sin él, solo la carpeta, vacía).
tecnico@srv-obrador:~$ zip -r ~/pedidos.zip /srv/pedidos adding: srv/pedidos/ (stored 0%) adding: srv/pedidos/pedido-0410.txt (stored 0%) adding: srv/pedidos/pedido-0413.txt (stored 0%) adding: srv/pedidos/pedido-0418.txt (deflated 5%) …
Como tar, guarda las rutas sin la / del principio. deflated es cuánto ha comprimido;
stored, que lo ha guardado tal cual (en archivos tan pequeños, comprimir no ahorra nada).
unzip -l enseña lo que hay dentro sin sacarlo, y unzip -d lo saca en otra carpeta:
tecnico@srv-obrador:~$ unzip -l ~/pedidos.zip Archive: /home/tecnico/pedidos.zip Length Date Time Name --------- ---------- ----- ---- 0 2026-11-10 10:20 srv/pedidos/ 40 2026-11-10 10:13 srv/pedidos/pedido-0410.txt 39 2026-11-10 08:53 srv/pedidos/pedido-0413.txt … --------- ------- 279 8 files
tecnico@srv-obrador:~$ unzip ~/pedidos.zip -d /tmp/revision Archive: /home/tecnico/pedidos.zip creating: /tmp/revision/srv/pedidos/ extracting: /tmp/revision/srv/pedidos/pedido-0410.txt extracting: /tmp/revision/srv/pedidos/pedido-0413.txt inflating: /tmp/revision/srv/pedidos/pedido-0418.txt …
zip y unzip tampoco vienen de serie: se instalan con apt, que viene ahora.
Ejercicio
Para la gestoría: zip y unzip
La gestoría lleva las cuentas en Windows y pide los pedidos de hoy en un .zip, que abre sin instalar nada. Prepáralo y compruébalo antes de mandarlo.
Preparando la máquina…
Instalar lo que falta: apt
Si un script usa un programa que no está, falla con command not found (código 127). Los
programas se instalan con apt, desde los repositorios de Ubuntu, y hace falta ser root:
tecnico@srv-obrador:~$ apt install sqlite3 E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied) E: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), are you root?
tecnico@srv-obrador:~$ sudo apt install sqlite3 Reading package lists... Done Building dependency tree... Done Reading state information... Done Suggested packages: sqlite3-doc The following NEW packages will be installed: sqlite3 0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded. … Setting up sqlite3 (3.37.2-2ubuntu0.7) ...
Si apt va a instalar más de lo que pediste, pregunta [Y/n] antes; con -y contesta que sí.
apt list --installed dice lo que hay instalado, y apt show qué es un paquete.
Que se haga solo: cron
cron es el servicio que ejecuta tareas a su hora. Cada línea son cinco campos de tiempo y la orden:
┌───────── minuto (0-59)
│ ┌─────── hora (0-23)
│ │ ┌───── día del mes (1-31)
│ │ │ ┌─── mes (1-12)
│ │ │ │ ┌─ día de la semana (0-7; 0 y 7 son el domingo)
│ │ │ │ │
30 3 * * * /home/tecnico/pedidos.sh > /tmp/pedidos.txt
* es “cualquiera”: esa línea es todos los días a las 3:30. */15 en el minuto sería cada
cuarto de hora; 1-5 en el día de la semana, de lunes a viernes. Cada usuario tiene su lista, que
se ve con crontab -l y se cambia con crontab (aquí, con un here-doc; en un servidor de verdad,
crontab -e abre un editor):
tecnico@srv-obrador:~$ crontab <<'EOF' > 30 3 * * * /home/tecnico/pedidos.sh > /tmp/pedidos.txt > EOF
tecnico@srv-obrador:~$ crontab -l 30 3 * * * /home/tecnico/pedidos.sh > /tmp/pedidos.txt
Las tareas del sistema van en /etc/crontab o, mejor, en un archivo propio dentro de
/etc/cron.d. Ahí cada línea lleva el usuario entre la hora y la orden:
0 3 * * * root /usr/local/bin/copia-pedidos.sh
Cron lo apunta todo en su diario. Al día siguiente se ve que la tarea se lanzó, y con qué usuario:
tecnico@srv-obrador:~$ journalctl -u cron --no-pager | grep tecnico Nov 11 03:30:00 srv-obrador CRON[417]: pam_unix(cron:session): session opened for user tecnico(uid=1000) by (uid=0) Nov 11 03:30:00 srv-obrador CRON[417]: (tecnico) CMD (/home/tecnico/pedidos.sh > /tmp/pedidos.txt) Nov 11 03:30:00 srv-obrador CRON[417]: pam_unix(cron:session): session closed for user tecnico
Si una línea está mal (un campo que no existe, o falta el usuario en /etc/cron.d), cron no la
ejecuta nunca, y el diario dice por qué: Error: bad hour, Error: bad username… Mira siempre el
diario después de tocar cron.
Ejercicio
Programar: cron
La copia tiene que hacerse sola, cada noche, aunque nadie esté conectado. Eso es cron: tu crontab dice qué se ejecuta y cuándo.
Preparando la máquina…