Ho una variabile che contiene loutput su più righe di un comando. Qual è il modo più efficiente per leggere loutput riga per riga dalla variabile?
Ad esempio:
jobs="$(jobs)" if [ "$jobs" ]; then # read lines from $jobs fi
Risposta
Puoi utilizzare un ciclo while con sostituzione del processo:
while read -r line do echo "$line" done < <(jobs)
Un modo ottimale per leggere una variabile su più righe significa impostare una IFS
variabile vuota e printf
la variabile con una nuova riga finale:
# Printf "%s\n" "$var" is necessary because printf "%s" "$var" on a # variable that doesn"t end with a newline then the while loop will # completely miss the last line of the variable. while IFS= read -r line do echo "$line" done < <(printf "%s\n" "$var")
Nota: secondo shellcheck sc2031 , è preferibile utilizzare la sostituzione di processo rispetto a una pipe per evitare [sottilmente] creazione di una subshell.
Inoltre, tieni presente che nominando la variabile jobs
si può creare confusione poiché questo è anche il nome di un comando di shell comune.
Commenti
Answer
Per elaborare il output di una riga di comando per riga ( spiegazione ):
jobs | while IFS= read -r line; do process "$line" done
Se hai il dati già in una variabile:
printf %s "$foo" | …
printf %s "$foo"
è quasi identico a echo "$foo"
, ma stampa $foo
letteralmente, mentre echo "$foo"
potrebbe interpretare $foo
come unopzione al comando echo se inizia con -
e potrebbe espandere le sequenze di barre rovesciate in $foo
in alcune shell.
Nota che in alcune shell (ash, bash, pdksh, ma non ksh o zsh), il lato destro di una pipeline viene eseguito in un processo separato, quindi qualsiasi variabile impostata nel ciclo viene persa. Ad esempio, il seguente script di conteggio delle righe stampa 0 in queste shell:
n=0 printf %s "$foo" | while IFS= read -r line; do n=$(($n + 1)) done echo $n
Una soluzione alternativa consiste nellinserire il resto dello script (o almeno la parte che richiede il valore di $n
dal ciclo) in un elenco di comandi:
n=0 printf %s "$foo" | { while IFS= read -r line; do n=$(($n + 1)) done echo $n }
Se agire sulle righe non vuote è abbastanza buono e linput non è enorme, puoi usare la suddivisione delle parole:
IFS=" " set -f for line in $(jobs); do # process line done set +f unset IFS
Spiegazione: impostazione IFS
a un singolo ritorno a capo fa sì che la suddivisione delle parole avvenga solo a fine riga (al contrario di qualsiasi carattere di spazio bianco sotto limpostazione predefinita). set -f
disattiva il globbing (ovvero lespansione dei caratteri jolly), che altrimenti si verificherebbe in seguito a una sostituzione del comando $(jobs)
o alla sostituzione di una variabile $foo
. Il ciclo for
agisce su tutti i pezzi di $(jobs)
, che sono tutte le righe non vuote nelloutput del comando. Infine, ripristina le impostazioni di globbing e IFS
su valori equivalenti a quelli predefiniti.
Commenti
- Ho avuto problemi con limpostazione di IFS e la disattivazione di IFS. Penso che la cosa giusta da fare sia memorizzare il vecchio valore di IFS e reimpostare IFS su quel vecchio valore. ‘ non sono un esperto di bash, ma nella mia esperienza, questo ti riporta al bahavior originale.
- @BjornRoche: allinterno di una funzione, usa
local IFS=something
. ‘ non influirà sul valore dellambito globale. IIRC,unset IFS
‘ non riporta al valore predefinito (e certamente ‘ t funziona se non era ‘ limpostazione predefinita in anticipo). - Mi chiedo se utilizzare
set
nel modo mostrato nellultimo esempio è corretto.Lo snippet di codice presume cheset +f
fosse attivo allinizio e quindi ripristina limpostazione alla fine. Tuttavia, questa ipotesi potrebbe essere sbagliata. E seset -f
fosse attivo allinizio? - @Binarus Ripristino solo le impostazioni equivalenti a quelle predefinite. In effetti, se vuoi ripristinare le impostazioni originali, devi fare più lavoro. Per
set -f
, salva loriginale$-
. PerIFS
, ‘ è fastidiosamente complicato se non ‘ non hailocal
e desideri supportare il caso non impostato; se vuoi ripristinarlo, ti consiglio di applicare linvariante cheIFS
rimane impostato. - Luso di
local
in effetti è la soluzione migliore, perchélocal -
rende le opzioni della shell locali elocal IFS
faIFS
locale. Sfortunatamente,local
è valido solo allinterno delle funzioni, il che rende necessaria la ristrutturazione del codice. Anche il tuo suggerimento di introdurre la politica secondo cuiIFS
è sempre impostato sembra molto ragionevole e risolve la maggior parte del problema. Grazie!
Risposta
Problema: se usi il ciclo while verrà eseguito nella subshell e tutte le variabili saranno perso. Soluzione: usa il ciclo for
# change delimiter (IFS) to new line. IFS_BAK=$IFS IFS=$"\n" for line in $variableWithSeveralLines; do echo "$line" # return IFS back if you need to split new line by spaces: IFS=$IFS_BAK IFS_BAK= lineConvertedToArraySplittedBySpaces=( $line ) echo "{lineConvertedToArraySplittedBySpaces[0]}" # return IFS back to newline for "for" loop IFS_BAK=$IFS IFS=$"\n" done # return delimiter to previous value IFS=$IFS_BAK IFS_BAK=
Commenti
- GRAZIE MOLTO !! Tutte le soluzioni precedenti non sono riuscite per me.
- piping in un
while read
loop in bash significa che il ciclo while è in una subshell, quindi le variabili non sono ‘ t globale.while read;do ;done <<< "$var"
rende il corpo del loop non una subshell. (La bash recente ha unopzione per mettere il corpo di uncmd | while
loop non in una subshell, come ha sempre fatto ksh.) - Vedi anche questo post correlato .
- In situazioni simili, ho trovato sorprendentemente difficile trattare
IFS
correttamente. Anche questa soluzione presenta un problema: cosa succede seIFS
non è impostato affatto allinizio (cioè non è definito)? Sarà definito in ogni caso dopo quello snippet di codice; questo ‘ non sembra corretto.
Risposta
jobs="$(jobs)" while IFS= read -r do echo "$REPLY" done <<< "$jobs"
Riferimenti:
Commenti
-
-r
è anche una buona idea; Impedisce\` interpretation... (it is in your links, but its probably worth mentioning, just to round out your
IFS = `(che è essenziale per evitare di perdere spazi) - Solo questa soluzione ha funzionato per me. Grazie brah.
- ‘ t questa soluzione soffre dello stesso problema menzionato nei commenti a @dogbane ‘ s risposta? E se lultima riga della variabile non è terminata da un carattere di nuova riga?
- Questa risposta fornisce il modo più pulito per fornire il contenuto di una variabile a
while read
costruire.
Risposta
Nelle recenti versioni di bash, usa mapfile
o readarray
per leggere in modo efficiente loutput del comando negli array
$ readarray test < <(ls -ltrR) $ echo ${#test[@]} 6305
Dichiarazione di non responsabilità: esempio orribile, ma puoi probabilmente venire con un comando migliore da usare rispetto a te stesso
Commenti
- È ‘ un bel modo, ma litters / var / tmp con file temporanei sul mio sistema. +1 comunque
- @eugene: che ‘ è divertente. Su quale sistema (distro / OS) è installato?
- È ‘ s FreeBSD 8. Come riprodurre: put
readarray
in una funzione e chiama la funzione alcune volte. - Bella, @sehe. +1
Risposta
I modelli comuni per risolvere questo problema sono stati forniti nelle altre risposte.
Tuttavia, vorrei aggiungere il mio approccio, anche se non sono sicuro di quanto sia efficiente. Ma è (almeno per me) abbastanza comprensibile, non altera la variabile originale (tutte le soluzioni che utilizzano read
deve avere la variabile in questione con una nuova riga finale e quindi aggiungerla, che altera la variabile), non crea subshell (cosa che fanno tutte le soluzioni basate su pipe), non usa qui -strings (che hanno i loro problemi) e non utilizza la sostituzione del processo (niente contro di essa, ma a volte un po difficile da capire).
In realtà, non capisco perché ” sono usate così raramente.Forse non sono portatili, ma poiché lOP ha utilizzato il tag bash
, questo non mi fermerà 🙂
#!/bin/bash function ProcessLine() { printf "%s" "$1" } function ProcessText1() { local Text=$1 local Pattern=$"^([^\n]*\n)(.*)$" while [[ "$Text" =~ $Pattern ]]; do ProcessLine "${BASH_REMATCH[1]}" Text="${BASH_REMATCH[2]}" done ProcessLine "$Text" } function ProcessText2() { local Text=$1 local Pattern=$"^([^\n]*\n)(.*)$" while [[ "$Text" =~ $Pattern ]]; do ProcessLine "${BASH_REMATCH[1]}" Text="${BASH_REMATCH[2]}" done } function ProcessText3() { local Text=$1 local Pattern=$"^([^\n]*\n?)(.*)$" while [[ ("$Text" != "") && ("$Text" =~ $Pattern) ]]; do ProcessLine "${BASH_REMATCH[1]}" Text="${BASH_REMATCH[2]}" done } MyVar1=$"a1\nb1\nc1\n" MyVar2=$"a2\n\nb2\nc2" MyVar3=$"a3\nb3\nc3" ProcessText1 "$MyVar1" ProcessText1 "$MyVar2" ProcessText1 "$MyVar3"
Risultato:
root@cerberus:~/scripts# ./test4 a1 b1 c1 a2 b2 c2a3 b3 c3root@cerberus:~/scripts#
Alcune note:
Il comportamento dipende da quale variante di ProcessText
che utilizzi. Nellesempio sopra, ho utilizzato ProcessText1
.
Nota che
-
ProcessText1
mantiene i caratteri di nuova riga alla fine delle righe -
ProcessText1
elabora lultima riga della variabile (che contiene il testoc3
) sebbene tale riga non contenga un carattere di nuova riga finale. A causa della nuova riga finale mancante, il prompt dei comandi dopo lesecuzione dello script viene aggiunto allultimo riga della variabile senza essere separata dalloutput. -
ProcessText1
considera sempre la parte tra lultima riga a capo della variabile e la fine della variabile come una riga , anche se è vuoto; ovviamente, quella riga, vuota o meno, non ha un carattere di nuova riga finale. Cioè, anche se lultimo carattere nella variabile è una nuova riga,ProcessText1
tratterà la parte vuota (stringa nulla) tra lultima riga e la fine della variabile come una (ancora vuoto) e lo passerà allelaborazione della riga. Puoi facilmente prevenire questo comportamento avvolgendo la seconda chiamata aProcessLine
in una condizione di controllo se vuoto appropriata; tuttavia, penso che sia più logico lasciarlo così comè.
ProcessText1
deve chiamare ProcessLine
in due punti, il che potrebbe essere scomodo se si desidera inserire un blocco di codice che elabora direttamente la riga, invece di chiamare una funzione che elabora la riga; dovresti ripetere il codice che è soggetto a errori.
Al contrario, ProcessText3
elabora la riga o chiama la rispettiva funzione solo in un punto, sostituendo la chiamata di funzione da un blocco di codice un gioco da ragazzi. Ciò ha il costo di due while
condizioni invece di una. A parte le differenze di implementazione, ProcessText3
si comporta esattamente come ProcessText1
, tranne per il fatto che non considera la parte tra lultimo carattere di nuova riga in la variabile e la fine della variabile come riga se quella parte è vuota. Cioè, ProcessText3
non entrerà in elaborazione riga dopo lultimo carattere di nuova riga della variabile se tale carattere di nuova riga è lultimo carattere della variabile.
ProcessText2
funziona come ProcessText1
, tranne per il fatto che le righe devono avere un carattere di nuova riga finale. Cioè, la parte tra lultimo carattere di nuova riga nella variabile e la fine della variabile non è considerata una riga e viene gettata via silenziosamente. Di conseguenza, se la variabile non contiene alcun carattere di nuova riga, non avviene alcuna elaborazione di riga.
Mi piace questo approccio più delle altre soluzioni mostrate sopra, ma probabilmente mi sono perso qualcosa (non essendo molto esperto in bash
programmazione e non essere molto interessato ad altre shell).
Rispondi
Puoi utilizzare < < < per leggere semplicemente dalla variabile contenente la nuova riga -dati separati:
while read -r line do echo "A line of input: $line" done <<<"$lines"
Commenti
- Benvenuto in Unix & Linux! Questo essenzialmente duplica una risposta di quattro anni fa. Per favore non pubblicare una risposta a meno che tu non abbia qualcosa di nuovo con cui contribuire.
while IFS= read
…. Se tu vuoi impedire linterpretazione, quindi usaread -r
echo
inprintf %s
, in modo che lo script funzioni anche con un input non addomesticato./tmp
sia scrivibile, poiché si basa sullessere in grado di creare un file di lavoro temporaneo. Se dovessi trovarti su un sistema limitato con/tmp
di sola lettura (e non modificabile da te), sarai felice della possibilità di utilizzare una soluzione alternativa, e. g. con laprintf
pipe.printf "%s\n" "$var" | while IFS= read -r line