Si has llegado hasta aquí, dominas variables, bucles, condicionales, funciones, arrays, diccionarios, redirecciones, tuberías, coprocesos y señales. Pero todos tus datos viven en archivos de texto plano — CSV, JSON, logs, configuraciones. ¿Y si necesitas consultas estructuradas, relaciones entre datos, transacciones atómicas o respaldos consistentes?
Ahí entran las bases de datos.
Bash no es un lenguaje de aplicaciones web, pero es el mejor lenguaje para automatización de infraestructura. Y la infraestructura moderna habla con bases de datos: respaldos, migraciones, informes periódicos, limpieza de datos, agregación de logs, monitoreo de métricas. Todo eso se hace desde Bash.
En este capítulo verás como,
- Usar SQLite, la base de datos embebida que no necesita servidor
- Consultar MySQL/MariaDB y PostgreSQL desde scripts
- Prevenir SQL injection cuando interpolas variables
- Exportar datos a JSON y CSV desde cualquier motor
- Hacer backups automatizados con cron
- Construir herramientas completas: gestor de contactos, agregador de logs, generador de informes
Filosofía del capítulo: no vas a aprender SQL aquí. Vas a aprender cómo Bash se convierte en el pegamento que conecta tus scripts con los motores de base de datos. Damos SQL por conocido; nos centramos en la orquestación.
SQLite: la base de datos embebida
SQLite es la base de datos más usada del mundo. Está en tu teléfono, en tu navegador, en millones de dispositivos. Y en Bash, es tu mejor aliada porque:
- No necesita servidor — la base de datos es un archivo
.db - Viene instalada en prácticamente todos los Linux (
sqlite3) - Soporta transacciones (ACID) incluso desde scripts
- Ocupa ~500 KB — no hay dependencias pesadas
- Permite exportar a JSON, CSV, HTML nativamente
Primeros pasos con sqlite3 CLI
El comando sqlite3 abre una base de datos (la crea si no existe) y entra en modo interactivo:
# Abrir (o crear) una base de datos
sqlite3 contactos.db
# Desde ahí, comandos SQL directamente:
sqlite> CREATE TABLE contactos (
...> id INTEGER PRIMARY KEY AUTOINCREMENT,
...> nombre TEXT NOT NULL,
...> email TEXT UNIQUE,
...> telefono TEXT,
...> creado_en TEXT DEFAULT (datetime('now'))
...> );
sqlite> .tables
contactos
sqlite> .quit
Pero el poder real está en no entrar al modo interactivo. Puedes pasar consultas directamente como argumento:
# Consulta única desde línea de comandos
sqlite3 contactos.db "SELECT count(*) FROM contactos;"
# Crear tabla sin entrar al shell
sqlite3 contactos.db "CREATE TABLE IF NOT EXISTS usuarios (id INTEGER PRIMARY KEY, nombre TEXT);"
O usar un heredoc para múltiples consultas:
sqlite3 contactos.db <<EOF
INSERT INTO contactos (nombre, email, telefono) VALUES
('Ana García', 'ana@example.com', '555-0101'),
('Carlos López', 'carlos@example.com', '555-0102');
SELECT * FROM contactos;
EOF
Comandos punto (dot commands)
Los comandos que empiezan con . son instrucciones para el CLI de sqlite3, no SQL. Controlan el formato de salida, la carga de datos y la configuración:
# .headers ON — Muestra nombres de columna
sqlite3 -headers contactos.db "SELECT * FROM contactos;"
# id|nombre|email|telefono|creado_en
# 1|Ana García|ana@example.com|555-0101|2026-06-11 10:00:00
# .mode — Controla el formato de salida
sqlite3 -header -column contactos.db "SELECT * FROM contactos;"
# id nombre email telefono creado_en
# -- ------------ ----------------- --------- -------------------
# 1 Ana García ana@example.com 555-0101 2026-06-11 10:00:00
# 2 Carlos López carlos@example.com 555-0102 2026-06-11 10:00:05
# .tables — Lista tablas
sqlite3 contactos.db ".tables"
# .schema — Muestra el CREATE TABLE de cada tabla
sqlite3 contactos.db ".schema"
# CREATE TABLE contactos (
# id INTEGER PRIMARY KEY AUTOINCREMENT,
# nombre TEXT NOT NULL,
# ...
# .dump — Volca toda la base como sentencias SQL (ideal para backups)
sqlite3 contactos.db ".dump" > backup.sql
# .import — Carga un CSV en una tabla
sqlite3 contactos.db ".import --csv datos.csv usuarios"
# .output — Redirige la salida a un archivo
sqlite3 contactos.db <<EOF
.output reporte.txt
.mode column
.headers on
SELECT * FROM contactos;
EOF
Puedes combinar varios comandos punto en un solo heredoc:
sqlite3 ventas.db <<EOF
.mode column
.headers on
.width 5 20 10 10
SELECT id, producto, cantidad, precio FROM ventas
WHERE fecha >= date('now', '-7 days');
EOF
INSERT, UPDATE, DELETE desde Bash
La forma más legible de hacer operaciones CRUD desde Bash es con heredocs:
DB="tienda.db"
# INSERT
sqlite3 "$DB" <<EOF
INSERT INTO productos (nombre, precio, stock)
VALUES ('Laptop X200', 899.99, 15);
EOF
# UPDATE
sqlite3 "$DB" <<EOF
UPDATE productos
SET stock = stock - 1
WHERE id = 42 AND stock > 0;
EOF
# DELETE
sqlite3 "$DB" <<EOF
DELETE FROM productos WHERE stock <= 0;
EOF
# SELECT con captura de resultado
EXISTE=$(sqlite3 "$DB" "SELECT count(*) FROM productos WHERE id = 42;")
if [[ "$EXISTE" -gt 0 ]]; then
echo "El producto 42 existe"
fi
# SELECT con múltiples columnas (separadas por | por defecto)
sqlite3 -separator ' | ' "$DB" "SELECT id, nombre, precio FROM productos;"
Crear tablas con heredoc
Para scripts que crean su propia base de datos al vuelo, el patrón estándar es:
DB="/var/lib/mi_app/datos.db"
mkdir -p "$(dirname "$DB")"
sqlite3 "$DB" <<'EOF'
CREATE TABLE IF NOT EXISTS usuarios (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT NOT NULL,
hash_pass TEXT NOT NULL,
creado_en TEXT DEFAULT (datetime('now')),
activo INTEGER DEFAULT 1
);
CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
usuario_id INTEGER REFERENCES usuarios(id),
accion TEXT NOT NULL,
timestamp TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_logs_usuario ON logs(usuario_id);
CREATE INDEX IF NOT EXISTS idx_logs_fecha ON logs(timestamp);
EOF
El 'EOF' con comillas evita la expansión de variables dentro del heredoc — importante cuando el SQL contiene caracteres que Bash podría interpretar.
.import — Cargar CSV directamente
SQLite puede importar archivos CSV directamente sin necesidad de scripts externos:
# Crear la tabla (sqlite3 no la crea automáticamente)
sqlite3 datos.db "CREATE TABLE IF NOT EXISTS ventas (
id INTEGER, producto TEXT, cantidad INTEGER, precio REAL, fecha TEXT
);"
# Importar el CSV
sqlite3 datos.db ".import --csv ventas.csv ventas"
# Verificar
sqlite3 -header -column datos.db "SELECT * FROM ventas LIMIT 5;"
El flag --csv (disponible desde SQLite 3.32, 2020) indica que el archivo es CSV con comillas y escapes. Sin él, sqlite3 asume un formato más simple (pipe-delimited).
Para CSV sin cabecera (solo datos):
sqlite3 datos.db ".import --csv --skip 1 datos_con_cabecera.csv mi_tabla"
.output y .dump — Exportar y respaldar
# Exportar consulta a archivo de texto
sqlite3 db.sqlite <<EOF
.mode column
.headers on
.output /tmp/reporte_ventas.txt
SELECT * FROM ventas WHERE fecha >= '2026-01-01';
EOF
# Volcar toda la base a SQL (dump)
sqlite3 db.sqlite ".dump" > respaldo_$(date +%F).sql
# Volcar solo una tabla
sqlite3 db.sqlite ".dump contactos" > contactos_backup.sql
# Restaurar desde dump
sqlite3 db_restaurada.db < respaldo_2026-06-11.sql
-json y -csv — Formatos de salida estructurados
SQLite 3.33+ (2020) añadió salida nativa a JSON. Es una de las características más potentes para scripts modernos:
# Salida JSON (cada fila es un objeto, todas en un array)
sqlite3 -json contactos.db "SELECT id, nombre, email FROM contactos;"
# [{"id":1,"nombre":"Ana García","email":"ana@example.com"},
# {"id":2,"nombre":"Carlos López","email":"carlos@example.com"}]
# Salida CSV (con cabecera)
sqlite3 -csv -header contactos.db "SELECT * FROM contactos;" > contactos_export.csv
# Combinar con jq para procesamiento avanzado
sqlite3 -json contactos.db "SELECT * FROM contactos;" | \
jq '.[] | {nombre: .nombre, email: .email}'
# Formato HTML (ideal para informes rápidos)
sqlite3 -html contactos.db "SELECT * FROM contactos;" > tabla.html
Prevención de SQL injection con printf %q
Este es el punto más importante del capítulo. Cuando interpolas variables de Bash en consultas SQL, abres la puerta a SQL injection. Un usuario que introduce '; DROP TABLE contactos; -- como nombre puede destruir tu base de datos.
Bash tiene una herramienta built-in para esto: printf %q. Escapa cualquier cadena para que sea segura en SQL:
# ❌ PELIGRO: SQL injection directa
NOMBRE="Ana'; DROP TABLE contactos; --"
sqlite3 db.db "INSERT INTO usuarios (nombre) VALUES ('$NOMBRE');"
# ¡La tabla contactos acaba de desaparecer!
# ✅ SEGURO: printf %q escapa la cadena
NOMBRE="Ana'; DROP TABLE contactos; --"
NOMBRE_SEGURO=$(printf '%q' "$NOMBRE")
sqlite3 db.db "INSERT INTO usuarios (nombre) VALUES ($NOMBRE_SEGURO);"
# Se inserta literalmente: Ana'; DROP TABLE contactos; --
printf %q escapa caracteres especiales de shell, lo que también funciona para SQL porque escapa comillas simples. Crea una función genérica:
# Función de sanitización para SQLite
sql_escape() {
printf '%q' "$1"
}
# Uso
NOMBRE=$(sql_escape "$nombre_usuario")
EMAIL=$(sql_escape "$email_usuario")
sqlite3 db.db "INSERT INTO usuarios (nombre, email) VALUES ($NOMBRE, $EMAIL);"
Importante:
printf %qescapa para shell, no para SQL. En la práctica, para SQLite con valores entrecomillados con comillas simples, funciona perfectamente porque escapa la comilla simple como'\'', que SQL interpreta como una comilla literal dentro de la cadena. Para MySQL/PostgreSQL, considera usar consultas parametrizadas o escapar específicamente.
Para MySQL, puedes usar mysql_real_escape_string si usas mysql --safe-updates, pero no hay equivalente directo desde Bash. La mejor práctica es nunca interpolar directamente:
# Alternativa: usar variables de sqlite3 (3.44+)
sqlite3 db.db "INSERT INTO usuarios (nombre) VALUES (@nombre)" \
-bind @nombre "$nombre_usuario"
MySQL/MariaDB desde línea de comandos
MySQL y MariaDB requieren un servidor en ejecución, pero el cliente mysql desde Bash es igual de potente que sqlite3.
Consultas inline con mysql -e
# Consulta simple
mysql -u usuario -p -h localhost -e "SELECT * FROM empleados;"
# Con base de datos específica
mysql -u usuario -p mi_base -e "SELECT count(*) FROM empleados;"
El flag -e ejecuta una consulta y termina. Sin -e, mysql entra en modo interactivo.
Modos batch, silent y raw
Para scripting, estos flags son esenciales:
# --batch: formato tabulado, sin bordes, sin cabecera
mysql -u root -p --batch -e "SELECT id, nombre FROM empleados;"
# --silent: aún más silencioso (oculta cabeceras y contadores)
mysql -u root -p --silent -e "SELECT count(*) FROM empleados;"
# 42
# --raw: no escapa caracteres especiales
mysql -u root -p --raw --batch -e "SELECT nombre FROM empleados;"
# Combinación típica para scripting
mysql -u root -p --batch --silent --raw -e "SELECT email FROM empleados WHERE id=1;"
.my.cnf y mysql_config_editor
Nunca pongas contraseñas en scripts. Usa archivos de configuración seguros:
Opción 1: ~/.my.cnf (permisos 600)
# ~/.my.cnf
[client]
user = root
password = MiPasswordSegura
host = localhost
# Uso: ya no necesitas -p
mysql -e "SHOW DATABASES;"
Opción 2: mysql_config_editor (más seguro, almacena credenciales ofuscadas)
# Configurar login
mysql_config_editor set --login-path=produccion \
--host=db.example.com --user=admin --password
# Usarlo en scripts
mysql --login-path=produccion -e "SHOW DATABASES;"
# Listar logins configurados
mysql_config_editor print --all
# Eliminar un login
mysql_config_editor remove --login-path=produccion
mysqldump — Backup y restauración
# Backup de una base de datos
mysqldump -u root mi_base > /backups/mi_base_$(date +%F).sql
# Backup comprimido
mysqldump -u root mi_base | gzip > /backups/mi_base_$(date +%F).sql.gz
# Backup de varias bases
mysqldump -u root --databases db1 db2 db3 > multi_backup.sql
# Backup de todas las bases
mysqldump -u root --all-databases > all_backup.sql
# Backup solo estructura (sin datos)
mysqldump -u root --no-data mi_base > esquema.sql
# Restaurar
mysql -u root mi_base < /backups/mi_base_2026-06-11.sql
# Restaurar desde gzip
gunzip < /backups/mi_base_2026-06-11.sql.gz | mysql -u root mi_base
Flags útiles para scripting:
mysqldump \
--single-transaction \ # Consistencia sin bloquear tablas (InnoDB)
--routines \ # Incluye procedimientos almacenados
--triggers \ # Incluye triggers
--events \ # Incluye eventos
--quick \ # No cachear en memoria
--compress \ # Comprimir en la conexión
-u root \
mi_base > respaldo.sql
PostgreSQL desde línea de comandos
PostgreSQL usa psql como cliente CLI. Es más verboso que mysql pero igual de potente para scripting.
psql -c, -t, -A, -X
# Consulta simple con -c
psql -U postgres -d mi_base -c "SELECT count(*) FROM usuarios;"
# -t (--tuples-only): solo filas, sin cabecera ni pie
psql -U postgres -d mi_base -t -c "SELECT count(*) FROM usuarios;"
# -A (--no-align): salida sin relleno, ideal para scripting
psql -U postgres -d mi_base -A -t -c "SELECT email FROM usuarios;"
# -X (--no-psqlrc): no leer .psqlrc (evita configuraciones locales)
psql -U postgres -X -c "SELECT 1;"
# Combinación típica para scripting
psql -U postgres -d mi_base -t -A -X -c "SELECT email FROM usuarios WHERE id=1;"
PGPASSWORD y .pgpass
Opción 1: Variable PGPASSWORD (útil pero visible en ps)
export PGPASSWORD="MiPasswordSegura"
psql -U postgres -d mi_base -c "SELECT now();"
unset PGPASSWORD # Limpiar después
Opción 2: Archivo ~/.pgpass (recomendado)
# Formato: host:puerto:base_datos:usuario:password
# ~/.pgpass
localhost:5432:mi_base:postgres:MiPasswordSegura
*:*:*:postgres:MiPasswordSegura
# Permisos estrictos
chmod 600 ~/.pgpass
psql -U postgres -d mi_base -c "SELECT 1;" # No pide password
Opción 3: Variable PGPASSFILE (para rutas alternativas)
export PGPASSFILE="/etc/secrets/pg_produccion.pass"
psql -U admin -d produccion -c "SELECT 1;"
pg_dump — Backup y restauración
# Backup de una base de datos
pg_dump -U postgres mi_base > /backups/mi_base_$(date +%F).sql
# Backup comprimido
pg_dump -U postgres mi_base | gzip > /backups/mi_base_$(date +%F).sql.gz
# Backup solo estructura (sin datos)
pg_dump -U postgres --schema-only mi_base > esquema.sql
# Backup solo datos (sin estructura)
pg_dump -U postgres --data-only mi_base > datos.sql
# Backup en formato custom (paralelizable, comprimido)
pg_dump -U postgres -Fc mi_base > mi_base.dump
# Restaurar formato SQL
psql -U postgres mi_base < /backups/mi_base_2026-06-11.sql
# Restaurar formato custom
pg_restore -U postgres -d mi_base mi_base.dump
# Backup de todas las bases (como postgres)
pg_dumpall -U postgres > all_backup.sql
Importación y exportación de datos
Bash es el mejor lugar para tuberías de datos entre formatos. Aquí tienes los patrones esenciales.
CSV a SQLite
DB="analisis.db"
# Método 1: .import directo
sqlite3 "$DB" <<EOF
.mode csv
.import ventas_mensuales.csv ventas
EOF
# Método 2: desde línea
sqlite3 "$DB" ".import --csv ventas_mensuales.csv ventas"
# Método 3: creando la tabla explícitamente primero
sqlite3 "$DB" "CREATE TABLE IF NOT EXISTS ventas (
id INTEGER, producto TEXT, cantidad INTEGER, total REAL
);"
sqlite3 "$DB" ".import --csv ventas_mensuales.csv ventas"
SQLite a JSON
DB="tienda.db"
# Exportar toda una tabla a JSON
sqlite3 -json "$DB" "SELECT * FROM productos WHERE stock > 0;" > inventario.json
# Exportar y procesar con jq
sqlite3 -json "$DB" "
SELECT p.nombre, p.precio, c.nombre AS categoria
FROM productos p
JOIN categorias c ON p.categoria_id = c.id
" | jq 'group_by(.categoria) | {categorias: .}' > categorizado.json
# Exportar consulta agregada
sqlite3 -json "$DB" "
SELECT strftime('%Y-%m', fecha) AS mes,
SUM(total) AS ingresos,
COUNT(*) AS transacciones
FROM ventas
GROUP BY mes
ORDER BY mes
;" > resumen_mensual.json
SQLite a CSV
DB="analisis.db"
# Exportar tabla completa a CSV
sqlite3 -csv -header "$DB" "SELECT * FROM ventas;" > ventas_export.csv
# Exportar consulta con filtros
sqlite3 -csv -header "$DB" "
SELECT id, producto, cantidad, fecha
FROM ventas
WHERE fecha BETWEEN '2026-01-01' AND '2026-06-30'
" > ventas_semestre1.csv
# Exportar y comprimir
sqlite3 -csv "$DB" "SELECT * FROM logs;" | gzip > logs_export.csv.gz
Entre motores: MySQL → SQLite
# 1. Exportar desde MySQL a CSV
mysql -u root --batch --silent --raw -e "
SELECT id, nombre, email, fecha_registro
FROM usuarios
" mi_base > /tmp/usuarios.csv
# 2. Crear tabla en SQLite e importar
sqlite3 migracion.db <<EOF
CREATE TABLE usuarios (
id INTEGER PRIMARY KEY,
nombre TEXT,
email TEXT,
fecha_registro TEXT
);
.mode csv
.import /tmp/usuarios.csv usuarios
EOF
# 3. Verificar
sqlite3 -header -column migracion.db "SELECT count(*) FROM usuarios;"
Mantenimiento automatizado con cron
Las bases de datos necesitan mantenimiento periódico. Bash + cron es la combinación perfecta.
VACUUM, REINDEX, ANALYZE en SQLite
#!/bin/bash
# maintenance_sqlite.sh — Mantenimiento semanal de bases SQLite
DB="/var/data/aplicacion.db"
LOG="/var/log/sqlite_maintenance.log"
fecha=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$fecha] Iniciando mantenimiento de $DB" >> "$LOG"
# VACUUM — Reconstruye la base liberando espacio no usado
sqlite3 "$DB" "VACUUM;"
echo " VACUUM completado" >> "$LOG"
# REINDEX — Reconstruye índices (útil después de muchas escrituras)
sqlite3 "$DB" "REINDEX;"
echo " REINDEX completado" >> "$LOG"
# ANALYZE — Actualiza estadísticas para el optimizador de consultas
sqlite3 "$DB" "ANALYZE;"
echo " ANALYZE completado" >> "$LOG"
# Tamaño antes/después
tamano=$(du -h "$DB" | cut -f1)
echo " Tamaño actual: $tamano" >> "$LOG"
echo "[$fecha] Mantenimiento completado" >> "$LOG"
Crontab semanal:
# Cada domingo a las 03:00
0 3 * * 0 /usr/local/bin/maintenance_sqlite.sh
Respaldos programados
#!/bin/bash
# backup_diario.sh — Respaldo automático de bases de datos
FECHA=$(date +%F)
HORA=$(date +%H%M)
BASE_DIR="/backups"
LOG="/var/log/backups.log"
mkdir -p "$BASE_DIR"/{sqlite,mysql,postgres}
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Iniciando backups..." >> "$LOG"
# SQLite — copia simple del archivo + dump SQL
for db in /var/data/*.db; do
nombre=$(basename "$db" .db)
cp "$db" "$BASE_DIR/sqlite/${nombre}_${FECHA}.db"
sqlite3 "$db" ".dump" | gzip > "$BASE_DIR/sqlite/${nombre}_${FECHA}.sql.gz"
echo " SQLite: $nombre respaldado" >> "$LOG"
done
# MySQL — mysqldump
for db in $(mysql -e "SHOW DATABASES;" | grep -v -E 'Database|information_schema|performance_schema'); do
mysqldump --single-transaction --routines --triggers "$db" | \
gzip > "$BASE_DIR/mysql/${db}_${FECHA}.sql.gz"
echo " MySQL: $db respaldado" >> "$LOG"
done
# PostgreSQL — pg_dump
export PGPASSWORD="$PG_BACKUP_PASS"
for db in $(psql -U postgres -t -A -c "SELECT datname FROM pg_database
WHERE datistemplate = false;"); do
pg_dump -U postgres "$db" | gzip > "$BASE_DIR/postgres/${db}_${FECHA}.sql.gz"
echo " PostgreSQL: $db respaldado" >> "$LOG"
done
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backups completados" >> "$LOG"
Rotación de backups
Nunca acumules backups sin rotación. Un script simple de limpieza:
#!/bin/bash
# rotar_backups.sh — Conserva solo los últimos 30 días de backups
BASE_DIR="/backups"
echo "Rotando backups anteriores a 30 días..."
find "$BASE_DIR" -name "*.sql.gz" -mtime +30 -delete
find "$BASE_DIR" -name "*.db" -mtime +30 -delete
find "$BASE_DIR" -name "*.dump" -mtime +30 -delete
echo "Rotación completada."
Combínalo en un solo cron:
# Backup diario a las 02:00, rotación a las 02:30
0 2 * * * /usr/local/bin/backup_diario.sh
30 2 * * * /usr/local/bin/rotar_backups.sh
Aplicaciones prácticas
Contact manager CRUD
Un gestor de contactos completo desde Bash (la versión completa está en la sección 10). Patrón básico:
CONTACTS_DB="$HOME/.contacts.db"
# Crear tabla si no existe
sqlite3 "$CONTACTS_DB" <<'EOF'
CREATE TABLE IF NOT EXISTS contactos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL,
email TEXT,
telefono TEXT,
grupo TEXT DEFAULT 'general',
creado_en TEXT DEFAULT (datetime('now'))
);
EOF
# Añadir contacto
agregar_contacto() {
local nombre=$(printf '%q' "$1")
local email=$(printf '%q' "$2")
local telefono=$(printf '%q' "$3")
sqlite3 "$CONTACTS_DB" \
"INSERT INTO contactos (nombre, email, telefono) VALUES ($nombre, $email, $telefono);"
echo "Contacto añadido: $1"
}
# Buscar contacto
buscar_contacto() {
local term=$(printf '%q' "$1")
sqlite3 -header -column "$CONTACTS_DB" \
"SELECT id, nombre, email, telefono FROM contactos
WHERE nombre LIKE '%' || $term || '%'
OR email LIKE '%' || $term || '%';"
}
# Listar todos
listar_contactos() {
sqlite3 -header -column "$CONTACTS_DB" \
"SELECT id, nombre, email, telefono, grupo FROM contactos ORDER BY nombre;"
}
# Eliminar contacto
eliminar_contacto() {
local id=$(printf '%q' "$1")
sqlite3 "$CONTACTS_DB" "DELETE FROM contactos WHERE id = $id;"
echo "Contacto $id eliminado."
}
Log aggregator — Insertar logs en SQLite
Ideal para centralizar logs de múltiples scripts en una base consultable:
LOG_DB="/var/log/agregados/eventos.db"
mkdir -p "$(dirname "$LOG_DB")"
# Inicializar esquema (una vez)
sqlite3 "$LOG_DB" <<'EOF'
CREATE TABLE IF NOT EXISTS eventos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT DEFAULT (datetime('now')),
nivel TEXT NOT NULL CHECK(nivel IN ('INFO','WARN','ERROR','DEBUG')),
script TEXT NOT NULL,
mensaje TEXT NOT NULL,
pid INTEGER
);
CREATE INDEX IF NOT EXISTS idx_eventos_fecha ON eventos(timestamp);
CREATE INDEX IF NOT EXISTS idx_eventos_nivel ON eventos(nivel);
EOF
# Función para insertar log
log_to_db() {
local nivel="$1"
local mensaje="$2"
local script="${3:-$0}"
local nivel_s=$(printf '%q' "$nivel")
local msg_s=$(printf '%q' "$mensaje")
local script_s=$(printf '%q' "$script")
sqlite3 "$LOG_DB" \
"INSERT INTO eventos (nivel, script, mensaje, pid)
VALUES ($nivel_s, $script_s, $msg_s, $$);"
}
# Uso
log_to_db "INFO" "Iniciando proceso de backup"
log_to_db "ERROR" "No se pudo conectar a la base de datos remota"
log_to_db "WARN" "Disco al 85% de capacidad"
# Consultar errores recientes
sqlite3 -json "$LOG_DB" \
"SELECT timestamp, script, mensaje FROM eventos
WHERE nivel = 'ERROR' AND timestamp >= datetime('now', '-1 day')
ORDER BY timestamp DESC;"
Report generator — Consultas con formato
#!/bin/bash
# reporte_ventas.sh — Genera reporte semanal de ventas
DB="/var/data/tienda.db"
REPORTE_DIR="/var/www/reportes"
FECHA=$(date +%F)
mkdir -p "$REPORTE_DIR"
# Reporte en HTML
sqlite3 -html "$DB" "
SELECT p.nombre AS Producto,
SUM(v.cantidad) AS Vendidos,
SUM(v.total) AS Ingresos
FROM ventas v
JOIN productos p ON v.producto_id = p.id
WHERE v.fecha >= date('now', '-7 days')
GROUP BY p.nombre
ORDER BY Ingresos DESC
LIMIT 10;
" > "$REPORTE_DIR/top_10_${FECHA}.html"
# Reporte en JSON para dashboard
sqlite3 -json "$DB" "
SELECT strftime('%w', fecha) AS dia_semana,
SUM(total) AS ingresos
FROM ventas
WHERE fecha >= date('now', '-7 days')
GROUP BY dia_semana
ORDER BY dia_semana;
" > "$REPORTE_DIR/semana_${FECHA}.json"
# Reporte resumen en texto
{
echo "=== Reporte Semanal de Ventas ==="
echo "Fecha: $FECHA"
echo ""
echo "Ingresos totales:"
sqlite3 "$DB" "SELECT printf('$%.2f', SUM(total)) FROM ventas
WHERE fecha >= date('now', '-7 days');"
echo ""
echo "Transacciones:"
sqlite3 "$DB" "SELECT COUNT(*) FROM ventas
WHERE fecha >= date('now', '-7 days');"
} > "$REPORTE_DIR/resumen_${FECHA}.txt"
echo "Reportes generados en $REPORTE_DIR"
Migration helper — Esquemas versionados
#!/bin/bash
# migrate.sh — Sistema de migraciones para SQLite
DB="/var/data/app.db"
MIGRATIONS_DIR="/usr/local/share/migraciones"
CURRENT=$(sqlite3 "$DB" "PRAGMA user_version;")
echo "Versión actual del esquema: $CURRENT"
for mig in $(ls "$MIGRATIONS_DIR"/*.sql | sort); do
version=$(basename "$mig" | cut -d'_' -f1)
if [[ "$version" -gt "$CURRENT" ]]; then
echo "Aplicando migración $version: $mig"
sqlite3 "$DB" < "$mig"
sqlite3 "$DB" "PRAGMA user_version = $version;"
echo " ✓ Migración $version aplicada"
fi
done
echo "Esquema actualizado a versión $(sqlite3 "$DB" 'PRAGMA user_version;')"
Archivos de migración siguen el patrón 001_crear_usuarios.sql, 002_añadir_email.sql:
-- 001_crear_usuarios.sql
CREATE TABLE IF NOT EXISTS usuarios (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL
);
Credenciales y seguridad
.my.cnf para MySQL
Crea el archivo con permisos restrictivos:
cat > ~/.my.cnf <<'EOF'
[client]
user = root
password = SuperSegura123
host = localhost
EOF
chmod 600 ~/.my.cnf
Para múltiples entornos:
# ~/.my.cnf
[client]
user = root
[client_produccion]
user = admin
password = ProdPass456
host = db.prod.example.com
[client_testing]
user = tester
password = TestPass789
host = db.test.example.com
Uso:
# Usar perfil por defecto
mysql -e "SELECT 1;"
# Especificar sección
mysql --defaults-group-suffix=_produccion -e "SHOW DATABASES;"
mysql_config_editor
# Configurar (pide la contraseña de forma segura)
mysql_config_editor set --login-path=produccion \
--host=db.prod.example.com \
--user=admin \
--password
# Usar
mysql --login-path=produccion -e "SELECT * FROM usuarios;"
# Verificar
mysql_config_editor print --all
# Eliminar
mysql_config_editor remove --login-path=produccion
.pgpass para PostgreSQL
# ~/.pgpass
# host:puerto:base:usuario:password
localhost:5432:mi_base:postgres:MiPass
192.168.1.100:5432:*:admin:SupraSecret
chmod 600 ~/.pgpass
Variables de entorno temporales
Para scripts que necesitan credenciales sin archivos permanentes:
#!/bin/bash
# Script con credenciales temporales
# Solo existen durante la ejecución del script
export PGPASSWORD="${PG_PASS:?Variable PG_PASS no definida}"
export MYSQL_PWD="${MYSQL_PASS:?Variable MYSQL_PASS no definida}"
# Las consultas se autentican con las variables
mysql -u admin -e "SELECT count(*) FROM usuarios;" mi_base
psql -U admin -d mi_base -c "SELECT count(*) FROM usuarios;"
# Al terminar el script, las variables desaparecen
# (no persisten más allá del proceso)
⚠️ Advertencia: las variables de entorno son visibles en
/proc/PID/environpara el usuario root y en la tabla de procesos. En entornos compartidos, usa archivos de contraseñas con permisos 600.
Funciones profesionales
Funciones reutilizables para tu biblioteca personal (~/.bashrc o librería aparte):
# ──────────────────────────────────────────────
# Funciones profesionales para bases de datos
# ──────────────────────────────────────────────
# sqlite_query — Ejecuta consulta y devuelve resultado en el formato deseado
# Uso: sqlite_query <archivo.db> <consulta> [formato: column|json|csv|html]
sqlite_query() {
local db="$1"
local query="$2"
local format="${3:-column}"
if [[ ! -f "$db" ]]; then
echo "Error: Base de datos '$db' no encontrada" >&2
return 1
fi
case "$format" in
json) sqlite3 -json "$db" "$query" ;;
csv) sqlite3 -csv -header "$db" "$query" ;;
html) sqlite3 -html "$db" "$query" ;;
column) sqlite3 -header -column "$db" "$query" ;;
*) sqlite3 "$db" "$query" ;;
esac
}
# sqlite_to_csv — Exporta tabla/consulta a archivo CSV
# Uso: sqlite_to_csv <archivo.db> <consulta> <archivo_salida.csv>
sqlite_to_csv() {
local db="$1"
local query="$2"
local output="$3"
sqlite3 -csv -header "$db" "$query" > "$output"
echo "Exportadas $(wc -l < "$output") líneas a $output"
}
# mysql_backup — Respaldo de base MySQL con rotación
# Uso: mysql_backup <base> [dir_destino]
mysql_backup() {
local db="$1"
local dest="${2:-/backups/mysql}"
local file="${dest}/${db}_$(date +%F).sql.gz"
mkdir -p "$dest"
echo "Respaldando MySQL: $db → $file"
mysqldump --single-transaction --routines --triggers "$db" | gzip > "$file"
echo "Tamaño: $(du -h "$file" | cut -f1)"
}
# pg_database_list — Lista bases PostgreSQL no-template
# Uso: pg_database_list
pg_database_list() {
psql -U postgres -t -A -X -c "
SELECT datname FROM pg_database
WHERE datistemplate = false
ORDER BY datname;
"
}
# db_export_json — Exporta cualquier consulta SQLite a JSON, opcionalmente
# filtrando/transformando con jq
# Uso: db_export_json <archivo.db> <consulta> [filtro_jq]
db_export_json() {
local db="$1"
local query="$2"
local jq_filter="${3:-.}"
if command -v jq &>/dev/null; then
sqlite3 -json "$db" "$query" | jq "$jq_filter"
else
sqlite3 -json "$db" "$query"
fi
}
# db_size — Muestra tamaño y estadísticas de una base SQLite
# Uso: db_size <archivo.db>
db_size() {
local db="$1"
if [[ ! -f "$db" ]]; then
echo "Error: $db no existe" >&2
return 1
fi
echo "=== Estadísticas de $db ==="
echo "Tamaño: $(du -h "$db" | cut -f1)"
echo "Tablas: $(sqlite3 "$db" '.tables' | wc -w)"
echo "Filas totales:"
for tabla in $(sqlite3 "$db" '.tables'); do
count=$(sqlite3 "$db" "SELECT count(*) FROM \"$tabla\";")
echo " $tabla: $count filas"
done
}
Script completo: gestor_contactos.sh
Un gestor de contactos completo en ~100 líneas usando SQLite, con operaciones CRUD y búsqueda:
#!/bin/bash
# ============================================================
# gestor_contactos.sh — Gestor de contactos con SQLite
#
# Descripción: CRUD completo de contactos usando SQLite
# desde Bash, con sanitización de entrada.
#
# Autor: Tutorial Bash
# Versión: 1.0.0
# Licencia: MIT
# Uso: ./gestor_contactos.sh [comando] [args...]
#
# Comandos:
# add <nombre> [email] [tel] [grupo] — Añadir contacto
# search <termino> — Buscar contactos
# list [grupo] — Listar contactos
# delete <id> — Eliminar contacto
# count [grupo] — Contar contactos
# export <archivo.csv> — Exportar a CSV
# import <archivo.csv> — Importar desde CSV
# ============================================================
set -euo pipefail
DB="${CONTACTS_DB:-$HOME/.contacts.db}"
# ──────────────────────────────────────────────
# Inicialización
# ──────────────────────────────────────────────
init_db() {
sqlite3 "$DB" <<'EOF'
CREATE TABLE IF NOT EXISTS contactos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL,
email TEXT,
telefono TEXT,
grupo TEXT DEFAULT 'general',
creado_en TEXT DEFAULT (datetime('now'))
);
EOF
}
# ──────────────────────────────────────────────
# Funciones
# ──────────────────────────────────────────────
sanitizar() {
printf '%q' "$1"
}
cmd_add() {
local nombre=$(sanitizar "${1:?Error: nombre requerido}")
local email=$(sanitizar "${2:-}")
local telefono=$(sanitizar "${3:-}")
local grupo=$(sanitizar "${4:-general}")
sqlite3 "$DB" \
"INSERT INTO contactos (nombre, email, telefono, grupo)
VALUES ($nombre, $email, $telefono, $grupo);"
echo "✓ Contacto añadido: $1"
}
cmd_search() {
local term=$(sanitizar "${1:?Error: término de búsqueda requerido}")
sqlite3 -header -column "$DB" "
SELECT id, nombre, email, telefono, grupo
FROM contactos
WHERE nombre LIKE '%' || $term || '%'
OR email LIKE '%' || $term || '%'
OR telefono LIKE '%' || $term || '%'
ORDER BY nombre;
"
}
cmd_list() {
local grupo=$(sanitizar "${1:-}")
if [[ -z "$grupo" ]]; then
sqlite3 -header -column "$DB" "
SELECT id, nombre, email, telefono, grupo
FROM contactos ORDER BY nombre;
"
else
sqlite3 -header -column "$DB" "
SELECT id, nombre, email, telefono, grupo
FROM contactos WHERE grupo = $grupo
ORDER BY nombre;
"
fi
}
cmd_delete() {
local id="${1:?Error: ID requerido}"
# Verificar que existe
local existe
existe=$(sqlite3 "$DB" "SELECT count(*) FROM contactos WHERE id = $id;")
if [[ "$existe" -eq 0 ]]; then
echo "Error: No existe contacto con ID $id" >&2
return 1
fi
local nombre
nombre=$(sqlite3 "$DB" "SELECT nombre FROM contactos WHERE id = $id;")
sqlite3 "$DB" "DELETE FROM contactos WHERE id = $id;"
echo "✓ Contacto eliminado: $nombre (ID: $id)"
}
cmd_count() {
local grupo=$(sanitizar "${1:-}")
if [[ -z "$grupo" ]]; then
sqlite3 "$DB" "SELECT COUNT(*) || ' contactos en total' FROM contactos;"
else
sqlite3 "$DB" "
SELECT COUNT(*) || ' contactos en grupo ' || $grupo
FROM contactos WHERE grupo = $grupo;
"
fi
}
cmd_export() {
local archivo="${1:?Error: nombre de archivo requerido}"
sqlite3 -csv -header "$DB" "SELECT * FROM contactos ORDER BY id;" > "$archivo"
echo "✓ Exportados $(wc -l < "$archivo") contactos a $archivo"
}
cmd_import() {
local archivo="${1:?Error: nombre de archivo requerido}"
if [[ ! -f "$archivo" ]]; then
echo "Error: Archivo '$archivo' no encontrado" >&2
return 1
fi
# Crear tabla temporal para importación
sqlite3 "$DB" <<EOF
.mode csv
.import '$archivo' contactos_temp
EOF
echo "✓ Importados datos desde $archivo"
}
# ──────────────────────────────────────────────
# Main
# ──────────────────────────────────────────────
main() {
init_db
local comando="${1:-help}"
shift 2>/dev/null || true
case "$comando" in
add) cmd_add "$@" ;;
search) cmd_search "$@" ;;
list) cmd_list "$@" ;;
delete) cmd_delete "$@" ;;
count) cmd_count "$@" ;;
export) cmd_export "$@" ;;
import) cmd_import "$@" ;;
help|*)
echo "Uso: $0 <comando> [args...]"
echo ""
echo "Comandos:"
echo " add <nombre> [email] [tel] [grupo] — Añadir contacto"
echo " search <termino> — Buscar contactos"
echo " list [grupo] — Listar contactos"
echo " delete <id> — Eliminar contacto"
echo " count [grupo] — Contar contactos"
echo " export <archivo.csv> — Exportar a CSV"
echo " import <archivo.csv> — Importar desde CSV"
;;
esac
}
main "$@"
Cómo usarlo:
chmod +x gestor_contactos.sh
# Añadir contactos
./gestor_contactos.sh add "Ana García" ana@email.com "555-0101" amigos
./gestor_contactos.sh add "Carlos López" carlos@email.com "555-0102" trabajo
./gestor_contactos.sh add "María Pérez" maria@email.com "555-0103" amigos
# Listar todos
./gestor_contactos.sh list
# Buscar
./gestor_contactos.sh search "Ana"
# Contar por grupo
./gestor_contactos.sh count amigos
# Exportar
./gestor_contactos.sh export mis_contactos.csv
# Eliminar
./gestor_contactos.sh delete 2
Errores comunes (10)
Error 1: No instalar sqlite3 y asumir que existe
# ❌ El script falla silenciosamente
sqlite3 mi.db "SELECT 1;"
# bash: sqlite3: command not found
# ✅ Verificar antes de usar
if ! command -v sqlite3 &>/dev/null; then
echo "Error: sqlite3 no está instalado"
echo "Instalar: sudo apt install sqlite3"
exit 1
fi
Error 2: No sanitizar variables en consultas SQL
# ❌ SQL injection directa
NOMBRE="Juan'; DROP TABLE usuarios; --"
sqlite3 db.db "INSERT INTO usuarios (nombre) VALUES ('$NOMBRE');"
# ✅ Siempre sanitizar
sqlite3 db.db "INSERT INTO usuarios (nombre) VALUES ($(printf '%q' "$NOMBRE"));"
Error 3: Olvidar permisos en .my.cnf o .pgpass
# ❌ MySQL ignora .my.cnf con permisos incorrectos
chmod 644 ~/.my.cnf
# mysql: [Warning] Using a password on the command line interface can be insecure.
# ✅ Permisos estrictos
chmod 600 ~/.my.cnf
chmod 600 ~/.pgpass
Error 4: Confundir -c de psql con -e de mysql
# ❌ psql no tiene -e, falla
psql -U postgres -e "SELECT 1;"
# psql: invalid option -- 'e'
# ✅ psql usa -c para consultas inline
psql -U postgres -c "SELECT 1;"
# ❌ mysql no usa -c para consultas
mysql -u root -c "SELECT 1;"
# ✅ mysql usa -e
mysql -u root -e "SELECT 1;"
Error 5: No escapar caracteres especiales en heredocs SQL
# ❌ El shell expande $ dentro del heredoc
sqlite3 db.db <<EOF
INSERT INTO usuarios (nombre) VALUES ('$usuario');
EOF
# Si $usuario contiene comillas, se rompe
# ✅ Heredoc con comillas: 'EOF' evita expansión
sqlite3 db.db <<'EOF'
INSERT INTO usuarios (nombre) VALUES ('$usuario');
EOF
# Inserta literalmente '$usuario'
Error 6: Asumir que sqlite3 crea la tabla al importar CSV
# ❌ .import falla si la tabla no existe
sqlite3 db.db ".import --csv datos.csv usuarios"
# Error: table usuarios does not exist
# ✅ Crear tabla primero
sqlite3 db.db "CREATE TABLE IF NOT EXISTS usuarios (...);"
sqlite3 db.db ".import --csv datos.csv usuarios"
Error 7: No usar –single-transaction en mysqldump con InnoDB
# ❌ Respaldo inconsistente (tablas MyISAM o sin --single-transaction)
mysqldump -u root mi_base > backup.sql
# Los datos pueden cambiar durante el dump
# ✅ Consistencia garantizada (InnoDB)
mysqldump --single-transaction -u root mi_base > backup.sql
Error 8: Usar PGPASSWORD en scripts compartidos (seguridad)
# ❌ Cualquier usuario en el sistema puede verla
export PGPASSWORD="MiSuperPassword"
psql ...
# (visible en /proc/$$/environ y en ps aux)
# ✅ Usar .pgpass con permisos adecuados
echo "localhost:5432:*:postgres:MiSuperPassword" > ~/.pgpass
chmod 600 ~/.pgpass
Error 9: No usar -t -A en psql, obteniendo formato de tabla con bordes
# ❌ Formato humano, no parseable
psql -U postgres -c "SELECT id, nombre FROM usuarios;"
# id | nombre
# ----+---------
# 1 | Ana
# 2 | Carlos
# ✅ Formato parseable para scripts
psql -U postgres -t -A -c "SELECT id, nombre FROM usuarios;"
# 1|Ana
# 2|Carlos
Error 10: No verificar código de retorno de operaciones SQL
# ❌ El script continúa aunque la consulta falle
sqlite3 db.db "INSERT INTO usuarios (email) VALUES ('email_invalido');"
echo "Contacto añadido" # Se ejecuta aunque haya fallado
# ✅ Verificar con set -e o chequeo explícito
set -euo pipefail # Detiene el script en cualquier error
# O explícitamente:
if ! sqlite3 db.db "INSERT INTO usuarios (email) VALUES ('$email');"; then
echo "Error al insertar usuario" >&2
exit 1
fi
Resumen
| Concepto | Comando / Sintaxis | Propósito |
|---|---|---|
| SQLite CLI | sqlite3 archivo.db "SQL;" | Ejecuta consulta SQL sobre archivo .db |
| SQLite heredoc | sqlite3 db <<EOF ... EOF | Múltiples consultas sin entrar al shell |
| .mode column | sqlite3 -column | Salida formateada en columnas |
| .mode csv | sqlite3 -csv | Salida CSV (ideal para exportar) |
| .mode json | sqlite3 -json | Salida JSON nativa (v3.33+) |
| .headers on | sqlite3 -header | Muestra nombres de columna |
| .import –csv | .import --csv archivo tabla | Carga CSV en tabla existente |
| .dump | .dump [tabla] | Volca base/tabla a SQL |
| .backup | .backup archivo.db | Respaldo consistente de SQLite |
| printf %q | printf '%q' "$var" | Escapa variable para SQL (anti-injection) |
| MySQL -e | mysql -e "SQL;" | Consulta inline en MySQL |
| MySQL –batch | --batch --silent --raw | Modo scripting (sin bordes, sin cabeceras) |
| MySQL config | mysql_config_editor set | Almacena credenciales de forma segura |
| mysqldump | mysqldump db > backup.sql | Backup de MySQL |
| psql -c | psql -c "SQL;" | Consulta inline en PostgreSQL |
| psql -t -A | -t -A -X | Modo scripting (tuplas, sin alinear, sin rc) |
| .pgpass | host:puerto:db:user:pass | Autenticación sin contraseña interactiva |
| pg_dump | pg_dump db > backup.sql | Backup de PostgreSQL |
| VACUUM | sqlite3 db "VACUUM;" | Reconstruye DB liberando espacio |
| REINDEX | sqlite3 db "REINDEX;" | Reconstruye índices |
| ANALYZE | sqlite3 db "ANALYZE;" | Actualiza estadísticas del optimizador |
Más información
- SQLite Official Documentation — La referencia definitiva de SQLite
- SQLite CLI Tricks — Trucos y comandos punto del CLI
- MySQL Shell Automation — Automatización con mysql
- PostgreSQL psql Automation — Scripting con psql
- Bash MySQL Backup Script — Script de respaldo MySQL de referencia
- pgDash — PostgreSQL Monitoring — Guía de scripting con psql
- SQL Injection Prevention Cheat Sheet — Prevención de SQL injection
- cron(8) — Programación de tareas periódicas
- «The Linux Programming Interface» de Michael Kerrisk — Capítulo sobre bases de datos y persistencia desde scripting
- «Learning SQLite» de Mike Owens — La guía práctica de SQLite para desarrolladores
- «Bash Cookbook» de Carl Albing — Recetas con bases de datos desde Bash
- «High Performance MySQL» de Baron Schwartz — Estrategias de backup y mantenimiento
- «PostgreSQL: Up and Running» de Regina Obe — Automatización y scripting con PostgreSQL