Использовать read как подсказку внутри цикла while, управляемого read?

9

У меня есть случай использования, когда мне нужно читать несколько переменных в начале каждой итерации и читать входные данные пользователя в цикле.

Возможные пути решения, которые я не знаю, как исследовать -

  1. Для назначения используйте другой дескриптор файла вместо стандартного ввода
  2. Используйте forцикл вместо ... | while read ...... Я не знаю, как назначить несколько переменных внутри forцикла

    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
Дебанжан Басу
источник

Ответы:

9

Если я понял это правильно, я думаю, что вы хотите в основном перебирать списки значений, а затем readеще один в цикле.

Вот несколько вариантов, 1 и 2, вероятно, самые разумные.

1. Эмулируйте массивы со строками

Было бы неплохо иметь двумерные массивы, но в Bash это невозможно. Если ваши значения не имеют пробелов, используйте один обходной путь, то есть вставьте каждый набор из трех чисел в строку и разбейте строки внутри цикла:

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

Конечно, вы можете использовать и другой разделитель, например, for x in 1:2:3 ...и IFS=: read a b c <<< "$x".


2. Замените канал другим перенаправлением, чтобы освободить стандартный ввод.

Другой возможностью является read a b cчтение с другого fd и направление ввода на него (это должно работать в стандартной оболочке):

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

И здесь вы также можете использовать подстановку процесса, если вы хотите получить данные из команды: while read a b c <&3; ...done 3< <(echo $'1 2 3\n4 5 6')(подстановка процесса - это функция bash / ksh / zsh)


3. Взять пользовательский ввод вместо stderr

Или, наоборот, используя канал, как в вашем примере, но используйте пользовательский ввод readиз stderr(fd 2) вместо того, stdinоткуда пришел канал:

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

Чтение из stderrнемного странно, но на самом деле часто работает в интерактивном сеансе. (Вы также можете явно открыть /dev/tty, предполагая, что вы хотите фактически обойти любые перенаправления, вот что вроде как lessиспользует для получения ввода пользователя, даже когда данные передаются на него.)

Хотя stderrтакое использование может работать не во всех случаях, и если вместо него вы используете какую-то внешнюю команду read, вам, по крайней мере, нужно добавить в команду несколько перенаправлений.

Кроме того, см. Почему моя переменная локальна в одном цикле «пока читается», а не в другом, казалось бы, похожем цикле? для некоторых вопросов, касающихся ... | while.


4. Нарезать части массива по мере необходимости

Я полагаю, вы также можете аппроксимировать двумерный массив, копируя срезы обычного одномерного:

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

Вы также можете назначить ${a[0]}и т. Д. a, И bт. Д., Если вы хотите имена для переменных, но Zsh сделает это намного лучше .

ilkkachu
источник
1
это было здорово! И какой отличный обзор различных обходных путей!
Дебанжан Басу
@ StéphaneChazelas, ах да, ты прав. Использовать stderrподобное немного странно, но я вспомнил, что какая-то утилита это делает. Но сейчас я могу найти только те, которые просто используют /dev/tty. Ну что ж.
ilkkachu
Обратите внимание, что при использовании <&2(а также </dev/tty) избегайте чтения из stdin скрипта. Это не сработает printf '682\n739' | ./script. Также обратите внимание, что read -pработают только в bash.
Исаак
@Isaac, в том, что для чтения из stderr, есть также канал от эха к этому whileциклу, так что вы не можете использовать stdin скрипта в любом случае ... read -uэто также bash, но его можно заменить перенаправлениями, и <<<в первый тоже нестандартный, но обойти его немного сложнее.
ilkkachu
Все можно решить, пожалуйста: прочитайте мой ответ
Исаак
3

Есть только один /dev/stdin, readбудет читать с него везде, где он используется (по умолчанию).

Решением является использование некоторого другого файлового дескриптора вместо 1 ( /dev/stdin).

От эквивалента кода (в bash) до того, что вы опубликовали [1] (см. Ниже),
просто добавьте 0</dev/tty(например), чтобы прочитать из «настоящего» 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')"

По исполнению:

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

Другой альтернативой является использование 0<&2(что может показаться странным, но допустимо).

Обратите внимание, что чтение из /dev/tty(также 0<&2) будет обходить стандартный сценарий, это не будет читать значения из эха:

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

Другие решения

Что нужно, так это перенаправить один ввод в другой fd (дескриптор файла).
Действительно в ksh, bash и 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')"

Или с помощью 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>&-

Решение, которое работает в sh ( <<<не работает):

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

Но это, вероятно, легче понять:

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 более простой код

Ваш код:

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

Упрощенный код (в bash):

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

Который, если выполняется, печатает:

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

Который просто показывает, что var d читается из того же /dev/stdin.

Исаак
источник
2

С помощью zshвы можете написать это вместо:

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
}

Для 2D-массивов см. Также ksh93оболочку:

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
Стефан Шазелас
источник
приятно ... я искал ответ на основе bash, но это приятно знать!
Дебанжан Басу