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 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 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 ‘ paslocal
et vous souhaitez prendre en charge la casse non définie; si vous souhaitez le restaurer, je vous recommande dappliquer linvariant queIFS
reste défini. - Utiliser
local
en effet être la meilleure solution, carlocal -
rend les options du shell locales, etlocal IFS
rendIFS
local. Malheureusement,local
nest valide que dans les fonctions, ce qui rend la restructuration du code nécessaire. Votre suggestion dintroduire la politique queIFS
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 bouclecmd | 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 siIFS
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 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. -
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.
while IFS= read
…. Si vous voulez empêcher \ interprétation, utilisezread -r
echo
enprintf %s
, afin que votre script fonctionne même avec une entrée non apprivoisée./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 tubeprintf
.printf "%s\n" "$var" | while IFS= read -r line