Bash es el intérprete de órdenes que abre casi cualquier servidor Linux, el que viene en WSL y en Git Bash para Windows y el que corre dentro de la mayoría de los contenedores. Con él se manejan archivos, se lanzan programas y, sobre todo, se automatiza: lo que haces a mano tres veces se convierte en un script que lo hace solo, a las tres de la mañana si hace falta. Este curso empieza desde cero, usa Bash 5.3, la versión actual, y en cada lección enseña la herramienta que más errores evita: ShellCheck, que revisa tus scripts antes de que borren lo que no debían.
La ruta: ocho lecciones
Están pensadas para leerse en orden. Las dos primeras están en esta misma página y las demás tienen la suya. Cada lección tiene un recuadro «El error que te ahorra», con un fallo típico y el aviso real de ShellCheck o de Bash que lo delata, un recuadro «Ojo con la seguridad», al menos un esquema y retos al final con la solución escondida. Al terminar hay un test de 24 preguntas: adivinar qué imprime cada script.
- 1Primeros pasosQué es Bash, la terminal, órdenes y argumentos, y el primer script.En esta página
- 2Variables, comillas y expansionesVariables, la regla de las comillas, $( ), aritmética y read.En esta página
- 3Condiciones, bucles y códigos de salidaif, test, [[ ]], case, for, while y exit.Leer la lección →
- 4Funciones, arrays y cadenasFunciones con local, arrays, recortar y cambiar texto, printf.Leer la lección →
- 5Redirecciones, tuberías y procesos>, 2>&1, tuberías, here-docs, procesos en segundo plano.Leer la lección →
- 6Procesar texto: grep, sed y awkBuscar, cambiar y sumar en archivos de texto y registros.Leer la lección →
- 7Scripts robustosset -euo pipefail, trap, getopts, mktemp y ShellCheck.Leer la lección →
- 8Un proyecto completoUn script con opciones, registro, pruebas y cron.Leer la lección →
Lección 1. Primeros pasos
Qué es Bash
Una shell es el programa que lee lo que escribes en la terminal, lo interpreta y lanza los programas que pides. La primera shell de Unix que se parecía a las de hoy fue la de Stephen Bourne, en los Laboratorios Bell, que llegó con Unix Version 7 en 1979 y que todo el mundo conoce como sh. Diez años después, el 8 de junio de 1989, Brian Fox publicó para la Free Software Foundation la primera beta de Bash, el Bourne-Again SHell: un sustituto libre de sh, compatible con él y con muchas mejoras. Desde principios de los noventa lo mantiene Chet Ramey, y hoy es la shell por omisión de casi todas las distribuciones de Linux.
No es la única. En macOS la shell por omisión es zsh desde Catalina (2019), aunque el sistema sigue trayendo un Bash 3.2 de 2007. En Debian y Ubuntu, /bin/sh no es Bash sino dash, una shell más pequeña y rápida que solo entiende lo que exige el estándar POSIX; Ubuntu hizo el cambio en 2006 y Debian en 2011. Por eso importa qué intérprete ejecuta tu script, y lo vemos enseguida con el shebang. Las diferencias entre versiones están en Versiones y parches.
Abrir una terminal
- Linux: cualquier terminal (GNOME Terminal, Konsole…) abre Bash. Comprueba la versión con
bash --version. - macOS: la app Terminal abre zsh. Para seguir el curso, instala un Bash moderno con Homebrew (
brew install bash) y ábrelo escribiendobash; los scripts funcionarán igual porque llevan su propio shebang. - Windows: lo más fiel es WSL, que instala un Ubuntu completo con
wsl --install. Git Bash, que viene con Git para Windows, también sirve para casi todo el curso. - Con Docker, sin instalar nada:
docker run --rm -it ubuntu:26.04 bash.
Lo primero, saber qué tienes. En los bloques de terminal, lo que va detrás de $ es lo que se teclea, y debajo, lo que contesta el sistema:
terminal$ bash --version | head -1 GNU bash, version 5.3.9(1)-release (x86_64-pc-linux-gnu) $ echo "$BASH_VERSION" 5.3.9(1)-release $ whoami ana $ pwd /home/ana/proyecto
whoami dice con qué usuario estás y pwd (print working directory), en qué carpeta. Todo el curso se ejecutó como la usuaria ana, dentro de /home/ana/proyecto.
Órdenes y argumentos
Una orden es una palabra seguida de sus argumentos, separados por espacios. Los que empiezan por guion suelen ser opciones que cambian lo que hace la orden:
terminal$ type echo ls cd echo is a shell builtin ls is /usr/bin/ls cd is a shell builtin $ ls notas.txt tareas.txt $ ls -l --time-style=+ notas.txt -rw-r--r-- 1 ana ana 56 notas.txt $ wc -l notas.txt tareas.txt 3 notas.txt 2 tareas.txt 5 total $ echo Hola, mundo Hola, mundo
Fíjate en dos cosas. echo recibió dos argumentos, Hola, y mundo, y los escribió con un solo espacio: los espacios de más no llegan a la orden, porque Bash los usa para separar palabras. Y type cuenta de dónde sale cada orden: echo y cd son órdenes internas (builtins) de Bash, y ls es un programa que está en /usr/bin. Bash lo encontró porque esa carpeta está en la variable PATH. El esquema resume lo que hace Bash cada vez que pulsas Intro:
Las órdenes más útiles del día a día, de cp y mv a find y tar, están en Los comandos de Linux que de verdad se usan. Aquí nos centramos en el lenguaje que las une.
El primer script
Un script es un archivo de texto con órdenes, una por línea, que Bash ejecuta de arriba abajo. Crea hola.sh con tu editor:
#!/usr/bin/env bash # Mi primer script: saluda y dice dónde está. echo "Hola, $USER" echo "Estás en $PWD"
La primera línea es el shebang: #! seguido del intérprete que debe ejecutar el archivo. /usr/bin/env bash busca bash en el PATH, así que funciona aunque esté instalado en otra carpeta, como el de Homebrew en macOS. Las líneas que empiezan por # son comentarios. Ahora, a ejecutarlo:
terminal$ ./hola.sh bash: ./hola.sh: Permission denied $ bash hola.sh Hola, ana Estás en /home/ana/proyecto $ chmod +x hola.sh $ ./hola.sh Hola, ana Estás en /home/ana/proyecto $ hola.sh bash: hola.sh: command not found
Tres lecciones en cinco órdenes:
- Un archivo recién creado no tiene permiso de ejecución, y
./hola.shfalla conPermission denied.bash hola.shsí funciona, porque el que se ejecuta es Bash y el script solo es un archivo que lee. chmod +xle da ese permiso. A partir de ahí,./hola.shfunciona y el sistema usa el shebang para saber con qué ejecutarlo.- Sin el
./delante, Bash buscahola.shen las carpetas del PATH, y la carpeta actual no está entre ellas:command not found. Es a propósito, para que nadie pueda colarte un programa llamadolsen una carpeta que visitas.
echo y printf
#!/usr/bin/env bash nombre="Ana" echo "Hola, $nombre" echo -n "Sin salto de línea... " echo "y ahora sí" printf '%s tiene %d tareas y un %.1f de nota\n' "$nombre" 3 8.75 printf '%-10s|%5s|\n' "izquierda" "der"
salidaHola, Ana Sin salto de línea... y ahora sí Ana tiene 3 tareas y un 8.8 de nota izquierda | der|
echo escribe sus argumentos y un salto de línea. Es cómodo, pero sus opciones cambian de una shell a otra (-n y -e no funcionan igual en dash ni en el Bash de macOS). printf es más fiable: el primer argumento es un formato, con %s para texto, %d para enteros y %.1f para decimales, y no añade el salto de línea si no pones \n. Para cualquier cosa que no sea un mensaje sencillo, usa printf.
El código de salida
Cada orden termina con un número, su código de salida, que Bash guarda en la variable $?. Hay que leerlo justo después, porque la orden siguiente lo sobrescribe; por eso aquí va en la misma línea, tras un ;, que separa dos órdenes. 0 significa que todo fue bien y cualquier otro número, que algo falló. Es al revés que en muchos lenguajes, donde 0 es «falso», y es la base de todas las condiciones de Bash, como verás en la lección 3:
terminal$ ls notas.txt; echo "código: $?" notas.txt código: 0 $ ls no-existe.txt; echo "código: $?" ls: cannot access 'no-existe.txt': No such file or directory código: 2 $ grep -q pan notas.txt; echo "código: $?" código: 0 $ grep -q queso notas.txt; echo "código: $?" código: 1
Cada programa decide sus códigos: ls devuelve 2 si no encuentra el archivo y grep, 1 si no encuentra el texto. Un script devuelve el código de la última orden que ejecutó, o el que le digas con exit.
El error que te ahorra: el script escrito en Windows
Si escribes o copias un script en Windows, sus líneas acaban en \r\n en vez de \n. Bash toma ese \r invisible como parte de las palabras, y el error es de lo más confuso: dice que no encuentra un programa llamado bash\r. Aquí creamos un archivo así a propósito, con printf, para ver el fallo, cómo lo explican file y ShellCheck, y cómo se arregla:
terminal$ printf '#!/usr/bin/env bash\r\necho "Hola"\r\n' > windows.sh
$ chmod +x windows.sh
$ ./windows.sh
env: $'bash\r': No such file or directory
env: use -[v]S to pass options in shebang lines
$ file windows.sh
windows.sh: Bourne-Again shell script, ASCII text executable, with CRLF line terminators
$ shellcheck windows.sh
In windows.sh line 1:
#!/usr/bin/env bash
^-- SC1017 (error): Literal carriage return. Run script through tr -d '\r' .
In windows.sh line 2:
echo "Hola"
^-- SC1017 (error): Literal carriage return. Run script through tr -d '\r' .
For more information:
https://www.shellcheck.net/wiki/SC1017 -- Literal carriage return. Run scri...
$ sed -i 's/\r$//' windows.sh
$ ./windows.sh
Holased quita el \r del final de cada línea (también sirve dos2unix, si lo tienes). Para que no vuelva a pasar, configura tu editor para guardar con saltos de línea de Unix (LF): en Visual Studio Code está abajo a la derecha, donde pone CRLF.
Lección 2. Variables, comillas y expansiones
Variables
Una variable se crea al darle un valor, sin declararla antes, y se lee poniendo $ delante del nombre. La regla que más tropiezos causa: sin espacios alrededor del =.
terminal$ nombre=Ana
$ echo "Hola, $nombre"
Hola, Ana
$ edad=30
$ echo "$nombre tiene $edad años"
Ana tiene 30 años
$ nombre = Luis
bash: nombre: command not found
$ echo "Archivo de $nombre: ${nombre}_notas.txt"
Archivo de Ana: Ana_notas.txt
$ echo "Archivo de $nombre: $nombre_notas.txt"
Archivo de Ana: .txtCon espacios, Bash entiende nombre = Luis como la orden nombre con dos argumentos, = y Luis, y no existe ninguna orden con ese nombre. La última línea enseña para qué sirven las llaves: $nombre_notas busca una variable llamada nombre_notas, que no existe, y Bash la sustituye por nada, sin quejarse. ${nombre}_notas deja claro dónde acaba el nombre.
Las variables de Bash guardan siempre texto. No hay enteros ni decimales, aunque Bash sabe hacer cuentas con texto que parece un número, como verás más abajo. Por costumbre, las variables propias van en minúsculas y las del entorno, como HOME, USER o PATH, en mayúsculas.
La regla de las comillas
Este es el tema más importante de todo el curso, y el origen de la mayoría de los errores. Hay tres formas de escribir un texto, y Bash hace cosas distintas con cada una:
#!/usr/bin/env bash archivo="Mi informe final.txt" echo 'Comillas simples: $archivo y $(whoami)' echo "Comillas dobles: $archivo y $(whoami)" printf '[%s] ' $archivo; echo printf '[%s] ' "$archivo"; echo patron="*.txt" echo Sin comillas: $patron echo "Con comillas: $patron"
salidaComillas simples: $archivo y $(whoami) Comillas dobles: Mi informe final.txt y ana [Mi] [informe] [final.txt] [Mi informe final.txt] Sin comillas: Mi informe final.txt notas.txt Con comillas: *.txt
- Comillas simples: el texto va tal cual, sin sustituir nada. Útiles para escribir
$o*literales. - Comillas dobles: se sustituyen
$variablesy$(órdenes), pero el resultado se queda como un solo argumento, con sus espacios y sus asteriscos. - Sin comillas: después de sustituir, Bash parte el resultado en palabras por los espacios y expande los comodines. Por eso
printfrecibió tres argumentos en vez de uno, y$patronse convirtió en la lista de archivos.txtde la carpeta.
La regla práctica cabe en una línea: pon siempre las variables entre comillas dobles, "$archivo", salvo que quieras de verdad que se partan o que se expandan los comodines. ShellCheck avisa cada vez que se te olvida. Esto es lo que dice del script de arriba:
shellcheckIn comillas.sh line 3:
echo 'Comillas simples: $archivo y $(whoami)'
^-- SC2016 (info): Expressions don't expand in single quotes, use double quotes for that.
In comillas.sh line 5:
printf '[%s] ' $archivo; echo
^------^ SC2086 (info): Double quote to prevent globbing and word splitting.
Did you mean:
printf '[%s] ' "$archivo"; echo
In comillas.sh line 8:
echo Sin comillas: $patron
^-----^ SC2086 (info): Double quote to prevent globbing and word splitting.
Did you mean:
echo Sin comillas: "$patron"
For more information:
https://www.shellcheck.net/wiki/SC2016 -- Expressions don't expand in singl...
https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...El aviso SC2086 es, de lejos, el que más vas a ver. Su ficha en shellcheck.net lo explica con más ejemplos.
Ojo con la seguridad: un nombre de archivo con espacios puede borrar otro
Las comillas no son una cuestión de estilo. Aquí hay dos archivos, informe 2025.txt, que queremos borrar, y 2025.txt, que no. Mira qué hace rm sin comillas:
#!/usr/bin/env bash borrar="informe 2025.txt" ls rm $borrar echo "--- después:" ls
salida2025.txt informe 2025.txt rm_sin_comillas.sh rm: cannot remove 'informe': No such file or directory --- después: informe 2025.txt rm_sin_comillas.sh
rm recibió dos argumentos, informe y 2025.txt: el primero no existía, pero el segundo sí, y lo borró. El archivo que queríamos sigue ahí. Con rm "$borrar" no habría pasado. Ahora imagina la variable con un nombre que llega de fuera, de un formulario o de otro programa.
Más en Los fallos de seguridad típicos de los scripts de Bash.
Expansiones: valores por omisión, órdenes y cuentas
#!/usr/bin/env bash
nombre="Ana"
vacia=""
echo "1: ${nombre}"
echo "2: ${sin_definir:-desconocido}"
echo "3: ${vacia:-vacía}"
echo "4: ${#nombre} letras"
lineas=$(wc -l < notas.txt)
echo "5: notas.txt tiene $lineas líneas"
echo "6: $(( 7 / 2 )) y resto $(( 7 % 2 ))"
echo "7: $(( 2 ** 10 ))"
precio=1299
echo "8: $(( precio * 3 / 100 ))"
echo "9: $(echo 'scale=2; 7 / 2' | bc)"salida1: Ana 2: desconocido 3: vacía 4: 3 letras 5: notas.txt tiene 3 líneas 6: 3 y resto 1 7: 1024 8: 38 9: 3.50
${var:-valor}davalorsi la variable no existe o está vacía, sin cambiarla. Es la forma de poner valores por omisión.${#var}es la longitud del texto.$(orden)es la sustitución de órdenes: ejecuta la orden y pone en su lugar lo que escribió, sin el salto de línea final. Verás también la forma antigua, con comillas invertidas`orden`; la de$( )es más legible y se puede anidar.$(( ))hace cuentas con enteros:7 / 2da 3. Dentro, las variables no necesitan$. Para decimales hay que llamar a otro programa, comobc.
Leer del teclado
#!/usr/bin/env bash printf '¿Cómo te llamas? ' read -r nombre printf '¿En qué año naciste? ' read -r anio echo echo "Hola, $nombre. En 2026 cumples $(( 2026 - anio )) años."
salida (con «Ana» y «1990» como respuestas)¿Cómo te llamas? ¿En qué año naciste? Hola, Ana. En 2026 cumples 36 años.
read -r variable lee una línea entera de la entrada y la guarda en la variable. La -r evita que las barras invertidas se coman caracteres: úsala siempre. En la salida no se ven las respuestas porque al ejecutar el curso llegaron desde un archivo y no desde el teclado; en tu terminal sí las verás. Ojo: si en el año escribes letras, $(( )) no da error, las toma como el nombre de una variable vacía y calcula con 0. Validar lo que llega es parte de la lección 3.
Variables de entorno
Una variable normal solo existe en la shell donde la creas. Para que la vean los programas que lanzas desde ella hay que exportarla, y entonces pasa a ser una variable de entorno:
terminal$ color=azul $ bash -c 'echo "Dentro: [$color]"' Dentro: [] $ export color $ bash -c 'echo "Dentro: [$color]"' Dentro: [azul] $ TZ=America/Mexico_City date -d '2026-10-30 18:00 UTC' '+%H:%M en Ciudad de México' 12:00 en Ciudad de México $ echo "$HOME y $USER" /home/ana y ana
La línea de TZ=… enseña un truco muy usado: poner VARIABLE=valor delante de una orden le da esa variable solo a esa orden, sin tocar la shell. Las variables de entorno más importantes son HOME (tu carpeta personal), USER, PATH (dónde se buscan las órdenes), LANG (idioma) y TZ (zona horaria).
El error que te ahorra: el espacio que convierte una asignación en una orden
En una terminal, nombre = Luis da un error que se ve. En un script, el error pasa por delante y el script sigue, con la variable vacía:
#!/usr/bin/env bash destino = /tmp/copias echo "Destino: [$destino]"
salida./espacios.sh: line 2: destino: command not found Destino: []
ShellCheck lo detecta antes de ejecutar nada, y además se da cuenta de que destino nunca recibe un valor:
shellcheckIn espacios.sh line 2:
destino = /tmp/copias
^-- SC2283 (error): Remove spaces around = to assign (or use [ ] to compare, or quote '=' if literal).
In espacios.sh line 3:
echo "Destino: [$destino]"
^------^ SC2154 (warning): destino is referenced but not assigned.
For more information:
https://www.shellcheck.net/wiki/SC2283 -- Remove spaces around = to assign ...
https://www.shellcheck.net/wiki/SC2154 -- destino is referenced but not ass...Retos
Reto 1. Escribe un script que salude a quien le pases como primer argumento ($1) y, si no le pasas ninguno, salude al «mundo». Una sola línea con ${…:-…} basta.
Ver la solución
#!/usr/bin/env bash
echo "Hola, ${1:-mundo}"./reto1.shHola, mundo
./reto1.sh Luis
./reto1.sh LuisHola, Luis
$1 es el primer argumento del script; si no existe, ${1:-mundo} pone «mundo». Los argumentos se ven a fondo en la lección 4.
Reto 2. Lee una cantidad de segundos con read y escríbela en horas, minutos y segundos (por ejemplo, 3725 son 1 h 2 min 5 s).
Ver la solución
#!/usr/bin/env bash read -r total h=$(( total / 3600 )) m=$(( total % 3600 / 60 )) s=$(( total % 60 )) echo "$total segundos son $h h $m min $s s"
salida (con 3725)3725 segundos son 1 h 2 min 5 s
La división entera y el resto hacen todo el trabajo: 3725 / 3600 da 1 hora, lo que sobra (125) entre 60 da 2 minutos y 3725 % 60, 5 segundos.
Reto 3. ¿Qué escribe este script? Piénsalo antes de mirar: a=5, b="$a+1", y después echo "$b" y echo "$(( a + 1 ))".
Ver la solución
#!/usr/bin/env bash a=5 b="$a+1" echo "$b" echo "$(( a + 1 ))"
salida5+1 6
Las variables son texto: b guarda los tres caracteres 5+1, sin calcular nada. Solo dentro de $(( )) Bash hace cuentas.
Cómo está hecho este curso
Cada script se ejecutó con GNU bash, version 5.3.9(1)-release (x86_64-pc-linux-gnu) en un contenedor limpio de Ubuntu 26.04, sin acceso a la red, con la usuaria ana y la zona horaria UTC, y se revisó con ShellCheck 0.11. Las salidas, los errores de Bash y los avisos de ShellCheck están copiados de esa ejecución. Los mensajes salen en inglés, como en la mayoría de los servidores. Las órdenes de GNU (sed, date, grep) son las de Linux; en macOS algunas opciones cambian, y lo señalamos cuando importa.
¿Buscas una receta concreta en vez de una lección? En Ejemplos de Bash hay tareas resueltas: leer archivos, renombrar en lote, procesar registros, copias de seguridad, cron… Y antes de poner un script en un servidor, lee Los fallos de seguridad típicos de los scripts de Bash.
