← Terminal y Línea de Comandos en Linux Bloque 9 · Integración y cierre · Tema 41 de 43

Tema 41

De la línea al archivo: tu primer script

En el tema 25 escribimos una línea que respaldaba un directorio, y al terminar dijimos que había dejado de ser un comando: era un programa que pedía un archivo propio. Lo mismo pasó en el tema 37 con el informe de estado del sistema, y en el 38 con la búsqueda de lo que llena el disco. Cada una de esas líneas resolvía un problema, y cada una se perdió en el historial. Este tema da el paso que faltaba, y es más corto de lo que parece: no hay que aprender un lenguaje nuevo. Un script se escribe con los mismos comandos de los cuarenta temas anteriores, puestos en un archivo. Lo nuevo son media docena de convenciones, y un hábito.

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ñalLo 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:

AliasFunciónScript
Vive en.bashrc.bashrcUn archivo propio
Se ejecuta enLa shell actualLa shell actualUn proceso hijo
Lo pueden usarTus shells interactivasLas shells que la definanCualquier usuario, cualquier programa, cron, ssh
Conviene paraUn comando con sus opcionesDos o tres comandos; lo que debe cambiar la shellTodo lo demás

2. De la línea al archivo

Partimos de la línea del tema 25, tal como la dejamos:

la línea original
$ 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:

~/practica/respaldo.sh — primera versió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:

terminal, en ~/practica
$ 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.

anatomía del shebang
#!/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.

Al escribir ./respaldo.sh, la shell le pide al kernel que ejecute el archivo; el kernel lee la primera línea, encuentra el shebang y lanza /bin/bash con el script como argumento; ese Bash nuevo, un proceso hijo, lee y ejecuta las líneas una por una y al terminar devuelve un código de salida a la shell Tu shell escribes ./respaldo.sh El kernel lee la primera línea #!/bin/bash Un Bash nuevo proceso hijo lee línea por línea Termina código de salida de vuelta en tu shell el script se ejecuta en otro proceso: lo que cambie en sí mismo no afecta a tu shell
La primera línea no la lee Bash: la lee el kernel, para saber a quién entregarle el archivo.
Primera líneaEl script lo interpretaSe usa
#!/bin/bashBash.La forma habitual, y la de este curso.
#!/usr/bin/env bashEl Bash que aparezca primero en PATH.En scripts que deben funcionar en sistemas donde Bash está en otra ruta.
#!/bin/shLa shell mínima del sistema, que en Debian y Ubuntu no es Bash.En scripts escritos a propósito para ser universales.
NingunaLa shell desde la que se lo lance, cualquiera sea.Nunca: el resultado depende de quién lo ejecute.
sh no es Bash

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.

terminal
$ ./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 escribeNecesita permiso xUsa el shebangCorre enSe usa para
bash script.shNoNo: lo interpreta BashUn proceso hijoProbar; ejecutar con opciones, como bash -x.
./script.shSíSíUn proceso hijoLa forma normal, desde su directorio.
scriptSíSíUn proceso hijoUn script instalado en PATH. Sección 10.
source script.shNoNoLa shell actualArchivos 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:

terminal
$ 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.

~/practica/respaldo.sh — con encabezado
#!/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:

CostumbrePor 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:

dos maneras de partir una línea
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.

~/practica/respaldo.sh — con variables
#!/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:

ReglaMalBien
Sin espacios alrededor del signo igual.destino = /tmpdestino=/tmp
Sin $ al asignar; con $ al usar.$destino=/tmpdestino=/tmp
Entre comillas dobles al usar, siempre.mkdir -p $destinomkdir -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.

~/practica/args.sh
#!/bin/bash
echo "Nombre del script: $0"
echo "Cantidad:          $#"
echo "Primero:           $1"
echo "Segundo:           $2"
echo "Todos:             $@"
terminal
$ 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ámetroContiene
$1, $2 … $9El 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.
$0El 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 escribeValeSe 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.
~/practica/respaldo.sh — con un argumento optativo
#!/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"
terminal
$ ./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:

terminal
$ ./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ónCon ella, el script…Evita
set -eTermina 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 -uTrata 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 pipefailConsidera 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:

~/practica/respaldo.sh — versión final
#!/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"
terminal
$ ./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.

Un script sin set -e sigue adelante después de un error

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.

dos líneas de un script
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.

~/practica/frecuentes.sh
#!/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"
terminal
$ 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í.

