Bash Scripting I
Introducción a la creación de scripts con Bash.
1. Introducción
Bash (Bourne Again SHell) es un intérprete de comandos: recibe lo que escribimos en la terminal y le indica al sistema qué ejecutar. Surgió a finales de los años ochenta, cuando Brian Fox lo desarrolló para el proyecto GNU como alternativa libre a la shell Bourne de Unix y con el tiempo se convirtió en una herramienta habitual en los sistemas GNU/Linux.
Podemos usar Bash para movernos entre carpetas, buscar archivos o arrancar programas, escribiendo los comandos uno a uno. Pero si los guardamos en un archivo para ejecutarlos juntos, ya estamos haciendo Bash scripting. Puede ser algo tan sencillo como copiar unos archivos o tan elaborado como preparar todo un entorno de trabajo.
Aunque a veces suele descuidarse, sin duda que vale la pena aprenderlo, ya que nos va a permitir preparar o, cuanto menos, entender mejor muchas tareas habituales en desarrollo y DevOps, como ejecutar pruebas, generar archivos, revisar logs o desplegar una aplicación. Un script permite repetir esos pasos sin tener que recordarlos ni escribirlos cada vez. También aparece constantemente alrededor de Docker, en scripts de arranque de contenedores, herramientas de construcción y procesos de despliegue. Y cuando algo falla, entender qué hace ese script suele ser bastante más útil que volver a ejecutarlo con los dedos cruzados.
Y también es útil conocerlo para proyectos de IA, en los que podemos usar Bash para preparar datos, lanzar experimentos o encadenar herramientas; y en ciberseguridad, donde sirve para inspeccionar archivos, filtrar registros y automatizar comprobaciones.
En síntesis, no hace falta convertirse en administrador de sistemas para sacarle partido. Basta con entender cómo se combinan los comandos, cómo se pasan datos de uno a otro y qué ocurre cuando un script recibe una entrada que no esperábamos. Y esa base nos servirá tanto para escribir nuestras propias automatizaciones como para leer con criterio las que encontramos por ahí.
2. Nuestro primer script
Un script de Bash es un archivo de texto. Podemos escribirlo con cualquier editor, guardarlo con la extensión .sh y ejecutarlo desde una terminal que tenga Bash. Para empezar, podemos crear un archivo llamado hola.sh con este contenido:
#!/usr/bin/env bash
echo "Hola desde Bash"
La primera línea se denomina shebang e indica qué intérprete debe ejecutar el archivo cuando lo lancemos directamente. La segunda imprime un mensaje. De momento no hay más misterio: guardamos el archivo y abrimos una terminal en la carpeta donde está.
2.1. En Linux
En muchas distribuciones Bash ya está instalado. Podemos comprobarlo con bash --version y ejecutar el archivo así:
bash hola.sh
Esto funciona aunque el archivo no tenga permiso de ejecución. Si queremos lanzarlo como un programa, primero le damos ese permiso y después usamos ./ para señalar el archivo de la carpeta actual:
chmod +x hola.sh
./hola.sh
2.2. En macOS
Abrimos Terminal y seguimos exactamente los mismos pasos: bash hola.sh o, después de chmod +x hola.sh, ./hola.sh. Aunque macOS abre zsh por defecto, eso no nos impide escribir scripts de Bash. Al invocarlo con bash o ejecutarlo mediante la primera línea del archivo, será Bash quien lo interprete.
2.3. En Windows
PowerShell y Bash son intérpretes distintos, así que no basta con abrir PowerShell y escribir ./hola.sh. Para trabajar con un entorno Linux en Windows podemos instalar WSL. Desde PowerShell, abierto como administrador, ejecutamos:
wsl --install
Después de completar la instalación y el reinicio si se solicita, abrimos la distribución Linux instalada. Allí creamos hola.sh con el mismo contenido y lo ejecutamos con bash hola.sh, igual que en Linux. Si ya tenemos WSL instalado, podemos saltarnos la instalación.
Mucho más sencillo y válido para esta serie sobre Bash, para scripts sencillos también existe Git Bash, incluido con Git for Windows, que se puede utilizar directamente en el Visual Studio; aunque WSL resulta más parecido al entorno Linux que encontraremos en servidores y contenedores.
3. Algo más complicado
Ahora vamos a complicar un poco más el script. Abrimos el archivo de antes y preparamos algo así:
#!/usr/bin/env bash
echo "Hello from Bash!"
echo "Current user: $USER"
echo "Home directory: $HOME"
echo "Process ID: $$"
Al ejecutarlo debería salir por consola algo parecido a esto.
Hello from Bash!
Current user: marcos
Home directory: /Users/marcos
Process ID: 34821
La salida es orientativa: en tu equipo aparecerán tu usuario, tu carpeta personal y otro número de proceso. Si lo ejecutas varias veces, verás que este último cambia.
echo muestra texto por pantalla, pero aquí no todo lo que hay entre comillas se imprime tal cual. Cuando Bash encuentra $USER, sustituye esa variable por el nombre del usuario; con $HOME hace lo mismo con la ruta de su carpeta personal. Son variables que normalmente ya están disponibles en la sesión, así que no hemos tenido que definirlas nosotros. Por ejemplo, si $USER vale marcos, Bash convierte "Current user: $USER" en "Current user: marcos" antes de mostrarlo.
$$ tiene un significado especial: contiene el identificador del proceso de Bash que está ejecutando el script. No solemos necesitarlo para saludar al usuario, claro, pero nos sirve para ver que cada ejecución arranca su propio proceso.
Fíjate también en las comillas dobles. Permiten que Bash sustituya las variables por sus valores. Con comillas simples, en cambio, imprimiríamos el texto literalmente. Equivale a cualquier sistema de interpolación en otro lenguaje, como las en react.
Ya tenemos algo más que una lista de mensajes fijos: el script puede consultar información de su entorno y usarla para decidir qué mostrar o qué hacer.
Vamos a complicarlo un poco más añadiendo algo de lógica con un script que nos sirva para comprobar si estamos en un proyecto de Node.js. En un archivo que podemos llamar check-node.sh podemos escribir algo parecido a esto:
#!/usr/bin/env bash
echo "Checking project..."
if [[ -f package.json ]]; then
echo "Node.js project detected"
else
echo "No package.json found"
fi
echo "Check completed"
Observa lo que hemos introducido:
- if: inicia una condición.
- [[ ... ]]: evalúa una expresión condicional de Bash.
- -f: comprueba si existe un archivo regular.
- then: indica qué ejecutar si se cumple la condición.
- else: establece el comportamiento alternativo.
- fi: termina el bloque condicional.
Este pequeño programa ya toma una decisión en función del entorno. No está mal para empezar ^^.
Códigos de salida
Hasta ahora nos hemos fijado en los mensajes que aparecen en la terminal, pero cada comando nos da otra pista: su código de salida. Es un número que indica cómo ha terminado. Por convención, 0 significa que el comando ha terminado correctamente; cualquier otro valor indica un error o una condición que ese programa ha decidido señalar.
Bash guarda el código del último comando en $?. Podemos comprobarlo con uno que debería funcionar:
ls .
echo $?
ls . muestra los archivos de la carpeta actual. Si todo va bien, echo $? imprimirá 0. Ahora probemos con una carpeta que no existe:
ls /directorio-que-no-existe
echo $?
Esta vez ls mostrará un error y el número será distinto de cero. Hay que consultar $? justo después del comando que nos interesa, porque el siguiente también lo modifica. De hecho, tras ejecutar echo $?, el valor de $? ya será el código de salida de echo.
Esto nos permite ver un detalle del script anterior. Si no encuentra package.json, imprime «No package.json found», pero después ejecuta echo "Check completed", que normalmente termina con 0. Por tanto, el script completo puede comunicar que todo ha ido bien aunque no haya encontrado el archivo. Si queremos que esa ausencia se considere un fallo, podemos añadir exit 1 en la rama else, antes de fi. Así el script termina en ese punto y devuelve un código diferente de cero.
¿Y qué tiene que ver esto con los agentes de IA? Imagina que un agente ejecuta npm test. La herramienta que lanza el comando puede devolverle algo como esto:
{
"command": "npm test",
"exitCode": 1,
"stdout": "...",
"stderr": "..."
}
El texto le cuenta qué ha ocurrido, pero exitCode: 1 le da una señal clara de que las pruebas han fallado. Con esa información puede detenerse, informar del problema o intentar corregirlo antes de continuar. stdout es la salida normal del programa y stderr la salida de errores; veremos ambos canales con más detalle en otra lección.
Eso sí, un 0 no significa que el resultado sea correcto o seguro en todos los sentidos. Solo significa que el proceso terminó según sus propios criterios de éxito. Nuestro check-node.sh, antes de añadir exit 1, es un buen ejemplo de por qué conviene mirar también qué hace realmente el script.
Bueno, pues por hoy vamos a dejarlo aquí.