Creazione e sourcing di librerie Bash riutilizzabili
Organizzi gli helper condivisi in file libreria .sh caricabili con guardie di inclusione e prefissi di funzione con namespace.
Creazione e sourcing di librerie Bash riutilizzabili è una lezione Linux Command Line & Bash Scripting Mastery gratuita su CoddyKit. Questa è la lezione 2 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Linux Command Line & Bash Scripting Mastery, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Linux Command Line & Bash Scripting Mastery include 4 lezioni in totale.
Che cos'è una libreria Bash?
Nell'ingegneria del software, una libreria è una raccolta di funzioni riutilizzabili che può essere condivisa da più programmi. Bash supporta lo stesso concetto tramite script di shell caricabili.
Anziché copiare e incollare le funzioni di utilità in ogni script, le inserisca in un file .sh dedicato e lo carichi con il comando source (o con la sua forma abbreviata .). Tutte le funzioni, variabili o alias definiti in quel file diventano disponibili nella sessione della shell corrente dello script chiamante.
- Promuove il principio DRY (Don't Repeat Yourself, non ripetersi)
- Centralizza le correzioni dei bug: una sola correzione, vantaggi per tutti i chiamanti
- Rende i singoli script più brevi e facili da leggere
- Consente di mantenere una coerenza condivisa dal team nella registrazione, nella gestione degli errori e nella logica delle utilità
Un progetto Bash ben strutturato contiene in genere una directory lib/ con questi file condivisi, secondo convenzioni simili a quelle dei linguaggi di livello superiore.
Il comando source e l'operatore punto
Esistono due modi equivalenti per caricare un file di libreria nell'ambiente della shell corrente:
source /path/to/lib.sh— la forma esplicita e leggibile. /path/to/lib.sh— la forma abbreviata compatibile con POSIX
Entrambi eseguono il file nel processo della shell corrente, non in una subshell; pertanto ogni funzione e variabile definita al suo interno entra immediatamente a far parte dell'ambiente dello script dopo la chiamata.
Un modello comune consiste nell'individuare la libreria in relazione allo script chiamante usando $BASH_SOURCE, rendendo il progetto portabile indipendentemente dal percorso di installazione.
#!/usr/bin/env bash
# main.sh — load a library relative to this script's own location
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/utils.sh"
echo "Library loaded. Calling greet..."
greet "World"Creare il primo file di libreria
Un file di libreria è un normale file .sh che contiene esclusivamente definizioni di funzioni (e occasionalmente costanti). Non dovrebbe eseguire codice con effetti collaterali al livello superiore: è destinato a essere caricato con source, non eseguito direttamente.
Convenzioni principali:
- Inizi con un commento shebang che descriva lo scopo della libreria
- Definisca solo funzioni: nessuna logica
mainal livello superiore - Utilizzi
returnall'interno delle funzioni (maiexit, che terminerebbe il chiamante) - Mantenga il file in una sottodirectory
lib/del progetto
#!/usr/bin/env bash
# lib/utils.sh — General-purpose utility functions
# Print a greeting message
greet() {
local name="${1:-stranger}"
echo "Hello, ${name}!"
}
# Print a timestamped log line to stderr
log_info() {
echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2
}
# Print an error message and return a failure code
log_error() {
echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2
return 1
}Include guard: prevenire il caricamento multiplo
Quando diversi script caricano la stessa libreria, oppure quando una libreria ne carica un'altra che viene caricata anche dallo script principale, le funzioni possono essere definite più volte. Questo spreca tempo e può causare bug difficili da individuare se il corpo di una funzione viene sostituito durante l'esecuzione.
La soluzione è un include guard: una variabile che funge da flag. Al primo caricamento la variabile non è impostata, quindi il file prosegue. A ogni caricamento successivo, la guardia è già impostata e il file restituisce immediatamente il controllo.
È l'equivalente Bash di #pragma once in C/C++ o dei modelli if not already imported presenti in altri linguaggi.
#!/usr/bin/env bash
# lib/utils.sh — with include guard
# Guard: if already sourced, do nothing
[[ -n "${_LIB_UTILS_LOADED:-}" ]] && return 0
_LIB_UTILS_LOADED=1
greet() {
local name="${1:-stranger}"
echo "Hello, ${name}!"
}
log_info() {
echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2
}
log_error() {
echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2
return 1
}Prefissi di funzione con namespace
Bash dispone di un unico namespace globale per le funzioni. Se due librerie definiscono entrambe una funzione chiamata log o init, la seconda definizione sovrascrive silenziosamente la prima.
La difesa standard consiste in un prefisso di namespace: ogni funzione di una libreria è preceduta dal nome breve della libreria seguito da due due punti (::) o da un carattere di sottolineatura. Ad esempio, una libreria di utilità per le stringhe utilizza str::, mentre una libreria per i file utilizza file::.
- Le collisioni diventano estremamente improbabili
- Il codice chiamante è autoesplicativo:
str::trimindica esattamente dove si trova la funzione - La ricercabilità con grep migliora:
grep 'str::' main.shmostra immediatamente tutte le chiamate alla libreria delle stringhe
#!/usr/bin/env bash
# lib/str.sh — String utility library (namespaced)
[[ -n "${_LIB_STR_LOADED:-}" ]] && return 0
_LIB_STR_LOADED=1
# Trim leading and trailing whitespace
str::trim() {
local s="$1"
s="${s#"${s%%[![:space:]]*}"}"
s="${s%"${s##*[![:space:]]}"}"
echo "$s"
}
# Convert string to uppercase
str::upper() {
echo "${1^^}"
}
# Convert string to lowercase
str::lower() {
echo "${1,,}"
}
# Check if a string contains a substring
str::contains() {
[[ "$1" == *"$2"* ]]
}Organizzare una directory lib/
Con la crescita del progetto, un unico file utils.sh diventa difficile da gestire. Suddivida le responsabilità in file di libreria mirati all'interno di una directory lib/:
lib/log.sh— utilità per la registrazione (log::info,log::warn,log::error)lib/str.sh— manipolazione delle stringhe (str::trim,str::upper)lib/fs.sh— utilità per il filesystem (fs::require_dir,fs::safe_rm)lib/net.sh— controlli di rete (net::wait_for_port,net::is_online)
Un unico file di bootstrap (lib/bootstrap.sh) può caricarli tutti nell'ordine corretto, così ogni script deve effettuare una sola chiamata a source.
#!/usr/bin/env bash
# lib/bootstrap.sh — Load all project libraries in dependency order
[[ -n "${_LIB_BOOTSTRAP_LOADED:-}" ]] && return 0
_LIB_BOOTSTRAP_LOADED=1
_BOOTSTRAP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${_BOOTSTRAP_DIR}/log.sh"
source "${_BOOTSTRAP_DIR}/str.sh"
source "${_BOOTSTRAP_DIR}/fs.sh"
source "${_BOOTSTRAP_DIR}/net.sh"
log::info "All libraries loaded."Una libreria completa per il logging
Il logging è la preoccupazione trasversale più comune tra gli script. Una lib/log.sh dedicata centralizza la formattazione dell'output, i livelli di log e i codici colore.
Con una libreria di questo tipo, ogni script del progetto stampa messaggi coerenti, con timestamp e codificati a colori, senza duplicare il codice di formattazione.
#!/usr/bin/env bash
# lib/log.sh — Coloured, levelled logging library
[[ -n "${_LIB_LOG_LOADED:-}" ]] && return 0
_LIB_LOG_LOADED=1
# Colour codes (disabled when not writing to a terminal)
_LOG_RED=''; _LOG_YEL=''; _LOG_GRN=''; _LOG_RST=''
if [[ -t 2 ]]; then
_LOG_RED='\033[0;31m'
_LOG_YEL='\033[0;33m'
_LOG_GRN='\033[0;32m'
_LOG_RST='\033[0m'
fi
_log::_print() {
local level="$1" colour="$2"; shift 2
printf "%b[%s]%b %s %s\n" \
"$colour" "$level" "$_LOG_RST" \
"$(date '+%H:%M:%S')" "$*" >&2
}
log::info() { _log::_print 'INFO ' "$_LOG_GRN" "$@"; }
log::warn() { _log::_print 'WARN ' "$_LOG_YEL" "$@"; }
log::error() { _log::_print 'ERROR' "$_LOG_RED" "$@"; return 1; }
log::fatal() { _log::_print 'FATAL' "$_LOG_RED" "$@"; exit 1; }Una libreria di funzioni di supporto per il filesystem
Gli script che manipolano file e directory spesso ripetono gli stessi controlli difensivi: questa directory esiste? Questo percorso è scrivibile? Sto per eliminare qualcosa di importante?
Centralizzare questi controlli in lib/fs.sh rende ogni script che li utilizza più sicuro e leggibile. Notate come ogni funzione utilizzi return 1 in caso di errore anziché exit, mantenendo la possibilità per il chiamante di gestire l'errore in modo appropriato.
#!/usr/bin/env bash
# lib/fs.sh — Filesystem helper library
[[ -n "${_LIB_FS_LOADED:-}" ]] && return 0
_LIB_FS_LOADED=1
# Ensure a directory exists; create it if not
fs::require_dir() {
local dir="$1"
if [[ ! -d "$dir" ]]; then
mkdir -p "$dir" || { echo "[fs] Cannot create directory: $dir" >&2; return 1; }
fi
}
# Remove a file only if it exists (no error on missing)
fs::safe_rm() {
local target="$1"
[[ -e "$target" ]] && rm -rf -- "$target"
return 0
}
# Assert that a file exists and is readable
fs::require_file() {
local file="$1"
[[ -f "$file" && -r "$file" ]] || {
echo "[fs] Required file missing or unreadable: $file" >&2
return 1
}
}Versionare la libreria con una costante
Quando le librerie vengono condivise tra più progetti o distribuite a un team, diventa importante sapere quale versione di una libreria viene caricata a runtime. Una semplice convenzione consiste nell'esportare una costante di versione da ogni libreria.
I chiamanti possono quindi verificare all'avvio una versione minima, rilevando tempestivamente le incompatibilità invece di dover analizzare in seguito errori inspiegabili. La variabile di guardia funge anche da stringa della versione, riunendo due responsabilità in un'unica variabile.
#!/usr/bin/env bash
# lib/str.sh — versioned example
# Guard doubles as the version identifier
[[ -n "${_LIB_STR_LOADED:-}" ]] && return 0
readonly _LIB_STR_LOADED='1.3.0'
# Caller can validate the version
str::version() { echo "$_LIB_STR_LOADED"; }
# ---- Utility functions ----
str::trim() {
local s="$1"
s="${s#"${s%%[![:space:]]*}"}"
s="${s%"${s##*[![:space:]]}"}"
echo "$s"
}
str::repeat() {
local str="$1" count="$2" result=''
for (( i=0; i<count; i++ )); do result+="$str"; done
echo "$result"
}Una demo autonoma: usare più librerie
Questa scena mostra uno script realistico che carica due librerie e utilizza funzioni di entrambe. Notate come lo script principale rimanga pulito: esprime l'intento, mentre tutti i dettagli di implementazione risiedono nelle librerie.
Lo script può essere eseguito come file autonomo perché definisce le librerie inline utilizzando here-document scritti in file temporanei. In un progetto reale, ogni libreria risiederebbe in un proprio file all'interno di lib/.
#!/usr/bin/env bash
# Standalone demo: inline libs written to /tmp, then sourced
set -euo pipefail
# --- Create a temporary lib/log.sh ---
TMPDIR_LIBS="$(mktemp -d)"
trap 'rm -rf "$TMPDIR_LIBS"' EXIT
cat > "${TMPDIR_LIBS}/log.sh" <<'LIBEOF'
[[ -n "${_LIB_LOG_LOADED:-}" ]] && return 0
_LIB_LOG_LOADED=1
log::info() { echo "[INFO] $*"; }
log::error() { echo "[ERROR] $*" >&2; return 1; }
LIBEOF
cat > "${TMPDIR_LIBS}/str.sh" <<'LIBEOF'
[[ -n "${_LIB_STR_LOADED:-}" ]] && return 0
_LIB_STR_LOADED=1
str::upper() { echo "${1^^}"; }
str::trim() { local s="$1"; s="${s#"${s%%[![:space:]]*}"}";
s="${s%"${s##*[![:space:]]}"}" ; echo "$s"; }
LIBEOF
# --- Source both libraries ---
source "${TMPDIR_LIBS}/log.sh"
source "${TMPDIR_LIBS}/str.sh"
# --- Main logic ---
log::info "Libraries loaded successfully."
raw_input=" hello from bash libraries "
trimmed="$(str::trim "$raw_input")"
log::info "Trimmed: '${trimmed}'"
log::info "Uppercased: '$(str::upper "$trimmed")'"Buone pratiche e problemi comuni
Prima di distribuire una libreria per l'uso da parte del team, verificate i seguenti punti:
- Include guard — ogni libreria deve averne uno; documentate il nome della variabile di guardia all'inizio
- Nessun effetto collaterale al livello superiore — non eseguite mai
cd,echoné modificate lo stato globale al di fuori del corpo di una funzione - Usate
localper tutte le variabili all'interno delle funzioni — senzalocal, ogni assegnazione si riversa nell'ambito del chiamante - Restituite un valore, non usate mai exit —
exitall'interno di un file caricato con source termina l'intera shell chiamante - Convalidate gli input — controllate gli argomenti obbligatori e restituite un codice di errore significativo quando mancano
- Documentate con commenti — descrivete il comportamento di ogni funzione, i relativi parametri e il valore restituito
- Evitate
set -enei file di libreria — i chiamanti potrebbero avere una propria strategia di gestione degli errori; lasciate decidere a loro
Verifica delle conoscenze: include guard
Considerate un progetto in cui main.sh carica sia lib/bootstrap.sh sia lib/log.sh, mentre lib/bootstrap.sh carica internamente anche lib/log.sh. Qual è lo scopo principale dell'include guard in lib/log.sh?
Riepilogo della lezione: usare correttamente le librerie Bash
Avete esaminato tutte le tecniche essenziali per creare e utilizzare librerie Bash riutilizzabili. Ecco cosa dovete ricordare:
- Caricate con
sourceo.— carica un file nella shell corrente, rendendone immediatamente disponibili le funzioni - Usate
$BASH_SOURCEper risolvere i percorsi delle librerie in relazione allo script chiamante, mantenendo portabili i progetti - Include guard (
[[ -n "${_GUARD:-}" ]] && return 0) impediscono definizioni duplicate quando più file caricano la stessa libreria - Prefissi per i namespace (
log::,str::,fs::) eliminano i conflitti tra i nomi delle funzioni delle diverse librerie - Una directory
lib/con file mirati e a responsabilità singola mantiene gestibili i progetti di grandi dimensioni - Un loader di bootstrap (
lib/bootstrap.sh) offre a ogni script un'unica chiamata a source per caricare l'intero ecosistema - Non usate mai
exitné effetti collaterali al livello superiore nei file di libreria: al loro interno devono comparire solo definizioni di funzioni e costanti
Applicate questi schemi con coerenza e i vostri script shell saranno modulari e manutenibili quanto il codice scritto in qualsiasi linguaggio di livello superiore.
Domande Frequenti
La lezione «Creazione e sourcing di librerie Bash riutilizzabili» è gratuita?
Sì — il testo completo di «Creazione e sourcing di librerie Bash riutilizzabili» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Linux Command Line & Bash Scripting Mastery, passa a CoddyKit PRO. Il corso Linux Command Line & Bash Scripting Mastery include 4 lezioni in totale.
Cosa imparerò in «Creazione e sourcing di librerie Bash riutilizzabili»?
Organizzi gli helper condivisi in file libreria .sh caricabili con guardie di inclusione e prefissi di funzione con namespace. Eserciti Linux Command Line & Bash Scripting Mastery con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare Linux Command Line & Bash Scripting Mastery?
Non è richiesta alcuna esperienza precedente. Linux Command Line & Bash Scripting Mastery su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 2 di 4.
Quanto tempo richiede la lezione «Creazione e sourcing di librerie Bash riutilizzabili»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione Linux Command Line & Bash Scripting Mastery?
Sì. Ogni lezione Linux Command Line & Bash Scripting Mastery include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Progettazione di funzioni con ambito locale e codici di ritorno
- Creazione e sourcing di librerie Bash riutilizzabili
- Analisi di flag e argomenti con getopts
- Passaggio di array e mappe associative tra funzioni