Buenas prácticas al escribir scripts Bash: comillas, inyección y shellcheck
Introducción
Escribir scripts Bash parece sencillo, pero pequeños detalles pueden generar fallos graves: desde nombres de archivos con espacios que se rompen, hasta vulnerabilidades de inyección de comandos que permiten a un atacante ejecutar código arbitrario. Este tutorial está dirigido a desarrolladores, administradores de sistemas y cualquiera que automatice tareas con Bash. Al finalizar, sabrás por qué las comillas son esenciales, cómo prevenir la inyección de comandos y cómo usar Shellcheck para detectar problemas antes de que causen dolor.
Por qué las comillas importan
En Bash, la división de palabras y el globbing (expansión de patrones como * o ?) ocurren después de la expansión de variables. Si una variable contiene espacios o caracteres especiales y no está entre comillas, Bash la tratará como varias palabras separadas, lo que suele romper el comportamiento esperado.
División de palabras
Cuando Bash encuentra una variable sin comillas, la divide en palabras usando los caracteres del entorno IFS (por defecto espacio, tabulación y salto de línea). Por ejemplo:
archivo="mi documento.txt"
cp $archivo /tmp/Después de la expansión, la línea se interpreta como:
cp mi documento.txt /tmp/El comando cp recibe tres argumentos: mi, documento.txt y /tmp/. Obviamente fallará porque no encuentra un archivo llamado mi.
Globbing
Además, si la variable contiene caracteres como *, ? o [, Bash intentará expandirlos como coincidencias de nombres de archivo antes de pasar el argumento al comando. Esto puede llevar a borrar o modificar archivos inesperados.
patron="*.txt"
rm $patronSi en el directorio existen nota.txt y resumen.txt, la línea se convierte en rm nota.txt resumen.txt, borrando ambos archivos. Si no hay coincidencias, la cadena permanece literal, pero el comportamiento es impredecible.
Tipos de comillas y su uso correcto
Bash ofrece tres formas principales de cotear: comillas dobles ("), comillas simples (') y el backslash (\). Cada una tiene un comportamiento distinto respecto a la expansión de variables, sustitución de comandos y la interpretación de caracteres especiales.
Comillas dobles
Permiten la expansión de variables ($VAR), de comandos ($(cmd) o `cmd`) y de secuencias de escape como o . Los caracteres especiales como $, `, \ y " siguen teniendo significado especial, mientras que otros como * y ? se tratan literalmente.
Ejemplo seguro:
archivo="mi documento.txt"
cp "$archivo" /tmp/Ahora Bash pasa un solo argumento a cp: mi documento.txt.
Comillas simples
Impiden cualquier tipo de expansión. Todo lo que está entre ' se trata literalmente. Úsalas cuando el texto no necesita expansión de variables o comandos.
mensaje='El costo es $100'
echo "$mensaje"Salida:
El costo es $100Backslash
El carácter de escape \(barra invertida) hace que el carácter siguiente se trate literalmente. Es útil para escapar un solo carácter, por ejemplo un espacio dentro de una cadena sin comillas:
archivo=mi\ documento.txt
cp "$archivo" /tmp/Aunque funciona, es menos legible y propenso a errores; se prefiere usar comillas dobles.
Ejemplos de código bueno y malo
A continuación se presentan situaciones típicas, mostrando la versión incorrecta (que provoca división de palabras o globbing) y la versión correcta.
Ejemplo 1: Copiar un archivo con espacios
Malo:
src="informe final.pdf"
dest="/archivos de respaldo"
cp $src $destSalida esperada (error):
cp: cannot stat ‘informe’: No such file or directory
cp: cannot stat ‘final.pdf’: No such file or directoryBueno:
src="informe final.pdf"
dest=\"/archivos de respaldo\"" # nota: las comillas externas son parte del string
cp "$src" "$dest"Salida esperada:
# El archivo se copia correctamenteEjemplo 2: Listar archivos que coinciden con un patrón
Malo:
ext="txt"
ls *.$extSi ext está vacío o contiene espacios, el comando se rompe.
Bueno:
ext="txt"
ls -- *.$extEl -- indica a ls que lo que sigue no son opciones, evitando que un patrón como -l sea interpretado como opción. Además, la expansión de *.txt ocurre dentro de comillas dobles? En este caso no se usan comillas porque queremos el globbing, pero protegemos la variable.
Malo con globbing no deseado:
patron="*"
echo $patronSi el directorio contiene file1.txt file2.txt, la salida será:
file1.txt file2.txtBueno:
patron="*"
echo "$patron"Salida:
*Ejemplo 3: Lectura de entrada del usuario
Malo:
read -p "Ingrese su nombre: " nombre
echo Hola, $nombre!Si el usuario ingresa John Doe, todo funciona porque read guarda la línea completa. Pero si más adelante usamos $nombre sin comillas en un contexto que haga globbing, habrá problemas.
Bueno:
read -p "Ingrese su nombre: " nombre
echo "Hola, $nombre!"Prevención de inyección de comandos
La inyección de comandos ocurre cuando un script incorpora datos no confiables directamente en una cadena que será interpretada por la shell. Un atacante puede incluir caracteres como ;, &&, |, $() o backticks para ejecutar comandos adicionales.
Escenario típico: construcción dinámica de comandos
Imaginemos un script que permite al usuario buscar un archivo usando grep.
Malo:
read -p "Patrón a buscar: " patron
grep $patron *.logSi el usuario ingresa foo; rm -rf /, la línea se convierte en:
grep foo; rm -rf / *.logEl punto y coma termina el comando grep y ejecuta rm -rf /.
Bueno: usar comillas dobles y, cuando sea posible, pasar el patrón como argumento separado:
read -p "Patrón a buscar: " patron
grep -- "$patron" *.logEl -- evita que un patrón que empiece con - sea interpretado como opción. La variable está entre comillas dobles, así que cualquier ;, &, | o $() se trata como texto literal.
Uso de printf para evitar interpretación
Otra técnica segura es usar printf con formato %s y pasar la variable como argumento separado:
read -p "Mensaje: " msg
printf '%s
' "$msg"Incluso si msg contiene %n u otros especificadores de formato, printf los trata como texto porque el formato es fijo.
Evitar eval a menos que sea absolutamente necesario
eval vuelve a interpretar la cadena como código de shell, lo que magnifica cualquier riesgo de inyección. Si necesitás usar eval, asegurá de que la entrada provenga de una fuente de confianza y esté rigurosamente validada.
Malo:
read -p "Comando a ejecutar: " cmd
eval $cmdBueno (si realmente necesitás eval):
read -p "Comando a ejecutar: " cmd
# Validar que cmd solo contenga caracteres permitidos
if [[ $cmd =~ ^[a-zA-Z0-9./_-]+$ ]]; then
eval "$cmd"
else
echo "Entrada no válida" >&2
exit 1
fiEn la práctica, evitá eval y buscá alternativas más seguras (funciones, arrays, o llamadas directas).
Introducción a Shellcheck
Shellcheck es un analizador estático de scripts Bash que detecta errores comunes, problemas de portabilidad y posibles vulnerabilidades. Se integra fácilmente en editores, sistemas de CI o se ejecuta manualmente.
Instalación
En sistemas Debian/Ubuntu:
sudo apt-get install shellcheckEn macOS con Homebrew:
brew install shellcheckUso básico
shellcheck mi_script.shShellcheck imprimirá mensajes con niveles de gravedad: info, warning, error. Cada mensaje incluye un código (por ejemplo SC2086) y una breve explicación.
Ejemplos de salida
Código malo (división de palabras)
# script.sh
archivo="mi documento.txt"
cp $archivo /tmp/Ejecutando shellcheck script.sh produce:
In script.sh line 3:
cp $archivo /tmp/
^-- ^-- SC2086: Double quote to prevent globbing and word splitting.
Did you mean:
cp "$archivo" /tmp/
^------------------^^Código bueno (con comillas)
# script_fixed.sh
archivo="mi documento.txt"
cp "$archivo" /tmp/Salida de Shellcheck:
No issues detected!Código malo (inyección de comandos)
read -p "Patrón: " patron
grep $patron *.logSalida de Shellcheck:
In script.sh line 3:
grep $patron *.log
^-- ^-- SC2086: Double quote to prevent globbing and word splitting.
^-- ^-- SC2046: Quote this to prevent word splitting.Shellcheck también sugiere usar -- para evitar que un patrón que empiece con - sea tomado como opción:
In script.sh line 3:
grep $patron *.log
^-- ^-- SC2048: Use "--" to separate options from arguments when using variables.Código bueno (con comillas y --)
read -p "Patrón: " patron
grep -- "$patron" *.logShellcheck ya no reporta problemas.
Buenas prácticas y errores comunes
A continuación, una lista de recomendaciones que debés tener siempre en cuenta al escribir scripts Bash.
1. Siempre cotear las expansiones de variables
- Usá
"$VAR"cuando la variable pueda contener espacios, tabulaciones, saltos de línea o caracteres especiales. - Si la variable es conocida por ser un número o un identificador sin espacios, aún así es buena práctica cotearla; el costo es mínimo y la consistencia evita olvidos.
2. Usá comillas simples para literales que no necesitan expansión
- Ejemplo:
mensaje='Versión 1.0 (beta)'evita que el paréntesis sea interpretado como parte de una expansión de comandos.
3. Protegé los argumentos que provienen de entrada externa
- Anteponé
--antes de la variable cuando el comando lo permite (grep, diff, ffmpeg, etc.). - Si el comando no acepta
--, validá o sanificá la entrada antes de usarla.
4. Evitá la división de palabras y el globbing no intencional
- Además de comillear, podés usar arreglos (arrays) para almacenar listas de elementos:
archivos=("informe final.pdf" "resumen de año.pdf" )
cp "${archivos[@]}" /tmp/- En este caso, cada elemento del array se pasa como un argumento separado, sin riesgo de división adicional.
5. Validá y sanificá entradas numéricas
- Si esperás un número, usá expresiones regulares para confirmarlo:
if ! [[ $numero =~ ^[0-9]+$ ]]; then
echo "Entrada no numérica" >&2
exit 1
fi6. Preferí printf sobre echo para salida controlada
echotiene comportamientos variables según la implementación y las opciones activadas (-e,-n).printfes portable y permite controlar exactamente el formato.
7. Habilitá set -euo pipefail al inicio del script
-e: sale si cualquier comando falla (excepto en condicionales explícitos).-u: trata variables no definidas como error.-o pipefail: hace que una tubería falle si cualquier componente falla, no solo el último.
Ejemplo de encabezado:
#!/usr/bin/env bash
set -euo pipefail8. Usá Shellcheck de forma continua
- Integrá Shellcheck en tu editor (VS Code, Vim, Emacs) para que te avise mientras escribís.
- En pipelines de CI, agregá un paso que falle si Shellcheck detecta errores de gravedad
errorowarning. - Aprovechá los códigos de error (
SCxxxx) para buscar documentación rápida:shellcheck -s bash -SC2086.
9. Documentá el propósito y las suposiciones
- Al inicio del script, incluí un comentario breve que describa qué hace, qué se espera del entorno y qué supuestos tenés sobre las variables de entrada.
10. Probá con casos edge
- Creá pruebas que incluyan nombres de archivo con espacios, tabs, caracteres de nueva línea, caracteres especiales (
$,`,\,*,?,[,]) y entradas maliciosas (; rm -rf /,$(cat /etc/passwd)). - Usá un entorno aislado (contenedor o máquina virtual) para ejecutar esas pruebas.
Conclusión
Las comillas no son un detalle menor; son la primera línea de defensa contra la división de palabras, el globbing no deseado y la inyección de comandos. Aplicarlas correctamente, junto con técnicas como el uso de --, arrays y validación de entradas, hace que tus scripts Bash sean robustos y seguros.
Shellcheck se convierte en un aliado indispensable: detecta problemas antes de que lleguen a producción y te enseña, mediante sus mensajes, a escribir código más limpio y seguro. Al adoptar estas buenas prácticas y hacer de Shellcheck parte de tu flujo de trabajo, reducirás drásticamente la cantidad de bugs sorpresa y mejorarás la confianza en tus automatizaciones.
Próximos pasos:
- Revisá alguno de tus scripts existentes con
shellcheck -x script.shy corregí las advertencias. - Agregá un pre‑commit hook que ejecute Shellcheck en cada cambio.
- Practicá escribiendo pequeños scripts que manipulen archivos con nombres extraños y verificá que se comporten como esperás.
Con estas herramientas y hábitos, estarás bien equipado para escribir Bash de manera profesional y segura.
DUGLAS MORENO
Comentarios (0)
Sé el primero en comentar.