1. Qué es un script
Un script es un archivo de texto con comandos, que la shell lee y ejecuta en orden, uno tras otro, como si alguien los estuviera escribiendo en la terminal.
No hay más que eso, y ya hicimos uno. En el tema 18, para probar el permiso de ejecución, creamos hola.sh con una sola línea; en el 34, ~/bin/saludo, con dos. Y el .bashrc que editamos entonces es un script que Bash ejecuta al arrancar. Lo que cambia ahora es la intención: escribirlos a propósito, para trabajos de verdad.
La pregunta práctica es cuándo conviene. Una línea de comandos pide convertirse en script cuando se cumple alguna de estas condiciones:
| Señal | Lo que el script aporta |
|---|---|
| La vas a usar otra vez, mañana o dentro de un mes. | Queda guardada, con un nombre que se recuerda. |
| Es tan larga que cuesta leerla, y un error de tipeo la arruina. | Un paso por renglón, con comentarios. |
| Son varios comandos que deben ir siempre juntos y en orden. | No se puede olvidar ninguno. |
| Debe ejecutarse sin que estés: de noche, cada hora, en un servidor. | Un archivo es algo que el sistema puede lanzar solo. |
| Otra persona tiene que poder hacer lo mismo. | Se le entrega el archivo, en lugar de una explicación. |
El último punto tiene un efecto que se aprecia con el tiempo: un script es también documentación. Dice exactamente cómo se hace una tarea, sin ambigüedad, y no se desactualiza sin que alguien lo note, porque deja de funcionar.
En el tema 34 comparamos las tres formas de darle nombre a un trabajo. Vale repetirla, ahora desde el lado del script:
| Alias | Función | Script | |
|---|---|---|---|
| Vive en | .bashrc | .bashrc | Un archivo propio |
| Se ejecuta en | La shell actual | La shell actual | Un proceso hijo |
| Lo pueden usar | Tus shells interactivas | Las shells que la definan | Cualquier usuario, cualquier programa, cron, ssh |
| Conviene para | Un comando con sus opciones | Dos o tres comandos; lo que debe cambiar la shell | Todo lo demás |
2. De la línea al archivo
Partimos de la línea del tema 25, tal como la dejamos:
$ mkdir -p /tmp/respaldos && tar -czf "/tmp/respaldos/practica-$(date +%F).tar.gz" -C ~ practica && echo "Respaldo listo" || echo "El respaldo falló" >&2
Abrimos un archivo nuevo con el editor del tema 13, nano ~/practica/respaldo.sh, y escribimos los mismos comandos, uno por renglón:
#!/bin/bash
mkdir -p /tmp/respaldos
tar -czf "/tmp/respaldos/practica-$(date +%F).tar.gz" -C ~ practica
echo "Respaldo listo"
Se guarda con Ctrl+O y se sale con Ctrl+X. Para ejecutarlo, se le entrega el archivo a Bash:
$ cd ~/practica
$ bash respaldo.sh
Respaldo listo
$ ls -lh /tmp/respaldos
-rw-rw-r-- 1 ana ana 612K oct 13 15:12 practica-2026-10-13.tar.gz
Funciona, y ya es un script. Bash abrió el archivo, leyó la primera línea —que por empezar con # ignoró, y que explicamos en la sección siguiente—, ejecutó mkdir, esperó que terminara, ejecutó tar, esperó, ejecutó echo, y al llegar al final del archivo terminó.
En el pasaje se perdió algo, y conviene notarlo ahora. La línea original encadenaba con &&: cada paso dependía del éxito del anterior. En el archivo, los comandos están simplemente uno debajo del otro, que equivale a separarlos con punto y coma: se ejecutan todos, pase lo que pase. Si tar fallara, el script escribiría «Respaldo listo» igual. Lo corregimos en la sección 8; antes, hacemos del archivo un comando.
3. La primera línea: #!/bin/bash
La línea #!, llamada shebang, es la primera de un script e indica con qué programa debe interpretarse. Cuando se ejecuta el archivo, el kernel la lee, lanza ese programa y le entrega el script.
#!/bin/bash
││ │
││ └─ la ruta absoluta del intérprete
│└─ el signo de exclamación
└─ el numeral: para Bash, esta línea es un comentario
Tiene que ser exactamente la primera línea, empezar en la primera columna, y no llevar espacios antes del numeral. El mecanismo no es de Bash sino del kernel, y por eso sirve para cualquier lenguaje: un programa de Python empieza con #!/usr/bin/python3, y se ejecuta de la misma manera.
| Primera línea | El script lo interpreta | Se usa |
|---|---|---|
#!/bin/bash | Bash. | La forma habitual, y la de este curso. |
#!/usr/bin/env bash | El Bash que aparezca primero en PATH. | En scripts que deben funcionar en sistemas donde Bash está en otra ruta. |
#!/bin/sh | La shell mínima del sistema, que en Debian y Ubuntu no es Bash. | En scripts escritos a propósito para ser universales. |
| Ninguna | La shell desde la que se lo lance, cualquiera sea. | Nunca: el resultado depende de quién lo ejecute. |
En Ubuntu, /bin/sh es un enlace a dash, una shell pequeña y rápida que no entiende varias construcciones de Bash. Un script que empieza con #!/bin/sh, o que se ejecuta con sh script.sh, puede fallar con mensajes como Syntax error: "(" unexpected en líneas que en la terminal funcionan sin problema. Si el script se escribió y se probó en Bash, se declara Bash en la primera línea y se lo ejecuta con Bash.
4. Cuatro maneras de ejecutarlo
Con bash respaldo.sh le dimos el archivo a Bash como argumento. Para que el archivo se ejecute por sí mismo le falta lo que vimos en el tema 18: el permiso de ejecución.
$ ./respaldo.sh
bash: ./respaldo.sh: Permission denied
$ chmod +x respaldo.sh
$ ./respaldo.sh
Respaldo listo
$ respaldo.sh
respaldo.sh: command not found
El último intento falla por la razón del tema 3: la shell busca los comandos en los directorios de PATH, y el directorio actual no está entre ellos. El ./ es una ruta, y con una ruta no hay búsqueda. Las formas de ejecutar un script son cuatro, y difieren en lo que exigen y en dónde corre:
| Se escribe | Necesita permiso x | Usa el shebang | Corre en | Se usa para |
|---|---|---|---|---|
bash script.sh | No | No: lo interpreta Bash | Un proceso hijo | Probar; ejecutar con opciones, como bash -x. |
./script.sh | Sí | Sí | Un proceso hijo | La forma normal, desde su directorio. |
script | Sí | Sí | Un proceso hijo | Un script instalado en PATH. Sección 10. |
source script.sh | No | No | La shell actual | Archivos que deben modificar la shell: configuración, variables. |
La columna que tiene consecuencias es la cuarta, y es la herencia del tema 34. Un script se ejecuta en un proceso hijo, que recibe una copia del entorno y muere al terminar. Lo que haga consigo mismo —cambiar de directorio, definir variables— desaparece con él:
$ echo 'cd /tmp; pwd' > ir.sh
$ bash ir.sh
/tmp
$ pwd # la shell no se movió
/home/ana/practica
$ source ir.sh
/tmp
$ pwd # ahora sí
/tmp
$ cd ~/practica; rm ir.sh
Para un script, esto es una ventaja antes que una limitación: puede hacer cd a donde le convenga y definir las variables que quiera, sin alterar la terminal de quien lo llamó. Lo que sí perdura es lo que hace en el sistema: los archivos que crea, borra o modifica.
5. Comentarios y forma
En un script, todo lo que sigue a un # hasta el final de la línea es un comentario: Bash lo ignora. Los usamos en cada ejemplo del curso para anotar al costado de un comando; en un archivo que se va a releer dentro de seis meses pasan a ser parte del trabajo.
#!/bin/bash
# respaldo.sh: guarda una copia comprimida de ~/practica en /tmp/respaldos.
# Uso: ./respaldo.sh
# El directorio de destino puede no existir todavía
mkdir -p /tmp/respaldos
# -C ~ hace que tar guarde rutas relativas al directorio personal
tar -czf "/tmp/respaldos/practica-$(date +%F).tar.gz" -C ~ practica
echo "Respaldo listo"
Tres costumbres de ese ejemplo valen para todo script:
| Costumbre | Por qué |
|---|---|
| Un encabezado que dice qué hace y cómo se usa. | Es lo primero que se lee al abrir un script ajeno, o propio y olvidado, con head. |
| Comentar el porqué, no el qué. | # crea el directorio sobre un mkdir no agrega nada. # puede no existir todavía explica una decisión. |
| Líneas en blanco entre los pasos. | Bash las ignora, y separan el script en párrafos. |
Cuando un comando es demasiado largo para un renglón, se lo parte. Una barra invertida al final de la línea indica que continúa en la siguiente, y una línea que termina en |, && o || continúa sola, sin barra:
tar -czf "/tmp/respaldos/practica-$(date +%F).tar.gz" \
--exclude='*.tmp' \
-C ~ practica
sort ventas.csv |
uniq -c |
sort -rn |
head -n 5
La barra invertida debe ser el último carácter de la línea: un espacio después de ella rompe la continuación, y no se ve. La segunda forma es la manera de escribir, legible, las tuberías del bloque 6: una etapa por renglón.
En cuanto al nombre, la extensión .sh es una convención para las personas y para los editores, que la usan para colorear el texto. Linux no la necesita, como dijimos en el tema 12: lo que hace ejecutable a un archivo es su permiso, y lo que dice cómo interpretarlo, su primera línea.
6. Variables: un dato, un solo lugar
En la versión actual, la ruta /tmp/respaldos está escrita dos veces. Si mañana el destino cambia, habrá que corregirla en los dos lugares, y olvidarse de uno produce un script que crea un directorio y guarda en otro. Las variables del tema 34 resuelven eso: cada dato se escribe una vez, arriba, y se usa por su nombre.
#!/bin/bash
# respaldo.sh: guarda una copia comprimida de ~/practica en /tmp/respaldos.
# Uso: ./respaldo.sh
origen="$HOME/practica"
destino="/tmp/respaldos"
fecha="$(date +%F)"
nombre="$(basename "$origen")"
archivo="$destino/$nombre-$fecha.tar.gz"
mkdir -p "$destino"
tar -czf "$archivo" -C "$(dirname "$origen")" "$nombre"
echo "Respaldo listo: $archivo"
El script quedó en dos partes: arriba, los datos; abajo, las acciones. Las acciones ya no nombran ningún directorio en particular. basename, que usamos en el tema 25, entrega el último componente de una ruta —practica—, y dirname, su complemento, todo lo anterior: /home/ana.
Las reglas son las del tema 34, y en un script se pagan más caro, porque no hay nadie mirando:
| Regla | Mal | Bien |
|---|---|---|
| Sin espacios alrededor del signo igual. | destino = /tmp | destino=/tmp |
Sin $ al asignar; con $ al usar. | $destino=/tmp | destino=/tmp |
| Entre comillas dobles al usar, siempre. | mkdir -p $destino | mkdir -p "$destino" |
| Con llaves, si le sigue una letra, un dígito o un guion bajo. | "$nombre_viejo" | "${nombre}_viejo" |
$HOME en lugar de la virgulilla, dentro de comillas. | "~/practica" | "$HOME/practica" |
La tercera es la más importante del tema. Sin comillas, el valor de la variable se parte en los espacios y se expanden sus comodines, como vimos en el tema 9: un directorio llamado Mis documentos se convierte en dos argumentos, Mis y documentos. El script funciona durante meses, hasta el día en que aparece un nombre con un espacio. La quinta es su consecuencia: dentro de comillas la virgulilla no se expande, y $HOME sí.
Los nombres van en minúsculas, por la convención del tema 34: las mayúsculas se reservan para las variables de entorno, y así un script no pisa por accidente a PATH o a USER.
7. Argumentos: $1, $# y "$@"
El script respalda siempre el mismo directorio. Para que sirva con cualquiera hay que poder decírselo al llamarlo, igual que a cualquier comando: ./respaldo.sh ~/Documentos. Lo que se escribe después del nombre llega al script en unas variables especiales.
#!/bin/bash
echo "Nombre del script: $0"
echo "Cantidad: $#"
echo "Primero: $1"
echo "Segundo: $2"
echo "Todos: $@"
$ bash args.sh uno "dos tres" cuatro
Nombre del script: args.sh
Cantidad: 3
Primero: uno
Segundo: dos tres
Todos: uno dos tres cuatro
$ bash args.sh *.txt | head -n 2
Nombre del script: args.sh
Cantidad: 6
| Parámetro | Contiene |
|---|---|
$1, $2 … $9 | El primer argumento, el segundo… Del décimo en adelante, con llaves: ${10}. |
$# | La cantidad de argumentos. |
"$@" | Todos los argumentos, cada uno como una palabra separada. |
$0 | El nombre con que se llamó al script. |
Son los mismos $1 y $2 de la función mkcd del tema 34. El segundo ejemplo recuerda quién hace cada cosa: el comodín lo expandió la shell antes de lanzar el script, que recibió seis nombres de archivo y nunca vio el asterisco. Un script se entera de los comodines tanto como ls: nada.
"$@", con las comillas, es la forma de pasar a otro comando «todo lo que me dieron», conservando cada argumento entero aunque tenga espacios. Un script de una línea como ls -lh --color=auto "$@" es un ls con opciones propias, que acepta cualquier cantidad de rutas.
Valores por defecto y argumentos obligatorios
Si se llama al script sin argumentos, $1 está vacío. Bash tiene dos formas breves de decidir qué hacer en ese caso, sin escribir una condición:
| Se escribe | Vale | Se usa para |
|---|---|---|
${1:-valor} | El argumento, si se dio; si no, valor. | Un argumento optativo, con un valor por defecto. |
${1:?mensaje} | El argumento, si se dio; si no, el script termina mostrando el mensaje. | Un argumento obligatorio. |
#!/bin/bash
# respaldo.sh: guarda una copia comprimida de un directorio en /tmp/respaldos.
# Uso: ./respaldo.sh [directorio] (por defecto, ~/practica)
origen="${1:-$HOME/practica}"
destino="/tmp/respaldos"
fecha="$(date +%F)"
nombre="$(basename "$origen")"
archivo="$destino/$nombre-$fecha.tar.gz"
mkdir -p "$destino"
tar -czf "$archivo" -C "$(dirname "$origen")" "$nombre"
echo "Respaldo listo: $archivo"
$ ./respaldo.sh
Respaldo listo: /tmp/respaldos/practica-2026-10-13.tar.gz
$ ./respaldo.sh ~/bin
Respaldo listo: /tmp/respaldos/bin-2026-10-13.tar.gz
$ tar -tzf /tmp/respaldos/bin-2026-10-13.tar.gz
bin/
bin/saludo
Cambió una sola línea, la primera de los datos, y el script pasó de hacer una cosa a hacer una familia de cosas. Es el beneficio de haber separado los datos de las acciones en la sección anterior. Los corchetes del encabezado, [directorio], son la notación del tema 4 para un argumento optativo: la misma de las páginas de manual.
8. El código de salida y set -e
Queda la falla que señalamos en la sección 2. Le pedimos al script que respalde algo que no existe:
$ ./respaldo.sh /noexiste
tar: noexiste: Cannot stat: No such file or directory
tar: Exiting with failure status due to previous errors
Respaldo listo: /tmp/respaldos/noexiste-2026-10-13.tar.gz
$ echo $?
0
tar falló y lo dijo. El script siguió adelante, anunció un respaldo que no existe y terminó con código 0, que significa éxito. Para una persona que mira la pantalla, el error está a la vista. Para otro programa —cron, otro script, un &&— este script funcionó. Es un script que miente.
El código de salida de un script es el de su último comando, salvo que termine con exit N, que lo fija en N y detiene la ejecución en ese punto. Por defecto, Bash no se detiene cuando un comando falla: pasa al siguiente.
Aquí el último comando es echo, que siempre tiene éxito. Hay dos maneras de corregirlo, y se usan juntas. La primera es la del tema 25: encadenar con && y || lo que depende de un resultado. La segunda es pedirle a Bash que cambie su conducta, con una línea al principio:
| Opción | Con ella, el script… | Evita |
|---|---|---|
set -e | Termina en cuanto un comando falla, con el código de ese comando. | Seguir trabajando sobre el resultado de un paso que no se hizo. |
set -u | Trata como error el uso de una variable que no existe, en lugar de reemplazarla por nada. | Los errores de tipeo en un nombre de variable. |
set -o pipefail | Considera fallida una tubería si falla cualquiera de sus comandos, no solo el último. | Que un error en mitad de una tubería pase inadvertido. |
Son opciones de la shell, de las del tema 34, y se pueden escribir juntas: set -euo pipefail es una línea que se encuentra al principio de muchísimos scripts. La versión definitiva del nuestro:
#!/bin/bash
# respaldo.sh: guarda una copia comprimida de un directorio en /tmp/respaldos.
# Uso: ./respaldo.sh [directorio] (por defecto, ~/practica)
set -eu
origen="${1:-$HOME/practica}"
destino="/tmp/respaldos"
fecha="$(date +%F)"
nombre="$(basename "$origen")"
archivo="$destino/$nombre-$fecha.tar.gz"
mkdir -p "$destino"
tar -czf "$archivo" -C "$(dirname "$origen")" "$nombre"
echo "Respaldo listo: $archivo"
$ ./respaldo.sh /noexiste; echo "código: $?"
tar: noexiste: Cannot stat: No such file or directory
tar: Exiting with failure status due to previous errors
código: 2
$ ./respaldo.sh && echo "se puede seguir"
Respaldo listo: /tmp/respaldos/practica-2026-10-13.tar.gz
se puede seguir
Ahora el script se detiene donde falla, no anuncia lo que no hizo, y devuelve el código de tar. Se puede encadenar como cualquier comando, y dice la verdad.
El ejemplo de siempre son dos líneas: cd "$directorio" y, debajo, rm -rf *. Si el cd falla —porque el directorio no existe, o el nombre está mal escrito—, el script no se detiene: ejecuta el rm en el directorio donde estaba, que puede ser cualquiera. Con set -e, termina en el cd. Y con set -u, una línea como rm -rf "$directoro/", con una letra de menos en el nombre de la variable, es un error en lugar de convertirse en rm -rf "/". No son casos imaginarios: los dos han borrado sistemas enteros.
set -e no reemplaza al criterio. Hay comandos que «fallan» de manera legítima —grep devuelve 1 cuando no encuentra nada, y diff, cuando hay diferencias—, y con set -e detendrían el script. A esos se les agrega || true, que se lee «y si falla, no importa». Para terminar a propósito con un error propio, exit 1; por convención, 0 es éxito y cualquier otro número, de 1 a 255, una falla.
9. Mensajes, errores y tuberías
Un script bien hecho se comporta como los comandos que venimos usando, y eso significa respetar los tres canales del tema 22: los resultados van a la salida estándar, y los mensajes de error y de avance, al canal de errores, con >&2.
echo "Comprimiendo $origen…" >&2 # avance: para la persona que mira
echo "$archivo" # resultado: para quien use la salida
La razón es práctica. Si el script escribe sus avisos en la salida estándar, quien quiera usar su resultado —archivo=$(./respaldo.sh), o una tubería— recibe los avisos mezclados con los datos. Separándolos, ./script > resultado.txt guarda solo lo que importa, y los mensajes siguen apareciendo en la pantalla.
Y hay algo más, que casi nadie espera la primera vez: un script hereda la entrada estándar. Si adentro hay un comando que lee de ella, lee lo que le llegue al script. Un script puede ser, entonces, un filtro como los del bloque 6.
#!/bin/bash
# frecuentes.sh: muestra las palabras más repetidas del texto que recibe.
# Uso: comando | ./frecuentes.sh [cantidad] (por defecto, 10)
set -eu
cantidad="${1:-10}"
tr -cs '[:alpha:]' '\n' |
tr '[:upper:]' '[:lower:]' |
sort |
uniq -c |
sort -rn |
head -n "$cantidad"
$ chmod +x frecuentes.sh
$ ./frecuentes.sh 5 < gpl-3.0.txt
345 the
221 of
192 to
175 a
151 or
$ man bash | ./frecuentes.sh 3
4213 the
1487 is
1264 of
$ curl -s https://www.gnu.org/licenses/gpl-3.0.txt | ./frecuentes.sh 3 | awk '{print $2}'
the
of
to
El primer tr combina dos opciones del tema 26: con -c actúa sobre todo lo que no es una letra, lo reemplaza por un salto de línea, y con -s comprime los saltos seguidos, de modo que queda una palabra por renglón. Lo demás es el recuento de siempre. Como ese tr no tiene archivo de dónde leer, lee de la entrada estándar del script, que es lo que venga por la tubería.
El archivo gpl-3.0.txt es el que descargamos en el tema 40, y el tercer ejemplo lo lee directamente de la red. El script quedó en medio de una tubería, entre curl y awk, sin saber de ninguno de los dos. Seis comandos que había que recordar y tipear se convirtieron en una palabra, que se combina con las demás. Esa es la idea entera del curso, aplicada a algo que hiciste tú.
10. Instalarlo como un comando
Falta que el script se pueda llamar desde cualquier directorio, sin ruta. La pieza ya está puesta: en el tema 34 creamos ~/bin, y vimos que Ubuntu lo agrega a PATH en cada inicio de sesión. Instalar un script es copiarlo allí.
$ cp respaldo.sh ~/bin/respaldo
$ cp frecuentes.sh ~/bin/frecuentes
$ ls -l ~/bin
-rwxrwxr-x 1 ana ana 148 oct 13 15:48 frecuentes
-rwxrwxr-x 1 ana ana 412 oct 13 15:48 respaldo
-rwxrwxr-x 1 ana ana 53 oct 12 22:11 saludo
$ cd /tmp
$ respaldo ~/bin
Respaldo listo: /tmp/respaldos/bin-2026-10-13.tar.gz
$ type respaldo
respaldo is /home/ana/bin/respaldo
$ history | frecuentes 3
Al copiarlos les quitamos la extensión: un comando se llama respaldo, no respaldo.sh, y a quien lo usa no le interesa en qué lenguaje está escrito. Si el día de mañana se lo reescribe en Python, el nombre no cambia, y nada de lo que lo llama se rompe. cp conservó el permiso de ejecución.
| Directorio | Para scripts que usará | Requiere |
|---|---|---|
~/bin, ~/.local/bin | Solo tu usuario. | Nada. Es lo habitual. |
/usr/local/bin | Todos los usuarios de la máquina. | sudo. Es el lugar que el tema 8 reservó para lo que no administra el gestor de paquetes. |
Si el script se llama igual que un comando existente, la shell ejecuta el que encuentra primero, y no avisa. El caso célebre es test: es un comando interno de Bash, que no escribe nada, y los internos tienen precedencia sobre todo lo que hay en PATH. Quien nombra test a su primer script lo ejecuta, no ve ninguna salida ni ningún error, y pierde una hora. Antes de bautizar, type nombre: si responde not found, está libre. Nombres como test, time, kill, sort o script ya tienen dueño.
Si un script recién copiado no se encuentra, las causas son las del tema 34: el directorio no está en PATH —en Ubuntu, ~/bin entra al iniciar sesión, si ya existía—, falta el permiso de ejecución, o la shell recuerda una ubicación anterior, lo que se arregla con hash -r.
11. Depurar: bash -n, bash -x y shellcheck
Un script que no hace lo esperado se investiga con tres herramientas, de la más simple a la más completa.
bash -n: revisar sin ejecutar
bash -n script.sh lee el script, verifica que esté bien formado y no ejecuta nada. Si no escribe nada, la sintaxis es correcta. Detecta las comillas sin cerrar y los paréntesis sin pareja, que son los errores de tipeo más comunes:
$ bash -n respaldo.sh
$ echo 'echo "sin cerrar' > roto.sh; bash -n roto.sh
roto.sh: line 1: unexpected EOF while looking for matching `"'
$ rm roto.sh
bash -x: ver lo que hace
bash -x script.sh ejecuta el script mostrando cada comando antes de ejecutarlo, con las variables ya reemplazadas por sus valores. Es la herramienta que resuelve casi todo.
$ bash -x respaldo.sh
+ set -eu
+ origen=/home/ana/practica
+ destino=/tmp/respaldos
++ date +%F
+ fecha=2026-10-13
++ basename /home/ana/practica
+ nombre=practica
+ archivo=/tmp/respaldos/practica-2026-10-13.tar.gz
+ mkdir -p /tmp/respaldos
++ dirname /home/ana/practica
+ tar -czf /tmp/respaldos/practica-2026-10-13.tar.gz -C /home/ana practica
+ echo 'Respaldo listo: /tmp/respaldos/practica-2026-10-13.tar.gz'
Respaldo listo: /tmp/respaldos/practica-2026-10-13.tar.gz
Las líneas con + son los comandos, tal como quedaron después de todas las sustituciones; las de ++, los que se ejecutaron dentro de un $(…). Lo que no lleva signo es la salida normal. La mayoría de los errores de un script son una variable que no vale lo que se creía, y aquí se la ve con su valor real. Para rastrear solo un tramo, se escribe set -x antes y set +x después, dentro del script.
shellcheck: un revisor
shellcheck es un programa que lee un script y señala los errores probables y las malas costumbres, con una explicación de cada uno. Se instala con sudo apt install shellcheck.
$ cat limpiar.sh
#!/bin/bash
dir=$1
rm -rf $dir/*
$ shellcheck limpiar.sh
In limpiar.sh line 3:
rm -rf $dir/*
^--^ SC2115 (warning): Use "${var:?}" to ensure this never expands to /* .
^--^ SC2086 (info): Double quote to prevent globbing and word splitting.
Encontró, en un script de tres líneas, los dos problemas de la sección 8: la variable sin comillas, y el rm que borraría la raíz si la variable llegara vacía. Cada aviso lleva un código, y buscándolo se llega a una página que lo explica con ejemplos. Pasar por shellcheck todo lo que se escribe es la manera más rápida de aprender a escribir bien.
| Herramienta | Ejecuta el script | Encuentra |
|---|---|---|
bash -n script | No | Errores de sintaxis. |
shellcheck script | No | Errores probables, descuidos y riesgos. |
bash -x script | Sí | Qué hizo realmente, paso a paso. |
12. Un segundo script, y dónde ejecutarlo
La otra línea pendiente es el informe del tema 37. En un archivo se la puede ampliar sin que se vuelva ilegible:
#!/bin/bash
# estado: informe breve del estado del sistema.
# Uso: estado
set -eu
sistema="$(grep '^PRETTY_NAME=' /etc/os-release | cut -d= -f2 | tr -d '"')"
echo "== $(hostname) · $(date '+%F %T')"
echo "Sistema: $sistema"
echo "Kernel: $(uname -r)"
echo "Encendido: $(uptime -p)"
echo "Carga: $(cut -d' ' -f1-3 /proc/loadavg) (procesadores: $(nproc))"
echo
free -h
echo
df -h -x tmpfs -x squashfs -x efivarfs
$ chmod +x ~/bin/estado
$ estado
== equipo · 2026-10-13 16:02:17
Sistema: Ubuntu 24.04.3 LTS
Kernel: 6.8.0-87-generic
Encendido: up 7 hours, 33 minutes
Carga: 0.38 0.41 0.36 (procesadores: 8)
total used free shared buff/cache available
Mem: 7.7Gi 3.9Gi 512Mi 430Mi 3.8Gi 3.8Gi
Swap: 2.0Gi 0B 2.0Gi
Filesystem Size Used Avail Use% Mounted on
/dev/sda2 233G 61G 161G 28% /
/dev/sda1 1,1G 6,2M 1,1G 1% /boot/efi
No tiene un solo comando nuevo. Cada línea salió de un tema del bloque 8, y lo que el script agrega es que estén juntas, en orden y con un nombre. Un echo sin argumentos escribe una línea en blanco.
Como es un archivo, se lo puede llevar a donde los comandos ya llegaban. Tres destinos:
En otra máquina
Con el ssh del tema 40, un script local se puede ejecutar en un servidor sin copiarlo: se le pide a la máquina remota un Bash que lea los comandos de su entrada estándar, y se le envía el archivo por ella.
$ ssh servidor 'bash -s' < ~/bin/estado | head -n 4
== servidor · 2026-10-13 16:03:51
Sistema: Ubuntu 24.04.3 LTS
Kernel: 6.8.0-87-generic
Encendido: up 5 weeks, 6 days, 6 hours, 52 minutes
El mismo archivo, la misma salida, otra máquina. La redirección < es local, y el bash -s entre comillas, remoto: las reglas del tema anterior sobre dónde se ejecuta cada parte.
En un archivo, con fecha
Con las redirecciones del tema 23, cada ejecución se agrega a un registro propio: estado >> ~/estado.log 2>&1.
A una hora fija
El servicio cron, que usamos de ejemplo en el tema 39, existe para esto: ejecutar comandos en momentos programados. Cada usuario tiene su propia tabla de tareas, que se edita con crontab -e —el comando que, según vimos, abre el editor de la variable EDITOR— y se lista con crontab -l. Cada línea es una tarea:
30 20 * * 1-5 /home/ana/bin/respaldo >> /home/ana/respaldo.log 2>&1
│ │ │ │ │ │
│ │ │ │ │ └─ el comando, con su ruta completa
│ │ │ │ └─ día de la semana: de lunes (1) a viernes (5)
│ │ │ └─ mes: cualquiera
│ │ └─ día del mes: cualquiera
│ └─ hora: 20
└─ minuto: 30
Esa línea ejecuta el respaldo a las 20:30 de lunes a viernes. Un script lanzado por cron corre sin terminal y con un entorno mínimo, lo que explica las tres precauciones que se ven en ella. El comando va con su ruta completa, porque el PATH de cron es más corto que el tuyo. La salida se redirige a un archivo, porque no hay pantalla donde mostrarla. Y dentro del script no se puede contar con nada de .bashrc: ni alias, ni variables propias. El síntoma típico, «a mano funciona y desde cron no», tiene casi siempre una de esas tres causas, y el archivo de registro dice cuál.
13. Cuidados, y lo que falta
Un script hace lo que se le escribió, a toda velocidad y sin preguntar. Las costumbres que siguen son las que evitan los accidentes conocidos.
| Costumbre | Evita |
|---|---|
Empezar con #!/bin/bash y set -eu. | Que el script siga después de un error, o use una variable vacía. |
| Poner todas las variables entre comillas dobles. | Las fallas con nombres que tienen espacios o comodines. |
Probar primero con echo delante de cada comando destructivo. | Borrar o mover lo que no era. echo rm -rf "$dir" muestra lo que se ejecutaría. |
Probar sobre una copia, en /tmp. | Aprender con los datos de verdad. |
| No escribir contraseñas dentro del script. | Que queden a la vista de quien lea el archivo, o en una copia de respaldo. |
Usar rutas absolutas, o construidas con $HOME. | Que el resultado dependa del directorio desde donde se lo llame. |
Leer con less todo script ajeno antes de ejecutarlo. | Lo que advertimos en los temas 18 y 36. |
Cuando una tarea necesita privilegios, la tentación es llenar el script de sudo. Es preferible lo contrario: escribirlo sin ninguno, y ejecutarlo completo como administrador —sudo ./script.sh— cuando haga falta. Así no se detiene a pedir la contraseña por la mitad, se puede lanzar desde cron, y queda claro de un vistazo que todo lo que contiene corre como root. Por eso mismo, un script que se ejecuta con sudo se revisa dos veces, y el archivo no debe poder modificarlo nadie más: es una llave del sistema.
Los scripts de este tema son lineales: una lista de comandos que se ejecutan de arriba abajo, siempre los mismos. Con eso solo se automatiza una cantidad sorprendente de trabajo, y es el punto de partida correcto. Pero se nota enseguida lo que no se puede hacer:
| Querría que el script… | Hace falta |
|---|---|
| Compruebe si el directorio existe antes de respaldarlo, y avise con un mensaje claro. | Una condición: if. |
| Respalde cada uno de los directorios de una lista. | Un bucle: for, while. |
| Borre los respaldos de más de treinta días, si hay más de cinco. | Comparaciones y aritmética. |
Acepte opciones, como -d destino o --ayuda. | Procesar los argumentos uno por uno. |
| Pregunte algo y espere la respuesta. | Leer del teclado: read. |
| Reúna pasos repetidos bajo un nombre. | Funciones, como la mkcd del tema 34. |
Eso es programar en Bash, y es la materia completa del curso siguiente. Lo que este tema deja es la base sobre la que todo aquello se apoya: un archivo, su primera línea, sus argumentos, su código de salida, y la costumbre de guardar lo que funciona.
14. Recetas y errores comunes
| Quiero… | Se escribe |
|---|---|
| Empezar un script | #!/bin/bash en la primera línea, y set -eu |
| Hacerlo ejecutable | chmod +x script.sh |
| Ejecutarlo desde su directorio | ./script.sh |
| Ejecutarlo sin darle permiso | bash script.sh |
| Usarlo desde cualquier lugar | cp script.sh ~/bin/nombre |
| Usar el primer argumento, con un valor por defecto | dato="${1:-valor}" |
| Exigir un argumento | dato="${1:?Uso: script archivo}" |
| Pasar todos los argumentos a otro comando | comando "$@" |
| Guardar la salida de un comando en una variable | hoy="$(date +%F)" |
| Escribir un mensaje de error | echo "mensaje" >&2 |
| Terminar con un error | exit 1 |
| Que un comando pueda fallar sin detener el script | comando || true |
| Partir una línea larga | \ al final, o terminar la línea en | |
| Revisar la sintaxis sin ejecutar | bash -n script.sh |
| Ver cada comando al ejecutarse | bash -x script.sh |
| Buscar errores y descuidos | shellcheck script.sh |
| Ejecutar un script local en otra máquina | ssh máquina 'bash -s' < script.sh |
| Programar su ejecución | crontab -e |
| Guardar todo lo que escribe, con los errores | script >> registro.log 2>&1 |
| Síntoma | Causa | Solución |
|---|---|---|
bash: ./script.sh: Permission denied | Falta el permiso de ejecución. | chmod +x script.sh |
script.sh: command not found | El directorio actual no está en PATH. | ./script.sh, o instalarlo en ~/bin. |
/bin/bash^M: bad interpreter: No such file or directory | El archivo tiene finales de línea de Windows: se editó o se copió desde allí. | sed -i 's/\r$//' script.sh. Se confirma con file script.sh, que dice CRLF. |
bad interpreter: No such file or directory | La ruta del shebang está mal escrita, o ese intérprete no está instalado. | Revisar la primera línea; type bash. |
El script funciona con bash script.sh y falla con sh script.sh. | sh no es Bash. | Ejecutarlo con Bash; declarar #!/bin/bash. |
destino: command not found, en una línea de asignación. | Hay espacios alrededor del signo igual. | destino=/tmp |
| Falla solo con archivos que tienen espacios en el nombre. | Una variable sin comillas. | "$variable"; pasar shellcheck. |
unexpected EOF while looking for matching `"' | Una comilla sin cerrar, en esa línea o en una anterior. | bash -n; un editor con colores la delata. |
unbound variable | Con set -u: se usó una variable que no existe, o un argumento que no se dio. | Corregir el nombre; o ${1:-} si puede faltar. |
El script hizo cd, y la terminal sigue en el mismo lugar. | Se ejecutó en un proceso hijo. | Es lo esperado. Para cambiar la shell, una función o source. |
| El script definió una variable, y después no existe. | La misma: murió con el proceso hijo. | source script.sh |
| Un alias que uso siempre «no existe» dentro del script. | Los alias son de las shells interactivas. | Escribir el comando completo. |
| El script termina solo, sin llegar al final. | Con set -e: un comando devolvió un código distinto de cero. | bash -x muestra cuál; si es legítimo, || true. |
| El script dice que todo salió bien, y no fue así. | Sin set -e, sigue después de un error, y su código es el del último comando. | set -e. |
Llamé test a mi script, y no hace nada. | Se ejecuta el comando interno test. | Otro nombre; antes, type nombre. |
| Edité el script y sigue ejecutándose la versión vieja. | Hay otra copia antes en PATH: se editó una y se ejecuta la otra. | type -a nombre. |
La línea partida con \ da error. | Hay un espacio después de la barra. | Borrarlo: la barra debe ser el último carácter. |
A mano funciona, y desde cron no. | El entorno de cron es mínimo: otro PATH, sin .bashrc. | Rutas completas; redirigir la salida a un archivo y leerlo. |
La referencia del tema:
| Se escribe | Qué es |
|---|---|
#!/bin/bash | La primera línea: con qué programa se interpreta el script. |
# texto | Un comentario. |
set -e, set -u | Terminar ante un error; tratar como error una variable inexistente. |
nombre="valor", "$nombre" | Definir una variable; usarla. |
"$(comando)" | La salida de un comando, como texto. |
$1, $2, $#, "$@", $0 | Los argumentos, su cantidad, todos juntos, y el nombre del script. |
${1:-valor}, ${1:?mensaje} | Un argumento con valor por defecto; un argumento obligatorio. |
exit N | Terminar el script con el código N. |
>&2 | Enviar un mensaje al canal de errores. |
chmod +x script, ./script | Hacerlo ejecutable; ejecutarlo. |
bash script, source script | Ejecutarlo en un Bash hijo; en la shell actual. |
bash -n, bash -x, shellcheck | Revisar la sintaxis; rastrear la ejecución; buscar errores. |
crontab -e, crontab -l | Editar y listar las tareas programadas. |
En ~/bin hay ahora tres comandos propios más: respaldo, frecuentes y estado. Ninguno contiene nada que no supieras hacer antes de este tema; lo que cambió es que ya no hay que rehacerlo. Queda poner todo el curso a prueba con un trabajo completo. En el tema 42 tomamos un archivo de registro, de miles de líneas, y le sacamos un informe completo usando grep, sed, awk, sort y uniq, paso a paso.
15. Resumen
- Un script es un archivo de texto con comandos, que la shell ejecuta en orden. Se escribe con los mismos comandos que se usan en la terminal.
- Una línea pide ser un script cuando se va a repetir, cuando es larga, cuando debe ejecutarse sin vigilancia o cuando otro tiene que poder usarla.
- La primera línea,
#!/bin/bash, indica el intérprete. La lee el kernel.shno es Bash. chmod +xlo hace ejecutable, y se lo llama con./script.sh.bash script.shno necesita el permiso.- Un script corre en un proceso hijo: sus
cdy sus variables no afectan a la shell que lo llamó.sourcelo ejecuta en la shell actual. - Los comentarios empiezan con
#. Un encabezado dice qué hace el script y cómo se usa. - Las variables reúnen los datos al principio, y se usan siempre entre comillas dobles.
$1,$2… son los argumentos;$#, su cantidad;"$@", todos.${1:-valor}da un valor por defecto.- Por defecto, un script sigue después de un error, y su código de salida es el del último comando.
set -elo detiene en el primer fallo, yset -u, ante una variable inexistente. - Los resultados van a la salida estándar, y los mensajes, al canal de errores. Un script que lee su entrada estándar es un filtro, y entra en una tubería.
- Copiado a
~/bin, sin extensión, es un comando más. Antes de elegir el nombre,type. bash -nrevisa la sintaxis,bash -xmuestra cada comando al ejecutarse, yshellcheckseñala errores y riesgos.- Un script se puede ejecutar en otra máquina con
ssh, y a una hora fija concrontab, donde hacen falta rutas completas y una redirección de la salida. - Las condiciones, los bucles y las funciones son la materia del curso siguiente.
16. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué es un script, y qué hay que aprender de nuevo para escribir uno?
Es un archivo de texto con comandos, que la shell ejecuta en orden. Los comandos son los mismos de la terminal; lo nuevo son unas pocas convenciones: la primera línea, los argumentos, el código de salida.
2. ¿Qué indica la línea #!/bin/bash, y quién la lee?
Indica con qué programa debe interpretarse el archivo. La lee el kernel al ejecutarlo; para Bash es un comentario.
3. Un script funciona con bash script.sh y falla con sh script.sh. ¿Por qué?
Porque sh es otra shell —en Ubuntu, dash—, que no entiende todas las construcciones de Bash.
4. ¿Qué diferencia hay entre ./script.sh y bash script.sh?
La primera ejecuta el archivo: necesita el permiso de ejecución y usa el intérprete del shebang. La segunda le entrega el archivo a Bash como argumento: no necesita el permiso ni mira el shebang.
5. Un script hace cd /var/log. Al terminar, ¿en qué directorio queda la terminal?
En el que estaba. El script corre en un proceso hijo, que cambia su propio directorio y termina. Para que el cambio quede, se lo ejecuta con source.
6. ¿Por qué conviene definir los datos en variables al principio del script?
Porque cada dato queda escrito una sola vez: cambiarlo es tocar una línea, y no hay riesgo de corregirlo en un lugar y olvidarlo en otro.
7. ¿Por qué se escribe "$archivo" y no $archivo?
Porque sin comillas el valor se divide en los espacios y se expanden sus comodines: un nombre con espacios llegaría al comando como varios argumentos.
8. Se ejecuta ./informe.sh ventas.csv 2026. ¿Qué valen $1, $2, $# y $0?
$1 vale ventas.csv; $2, 2026; $#, 2; $0, ./informe.sh.
9. ¿Qué hace destino="${1:-/tmp}"?
Asigna a destino el primer argumento, si se dio; si no, el valor /tmp.
10. Un script de tres comandos falla en el segundo. Sin ninguna opción, ¿qué ocurre con el tercero, y con qué código termina el script?
El tercero se ejecuta igual, y el script termina con el código de ese último comando: si tuvo éxito, con 0, aunque el segundo haya fallado.
11. ¿Qué cambian set -e y set -u?
set -e hace que el script termine en cuanto un comando falla. set -u, que usar una variable que no existe sea un error, en lugar de valer una cadena vacía.
12. ¿Por qué los mensajes de avance y de error de un script se envían con >&2?
Para que vayan al canal de errores y no se mezclen con el resultado: quien redirija o encadene la salida estándar recibe solo los datos.
13. ¿Cómo puede un script funcionar en medio de una tubería?
Porque hereda la entrada estándar: si su primer comando lee de ella, lee lo que llega por la tubería, y lo que el script escribe sale hacia el comando siguiente.
14. ¿Qué hay que hacer para ejecutar respaldo.sh desde cualquier directorio, escribiendo solo respaldo?
Darle permiso de ejecución y copiarlo, con ese nombre, a un directorio de PATH, como ~/bin.
15. Nombraste test a tu script, lo ejecutas y no escribe nada. ¿Qué pasa?
Se está ejecutando el comando interno test de Bash, que tiene precedencia y no produce salida. Hay que elegir otro nombre, verificándolo antes con type.
16. ¿Para qué sirven bash -n y bash -x?
bash -n revisa la sintaxis sin ejecutar nada. bash -x ejecuta el script mostrando cada comando, con las variables ya sustituidas.
17. Un script falla con /bin/bash^M: bad interpreter. ¿Cuál es la causa?
El archivo tiene finales de línea de Windows, y el kernel busca un intérprete con ese carácter de más en el nombre. Se corrige con sed -i 's/\r$//' script.sh.
18. Un script funciona al ejecutarlo a mano y falla desde cron. ¿Qué tres cosas revisas?
Que los comandos tengan su ruta completa, porque el PATH de cron es reducido; que no dependa de alias o variables de .bashrc; y la salida, redirigiéndola a un archivo para leer el error.
Para practicar
- Escribe
respaldo.shen su primera versión, ejecútalo conbash, dale permiso de ejecución y ejecútalo con./. Verifica el contenido del archivo creado contar -tzf. - Llévalo a su versión final, paso a paso: encabezado, variables, argumento optativo y
set -eu. Después de cada cambio, pruébalo con un directorio que exista y con uno que no, y mira el código de salida. - Crea
args.shy llámalo sin argumentos, con tres, con uno entre comillas que tenga espacios, y con*. Explica cada resultado. - Escribe un script
ir.shque cambie de directorio y defina una variable. Ejecútalo conbashy consource, y comprueba en cada caso dónde quedó la terminal y si la variable existe. - Escribe
frecuentes.shy pruébalo con un archivo, con una tubería y conhistory. Agrégale un segundo argumento optativo: la longitud mínima de las palabras a contar, usandoawk 'length($0) >= n'. - Convierte en script la línea del tema 38 que muestra los subdirectorios más pesados: que reciba el directorio como primer argumento, con el actual por defecto, y la cantidad como segundo, con diez por defecto.
- Instala tus scripts en
~/binsin extensión, y ejecútalos desde/tmp. Verifica cada nombre contype. Prueba qué ocurre si llamastesta uno. - Ejecuta
bash -xsobrerespaldo.she identifica el valor de cada variable. Introduce a propósito tres errores —una comilla sin cerrar, un espacio en una asignación, un nombre de variable mal escrito— y encuentra cada uno conbash -n,bash -xoshellcheck. - Escribe el script
estadoy agrégale dos líneas a tu elección, con datos de los temas 39 y 40. Guarda su salida en un archivo con>>, dos veces. Si tienes otra máquina, ejecútalo en ella conssh … 'bash -s'. - Programa con
crontab -equeestadose ejecute cada cinco minutos, con*/5 * * * *, agregando su salida a un archivo. Compruébalo al rato contail, mira conjournalctl -u croncómo quedó registrado, y quita la línea.