Ho una variabile che contiene l'output multilinea di un comando. Qual è il modo più efficace per leggere l'output riga per riga dalla variabile?
Per esempio:
jobs="$(jobs)"
if [ "$jobs" ]; then
# read lines from $jobs
fi
Ho una variabile che contiene l'output multilinea di un comando. Qual è il modo più efficace per leggere l'output riga per riga dalla variabile?
Per esempio:
jobs="$(jobs)"
if [ "$jobs" ]; then
# read lines from $jobs
fi
Risposte:
È possibile utilizzare un ciclo while con sostituzione del processo:
while read -r line
do
echo "$line"
done < <(jobs)
Un modo ottimale per leggere una variabile multilinea è impostare una IFSvariabile vuota e printfla 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: come per shellcheck sc2031 , l'uso della sottostazione di processo è preferibile a una pipe per evitare [sottilmente] la creazione di una subshell.
Inoltre, tieni presente che nominando la variabile jobspuò causare confusione poiché è anche il nome di un comando shell comune.
echoa printf %s, in modo che lo script avrebbe funzionato anche con ingresso non addomesticato.
/tmpdirectory sia scrivibile, poiché si basa sulla possibilità di creare un file di lavoro temporaneo. Se dovessi mai trovarti su un sistema limitato con l' /tmpessere di sola lettura (e non modificabile da te), sarai felice della possibilità di utilizzare una soluzione alternativa, ad esempio con la printfpipe.
printf "%s\n" "$var" | while IFS= read -r line
Per elaborare l'output di una riga di comando riga per riga ( spiegazione ):
jobs |
while IFS= read -r line; do
process "$line"
done
Se hai già i dati in una variabile:
printf %s "$foo" | …
printf %s "$foo"è quasi identico a echo "$foo", ma stampa $fooletteralmente, mentre echo "$foo"potrebbe interpretare $foocome un'opzione per il comando echo se inizia con a -e potrebbe espandere le sequenze di barre rovesciate $fooin alcune shell.
Si noti 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 loop 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 nel mettere il resto dello script (o almeno la parte che necessita del valore di $ndal 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 l'input non è enorme, puoi usare la suddivisione delle parole:
IFS='
'
set -f
for line in $(jobs); do
# process line
done
set +f
unset IFS
Spiegazione: l'impostazione IFSsu una sola nuova riga fa sì che la divisione delle parole avvenga solo nelle nuove righe (al contrario di qualsiasi carattere di spazio vuoto sotto l'impostazione predefinita). set -fdisattiva il globbing (ovvero l'espansione dei caratteri jolly), che altrimenti sarebbe il risultato di una sostituzione di comando $(jobs)o di una sostituzione sostitutiva delle variabili $foo. Il forciclo agisce su tutti i pezzi di $(jobs), che sono tutte le righe non vuote nell'output del comando. Infine, ripristina il globbing e le IFSimpostazioni.
local IFS=something. Non influirà sul valore dell'ambito globale. IIRC, unset IFSnon ti riporta al valore predefinito (e certamente non funziona se non fosse il valore predefinito in precedenza).
Problema: se si utilizza while loop, verrà eseguito in subshell e tutte le variabili andranno perse. Soluzione: utilizzare per loop
# 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=
while readloop in bash significa che il ciclo while è in una subshell, quindi le variabili non sono globali. while read;do ;done <<< "$var"rende il corpo del ciclo non una subshell. (Recenti bash ha un'opzione per mettere il corpo di un cmd | whileloop non in una subshell, come ha sempre fatto ksh.)
Nelle recenti versioni di bash, utilizzare mapfileo readarrayper leggere in modo efficiente l'output del comando negli array
$ readarray test < <(ls -ltrR)
$ echo ${#test[@]}
6305
Disclaimer: esempio orribile, ma puoi prontamente inventare un comando migliore da usare di te stesso
readarrayuna funzione e chiama la funzione più volte.
jobs="$(jobs)"
while IFS= read
do
echo $REPLY
done <<< "$jobs"
Riferimenti:
-rè anche una buona idea; Previene \` interpretation... (it is in your links, but its probably worth mentioning, just to round out your IFS = `(che è essenziale per prevenire la perdita di spazi bianchi)
Puoi usare <<< per leggere semplicemente dalla variabile contenente i dati separati da una nuova riga:
while read -r line
do
echo "A line of input: $line"
done <<<"$lines"
while IFS= read.... Se vuoi prevenire \ interpretazione, allora usaread -r