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

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

  • Se vuoi mantenere tutti gli spazi, usa while IFS= read …. Se tu vuoi impedire linterpretazione, quindi usa read -r
  • Ho ‘ ho corretto i punti menzionati da fred.bear, così come ho cambiato echo in printf %s, in modo che lo script funzioni anche con un input non addomesticato.
  • Per leggere da una variabile multilinea, è preferibile una stringa di eredità rispetto a una stringa da printf (vedere l0b0 ‘ s answer).
  • @ata Anche se ‘ lho sentito ” preferable ” abbastanza spesso, va notato che una stringa di eredità richiede sempre che la directory /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 la printf pipe.
  • Nel secondo esempio, se la variabile multilinea non ‘ non contiene un trascinando la nuova riga perderai lultimo elemento. Modificalo in: printf "%s\n" "$var" | while IFS= read -r line

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 che set +f fosse attivo allinizio e quindi ripristina limpostazione alla fine. Tuttavia, questa ipotesi potrebbe essere sbagliata. E se set -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 $-. Per IFS, ‘ è fastidiosamente complicato se non ‘ non hai local e desideri supportare il caso non impostato; se vuoi ripristinarlo, ti consiglio di applicare linvariante che IFS rimane impostato.
  • Luso di local in effetti è la soluzione migliore, perché local - rende le opzioni della shell locali e local IFS fa IFS 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 cui IFS è 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 un cmd | 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 se IFS 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 testo c3) 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 a ProcessLine 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.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *