Padroneggiare i Comandi Esterni: Ottimizza le Prestazioni degli Script Bash
Scopri guadagni di prestazioni nascosti nei tuoi script Bash padroneggiando l'uso dei comandi esterni. Questa guida spiega il notevole overhead causato dalla creazione ripetuta di processi come `grep` o `sed`. Impara tecniche pratiche e attuabili per sostituire le chiamate esterne con built-in efficienti di Bash, operazioni batch utilizzando potenti utility e ottimizzare i loop di lettura dei file per ridurre drasticamente i tempi di esecuzione in attività di automazione ad alto throughput.
Padroneggiare i Comandi Esterni: Ottimizza le Prestazioni degli Script Bash
Lo script Bash più veloce è spesso quello che avvia meno programmi.
Bash è bravo nel lavoro di collante: leggere un file, decidere cosa fare, avviare un altro strumento, controllare lo stato di uscita e proseguire. Non è un linguaggio di elaborazione dati ad alte prestazioni. La trappola è usare Bash come se ogni minuscola operazione sulle stringhe necessitasse di sed, ogni confronto di expr e ogni ciclo sui file di un nuovo grep. Questo stile funziona su dieci righe. Diventa doloroso su 200.000 righe.
Il costo è l'avvio del processo. Quando uno script esegue grep, sed, awk, cut, tr, date o basename, la shell deve creare un altro processo e attendere. Una chiamata non è un problema. Una chiamata all'interno di un grande ciclo è un modello che vale la pena correggere.
Inizia cercando i comandi all'interno dei cicli:
grep -nE 'for |while ' script.sh
grep -nE 'grep|sed|awk|cut|tr|expr|basename|dirname|cat' script.sh
Ciò non significa che ogni corrispondenza sia negativa. Un singolo awk su un intero file di solito va bene. Un sed lanciato una volta per riga è il tipo di cosa che trasforma uno script di manutenzione in un misterioso guasto durante un deploy.
Sostituisci le Piccole Chiamate Esterne con Bash Stesso
Le vittorie più facili sono l'aritmetica, la lunghezza delle stringhe, i prefissi, i suffissi e le semplici sostituzioni. Bash sa già come fare queste cose.
Aritmetica esterna:
# Usa l'utility esterna 'expr'
RISULTATO=$(expr $A + $B)
Aritmetica built-in:
RISULTATO=$((A + B))
Sostituzione di stringhe esterna:
MIA_STRINGA="ciao mondo"
NUOVA_STRINGA=$(echo "$MIA_STRINGA" | sed 's/mondo/universo/')
Espansione dei parametri:
MIA_STRINGA="ciao mondo"
NUOVA_STRINGA=${MIA_STRINGA/mondo/universo}
printf '%s\n' "$NUOVA_STRINGA"
| Compito | Metodo Inefficiente (Esterno) | Metodo Efficiente (Built-in) |
|---|---|---|
| Estrazione di sottostringa | `echo "$STR" | cut -c 1-5` |
| Controllo lunghezza | expr length "$STR" |
${#STR} |
| Rimozione suffisso | basename "$file" .log |
${file%.log} |
| Rimozione percorso | basename "$path" |
${path##*/} |
| Rimozione nome file | dirname "$path" |
${path%/*} |
| Sostituisci prima corrispondenza | sed 's/foo/bar/' |
${value/foo/bar} |
| Sostituisci tutte le corrispondenze | sed 's/foo/bar/g' |
${value//foo/bar} |
Preferisci [[ ... ]] per i condizionali Bash. È una parola chiave della shell, gestisce il pattern matching in modo pulito ed evita alcune sorprese di quoting che si presentano con [ ... ].
if [[ $nome == *.log && -s $nome ]]; then
printf 'log non vuoto: %s\n' "$nome"
fi
Non forzare troppo questo approccio. La sostituzione di pattern di Bash non è un motore regex completo. Se la regola è veramente complessa, un passaggio con awk o perl è più pulito e di solito più veloce di un'espansione di shell ingegnosa.
Lavora in Batch Invece di Ripetere il Lavoro
Se uno strumento può elaborare molti input in una sola esecuzione, fornisci molti input. Questo è particolarmente importante per grep, awk, sed, find, strumenti di compressione, client di upload e tutto ciò che si connette a un servizio di rete.
Questo ciclo avvia un grep per file:
for file in *.log; do
grep "ERROR" "$file" > "${file}.errors"
done
Se hai bisogno solo di un risultato combinato, usa un solo grep:
grep "ERROR" *.log > all_errors.txt
Se hai bisogno di output per file, pensa se la suddivisione è davvero necessaria. A volte lo strumento a valle può leggere un prefisso del nome file da grep -H:
grep -H "ERROR" *.log > errors-with-filenames.txt
Per le trasformazioni orientate alle righe, comprime semplici catene grep | awk in un unico programma awk:
awk '/data/ {print $1}' input.txt | sort > output.txt
Questo esegue ancora sort, e va bene. Ordinare è esattamente il tipo di lavoro che uno strumento esterno dovrebbe fare. Il cambiamento utile è rimuovere il cat inutile e il grep separato.
Leggi i File Senza cat
Il ciclo standard di lettura delle righe è noioso per un motivo:
while IFS= read -r riga; do
printf 'Elaborazione: %s\n' "$riga"
done < file.txt
IFS= preserva gli spazi iniziali e finali. -r impedisce a read di trattare i backslash come escape. La redirezione mantiene il ciclo nella shell corrente, il che è importante se il ciclo aggiorna variabili che ti servono in seguito.
Questa versione sembra innocua ma di solito è peggiore:
cat file.txt | while read -r riga; do
conteggio=$((conteggio + 1))
done
printf '%s\n' "$conteggio"
In Bash, un segmento di pipeline viene comunemente eseguito in una subshell, quindi conteggio potrebbe non essere aggiornato nella shell padre. Inoltre avvia cat senza alcun beneficio.
Usa la sostituzione di processo quando l'input è effettivamente prodotto da un comando:
while IFS= read -r file; do
printf 'file grande: %s\n' "$file"
done < <(find /var/log -type f -size +100M)
Qui find sta facendo un lavoro reale. Mantenere il ciclo nella shell corrente è ancora utile.
Usa find -exec ... + e xargs con Cautela
I cicli sui file sono una fonte comune di lentezza accidentale:
for file in $(find . -name '*.tmp'); do
rm "$file"
done
Questo si rompe con gli spazi e avvia rm ripetutamente. Usa l'esecuzione in batch:
find . -name '*.tmp' -exec rm -f {} +
La forma + passa molti percorsi a ogni invocazione di rm. La forma più vecchia \; esegue il comando una volta per percorso.
Per i comandi che beneficiano della concorrenza, xargs -P può ridurre il tempo a muro:
xargs -n 1 -P 4 curl -fsS -O < urls.txt
Usa -0 quando sono coinvolti nomi di file:
find uploads -type f -print0 | xargs -0 -n 50 -P 4 ./process-file
Il parallelismo non è gratuito. Quattro lavori curl potrebbero essere più veloci di uno. Quaranta potrebbero farti limitare da un'API o saturare un host piccolo.
Misura Prima di Riscrivere Tutto
L'ottimizzazione giusta dipende da dove va il tempo. Usa prima una semplice misurazione del tempo:
time ./script.sh
Per script con molti processi, strace -c su Linux può mostrare se lo script sta spendendo tempo a creare processi, aprire file o attendere I/O:
strace -f -c ./script.sh
Il tracing della shell può rivelare comandi ripetuti:
PS4='+ $SECONDS ${BASH_SOURCE}:${LINENO}: '
bash -x ./script.sh
Se lo script passa il 95 percento del tempo ad attendere un'esportazione del database, sostituire ${value/foo/bar} non servirà a nulla. Se esegue sed 300.000 volte, servirà.
Sapere Quando gli Strumenti Esterni Sono Migliori
| Obiettivo | Strumento Migliore (Generalmente) | Note |
|---|---|---|
| Estrazione e filtraggio di campi | awk |
Meglio dei cicli Bash per testo tabellare. |
| Editing di flussi | sed |
Buono per un passaggio su un file. |
| Attraversamento di file | find |
Più sicuro di analizzare ls. |
| JSON | jq |
Non analizzare JSON con cut. |
| Lavori paralleli | xargs -P o GNU parallel |
Aggiungi limiti e gestisci gli errori. |
| Elaborazione di grandi testi | awk, perl, Python |
Spesso più chiari di un'eroica espansione di shell. |
I built-in di Bash sono veloci, ma la manutenibilità vince comunque. Preferirei mantenere uno script awk chiaro piuttosto che 40 righe di fragile espansione di parametri che solo l'autore originale capisce.
Una Checklist Pratica di Revisione
Quando uno script Bash sembra lento, percorrilo in questo ordine:
- Trova i comandi esterni all'interno dei cicli.
- Sostituisci le semplici operazioni aritmetiche e sulle stringhe con l'espansione Bash.
- Rimuovi le chiamate
catinutili. - Raggruppa gli argomenti dei file con
grep,awk,sed,find -exec ... +oxargs. - Mantieni i cicli di lettura delle righe nella shell corrente quando le variabili devono sopravvivere al ciclo.
- Misura di nuovo.
Non devi trasformare ogni script in un esercizio di benchmark. Le grandi vittorie di solito arrivano da pochi punti ovvi: un comando per riga, un comando per file o un comando per elemento API. Correggi quelli, mantieni lo script leggibile e fermati quando il tempo di esecuzione non è più un problema.