LinuxParty
Aprende a ejecutar consultas, exportar resultados, crear copias de seguridad y automatizar tareas con bases de datos sin guardar contraseñas ni introducir vulnerabilidades SQL
Bash no es el lenguaje más apropiado para desarrollar una aplicación empresarial completa, pero resulta excelente para automatizar tareas relacionadas con bases de datos.
Un script puede consultar el estado de un servidor, generar un informe, exportar registros a CSV, comprobar si existe un usuario o realizar una copia de seguridad programada.
Linux proporciona clientes de terminal para trabajar con los principales motores:
| Base de datos | Cliente | Copia de seguridad |
|---|---|---|
| SQLite | sqlite3 |
.backup, .dump |
| MariaDB | mariadb |
mariadb-dump |
| MySQL | mysql |
mysqldump |
| PostgreSQL | psql |
pg_dump y pg_restore |
Bash actúa como pegamento entre estas herramientas y otros comandos como grep, awk, jq, gzip, mail o curl.
Instalar los clientes necesarios
En Fedora podemos instalar las herramientas con:
sudo dnf install sqlite mariadb postgresql jq
En Debian y Ubuntu:
sudo apt install sqlite3 default-mysql-client postgresql-client jq
No es necesario instalar un servidor de MariaDB o PostgreSQL cuando solamente queremos conectarnos a una base alojada en otra máquina.
Comprobaremos las versiones mediante:
sqlite3 --version mariadb --version psql --version jq --version
Los nombres de algunos paquetes pueden cambiar ligeramente según la distribución.
Preparar correctamente un script
Un script que trabaje con datos importantes debe detenerse cuando se produzca un error.
Podemos comenzar con:
#!/usr/bin/env bash set -Eeuo pipefail trap 'echo "Error en la línea $LINENO" >&2' ERR
Estas opciones tienen diferentes funciones:
-e: detiene el script cuando falla un comando.-u: detecta variables no definidas.-o pipefail: considera fallida una tubería si falla cualquiera de sus comandos.-E: conserva el tratamiento de errores dentro de funciones.trap: muestra aproximadamente dónde se produjo el fallo.
Debemos entrecomillar siempre las variables que contengan rutas:
sqlite3 "$BASE_DATOS" "SELECT count(*) FROM usuarios;"
En cambio, entrecomillar una variable de Bash no la convierte automáticamente en un valor SQL seguro.
SQLite: una base completa dentro de un archivo
SQLite no necesita un servidor independiente. Toda la base de datos se almacena normalmente en un solo archivo, por lo que resulta adecuada para:
- Inventarios pequeños.
- Cachés.
- Scripts de administración.
- Registros de actividad.
- Aplicaciones locales.
- Informes temporales.
- Herramientas de escritorio.
Para abrir o crear una base:
sqlite3 contactos.db
En el modo interactivo podemos crear una tabla:
CREATE TABLE contactos ( id INTEGER PRIMARY KEY AUTOINCREMENT, nombre TEXT NOT NULL, email TEXT UNIQUE, creado_en TEXT DEFAULT CURRENT_TIMESTAMP );
Para salir:
.quit
Ejecutar consultas SQLite sin entrar en el cliente
Podemos pasar directamente la consulta:
sqlite3 contactos.db \ "SELECT id, nombre, email FROM contactos;"
Para mostrar columnas alineadas y sus encabezados:
sqlite3 -header -column contactos.db \ "SELECT id, nombre, email FROM contactos;"
El separador predeterminado suele ser |. Podemos cambiarlo:
sqlite3 -separator ';' contactos.db \ "SELECT id, nombre, email FROM contactos;"
Para guardar el resultado en una variable:
TOTAL=$(sqlite3 contactos.db \ "SELECT count(*) FROM contactos;") echo "Contactos registrados: $TOTAL"
Es conveniente comprobar que el resultado sea numérico antes de realizar comparaciones:
if [[ "$TOTAL" =~ ^[0-9]+$ ]] && (( TOTAL > 100 )); then echo "La agenda contiene más de cien contactos" fi
Ejecutar varias instrucciones con un heredoc
Un heredoc facilita la lectura de bloques SQL largos:
sqlite3 contactos.db <<'SQL' .bail on BEGIN; CREATE TABLE IF NOT EXISTS contactos ( id INTEGER PRIMARY KEY AUTOINCREMENT, nombre TEXT NOT NULL, email TEXT UNIQUE, creado_en TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_contactos_email ON contactos(email); COMMIT; SQL
Las comillas en <<'SQL' impiden que Bash expanda variables, órdenes o caracteres especiales dentro del bloque.
La instrucción:
.bail on
hace que el cliente se detenga al encontrar un error. Sin ella podría continuar ejecutando las instrucciones siguientes.
Exportar SQLite a CSV y JSON
Para crear un archivo CSV:
sqlite3 -header -csv contactos.db \ "SELECT id, nombre, email FROM contactos;" \ > contactos.csv
SQLite se encarga de colocar comillas correctamente cuando un campo contiene comas o caracteres especiales.
Para obtener JSON:
sqlite3 -json contactos.db \ "SELECT id, nombre, email FROM contactos;" \ > contactos.json
Después podemos procesarlo con jq:
jq '.[] | {nombre, email}' contactos.json
O directamente mediante una tubería:
sqlite3 -json contactos.db \ "SELECT nombre, email FROM contactos;" | jq -r '.[] | [.nombre, .email] | @tsv'
Importar un archivo CSV en SQLite
Primero crearemos la tabla con los tipos apropiados:
sqlite3 ventas.db <<'SQL' CREATE TABLE IF NOT EXISTS ventas ( id INTEGER PRIMARY KEY, producto TEXT NOT NULL, cantidad INTEGER NOT NULL, total REAL NOT NULL, fecha TEXT NOT NULL ); SQL
Después importaremos el archivo:
sqlite3 ventas.db <<'SQL' .mode csv .import --skip 1 ventas.csv ventas SQL
--skip 1 omite la primera línea cuando contiene los nombres de las columnas.
No debemos importar automáticamente archivos recibidos de fuentes desconocidas. Antes conviene validar su codificación, encabezados, número de columnas y tamaño.
Evitar la inyección SQL
Nunca debemos insertar directamente en una consulta un valor recibido de un usuario:
# PELIGROSO NOMBRE="$1" sqlite3 contactos.db \ "INSERT INTO contactos(nombre) VALUES ('$NOMBRE');"
Un valor especialmente construido podría cerrar las comillas y añadir una segunda instrucción SQL.
Tampoco debemos utilizar:
printf '%q' "$NOMBRE"
printf '%q' genera una representación reutilizable por Bash. No implementa las reglas de escape de SQLite, MariaDB o PostgreSQL y, por tanto, no constituye una defensa contra la inyección SQL.
El cliente de SQLite dispone del comando .parameter, pero introducir texto arbitrario desde el intérprete sigue exigiendo mucho cuidado con el tratamiento realizado antes de llegar al cliente.
Para valores constantes y controlados podemos utilizar SQL directo. Si los datos proceden de formularios, argumentos, archivos descargados o usuarios, debemos emplear consultas preparadas mediante un controlador real.
Un ejemplo seguro con Python sería:
#!/usr/bin/env bash set -Eeuo pipefail BASE_DATOS="contactos.db" NOMBRE="${1:?Debes indicar un nombre}" EMAIL="${2:?Debes indicar un correo}" python3 - "$BASE_DATOS" "$NOMBRE" "$EMAIL" <<'PY' import sqlite3 import sys ruta, nombre, email = sys.argv[1:4] with sqlite3.connect(ruta) as conexion: conexion.execute( "INSERT INTO contactos(nombre, email) VALUES (?, ?)", (nombre, email), ) PY
En este caso los valores se transmiten separados de la instrucción SQL. El controlador aplica la vinculación de parámetros sin confundir los datos con código.
La misma recomendación es válida para MySQL, MariaDB y PostgreSQL. Cuando el script recibe información no confiable, Bash debería orquestar el proceso, pero la consulta parametrizada debería realizarla un controlador de Python, PHP, Perl u otro lenguaje apropiado.
Copias de seguridad coherentes de SQLite
Puede resultar tentador copiar directamente el archivo:
cp aplicacion.db copia.db
Esto solamente es prudente cuando sabemos que ninguna aplicación está escribiendo en la base. Si SQLite utiliza WAL o existe una transacción activa, copiar exclusivamente el archivo principal puede producir una copia incompleta.
Para una base en uso podemos utilizar el comando .backup:
sqlite3 aplicacion.db \ ".backup '/backups/aplicacion-$(date +%F).db'"
Otra posibilidad es generar un volcado SQL:
sqlite3 aplicacion.db ".dump" | gzip > "/backups/aplicacion-$(date +%F).sql.gz"
Podemos comprobar la integridad de la copia binaria:
sqlite3 "/backups/aplicacion-$(date +%F).db" \ "PRAGMA integrity_check;"
El resultado esperado es:
ok
Una copia no debe considerarse válida hasta que haya sido verificada y, preferiblemente, restaurada en un entorno de prueba.
Consultar MariaDB o MySQL desde Bash
Para ejecutar una consulta en MariaDB:
mariadb \ --host=localhost \ --user=informes \ --database=empresa \ --execute="SELECT count(*) FROM clientes;"
En MySQL podemos utilizar mysql con opciones equivalentes:
mysql \ --host=localhost \ --user=informes \ --database=empresa \ --execute="SELECT count(*) FROM clientes;"
Para obtener una salida más cómoda para scripts:
mariadb \ --batch \ --skip-column-names \ --raw \ empresa \ --execute="SELECT email FROM clientes WHERE activo = 1;"
Estas opciones eliminan los bordes, ocultan los encabezados y reducen transformaciones de la salida.
Para recorrer los resultados:
while IFS=$'\t' read -r id nombre email; do printf 'ID: %s | Nombre: %s | Correo: %s\n' \ "$id" "$nombre" "$email" done < <( mariadb \ --batch \ --skip-column-names \ --raw \ empresa \ --execute=" SELECT id, nombre, email FROM clientes WHERE activo = 1; " )
Este procedimiento funciona para datos controlados, aunque los saltos de línea o tabuladores dentro de los campos requieren un tratamiento adicional.
No escribir la contraseña de MariaDB en el script
Debemos evitar órdenes como:
mysql -u root -pMiPassword empresa
La contraseña queda dentro del script, puede aparecer en el historial y podría ser visible para otros procesos o usuarios.
En MySQL podemos crear una ruta de acceso:
mysql_config_editor set \ --login-path=informes \ --host=localhost \ --user=informes \ --password
La contraseña se solicita de manera interactiva y se almacena en el archivo de configuración de acceso del usuario.
Después podremos ejecutar:
mysql \ --login-path=informes \ --database=empresa \ --execute="SELECT now();"
En MariaDB también podemos utilizar un archivo de opciones protegido:
[client] host=localhost user=informes password=UNA_CONTRASEÑA_SEGURA
Lo guardaremos, por ejemplo, en:
$HOME/.config/mariadb/backup.cnf
y restringiremos sus permisos:
chmod 600 "$HOME/.config/mariadb/backup.cnf"
Para utilizarlo:
mariadb \ --defaults-extra-file="$HOME/.config/mariadb/backup.cnf" \ empresa \ --execute="SELECT now();"
El usuario de la base debe poseer solamente los permisos necesarios. Un script de informes no necesita utilizar la cuenta root ni privilegios para borrar tablas.
Crear una copia de seguridad de MariaDB o MySQL
Para MariaDB:
mariadb-dump \ --defaults-extra-file="$HOME/.config/mariadb/backup.cnf" \ --single-transaction \ --routines \ --events \ --triggers \ empresa | gzip > "/backups/empresa-$(date +%F).sql.gz"
Para MySQL:
mysqldump \ --login-path=backup \ --single-transaction \ --routines \ --events \ --triggers \ empresa | gzip > "/backups/empresa-$(date +%F).sql.gz"
--single-transaction permite obtener una copia coherente de tablas transaccionales como InnoDB sin bloquearlas durante todo el volcado.
No ofrece las mismas garantías para tablas no transaccionales. También debemos evitar cambios de estructura mientras se ejecuta el respaldo.
Para restaurar una copia de MariaDB:
gunzip -c /backups/empresa-2026-10-25.sql.gz | mariadb \ --defaults-extra-file="$HOME/.config/mariadb/backup.cnf" \ empresa_restaurada
Nunca debemos probar una restauración sobre la base de producción.
Consultar PostgreSQL mediante psql
Una consulta sencilla sería:
psql \ --host=localhost \ --username=informes \ --dbname=empresa \ --command="SELECT count(*) FROM clientes;"
Para que la salida resulte más fácil de procesar:
psql \ --host=localhost \ --username=informes \ --dbname=empresa \ --no-psqlrc \ --tuples-only \ --no-align \ --command="SELECT email FROM clientes WHERE activo;"
Las opciones tienen estas funciones:
--no-psqlrc: ignora configuraciones personales que podrían modificar la salida.--tuples-only: elimina encabezados y totales.--no-align: evita espacios de alineación.--field-separator: permite elegir un separador.
Para generar CSV, PostgreSQL ofrece \copy:
psql \ --host=localhost \ --username=informes \ --dbname=empresa \ --no-psqlrc <<'SQL' \copy ( SELECT id, nombre, email FROM clientes WHERE activo ) TO STDOUT WITH CSV HEADER SQL
Podemos redirigir su salida:
psql \ --host=localhost \ --username=informes \ --dbname=empresa \ --no-psqlrc <<'SQL' > clientes.csv \copy ( SELECT id, nombre, email FROM clientes WHERE activo ) TO STDOUT WITH CSV HEADER SQL
Guardar las credenciales de PostgreSQL
PostgreSQL puede leer la contraseña desde:
~/.pgpass
Su formato es:
servidor:puerto:base:usuario:contraseña
Ejemplo:
localhost:5432:empresa:informes:UNA_CONTRASEÑA_SEGURA
El archivo debe estar protegido:
chmod 600 "$HOME/.pgpass"
Si los permisos son demasiado abiertos, PostgreSQL puede ignorarlo.
Para utilizar otro archivo:
export PGPASSFILE="/etc/secretos/postgresql.pass"
No deberíamos mantener contraseñas directamente en el script ni exportarlas permanentemente en el perfil del usuario.
Consultas parametrizadas en PostgreSQL
psql permite definir variables, pero debemos utilizar la forma de sustitución adecuada para que un valor sea tratado como literal SQL.
Ejemplo:
NOMBRE="${1:?Debes indicar el nombre}" psql \ --host=localhost \ --username=operador \ --dbname=empresa \ --set=nombre="$NOMBRE" \ --no-psqlrc <<'SQL' INSERT INTO clientes(nombre) VALUES (:'nombre'); SQL
La sintaxis:
:'nombre'
pide a psql que introduzca el valor como literal SQL entrecomillado.
No debe confundirse con:
:nombre
porque esta última forma realiza una sustitución sin entrecomillado.
Para aplicaciones complejas o entradas expuestas a usuarios, sigue siendo preferible utilizar un controlador con consultas preparadas y limitar Bash a la automatización.
Copias de seguridad de PostgreSQL
PostgreSQL permite crear un volcado SQL:
pg_dump \ --host=localhost \ --username=backup \ --dbname=empresa | gzip > "/backups/empresa-$(date +%F).sql.gz"
Para bases grandes resulta más flexible el formato personalizado:
pg_dump \ --host=localhost \ --username=backup \ --format=custom \ --file="/backups/empresa-$(date +%F).dump" \ empresa
La restauración se realiza con:
createdb \ --host=localhost \ --username=backup \ empresa_restaurada pg_restore \ --host=localhost \ --username=backup \ --dbname=empresa_restaurada \ "/backups/empresa-2026-10-25.dump"
El formato personalizado permite seleccionar objetos y ofrece mejores opciones de restauración que un archivo SQL plano.
Un script de copia de seguridad más seguro
Este ejemplo crea una copia de MariaDB, verifica que el archivo no esté vacío y conserva treinta días:
#!/usr/bin/env bash set -Eeuo pipefail umask 077 DESTINO="/backups/mariadb" FECHA=$(date +'%Y-%m-%d_%H%M%S') CONFIG="$HOME/.config/mariadb/backup.cnf" BASE="empresa" TEMP="$DESTINO/.${BASE}-${FECHA}.sql.gz.tmp" FINAL="$DESTINO/${BASE}-${FECHA}.sql.gz" mkdir -p "$DESTINO" limpiar() { rm -f -- "$TEMP" } trap limpiar EXIT trap 'echo "Error al crear la copia" >&2' ERR mariadb-dump \ --defaults-extra-file="$CONFIG" \ --single-transaction \ --routines \ --events \ --triggers \ "$BASE" | gzip -9 > "$TEMP" gzip -t "$TEMP" if [[ ! -s "$TEMP" ]]; then echo "La copia está vacía" >&2 exit 1 fi mv -- "$TEMP" "$FINAL" find "$DESTINO" \ -type f \ -name "${BASE}-*.sql.gz" \ -mtime +30 \ -delete echo "Copia creada: $FINAL"
umask 077 impide que otros usuarios puedan leer los archivos nuevos. El respaldo se escribe primero con un nombre temporal y solamente se renombra cuando ha terminado correctamente.
Aun así, gzip -t únicamente comprueba la integridad del archivo comprimido. No demuestra que el contenido SQL pueda restaurarse. Debemos realizar periódicamente una restauración completa en una base de pruebas.
Programar el script
Podemos usar cron:
15 2 * * * /usr/local/sbin/backup-empresa.sh >> /var/log/backup-empresa.log 2>&1
Esta entrada ejecuta la copia diariamente a las 02:15.
Antes de programarla debemos ejecutarla manualmente con el mismo usuario:
sudo -u usuario_backup \ /usr/local/sbin/backup-empresa.sh
Los trabajos programados utilizan un entorno reducido. Por ello conviene:
- Usar rutas absolutas.
- No depender del directorio actual.
- Definir claramente los archivos de configuración.
- Registrar los errores.
- Vigilar el espacio disponible.
- Configurar alertas ante fallos.
- Evitar ejecuciones simultáneas.
Podemos impedir dos copias concurrentes mediante flock:
15 2 * * * flock -n /run/lock/backup-empresa.lock /usr/local/sbin/backup-empresa.sh
Para servidores modernos, un temporizador de systemd ofrece mejor integración con el registro, los estados de error y las dependencias del sistema.
Errores que debemos evitar
Al automatizar bases de datos desde Bash, los fallos más peligrosos suelen proceder de unas pocas prácticas:
- Escribir contraseñas dentro del script.
- Utilizar el usuario administrador para tareas de lectura.
- Interpolar entradas externas directamente en SQL.
- Confundir
printf '%q'con un sistema de protección SQL. - Copiar una base SQLite activa sin utilizar su sistema de respaldo.
- Realizar volcados sin verificar que terminaron correctamente.
- Borrar copias antiguas antes de confirmar que existe una nueva.
- No controlar el espacio disponible.
- No registrar la salida de error.
- Considerar válido un respaldo que nunca se ha restaurado.
- Ejecutar
VACUUM,REINDEXo tareas pesadas en horas de máxima actividad. - Programar consultas destructivas sin transacciones ni condiciones de seguridad.
También debemos diferenciar entre una copia lógica y una copia física. mysqldump, mariadb-dump y pg_dump generan respaldos lógicos, pero no sustituyen necesariamente las herramientas físicas, la replicación o la recuperación a un punto temporal que requiere un servidor crítico.
Bash es el orquestador, no el controlador de la base de datos
Bash resulta excelente para encadenar tareas:
consultar → exportar → comprimir → copiar → verificar → avisar
Sin embargo, no dispone de una API nativa y común para crear consultas preparadas en todos los motores.
Para consultas estáticas, informes y mantenimiento, los clientes sqlite3, mariadb, mysql y psql resultan suficientes.
Cuando los valores proceden de usuarios o la lógica comienza a incluir numerosas transacciones, relaciones y validaciones, debemos utilizar un lenguaje con un controlador de base de datos apropiado.
La combinación más segura consiste en dejar que Bash organice el proceso y que SQLite, MariaDB, PostgreSQL o un controlador especializado se ocupen de interpretar y proteger los datos.
-
Bases de Datos
- Cómo consultar SQLite, MariaDB y PostgreSQL desde scripts Bash en Linux
- El invierno demográfico ya está aquí (III): Inteligencia Artificial, robótica y Linux frente al mayor desafío del siglo XXI (Especial LinuxParty – Parte 3 y última)
- El invierno demográfico ya está aquí (II): pensiones, empleo, economía y los cambios que transformarán nuestras vidas (Especial LinuxParty – Parte 2)
- FamilySearch: el mayor árbol genealógico colaborativo del mundo está al alcance de cualquiera
- Instalar Oracle Database en Oracle Linux 9.5 (igual para otras versiones)
- Cómo hacer copias de seguridad y restaurar MySQL/MariaDB y PostgreSQL con las herramientas 'Automysqlbackup' y 'Autopostgresqlbackup'
- La última versión de MySQL es decepcionante, dicen algunos expertos en bases de datos
- Cómo instalar MongoDB en AlmaLinux 9 con ejemplos de uso
- Cómo instalar PostgreSQL 16 en distribuciones basadas en RHEL, AlmaLinux, RockyLinux y Fedora
- Cómo instalar PhpMyAdmin con Apache en sistemas RHEL, Alma Linux, Rocky Linux, Centos y Fedora
- Cómo hacer una copia de seguridad y restaurar una base de datos PostgreSQL en Linux
- Cómo instalar phpPgAdmin y PostgreSQL en CentOS, AlmaLinux, RHEL, RockyLinux y similares



