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 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: 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 jobs
può causare confusione poiché è anche il nome di un comando shell comune.
echo
a printf %s
, in modo che lo script avrebbe funzionato anche con ingresso non addomesticato.
/tmp
directory sia scrivibile, poiché si basa sulla possibilità di creare un file di lavoro temporaneo. Se dovessi mai trovarti su un sistema limitato con l' /tmp
essere di sola lettura (e non modificabile da te), sarai felice della possibilità di utilizzare una soluzione alternativa, ad esempio con la printf
pipe.
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 $foo
letteralmente, mentre echo "$foo"
potrebbe interpretare $foo
come un'opzione per il comando echo se inizia con a -
e potrebbe espandere le sequenze di barre rovesciate $foo
in 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 $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 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 IFS
su 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 -f
disattiva 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 for
ciclo agisce su tutti i pezzi di $(jobs)
, che sono tutte le righe non vuote nell'output del comando. Infine, ripristina il globbing e le IFS
impostazioni.
local IFS=something
. Non influirà sul valore dell'ambito globale. IIRC, unset IFS
non 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 read
loop 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 | while
loop non in una subshell, come ha sempre fatto ksh.)
Nelle recenti versioni di bash, utilizzare mapfile
o readarray
per 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
readarray
una 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