Utilisez read comme invite dans une boucle while pilotée par read?

9

J'ai un cas d'utilisation où j'ai besoin de lire plusieurs variables au début de chaque itération et de lire une entrée de l'utilisateur dans la boucle.

Voies de solution possibles que je ne sais pas explorer -

  1. Pour l'affectation, utilisez un autre descripteur de fichier au lieu de stdin
  2. Utiliser une forboucle au lieu de ... | while read ...... Je ne sais pas comment assigner plusieurs variables à l'intérieur d'une forboucle

    echo -e "1 2 3\n4 5 6" |\
    while read a b c; 
    do 
      echo "$a -> $b -> $c";
      echo "Enter a number:";
      read d ;
      echo "This number is $d" ; 
    done
    
Debanjan Basu
la source

Réponses:

9

Si je comprends bien, je pense que vous voulez essentiellement parcourir les listes de valeurs, puis readune autre dans la boucle.

Voici quelques options, 1 et 2 sont probablement les plus saines.

1. Émuler des tableaux avec des chaînes

Avoir des tableaux 2D serait bien, mais pas vraiment possible dans Bash. Si vos valeurs n'ont pas d'espaces, une solution de contournement pour les rapprocher consiste à coller chaque ensemble de trois nombres dans une chaîne et à diviser les chaînes à l'intérieur de la boucle:

for x in "1 2 3" "4 5 6"; do 
  read a b c <<< "$x"; 
  read -p "Enter a number: " d
  echo "$a - $b - $c - $d ";
done

Bien sûr, vous pouvez également utiliser un autre séparateur, par exemple for x in 1:2:3 ...et IFS=: read a b c <<< "$x".


2. Remplacez le tuyau par une autre redirection pour libérer stdin

Une autre possibilité est d'avoir la read a b clecture d'un autre fd et de diriger l'entrée vers celle-ci (cela devrait fonctionner dans un shell standard):

while read a b c <&3; do
    printf "Enter a number: "
    read d
    echo "$a - $b - $c - $d ";
done 3<<EOF
1 2 3
4 5 6
EOF

Et ici, vous pouvez également utiliser une substitution de processus si vous souhaitez obtenir les données d'une commande: while read a b c <&3; ...done 3< <(echo $'1 2 3\n4 5 6')(la substitution de processus est une fonctionnalité bash / ksh / zsh)


3. Prenez plutôt la contribution de l'utilisateur de stderr

Ou, à l'inverse, en utilisant un tuyau comme dans votre exemple, mais avec l'entrée utilisateur readde stderr(fd 2) au lieu de stdinla provenance du tuyau:

echo $'1 2 3\n4 5 6' |
while read a b c; do 
    read -u 2 -p "Enter a number: " d
    echo "$a - $b - $c - $d ";
done

