Comment puis-je lire ligne par ligne à partir dune variable dans bash?

Jai une variable qui contient la sortie multiligne dune commande. Quelle est la manière la plus efficace de lire la sortie ligne par ligne de la variable?

Par exemple:

jobs="$(jobs)" if [ "$jobs" ]; then # read lines from $jobs fi 

Réponse

Vous pouvez utiliser une boucle while avec substitution de processus:

while read -r line do echo "$line" done < <(jobs) 

Un moyen optimal de lire une variable multiligne consiste à définir une variable IFS vide et printf la variable avec une nouvelle ligne à la fin:

# 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") 

Note: Comme pour shellcheck sc2031 , lutilisation de la sous-station process est préférable à un tube à éviter [subtilement] créer un sous-shell.

De plus, sachez quen nommant la variable jobs cela peut prêter à confusion car cest aussi le nom dune commande shell courante.

Commentaires

  • Si vous souhaitez conserver tous vos espaces, utilisez while IFS= read …. Si vous voulez empêcher \ interprétation, utilisez read -r
  • Jai ‘ jai corrigé les points fred.bear mentionnés, ainsi que changé echo en printf %s, afin que votre script fonctionne même avec une entrée non apprivoisée.
  • Pour lire à partir dune variable multiligne, un herestring est préférable au piping de printf (voir l0b0 ‘ s answer).
  • @ata Bien que ‘ ai entendu cela  » preferable  » assez souvent, il faut noter quun herestring nécessite toujours que le répertoire /tmp soit accessible en écriture, car il repose sur capable de créer un fichier de travail temporaire. Si jamais vous vous trouvez sur un système restreint avec /tmp en lecture seule (et non modifiable par vous), vous serez heureux de la possibilité dutiliser une solution alternative, e. g. avec le tube printf.
  • Dans le second exemple, si la variable multiligne ne contient ‘ fin de ligne, vous perdrez le dernier élément. Remplacez-le par: printf "%s\n" "$var" | while IFS= read -r line

Réponse

Pour traiter la sortie dune commande ligne par ligne ( explication ):

jobs | while IFS= read -r line; do process "$line" done 

Si vous avez le données dans une variable déjà:

printf %s "$foo" | … 

printf %s "$foo" est presque identique à echo "$foo", mais affiche $foo littéralement, alors que echo "$foo" peut interpréter $foo comme une option à la commande echo si elle commence par un -, et peut développer les séquences de barres obliques inverses dans $foo dans certains shells.

Notez que dans certains shells (ash, bash, pdksh, mais pas ksh ou zsh), le côté droit dun pipeline sexécute dans un processus séparé, donc toute variable que vous définissez dans la boucle est perdue. Par exemple, le script de comptage de lignes suivant imprime 0 dans ces shells:

n=0 printf %s "$foo" | while IFS= read -r line; do n=$(($n + 1)) done echo $n 

Une solution de contournement consiste à placer le reste du script (ou au moins la partie qui a besoin de la valeur de $n de la boucle) dans une liste de commandes:

n=0 printf %s "$foo" | { while IFS= read -r line; do n=$(($n + 1)) done echo $n } 

Si agir sur les lignes non vides est assez bon et lentrée nest pas énorme, vous pouvez utiliser le fractionnement de mots:

IFS=" " set -f for line in $(jobs); do # process line done set +f unset IFS 

Explication: réglage IFS à une seule nouvelle ligne fait que le fractionnement de mot se produit uniquement aux sauts de ligne (par opposition à tout caractère despacement sous le paramètre par défaut). set -f désactive le globbing (cest-à-dire lexpansion des caractères génériques), ce qui serait autrement le résultat dune substitution de commande $(jobs) ou dune substitution de variable $foo. La boucle for agit sur tous les morceaux de $(jobs), qui sont toutes les lignes non vides dans la sortie de la commande. Enfin, restaurez les paramètres de globbing et de IFS sur des valeurs équivalentes aux valeurs par défaut.

