Comment puis-je lire ligne par ligne à partir d'une variable dans bash?

49

J'ai une variable qui contient la sortie multiligne d'une commande. Quel est le moyen le plus efficace de lire la sortie ligne par ligne à partir de la variable?

Par exemple:

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

Réponses:

65

Vous pouvez utiliser une boucle while avec une substitution de processus:

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

Un moyen optimal de lire une variable multiligne consiste à définir une IFSvariable vierge et printfla variable avec un retour à 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")

Remarque: conformément à la vérification du shell sc2031 , l'utilisation d'un processus est préférable à un tuyau pour éviter [subtilement] la création d'un sous-shell.

Sachez également qu'en nommant la variable, jobsvous risquez de créer une confusion, puisqu'il s'agit également du nom d'une commande shell commune.

dogbane
la source
3
Si vous souhaitez conserver tous vos espaces, utilisez while IFS= read.... Si vous souhaitez éviter \ interprétation, utilisezread -r
Peter.O
J'ai corrigé les points mentionnés par fred.bear, ainsi que changés echoen printf %s, afin que votre script fonctionne même avec une entrée non apprivoisée.
Gilles 'SO- arrête d'être méchant'
Pour lire à partir d'une variable multiligne, une chaîne de caractères est préférable à une tuyauterie à partir de printf (voir la réponse de l0b0).
Ata
1
@ata Bien que j'aie souvent entendu parler de ce "préférable", il faut noter qu'un herestring nécessite toujours que le /tmprépertoire soit en écriture, car il dépend de la possibilité de créer un fichier de travail temporaire. Si vous vous trouvez sur un système restreint /tmpen lecture seule (et que vous ne pouvez pas le modifier), vous serez heureux de la possibilité d’utiliser une autre solution, par exemple avec le printftuyau.
erreur de syntaxe
1
Dans le deuxième exemple, si la variable multiligne ne contient pas de nouvelle ligne, vous perdrez le dernier élément. Changez-le en:printf "%s\n" "$var" | while IFS= read -r line
David H. Bennett le
26

Pour traiter la sortie d'une commande ligne par ligne ( explication ):

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

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

printf %s "$foo" | 

printf %s "$foo"est presque identique à echo "$foo", mais affiche $foolittéralement, alors que echo "$foo"peut être interprété $foocomme une option de la commande echo si elle commence par un -, et peut développer des séquences de barres obliques inverses $foodans certains shells.

Notez que dans certains shells (ash, bash, pdksh, mais pas ksh ou zsh), le côté droit d'un pipeline s'exécute dans un processus séparé, de sorte que toute variable définie 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 nécessitant la valeur de $nla 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 suffisant et que l'entrée n'est pas énorme, vous pouvez utiliser le fractionnement des mots:

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

Explication: le réglage IFSsur une nouvelle ligne entraîne la division des mots uniquement sur les lignes nouvelles (par opposition à tout caractère blanc sous le paramètre par défaut). set -fdésactive globbing (extension de caractères génériques), ce qui arriverait autrement au résultat d'une substitution de commande $(jobs)ou d'une variable substitutino $foo. La forboucle agit sur toutes les parties de $(jobs), qui sont toutes les lignes non vides de la sortie de la commande. Enfin, restaurez la navigation et les IFSparamètres.

Gilles, arrête de faire le mal
la source
J'ai eu du mal à configurer IFS et à désactiver IFS. Je pense que la bonne chose à faire est de stocker l'ancienne valeur d'IFS et de remettre l'IFS à cette valeur. Je ne suis pas un expert en matière de bash, mais d'après mon expérience, cela vous ramène au comportement d'origine.
Bjorn Roche
1
@BjornRoche: dans une fonction, utilisez local IFS=something. Cela n'affectera pas la valeur globale. IIRC, unset IFSne vous ramène pas à la valeur par défaut (et ne fonctionne certainement pas si ce n'était pas la valeur par défaut au préalable).
Peter Cordes
14

Problème: si vous utilisez la boucle while, elle s'exécutera dans un sous-shell et toutes les variables seront perdues. Solution: utiliser pour la boucle

# 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=
Dima
la source
MERCI BEAUCOUP!! Toutes les solutions ci-dessus ont échoué pour moi.
hax0r_n_code
piping dans une while readboucle dans bash signifie que la boucle while est dans un sous-shell, donc les variables ne sont pas globales. while read;do ;done <<< "$var"rend le corps de la boucle pas un sous-shell. (Récent bash a la possibilité de mettre le corps d'une cmd | whileboucle pas dans un sous-shell, comme ksh l'a toujours fait.)
Peter Cordes
Voir aussi ce post connexe .
Wildcard
10

Dans les dernières versions de bash, utilisez mapfileou readarraypour lire efficacement le résultat de la commande dans des tableaux.

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

Disclaimer: horrible exemple, mais vous pouvez toujours trouver une meilleure commande à utiliser que vous-même

voir
la source
C'est un bon moyen, mais lit / var / tmp avec des fichiers temporaires sur mon système. +1 quand même
Eugene Yarmash
@ eugene: c'est marrant. Quel système (distribution / système d'exploitation) est-ce sur?
Voir
C'est FreeBSD 8. Comment reproduire: insérez readarrayune fonction et appelez-la plusieurs fois.
Eugene Yarmash
Nice, @ elle +1
Teresa e Junior
7
jobs="$(jobs)"
while IFS= read
do
    echo $REPLY
done <<< "$jobs"

Références:

l0b0
la source
3
-rC'est une bonne idée aussi. Il 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)
Peter.O,
-1

Vous pouvez utiliser <<< pour lire simplement la variable contenant les données séparées par une nouvelle ligne:

while read -r line
do 
  echo "A line of input: $line"
done <<<"$lines"
Matthew Herrmann
la source
1
Bienvenue sur Unix & Linux! Cela fait essentiellement double emploi avec une réponse d’il ya quatre ans. S'il vous plaît, ne postez pas de réponse sauf si vous avez quelque chose de nouveau à contribuer.
G-Man dit 'Réintégrez Monica'