Garantizando la portabilidad de los scripts Bash en diferentes sistemas
Escribe scripts Bash portables que manejen las diferencias entre GNU, BSD y BusyBox en Linux, macOS y entornos CI.
Asegurando la Portabilidad de Scripts Bash en Diferentes Sistemas
Escribir scripts Bash que funcionen en tu portátil, un servidor Linux y un ejecutor de CI es más difícil de lo que parece. La portabilidad de scripts Bash generalmente se rompe por pequeñas diferencias: una bandera sed -i que funciona en Linux pero falla en macOS, una opción date que solo existe en GNU coreutils, o un script que asume que /bin/bash es la versión que probaste.
La dificultad principal es que Bash es solo una parte del entorno. Linux generalmente incluye utilidades GNU. macOS incluye utilidades de estilo BSD. Los contenedores basados en BusyBox pueden proporcionar implementaciones más pequeñas con menos opciones. Tu script debe ser claro sobre lo que requiere.
Esta guía se centra en scripts Bash, no estrictamente en scripts POSIX sh. Si necesitas verdadera portabilidad con /bin/sh, evita por completo la sintaxis exclusiva de Bash y prueba con shells como dash.
Comienza con un Contrato de Shell Claro
Usa un shebang que coincida con tu intención. Si el script requiere Bash, indícalo:
#!/usr/bin/env bash
/usr/bin/env localiza Bash a través de $PATH, lo cual es útil cuando los usuarios instalan un Bash más nuevo fuera de /bin. Si tus servidores de producción requieren una ruta de intérprete fija, documenta y aplica esa ruta en su lugar.
El modo estricto detecta muchos errores temprano, pero no es mágico:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
Estas opciones ayudan, con advertencias:
-e: Sale cuando muchos comandos simples devuelven un estado distinto de cero.-u: Trata las variables no definidas como errores.pipefail: Hace que una tubería falle si algún comando en la tubería falla.
Maneja los fallos esperados explícitamente:
if ! grep -q "ready" "$log_file"; then
echo "El servicio aún no está listo"
fi
Conoce tu Versión de Bash
No dependas accidentalmente de una característica de Bash que tus sistemas objetivo no tengan. macOS ha incluido históricamente un Bash más antiguo en /bin/bash, mientras que muchas distribuciones de Linux incluyen versiones más nuevas.
Las características que deben usarse con cuidado incluyen:
- Arreglos asociativos.
- Globbing avanzado como
**. - Sustitución de procesos como
<(command). - Comportamiento de expansión de parámetros más nuevo.
Si necesitas una versión mínima de Bash, verifícala cerca del principio:
if (( BASH_VERSINFO[0] < 4 )); then
echo "Este script requiere Bash 4 o más reciente." >&2
exit 1
fi
Maneja las Diferencias entre GNU, BSD y BusyBox
Los mayores problemas de portabilidad a menudo provienen de comandos externos, no de Bash en sí.
sed -i
GNU sed acepta -i sin una extensión de respaldo. BSD sed en macOS requiere un argumento de extensión después de -i, incluso si esa extensión es una cadena vacía.
file="data.txt"
pattern="s/error/success/g"
case "$(uname -s)" in
Darwin)
sed -i '' "$pattern" "$file"
;;
*)
sed -i "$pattern" "$file"
;;
esac
Para scripts críticos, un patrón más seguro es escribir en un archivo temporal y luego moverlo a su lugar. Esto evita depender del comportamiento de edición en el lugar.
date
El cálculo de fechas es diferente entre sistemas:
| Objetivo | GNU date |
BSD date en macOS |
|---|---|---|
| Hace 30 días | date -d "30 days ago" +%Y%m%d |
date -v-30d +%Y%m%d |
Si tu script necesita cálculos de fecha complejos, usa una dependencia consistente como Python, o requiere GNU coreutils en macOS y llama a gdate explícitamente. No asumas silenciosamente que date -d existe.
grep, find y xargs
Cíñete a opciones ampliamente compatibles cuando sea posible:
- Usa
grep -Een lugar de depender deegrep. - Evita
grep -Pa menos que verifiques que tienes GNU grep con soporte PCRE. - Ten cuidado con los predicados de
findque difieren entre las implementaciones GNU y BSD. - Prefiere tuberías delimitadas por nulo para nombres de archivo cuando sean compatibles:
find "$root" -type f -name '*.log' -print0 | xargs -0 rm -f
Gestiona Dependencias y Rutas
Usa $PATH para la búsqueda normal de comandos, pero verifica las herramientas requeridas antes de hacer el trabajo:
check_dependency() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Error: comando requerido '$1' no encontrado." >&2
exit 1
fi
}
check_dependency jq
check_dependency curl
Prefiere command -v sobre which porque es un builtin del shell en Bash y se comporta de manera más predecible en scripts.
Cita las variables a menos que quieras intencionalmente división de palabras:
cp "$source_file" "$target_dir/"
Esto es importante para rutas como Project Files/report.txt, y también te protege de la expansión de comodines en entradas inesperadas.
Usa Archivos Temporales de Forma Segura
Usa mktemp para trabajo temporal. Un patrón simple y portátil es crear un directorio temporal y poner archivos dentro de él:
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT
tmp_file="$tmp_dir/output.txt"
some_command > "$tmp_file"
La trampa con comillas simples evita que $tmp_dir se expanda hasta que se ejecute la trampa. Debido a que la variable aún está en el ámbito, la limpieza elimina el directorio correcto.
Vigila los Finales de Línea y la Sensibilidad de Mayúsculas del Sistema de Archivos
Los scripts editados en Windows pueden usar finales de línea CRLF. Un síntoma común es:
/usr/bin/env: bash\r: No such file or directory
Configura tu editor para guardar scripts de shell con finales de línea LF, o ejecuta dos2unix en tu proceso de compilación.
También recuerda que la mayoría de los sistemas de archivos Linux son sensibles a mayúsculas por defecto, mientras que las configuraciones APFS predeterminadas de macOS a menudo no distinguen entre mayúsculas y minúsculas. Si tu script escribe Config.yml y luego lee config.yml, puede funcionar en tu Mac y fallar en Linux.
Prueba en los Sistemas que Soporte
La mejor verificación de portabilidad es una pequeña matriz de pruebas:
- Linux con utilidades GNU.
- macOS con utilidades BSD.
- Contenedores mínimos si tu script se ejecuta en entornos Alpine o BusyBox.
Ejecuta también ShellCheck. No detectará todos los problemas de plataforma, pero detecta muchos patrones de comillas, variables no definidas y comandos frágiles antes de que tus usuarios lo hagan.
Conclusión
La portabilidad de scripts Bash proviene de hacer explícitas tus suposiciones. Elige el shell, verifica las dependencias, cita las variables, evita banderas exclusivas de GNU a menos que las requieras, y prueba en los mismos sistemas operativos que usan tus usuarios. Una pequeña matriz de CI con Linux y macOS detecta la mayoría de los errores de portabilidad antes de que tu automatización llegue a producción.