Assurer la portabilité des scripts Bash sur différents systèmes

Écrire des scripts Bash portables qui gèrent les différences GNU, BSD et BusyBox sur Linux, macOS et les environnements CI.

Assurer la portabilité des scripts Bash sur différents systèmes

Écrire des scripts Bash qui fonctionnent sur votre ordinateur portable, un serveur Linux et un runner CI est plus difficile qu'il n'y paraît. La portabilité des scripts Bash se brise généralement sur de petites différences : un drapeau sed -i qui fonctionne sur Linux mais échoue sur macOS, une option date qui n'existe que dans GNU coreutils, ou un script qui suppose que /bin/bash est la version que vous avez testée.

La difficulté principale est que Bash n'est qu'une partie de l'environnement. Linux fournit généralement les utilitaires GNU. macOS fournit des utilitaires de type BSD. Les conteneurs basés sur BusyBox peuvent fournir des implémentations plus petites avec moins d'options. Votre script doit être clair sur ce qu'il nécessite.

Ce guide se concentre sur les scripts Bash, pas strictement sur les scripts POSIX sh. Si vous avez besoin d'une véritable portabilité /bin/sh, évitez complètement la syntaxe propre à Bash et testez avec des shells comme dash.

Commencez par un contrat de shell clair

Utilisez un shebang qui correspond à votre intention. Si le script nécessite Bash, dites-le :

#!/usr/bin/env bash

/usr/bin/env localise Bash via $PATH, ce qui est utile lorsque les utilisateurs installent un Bash plus récent en dehors de /bin. Si vos hôtes de production nécessitent un chemin d'interpréteur fixe, documentez et appliquez ce chemin à la place.

Le mode strict détecte de nombreuses erreurs précoces, mais ce n'est pas magique :

#!/usr/bin/env bash

set -euo pipefail
IFS=$'\n\t'

Ces options aident, avec des mises en garde :

  • -e : Quitte lorsque de nombreuses commandes simples retournent un statut non nul.
  • -u : Traite les variables non définies comme des erreurs.
  • pipefail : Fait échouer un pipeline si une commande du pipeline échoue.

Gérez les échecs attendus explicitement :

if ! grep -q "ready" "$log_file"; then
    echo "Le service n'est pas encore prêt"
fi

Connaissez votre version de Bash

Ne dépendez pas accidentellement d'une fonctionnalité de Bash que vos systèmes cibles ne possèdent pas. macOS a historiquement fourni un Bash plus ancien dans /bin/bash, tandis que de nombreuses distributions Linux fournissent des versions plus récentes.

Les fonctionnalités à utiliser avec précaution incluent :

  • Les tableaux associatifs.
  • Le globbing avancé comme **.
  • La substitution de processus comme <(command).
  • Le comportement plus récent de l'expansion des paramètres.

Si vous avez besoin d'une version minimale de Bash, vérifiez-la près du début :

if (( BASH_VERSINFO[0] < 4 )); then
    echo "Ce script nécessite Bash 4 ou plus récent." >&2
    exit 1
fi

Gérez les différences GNU, BSD et BusyBox

Les plus gros problèmes de portabilité viennent souvent des commandes externes, pas de Bash lui-même.

sed -i

GNU sed accepte -i sans extension de sauvegarde. BSD sed sur macOS nécessite un argument d'extension après -i, même si cette extension est une chaîne vide.

file="data.txt"
pattern="s/error/success/g"

case "$(uname -s)" in
    Darwin)
        sed -i '' "$pattern" "$file"
        ;;
    *)
        sed -i "$pattern" "$file"
        ;;
esac

Pour les scripts critiques, un modèle plus sûr est d'écrire dans un fichier temporaire puis de le déplacer à sa place. Cela évite complètement de dépendre du comportement d'édition sur place.

date

Les calculs de date sont différents selon les systèmes :

Objectif GNU date BSD date sur macOS
Il y a 30 jours date -d "30 days ago" +%Y%m%d date -v-30d +%Y%m%d

Si votre script a besoin de calculs de date complexes, utilisez une dépendance cohérente comme Python, ou exigez GNU coreutils sur macOS et appelez gdate explicitement. Ne supposez pas silencieusement que date -d existe.

grep, find et xargs

Tenez-vous-en aux options largement supportées lorsque c'est possible :

  • Utilisez grep -E au lieu de compter sur egrep.
  • Évitez grep -P sauf si vous vérifiez que GNU grep avec support PCRE est disponible.
  • Soyez prudent avec les prédicats find qui diffèrent entre les implémentations GNU et BSD.
  • Préférez les pipelines délimités par null pour les noms de fichiers lorsque c'est supporté :
find "$root" -type f -name '*.log' -print0 | xargs -0 rm -f

Gérez les dépendances et les chemins

Utilisez $PATH pour la recherche normale de commandes, mais vérifiez les outils requis avant de travailler :

check_dependency() {
    if ! command -v "$1" >/dev/null 2>&1; then
        echo "Erreur : la commande requise '$1' est introuvable." >&2
        exit 1
    fi
}

check_dependency jq
check_dependency curl

Préférez command -v à which car c'est un builtin du shell dans Bash et se comporte de manière plus prévisible dans les scripts.

Mettez les variables entre guillemets sauf si vous voulez intentionnellement la division des mots :

cp "$source_file" "$target_dir/"

Ceci est important pour les chemins comme Project Files/report.txt, et cela vous protège également de l'expansion des wildcards dans une entrée inattendue.

Utilisez les fichiers temporaires en toute sécurité

Utilisez mktemp pour le travail temporaire. Un modèle simple et portable consiste à créer un répertoire temporaire et à mettre les fichiers à l'intérieur :

tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT

tmp_file="$tmp_dir/output.txt"
some_command > "$tmp_file"

Le trap entre guillemets simples empêche $tmp_dir d'être expansé avant l'exécution du trap. Comme la variable est toujours dans la portée, le nettoyage supprime le bon répertoire.

Surveillez les fins de ligne et la casse du système de fichiers

Les scripts édités sous Windows peuvent utiliser des fins de ligne CRLF. Un symptôme courant est :

/usr/bin/env: bash\r: Aucun fichier ou dossier de ce type

Configurez votre éditeur pour enregistrer les scripts shell avec des fins de ligne LF, ou exécutez dos2unix dans votre processus de construction.

Rappelez-vous aussi que la plupart des systèmes de fichiers Linux sont sensibles à la casse par défaut, tandis que les configurations APFS par défaut de macOS sont souvent insensibles à la casse. Si votre script écrit Config.yml et lit plus tard config.yml, cela peut fonctionner sur votre Mac et échouer sur Linux.

Testez sur les systèmes que vous supportez

La meilleure vérification de portabilité est une petite matrice de test :

  • Linux avec les utilitaires GNU.
  • macOS avec les utilitaires BSD.
  • Conteneurs minimaux si votre script s'exécute dans des environnements Alpine ou BusyBox.

Exécutez aussi ShellCheck. Il ne détectera pas tous les problèmes de plateforme, mais il détecte de nombreux problèmes de guillemets, de variables non définies et de motifs de commandes fragiles avant que vos utilisateurs ne le fassent.

À retenir

La portabilité des scripts Bash vient du fait de rendre vos hypothèses explicites. Choisissez le shell, vérifiez les dépendances, mettez les variables entre guillemets, évitez les drapeaux exclusifs à GNU sauf si vous les exigez, et testez sur les mêmes systèmes d'exploitation que ceux de vos utilisateurs. Une petite matrice CI avec Linux et macOS détecte la plupart des bugs de portabilité avant que votre automatisation n'atteigne la production.