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
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 IFSne ‘ 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
setde la manière indiqué dans le dernier exemple est correct.Lextrait de code suppose queset +fétait actif au début et restaure donc ce paramètre à la fin. Cependant, cette hypothèse pourrait être erronée. Et siset -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$-. PourIFS, il ‘ est assez compliqué si vous navez ‘ paslocalet vous souhaitez prendre en charge la casse non définie; si vous souhaitez le restaurer, je vous recommande dappliquer linvariant queIFSreste défini. - Utiliser
localen effet être la meilleure solution, carlocal -rend les options du shell locales, etlocal IFSrendIFSlocal. Malheureusement,localnest valide que dans les fonctions, ce qui rend la restructuration du code nécessaire. Votre suggestion dintroduire la politique queIFSest 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 readdans 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 bouclecmd | whilepas 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 siIFSnest 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
-
-rest également une bonne idée; Cela empêche\` interpretation... (it is in your links, but its probably worth mentioning, just to round out yourIFS = `(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 readconstruct.
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
readarraydans 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
-
ProcessText1conserve les caractères de nouvelle ligne à la fin des lignes -
ProcessText1traite la dernière ligne de la variable (qui contient le textec3) 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. -
ProcessText1considè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,ProcessText1traitera 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 àProcessLinedans 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.
while IFS= read…. Si vous voulez empêcher \ interprétation, utilisezread -rechoenprintf %s, afin que votre script fonctionne même avec une entrée non apprivoisée./tmpsoit 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/tmpen lecture seule (et non modifiable par vous), vous serez heureux de la possibilité dutiliser une solution alternative, e. g. avec le tubeprintf.printf "%s\n" "$var" | while IFS= read -r line