terminal
$ 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.

DirectorioPara scripts que usaráRequiere
~/bin, ~/.local/binSolo tu usuario.Nada. Es lo habitual.
/usr/local/binTodos los usuarios de la máquina.sudo. Es el lugar que el tema 8 reservó para lo que no administra el gestor de paquetes.
Antes de elegir un nombre, pregúntale a type

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:

terminal
$ 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.

terminal
$ 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.

terminal
$ 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.

HerramientaEjecuta el scriptEncuentra
bash -n scriptNoErrores de sintaxis.
shellcheck scriptNoErrores probables, descuidos y riesgos.
bash -x scriptSí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/estado
#!/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
terminal
$ 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.

terminal
$ 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:

anatomía de una línea de crontab
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.

CostumbreEvita
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.
En un script, sudo va afuera

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 ejecutablechmod +x script.sh
Ejecutarlo desde su directorio./script.sh
Ejecutarlo sin darle permisobash script.sh
Usarlo desde cualquier lugarcp script.sh ~/bin/nombre
Usar el primer argumento, con un valor por defectodato="${1:-valor}"
Exigir un argumentodato="${1:?Uso: script archivo}"
Pasar todos los argumentos a otro comandocomando "$@"
Guardar la salida de un comando en una variablehoy="$(date +%F)"
Escribir un mensaje de errorecho "mensaje" >&2
Terminar con un errorexit 1
Que un comando pueda fallar sin detener el scriptcomando || true
Partir una línea larga\ al final, o terminar la línea en |
Revisar la sintaxis sin ejecutarbash -n script.sh
Ver cada comando al ejecutarsebash -x script.sh
Buscar errores y descuidosshellcheck script.sh
Ejecutar un script local en otra máquinassh máquina 'bash -s' < script.sh
Programar su ejecucióncrontab -e
Guardar todo lo que escribe, con los erroresscript >> registro.log 2>&1
SíntomaCausaSolución
bash: ./script.sh: Permission deniedFalta el permiso de ejecución.chmod +x script.sh
script.sh: command not foundEl directorio actual no está en PATH../script.sh, o instalarlo en ~/bin.
/bin/bash^M: bad interpreter: No such file or directoryEl 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 directoryLa 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 variableCon 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 escribeQué es
#!/bin/bashLa primera línea: con qué programa se interpreta el script.
# textoUn comentario.
set -e, set -uTerminar 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, $#, "$@", $0Los argumentos, su cantidad, todos juntos, y el nombre del script.
${1:-valor}, ${1:?mensaje}Un argumento con valor por defecto; un argumento obligatorio.
exit NTerminar el script con el código N.
>&2Enviar un mensaje al canal de errores.
chmod +x script, ./scriptHacerlo ejecutable; ejecutarlo.
bash script, source scriptEjecutarlo en un Bash hijo; en la shell actual.
bash -n, bash -x, shellcheckRevisar la sintaxis; rastrear la ejecución; buscar errores.
crontab -e, crontab -lEditar 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

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

  1. Escribe respaldo.sh en su primera versión, ejecútalo con bash, dale permiso de ejecución y ejecútalo con ./. Verifica el contenido del archivo creado con tar -tzf.
  2. 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.
  3. Crea args.sh y llámalo sin argumentos, con tres, con uno entre comillas que tenga espacios, y con *. Explica cada resultado.
  4. Escribe un script ir.sh que cambie de directorio y defina una variable. Ejecútalo con bash y con source, y comprueba en cada caso dónde quedó la terminal y si la variable existe.
  5. Escribe frecuentes.sh y pruébalo con un archivo, con una tubería y con history. Agrégale un segundo argumento optativo: la longitud mínima de las palabras a contar, usando awk 'length($0) >= n'.
  6. 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.
  7. Instala tus scripts en ~/bin sin extensión, y ejecútalos desde /tmp. Verifica cada nombre con type. Prueba qué ocurre si llamas test a uno.
  8. Ejecuta bash -x sobre respaldo.sh e 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 con bash -n, bash -x o shellcheck.
  9. Escribe el script estado y 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 con ssh … 'bash -s'.
  10. Programa con crontab -e que estado se ejecute cada cinco minutos, con */5 * * * *, agregando su salida a un archivo. Compruébalo al rato con tail, mira con journalctl -u cron cómo quedó registrado, y quita la línea.