Garantire la Portabilità degli Script Bash tra Sistemi Diversi

Scrivi script Bash portabili che gestiscano le differenze tra GNU, BSD e BusyBox su Linux, macOS e ambienti CI.

Garantire la Portabilità degli Script Bash su Sistemi Diversi

Scrivere script Bash che funzionino sul tuo laptop, su un server Linux e su un runner CI è più difficile di quanto sembri. La portabilità degli script Bash di solito si rompe su piccole differenze: un flag sed -i che funziona su Linux ma fallisce su macOS, un'opzione date che esiste solo in GNU coreutils, o uno script che assume che /bin/bash sia la versione che hai testato.

La difficoltà principale è che Bash è solo una parte dell'ambiente. Linux solitamente fornisce utilità GNU. macOS fornisce utilità di tipo BSD. I container basati su BusyBox possono fornire implementazioni più piccole con meno opzioni. Il tuo script deve essere chiaro su ciò che richiede.

Questa guida si concentra sugli script Bash, non strettamente sugli script POSIX sh. Se hai bisogno di vera portabilità /bin/sh, evita completamente la sintassi solo Bash e testa con shell come dash.

Inizia con un Contratto di Shell Chiaro

Usa uno shebang che corrisponda alla tua intenzione. Se lo script richiede Bash, dillo:

#!/usr/bin/env bash

/usr/bin/env individua Bash tramite $PATH, il che è utile quando gli utenti installano un Bash più recente al di fuori di /bin. Se i tuoi host di produzione richiedono un percorso dell'interprete fisso, documenta e applica quel percorso invece.

La modalità rigorosa cattura molti errori precocemente, ma non è magica:

#!/usr/bin/env bash

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

Queste opzioni aiutano, con avvertenze:

  • -e: Esce quando molti comandi semplici restituiscono uno stato diverso da zero.
  • -u: Tratta le variabili non impostate come errori.
  • pipefail: Fa fallire una pipeline se un qualsiasi comando nella pipeline fallisce.

Gestisci i fallimenti attesi esplicitamente:

if ! grep -q "ready" "$log_file"; then
    echo "Il servizio non è ancora pronto"
fi

Conosci la Tua Versione di Bash

Non dipendere accidentalmente da una funzionalità di Bash che i tuoi sistemi di destinazione non hanno. macOS ha storicamente fornito un Bash più vecchio in /bin/bash, mentre molte distribuzioni Linux forniscono versioni più recenti.

Le funzionalità da usare con cautela includono:

  • Array associativi.
  • Globbing avanzato come **.
  • Sostituzione di processo come <(command).
  • Comportamento di espansione dei parametri più recente.

Se hai bisogno di una versione minima di Bash, controllala vicino all'inizio:

if (( BASH_VERSINFO[0] < 4 )); then
    echo "Questo script richiede Bash 4 o successivo." >&2
    exit 1
fi

Gestisci le Differenze tra GNU, BSD e BusyBox

I maggiori problemi di portabilità spesso provengono da comandi esterni, non da Bash stesso.

sed -i

GNU sed accetta -i senza un'estensione di backup. BSD sed su macOS richiede un argomento di estensione dopo -i, anche se quell'estensione è una stringa vuota.

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

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

Per script critici, un pattern più sicuro è scrivere in un file temporaneo e poi spostarlo nella posizione corretta. Questo evita di fare affidamento sul comportamento di modifica in-place.

date

I calcoli delle date sono diversi tra i sistemi:

Obiettivo GNU date BSD date su macOS
30 giorni fa date -d "30 days ago" +%Y%m%d date -v-30d +%Y%m%d

Se il tuo script ha bisogno di calcoli complessi di date, usa una dipendenza coerente come Python, o richiedi GNU coreutils su macOS e chiama gdate esplicitamente. Non assumere silenziosamente che date -d esista.

grep, find e xargs

Attieniti a opzioni ampiamente supportate quando possibile:

  • Usa grep -E invece di fare affidamento su egrep.
  • Evita grep -P a meno che tu non controlli la presenza di GNU grep con supporto PCRE.
  • Fai attenzione ai predicati find che differiscono tra le implementazioni GNU e BSD.
  • Preferisci pipeline delimitate da null per i nomi di file quando supportato:
find "$root" -type f -name '*.log' -print0 | xargs -0 rm -f

Gestisci Dipendenze e Percorsi

Usa $PATH per la normale ricerca dei comandi, ma controlla gli strumenti richiesti prima di lavorare:

check_dependency() {
    if ! command -v "$1" >/dev/null 2>&1; then
        echo "Errore: comando richiesto '$1' non trovato." >&2
        exit 1
    fi
}

check_dependency jq
check_dependency curl

Preferisci command -v a which perché è un built-in della shell in Bash e si comporta in modo più prevedibile negli script.

Cita le variabili a meno che tu non voglia intenzionalmente la suddivisione delle parole:

cp "$source_file" "$target_dir/"

Questo è importante per percorsi come Project Files/report.txt, e ti protegge anche dall'espansione dei wildcard in input imprevisti.

Usa i File Temporanei in Modo Sicuro

Usa mktemp per il lavoro temporaneo. Un pattern semplice e portabile è creare una directory temporanea e mettere i file al suo interno:

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

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

Il trap con apici singoli impedisce che $tmp_dir venga espanso fino a quando il trap non viene eseguito. Poiché la variabile è ancora in ambito, la pulizia rimuove la directory corretta.

Controlla le Terminazioni di Riga e la Sensibilità alle Maiuscole del Filesystem

Gli script modificati su Windows possono usare terminazioni di riga CRLF. Un sintomo comune è:

/usr/bin/env: bash\r: No such file or directory

Configura il tuo editor per salvare gli script shell con terminazioni LF, o esegui dos2unix nel tuo processo di build.

Ricorda anche che la maggior parte dei filesystem Linux è sensibile alle maiuscole per impostazione predefinita, mentre le impostazioni predefinite di APFS su macOS sono spesso insensibili alle maiuscole. Se il tuo script scrive Config.yml e successivamente legge config.yml, potrebbe funzionare sul tuo Mac e fallire su Linux.

Testa sui Sistemi che Supporti

Il miglior controllo di portabilità è una piccola matrice di test:

  • Linux con utilità GNU.
  • macOS con utilità BSD.
  • Container minimi se il tuo script viene eseguito in ambienti Alpine o BusyBox.

Esegui anche ShellCheck. Non catturerà tutti i problemi di piattaforma, ma cattura molti schemi di quoting, variabili non definite e comandi fragili prima che lo facciano i tuoi utenti.

Conclusione

La portabilità degli script Bash deriva dal rendere esplicite le tue ipotesi. Scegli la shell, controlla le dipendenze, cita le variabili, evita flag solo GNU a meno che non li richieda, e testa sugli stessi sistemi operativi che usano i tuoi utenti. Una piccola matrice CI con Linux e macOS cattura la maggior parte dei bug di portabilità prima che la tua automazione raggiunga la produzione.