Use read as a prompt inside a while loop driven by read?

8,971

Solution 1

If I got this right, I think you want to basically loop over lists of values, and then read another within the loop.

Here's a few options, 1 and 2 are probably the sanest.

1. Emulate arrays with strings

Having 2D arrays would be nice, but not really possible in Bash. If your values don't have whitespace, one workaround to approximate that is to stick each set of three numbers into a string, and split the strings inside the loop:

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

Of course you could use some other separator too, e.g. for x in 1:2:3 ... and IFS=: read a b c <<< "$x".


2. Replace the pipe with another redirection to free stdin

Another possibility is to have the read a b c read from another fd and direct the input to that (this should work in a standard shell):

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

And here you can also use a process substitution if you want to get the data from a command: while read a b c <&3; ...done 3< <(echo $'1 2 3\n4 5 6') (process substitution is a bash/ksh/zsh feature)


3. Take user input from stderr instead

Or, the other way around, using a pipe like in your example, but have the user input read from stderr (fd 2) instead of stdin where the pipe comes from:

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

Reading from stderr is a bit odd, but actually often works in an interactive session. (You could also explicitly open /dev/tty, assuming you want to actually bypass any redirections, that's what stuff like less uses to get the user's input even when the data is piped to it.)

Though using stderr like that might not work in all cases, and if you're using some external command instead of read, you'd at least need to add a bunch of redirections to the command.

Also, see Why is my variable local in one 'while read' loop, but not in another seemingly similar loop? for some issues regarding ... | while.


4. Slice parts of an array as needed

I suppose you could also approximate a 2D-ish array by copying slices of a regular one-dimensional one:

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

You could also assign ${a[0]} etc. to a, b etc if you want names for the variables, but Zsh would do that much more nicely.

Solution 2

There is only one /dev/stdin, read will read from it anywhere where it is used (by default).

The solution is to use some other file descriptor instead of 1 (/dev/stdin).

From code equivalent (in bash) to what you posted [1] (look below)
just add 0</dev/tty (for example) to read from the "real" 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')"

On execution:

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

Other alternative is to use 0<&2 (which might seem odd, but is valid).

Note that the read from /dev/tty (also 0<&2) will bypass the stdin of the script, this will not read the values from the echo:

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

Other solutions

What is needed is to redirect one input to some other fd (file descriptor).
Valid in ksh, bash and 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')"

Or, with 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>&-

A solution that works in sh (<<< doesn't work):

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>&-

But this is probably easier to understand:

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 Simpler code

Your code is:

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

A simplified code (in bash) is:

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

Which, if executed, prints:

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

Which is just showing that the var d is being read from the same /dev/stdin.

Solution 3

With zsh, you can write it instead:

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
}

For 2D arrays, see also the ksh93 shell:

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
Share:
8,971

Related videos on Youtube

Debanjan Basu
Author by

Debanjan Basu

Physics Student, almost into graduate school - loves "A Bit of Fry and Laurie"! EDIT: QUITE a trimester into grad school, and still LOVES "A Bit of Fry and Laurie"! EDIT: More than a year into grad school, and still LOVES "A Bit of Fry and Laurie"! Might see a doctor about it!

Updated on September 18, 2022

Comments

  • Debanjan Basu
    Debanjan Basu over 1 year

    I have a use case where I need to read in multiple variables at the start of each iteration and read in an input from the user into the loop.

    Possible paths to solution which I do not know how to explore --

    1. For assignment use another filehandle instead of stdin
    2. Use a for loop instead of ... | while read ... ... I do not know how to assign multiple variables inside a for loop

      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
    Debanjan Basu almost 6 years
    that was great! And what a great overview of the different workarounds!
  • Debanjan Basu
    Debanjan Basu almost 6 years
    nice... I was looking for a bash based answer, but this is nice to know!
  • ilkkachu
    ilkkachu almost 6 years
    @StéphaneChazelas, ah yes, you're right. Using stderr like that is a bit icky, but I had the recollection that some utility does it. But all I can find now is ones that just use /dev/tty. Oh well.
  • done
    done almost 6 years
    Note that using <&2 (as well as </dev/tty) avoid reading from the stdin of the script. This will not work printf '682\n739' | ./script. Also note that read -p only work in bash.
  • ilkkachu
    ilkkachu almost 6 years
    @Isaac, in the one with the read from stderr, there's also the pipe from the echo to that while loop, so you can't really use the script's stdin anyway... read -u is also bash, but can be replaced with redirections, and <<< in the first one is also nonstandard, but that's a bit harder to work around.
  • done
    done almost 6 years
    All could be solved, please: read my answer