LinuxParty

Inicio desactivadoInicio desactivadoInicio desactivadoInicio desactivadoInicio desactivado
 

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, REINDEX o 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.

Escribir un comentario

Código de seguridad
Refescar



Redes:



   

 

Encuestas

Cuál es tu escritorio gráfico favorito en 2026

Suscribete / Newsletter

Suscribete a nuestras Newsletter y periódicamente recibirás un resumen de las noticias publicadas.

Donar a LinuxParty

 

Dona a LInuxParty

Donar 10

Donar 20

 

 

Tutorial de Linux

Formulario de acceso