Sì, vediamo una serie di cose come:
while read line; do
echo $line | cut -c3
done
O peggio:
for line in `cat file`; do
foo=`echo $line | awk '{print $2}'`
echo whatever $foo
done
(non ridere, ne ho visti molti).
Generalmente dai principianti degli script di shell. Quelle sono ingenue traduzioni letterali di ciò che faresti in linguaggi imperativi come C o Python, ma non è così che fai le cose nelle shell, e quegli esempi sono molto inefficienti, completamente inaffidabili (potenzialmente portando a problemi di sicurezza), e se mai riuscirai per correggere la maggior parte dei bug, il codice diventa illeggibile.
concettualmente
In C o nella maggior parte delle altre lingue, i blocchi predefiniti sono solo un livello sopra le istruzioni del computer. Di 'al tuo processore cosa fare e poi cosa fare dopo. Prendi il tuo processore per mano e lo gestisci micro: apri quel file, leggi tanti byte, lo fai, lo fai con esso.
Le conchiglie sono un linguaggio di livello superiore. Si potrebbe dire che non è nemmeno una lingua. Sono davanti a tutti gli interpreti della riga di comando. Il lavoro viene svolto da quei comandi che esegui e la shell ha il solo scopo di orchestrarli.
Una delle grandi cose che Unix ha introdotto è stata la pipe e quei flussi stdin / stdout / stderr predefiniti che tutti i comandi gestiscono di default.
In 45 anni, non abbiamo trovato meglio di quell'API per sfruttare la potenza dei comandi e farli cooperare a un'attività. Questo è probabilmente il motivo principale per cui le persone usano ancora le shell oggi.
Hai uno strumento di taglio e uno strumento di traslitterazione e puoi semplicemente fare:
cut -c4-5 < in | tr a b > out
La shell sta semplicemente facendo le tubature (apri i file, installa i tubi, invoca i comandi) e quando è tutto pronto, scorre semplicemente senza che la shell faccia nulla. Gli strumenti svolgono il loro lavoro contemporaneamente, in modo efficiente al loro ritmo con sufficiente buffering in modo che non uno blocchi l'altro, è semplicemente bello eppure così semplice.
Il richiamo di uno strumento ha tuttavia un costo (e lo svilupperemo sul punto di prestazione). Tali strumenti possono essere scritti con migliaia di istruzioni in C. È necessario creare un processo, caricare, inizializzare lo strumento, quindi ripulirlo, distruggere il processo e attendere.
Invocare cut
è come aprire il cassetto della cucina, prendere il coltello, usarlo, lavarlo, asciugarlo, rimetterlo nel cassetto. Quando lo fai:
while read line; do
echo $line | cut -c3
done < file
È come per ogni riga del file, ottenere lo read
strumento dal cassetto della cucina (molto goffo perché non è stato progettato per quello ), leggere una riga, lavare lo strumento di lettura, rimetterlo nel cassetto. Quindi programmare una riunione per lo strumento echo
e cut
, estrarli dal cassetto, richiamarli, lavarli, asciugarli, rimetterli nel cassetto e così via.
Alcuni di questi strumenti ( read
e echo
) sono costruiti nella maggior parte delle shell, ma da allora non fa quasi differenza echo
e cut
devono ancora essere eseguiti in processi separati.
È come tagliare una cipolla ma lavare il coltello e rimetterlo nel cassetto della cucina tra una fetta e l'altra.
Qui il modo più ovvio è quello di estrarre lo cut
strumento dal cassetto, tagliare tutta la cipolla e rimetterla nel cassetto dopo aver completato l'intero lavoro.
IOW, nelle shell, in particolare per elaborare il testo, invochi il minor numero possibile di utility e le fai cooperare all'attività, non esegui migliaia di strumenti in sequenza in attesa che ciascuno si avvii, venga eseguito, ripulito prima di eseguire il successivo.
Ulteriori letture nella bella risposta di Bruce . Gli strumenti interni di elaborazione di testo di basso livello nelle shell (tranne forse per zsh
) sono limitati, ingombranti e generalmente non adatti all'elaborazione di testo generale.
Prestazione
Come detto in precedenza, l'esecuzione di un comando ha un costo. Un costo enorme se quel comando non è incorporato, ma anche se sono integrati, il costo è grande.
E le shell non sono state progettate per funzionare in questo modo, non hanno alcuna pretesa di essere linguaggi di programmazione performanti. Non lo sono, sono solo interpreti da riga di comando. Quindi, su questo fronte è stata fatta poca ottimizzazione.
Inoltre, le shell eseguono comandi in processi separati. Quei blocchi non condividono una memoria o uno stato comuni. Quando fai un fgets()
o fputs()
in C, questa è una funzione in stdio. stdio mantiene buffer interni per input e output per tutte le funzioni stdio, per evitare di effettuare chiamate di sistema costose troppo spesso.
I corrispondenti anche utilità incorporati della shell ( read
, echo
, printf
) non possono farlo. read
è pensato per leggere una riga. Se supera il carattere di nuova riga, significa che il prossimo comando che eseguirai mancherà. Quindi read
deve leggere l'input un byte alla volta (alcune implementazioni hanno un'ottimizzazione se l'input è un file normale in quanto leggono blocchi e cercano di nuovo, ma ciò funziona solo per file regolari e bash
ad esempio legge solo blocchi di 128 byte che è ancora molto meno di quanto faranno le utility di testo).
Lo stesso sul lato output, echo
non può semplicemente bufferizzare il suo output, deve emetterlo immediatamente perché il comando successivo che eseguirai non condividerà quel buffer.
Ovviamente, eseguire i comandi in sequenza significa che devi aspettarli, è una piccola danza dello scheduler che dà il controllo dalla shell, agli strumenti e viceversa. Ciò significa anche (al contrario di utilizzare istanze di strumenti di lunga durata in una pipeline) che non è possibile sfruttare più processori contemporaneamente quando disponibili.
Tra quel while read
ciclo e l'equivalente (presumibilmente) cut -c3 < file
, nel mio test rapido, c'è un rapporto tempo CPU di circa 40000 nei miei test (un secondo contro mezza giornata). Ma anche se usi solo i builtin della shell:
while read line; do
echo ${line:2:1}
done
(qui con bash
), è ancora circa 1: 600 (un secondo contro 10 minuti).
Affidabilità / leggibilità
È molto difficile ottenere quel codice giusto. Gli esempi che ho dato sono visti troppo spesso in natura, ma hanno molti bug.
read
è uno strumento utile che può fare molte cose diverse. Può leggere l'input dell'utente, dividerlo in parole per memorizzarlo in diverse variabili. read line
non senza leggere una riga di input, o forse legge una riga in un modo molto speciale. In realtà legge le parole dall'ingresso quelle separate da $IFS
e dove la barra rovesciata può essere usata per sfuggire ai separatori o al carattere di nuova riga.
Con il valore predefinito di $IFS
, su un input come:
foo\/bar \
baz
biz
read line
memorizzerà "foo/bar baz"
in $line
, non " foo\/bar \"
come ci si aspetterebbe.
Per leggere una riga, in realtà hai bisogno di:
IFS= read -r line
Non è molto intuitivo, ma è così, ricorda che le shell non dovevano essere usate in quel modo.
Lo stesso per echo
. echo
espande le sequenze. Non puoi usarlo per contenuti arbitrari come il contenuto di un file casuale. È necessario printf
qui invece.
E, naturalmente, c'è la tipica dimenticanza di citare la tua variabile in cui tutti cadono. Quindi è di più:
while IFS= read -r line; do
printf '%s\n' "$line" | cut -c3
done < file
Ora, qualche avvertimento in più:
- tranne
zsh
che, ciò non funziona se l'input contiene caratteri NUL mentre almeno le utility di testo GNU non avrebbero il problema.
- se ci sono dati dopo l'ultima riga, verranno saltati
- all'interno del ciclo, lo stdin viene reindirizzato, quindi è necessario prestare attenzione che i comandi in esso contenuti non leggano dallo stdin.
- per i comandi all'interno dei loop, non stiamo prestando attenzione al successo o meno. Di solito, le condizioni di errore (disco pieno, errori di lettura ...) verranno gestite in modo inadeguato, di solito più male che con l' equivalente corretto .
Se vogliamo affrontare alcuni di questi problemi sopra, ciò diventa:
while IFS= read -r line <&3; do
{
printf '%s\n' "$line" | cut -c3 || exit
} 3<&-
done 3< file
if [ -n "$line" ]; then
printf '%s' "$line" | cut -c3 || exit
fi
Sta diventando sempre meno leggibile.
Esistono numerosi altri problemi con il passaggio dei dati ai comandi tramite gli argomenti o il recupero del loro output in variabili:
- la limitazione della dimensione degli argomenti (alcune implementazioni di utilità di testo hanno un limite anche lì, sebbene l'effetto di quelli raggiunti sia generalmente meno problematico)
- il carattere NUL (anche un problema con le utility di testo).
- argomenti presi come opzioni quando iniziano con
-
(o +
talvolta)
- varie stranezze di vari comandi tipicamente utilizzati in quei loop come
expr
, test
...
- gli operatori (limitati) di manipolazione del testo di varie shell che gestiscono caratteri multi-byte in modi incoerenti.
- ...
Considerazioni sulla sicurezza
Quando inizi a lavorare con variabili shell e argomenti ai comandi , stai entrando in un campo minato.
Se dimentichi di citare le tue variabili , dimentica la fine del marker di opzione , lavora in locali con caratteri multi-byte (la norma in questi giorni), sei sicuro di introdurre bug che prima o poi diventeranno vulnerabilità.
Quando si desidera utilizzare i loop.
TBD
yes
scrive in un file così rapidamente?