Commentaires

  • Jai eu des problèmes avec la configuration dIFS et la désactivation dIFS. Je pense que la bonne chose à faire est de stocker lancienne valeur dIFS et de remettre IFS à cette ancienne valeur. Je ‘ ne suis pas un expert en bash, mais daprès mon expérience, cela vous ramène au comportement dorigine.
  • @BjornRoche: dans une fonction, utilisez local IFS=something. Cela naffectera pas ‘ la valeur de portée globale. IIRC, unset IFS ne ‘ vous ramène à la valeur par défaut (et certainement pas ‘ t fonctionne si ce nétait pas ‘ t la valeur par défaut au préalable).
  • Je me demande si vous utilisez set de la manière indiqué dans le dernier exemple est correct.Lextrait de code suppose que set +f était actif au début et restaure donc ce paramètre à la fin. Cependant, cette hypothèse pourrait être erronée. Et si set -f était actif au début?
  • @Binarus Je ne restaure que les paramètres équivalents aux valeurs par défaut. En effet, si vous souhaitez restaurer les paramètres dorigine, vous devez faire plus de travail. Pour set -f, enregistrez loriginal $-. Pour IFS, il ‘ est assez compliqué si vous navez ‘ pas local et vous souhaitez prendre en charge la casse non définie; si vous souhaitez le restaurer, je vous recommande dappliquer linvariant que IFS reste défini.
  • Utiliser local en effet être la meilleure solution, car local - rend les options du shell locales, et local IFS rend IFS local. Malheureusement, local nest valide que dans les fonctions, ce qui rend la restructuration du code nécessaire. Votre suggestion dintroduire la politique que IFS est toujours définie semble également très raisonnable et résout la plus grande partie du problème. Merci!

Réponse

Problème: si vous utilisez la boucle while, elle fonctionnera dans le sous-shell et toutes les variables seront perdu. Solution: utilisez la boucle 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= 

Commentaires

  • MERCI BEAUCOUP !! Toutes les solutions ci-dessus ont échoué pour moi.
  • piping dans une boucle while read dans bash signifie que la boucle while est dans un sous-shell, donc les variables sont ‘ t global. while read;do ;done <<< "$var" fait du corps de la boucle pas un sous-shell. (Le bash récent a une option pour mettre le corps dune boucle cmd | while pas dans un sous-shell, comme ksh la toujours fait.)
  • Voir aussi ce message associé .
  • Dans des situations similaires, jai trouvé étonnamment difficile de traiter correctement IFS. Cette solution pose également un problème: que se passe-t-il si IFS nest pas du tout défini au début (cest-à-dire indéfini)? Il sera défini dans tous les cas après cet extrait de code; cela ne ‘ t semble être correct.

Réponse

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

Références:

Commentaires

  • -r est également une bonne idée; Cela empêche \` interpretation... (it is in your links, but its probably worth mentioning, just to round out your IFS = `(ce qui est essentiel pour éviter de perdre des espaces)
  • Seule cette solution a fonctionné pour moi. Merci brah.
  • Ne ‘ que cette solution souffre du même problème qui est mentionné dans les commentaires à @dogbane ‘ réponse de div? Que faire si la dernière ligne de la variable nest pas terminée par un caractère de nouvelle ligne?
  • Cette réponse fournit le moyen le plus propre dalimenter le contenu dune variable vers while read construct.

Réponse

Dans les versions récentes de bash, utilisez mapfile ou readarray pour lire efficacement la sortie de la commande dans les tableaux