La lecture stderrest un peu étrange, mais fonctionne en fait souvent dans une session interactive. (Vous pouvez également ouvrir explicitement /dev/tty, en supposant que vous voulez réellement contourner toutes les redirections, c'est ce que des choses comme lessutilise pour obtenir l'entrée de l'utilisateur même lorsque les données y sont acheminées.)

Bien que l'utilisation de ce stderrtype puisse ne pas fonctionner dans tous les cas, et si vous utilisez une commande externe à la place read, vous auriez au moins besoin d'ajouter un tas de redirections à la commande.

Voir aussi Pourquoi ma variable est-elle locale dans une boucle "en lecture", mais pas dans une autre boucle apparemment similaire? pour certains problèmes concernant ... | while.


4. Découpez des parties d'un tableau selon vos besoins

Je suppose que vous pouvez également approximer un tableau 2D-ish en copiant des tranches d'un tableau unidimensionnel normal:

data=(1 2 3 
      4 5 6)

n=3
for ((i=0; i < "${#data[@]}"; i += n)); do
    a=( "${data[@]:i:n}" )
    read -p "Enter a number: " d
    echo "${a[0]} - ${a[1]} - ${a[2]} - $d "
done

Vous pouvez également affecter ${a[0]}etc. à a, betc si vous voulez des noms pour les variables, mais Zsh le ferait beaucoup plus bien .

ilkkachu
la source
1
C'était génial! Et quel excellent aperçu des différentes solutions de contournement!
Debanjan Basu
@ StéphaneChazelas, ah oui, tu as raison. Utiliser stderrcomme ça est un peu capricieux, mais j'ai le souvenir qu'un utilitaire le fait. Mais tout ce que je peux trouver maintenant, ce sont ceux qui ne font qu'utiliser /dev/tty. Tant pis.
ilkkachu
Notez que l'utilisation <&2(ainsi que </dev/tty) évite de lire à partir du stdin du script. Cela ne fonctionnera pas printf '682\n739' | ./script. Notez également que read -pne fonctionne qu'en bash.
Isaac
@Isaac, dans celui avec la lecture de stderr, il y a aussi le canal de l'écho à cette whileboucle, donc vous ne pouvez pas vraiment utiliser le stdin du script de toute façon ... read -uest aussi bash, mais peut être remplacé par des redirections, et <<<dans le premier est également non standard, mais c'est un peu plus difficile à contourner.
ilkkachu
Tout pourrait être résolu, s'il vous plaît: lisez ma réponse
Isaac
3

Il n'y en a qu'un /dev/stdin, readlira partout où il est utilisé (par défaut).

La solution consiste à utiliser un autre descripteur de fichier au lieu de 1 ( /dev/stdin).

De l'équivalent de code (en bash) à ce que vous avez publié [1] (regardez ci-dessous),
ajoutez simplement 0</dev/tty(par exemple) pour lire à partir du "vrai" tty:

while read a b c
do    read -p "Enter a number: " d  0</dev/tty   # 0<&2 is also valid
      echo "$a -> $b -> $c and ++> $d"
done  <<<"$(echo -e '1 2 3\n4 5 6')"

À l'exécution:

$ ./script
Enter a number: 789
1 -> 2 -> 3 and ++> 789
Enter a number: 333
4 -> 5 -> 6 and ++> 333

Une autre alternative est d'utiliser 0<&2(qui peut sembler étrange, mais qui est valide).

Notez que la lecture de /dev/tty(également 0<&2) contournera le stdin du script, cela ne lira pas les valeurs de l'écho:

$ echo -e "33\n44" | ./script

Autres solutions

Ce qui est nécessaire est de rediriger une entrée vers un autre fd (descripteur de fichier).
Valide en ksh, bash et zsh:

while read -u 7 a b c
do    printf "Enter a number: "
      read d
      echo "$a -> $b -> $c and ++> $d"
done  7<<<"$(echo -e '1 2 3\n4 5 6')"

Ou, avec exec:

exec 7<<<"$(echo -e '1 2 3\n4 5 6')"

while read -u 7 a b c
do    printf "Enter a number: "      
      read d
      echo "$a -> $b -> $c and ++> $d"
done  

exec 7>&-

Une solution qui fonctionne en sh ( <<<ne fonctionne pas):

exec 7<<-\_EOT_
1 2 3
4 5 6
_EOT_

while read a b c  <&7
do    printf "Enter a number: "      
      read d
      echo "$a -> $b -> $c and ++> $d"
done  

exec 7>&-

Mais c'est probablement plus facile à comprendre:

while read a b c  0<&7
do    printf "Enter a number: "      
      read d
      echo "$a -> $b -> $c and ++> $d"
done  7<<-\_EOT_
1 2 3
4 5 6
_EOT_

1 Code plus simple

Votre code est:

echo -e "1 2 3\n4 5 6" |\
while read a b c; 
do 
  echo "$a -> $b -> $c";
  echo "Enter a number: ";
  read d ;
  echo "This number is $d" ; 
done

Un code simplifié (en bash) est:

while read a b c
do    #0</dev/tty
      read -p "Enter a number: " d ;
      echo "$a -> $b -> $c and ++> $d";
done  <<<"$(echo -e '1 2 3\n4 5 6')"

Qui, s'il est exécuté, imprime:

$ ./script
1 -> 2 -> 3 and ++> 4 5 6

Ce qui montre simplement que le var d est lu à partir du même /dev/stdin.

Isaac
la source
2

Avec zsh, vous pouvez l'écrire à la place:

for a b c (
  1 2 3
  4 5 6
  'more complex' $'\n\n' '*** values ***'
) {
  read 'd?Enter a number: '
  do-something-with $a $b $c $d
}

Pour les tableaux 2D, voir également le ksh93shell:

a=(
  (1 2 3)
  (4 5 6)
  ('more complex' $'\n\n' '*** values ***')
)
for i in "${!a[@]}"; do
  read 'd?Enter a number: '
  do-something-with "${a[i][0]}" "${a[i][1]}" "${a[i][2]}" "$d"
done
Stéphane Chazelas
la source
nice ... Je cherchais une réponse basée sur bash, mais c'est agréable à savoir!
Debanjan Basu