Come posso leggere riga per riga da una variabile in bash?


49

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:


65

È 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.


3
Se vuoi mantenere tutto il tuo spazio bianco, usa while IFS= read.... Se vuoi prevenire \ interpretazione, allora usaread -r
Peter.O

Ho risolto i punti fred.bear menzionati, così come modificato echoa printf %s, in modo che lo script avrebbe funzionato anche con ingresso non addomesticato.
Gilles 'SO- smetti di essere malvagio' il

Per leggere da una variabile multilinea, è preferibile un herestring al piping da printf (vedere la risposta di l0b0).
ata

1
@ata Anche se ho sentito questo "preferibile" abbastanza spesso, va notato che un herestring richiede sempre che la /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.
syntaxerror,

1
Nel secondo esempio, se la variabile multilinea non contiene una nuova riga finale, perderai l'ultimo elemento. Modificalo in:printf "%s\n" "$var" | while IFS= read -r line
David H. Bennett il

26

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.


Ho avuto problemi con l'impostazione dell'IFS e il disinserimento dell'IFS. Penso che la cosa giusta da fare sia memorizzare il vecchio valore di IFS e riportare IFS su quel vecchio valore. Non sono un esperto bash, ma nella mia esperienza, questo ti riporta al comportamento originale.
Bjorn Roche,

1
@BjornRoche: all'interno di una funzione, utilizzare 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).
Peter Cordes,

14

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=

GRAZIE MILLE!! Tutte le soluzioni di cui sopra sono fallite per me.
hax0r_n_code l'

il piping in un 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.)
Peter Cordes,


10

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


È un bel modo, ma cucciolate / var / tmp con file temporanei sul mio sistema. +1 comunque
Eugene Yarmash,

@eugene: è divertente. Su quale sistema (distro / OS) è attivo?
vedi il

È FreeBSD 8. Come riprodurre: inserisci readarrayuna funzione e chiama la funzione più volte.
Eugene Yarmash,

Bello, @sehe. +1
Teresa e Junior,

7
jobs="$(jobs)"
while IFS= read
do
    echo $REPLY
done <<< "$jobs"

Riferimenti:


3
-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)
Peter.O

-1

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"

1
Benvenuti in Unix e 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 da contribuire.
G-Man dice "Reinstate Monica"
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.