$ readarray test < <(ls -ltrR) $ echo ${#test[@]} 6305 

Clause de non-responsabilité: exemple horrible, mais vous pouvez probablement venir avec une meilleure commande à utiliser que vous-même

Commentaires

  • Cest ‘ une belle façon, mais lit / var / tmp avec des fichiers temporaires sur mon système. +1 quand même
  • @eugene: ce ‘ est drôle. Quel système (distribution / OS) est-ce sur?
  • Il ‘ s FreeBSD 8. Comment reproduire: mettez readarray dans une fonction et appelez la fonction plusieurs fois.
  • Bien joué, @sehe. +1

Réponse

Les modèles courants pour résoudre ce problème ont été donnés dans les autres réponses.

Cependant, jaimerais ajouter mon approche, même si je ne suis pas sûr de son efficacité. Mais elle est (du moins pour moi) tout à fait compréhensible, ne modifie pas la variable originale (toutes les solutions qui utilisent read doit avoir la variable en question avec une nouvelle ligne à la fin et donc lajouter, ce qui modifie la variable), ne crée pas de sous-shell (ce que font toutes les solutions basées sur des tubes), ne lutilise pas ici -strings (qui ont leurs propres problèmes), et nutilise pas de substitution de processus (rien contre, mais parfois un peu difficile à comprendre).

En fait, je ne comprends pas pourquoi  » sont si rarement utilisés.Peut-être quils ne sont pas portables, mais comme lOP a utilisé la balise bash, cela ne marrêtera pas 🙂

#!/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" 

Résultat:

root@cerberus:~/scripts# ./test4 a1 b1 c1 a2 b2 c2a3 b3 c3root@cerberus:~/scripts# 

Quelques notes:

Le comportement dépend de la variante de ProcessText que vous utilisez. Dans lexemple ci-dessus, jai utilisé ProcessText1.

Notez que

  • ProcessText1 conserve les caractères de nouvelle ligne à la fin des lignes
  • ProcessText1 traite la dernière ligne de la variable (qui contient le texte c3) bien que cette ligne ne contienne pas de caractère de fin de ligne. En raison du saut de ligne de fin manquant, linvite de commande après lexécution du script est ajoutée à la dernière ligne de la variable sans être séparée de la sortie.
  • ProcessText1 considère toujours la partie entre le dernier saut de ligne dans la variable et la fin de la variable comme une ligne , même sil est vide; bien sûr, cette ligne, quelle soit vide ou non, na pas de caractère de fin de ligne. Autrement dit, même si le dernier caractère de la variable est une nouvelle ligne, ProcessText1 traitera la partie vide (chaîne nulle) entre cette dernière nouvelle ligne et la fin de la variable comme un (encore vide) et le passera au traitement en ligne. Vous pouvez facilement empêcher ce comportement en encapsulant le deuxième appel à ProcessLine dans une condition de vérification si vide appropriée; cependant, je pense quil est plus logique de le laisser tel quel.

ProcessText1 doit appeler ProcessLine à deux endroits, ce qui peut être inconfortable si vous souhaitez y placer un bloc de code qui traite directement la ligne, au lieu dappeler une fonction qui traite la ligne; vous devrez répéter le code qui est sujet aux erreurs.

En revanche, ProcessText3 traite la ligne ou nappelle la fonction respective quà un seul endroit, en remplaçant lappel de fonction par un bloc de code une évidence. Cela se fait au prix de deux conditions while au lieu dune. Hormis les différences dimplémentation, ProcessText3 se comporte exactement de la même manière que ProcessText1, sauf quil ne considère pas la partie entre le dernier caractère de nouvelle ligne la variable et la fin de la variable comme ligne si cette partie est vide. Autrement dit, ProcessText3 nentrera pas dans le traitement de ligne après le dernier caractère de nouvelle ligne de la variable si ce caractère de nouvelle ligne est le dernier caractère de la variable.

ProcessText2 fonctionne comme ProcessText1, sauf que les lignes doivent avoir un caractère de fin de ligne. Autrement dit, la partie entre le dernier caractère de nouvelle ligne de la variable et la fin de la variable nest pas considérée comme une ligne et est silencieusement rejetée. Par conséquent, si la variable ne contient aucun caractère de nouvelle ligne, aucun traitement de ligne ne se produit.

Jaime cette approche plus que les autres solutions présentées ci-dessus, mais jai probablement manqué quelque chose (je nai pas beaucoup dexpérience en bash programmation, et ne pas être très intéressé par d’autres shells).

Réponse

Vous pouvez utiliser < < < pour simplement lire à partir de la variable contenant la nouvelle ligne -données séparées:

while read -r line do echo "A line of input: $line" done <<<"$lines" 

Commentaires

  • Bienvenue dans Unix & Linux! Cela reproduit essentiellement une réponse dil y a quatre ans. Veuillez ne pas publier de réponse à moins que vous nayez quelque chose de nouveau à apporter.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *