Bash: Почему входной канал для "чтения" работает только при подаче в конструкцию "while read..."?

Я пытаюсь читать входные переменные среды из вывода программы следующим образом:

echo first second | read A B ; echo $A-$B 

И результат:

-

Оба A и B всегда пусты. Я читал о bash выполнении команд с каналами в суб-оболочке и в основном предотвращал чтение из ввода канала. Однако, следующее:

echo first second | while read A B ; do echo $A-$B ; done

Кажется, что работает, результат:

first-second

Может кто-нибудь объяснить, что здесь является логикой? Является ли это тем, что команды внутри конструкции while... done фактически выполняются в той же оболочке, что и echo, а не в под-оболочке?

Ответ 1

Как сделать цикл против stdin и получить результат, сохраненный в переменной

В (и другие также, когда вы что-то делаете, используя | к другой команде, вы будете неявно создавать вилку, подоболочку, которая является дочерним элементом текущей сессии и которая не может повлиять на текущую среду сеанса.

Итак, это:

TOTAL=0
printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 |
  while read A B;do
      ((TOTAL+=A-B))
      printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[A-B] $TOTAL
    done
echo final total: $TOTAL

не даст ожидаемого результата!

  9 -   4 =    5 -> TOTAL=    5
  3 -   1 =    2 -> TOTAL=    7
 77 -   2 =   75 -> TOTAL=   82
 25 -  12 =   13 -> TOTAL=   95
226 - 664 = -438 -> TOTAL= -343
echo final total: $TOTAL
final total: 0

Если вычисленный TOTAL не может быть повторно использован в главном script.

Инвертирование вилки

Используя Замена процесса, Здесь Documents или Here Strings вы можете инвертировать fork:

Здесь строки

read A B <<<"first second"
echo $A
first

echo $B
second

Здесь Документы

while read A B;do
    echo $A-$B
    C=$A-$B
  done << eodoc
first second
third fourth
eodoc
first-second
third-fourth

вне цикла:

echo : $C
: third-fourth

Здесь Команды

TOTAL=0
while read A B;do
    ((TOTAL+=A-B))
    printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[A-B] $TOTAL
  done < <(
    printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664
)
  9 -   4 =    5 -> TOTAL=    5
  3 -   1 =    2 -> TOTAL=    7
 77 -   2 =   75 -> TOTAL=   82
 25 -  12 =   13 -> TOTAL=   95
226 - 664 = -438 -> TOTAL= -343

# and finally out of loop:
echo $TOTAL
-343

Теперь вы можете использовать $TOTAL в своем основном script.

Подключение к списку команд

Но для работы только с stdin вы можете создать вид script в fork:

printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 | {
    TOTAL=0
    while read A B;do
        ((TOTAL+=A-B))
        printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[A-B] $TOTAL
    done
    echo "Out of the loop total:" $TOTAL
  }

Дает:

  9 -   4 =    5 -> TOTAL=    5
  3 -   1 =    2 -> TOTAL=    7
 77 -   2 =   75 -> TOTAL=   82
 25 -  12 =   13 -> TOTAL=   95
226 - 664 = -438 -> TOTAL= -343
Out of the loop total: -343

Примечание: $TOTAL не может использоваться в основном script (после последней правой фигурной скобки }).

Использование опции lastpipe bash

Как правильно указал @CharlesDuffy, существует опция bash, используемая для изменения этого поведения. Но для этого мы должны сначала отключить управление заданиями:

shopt -s lastpipe           # Set *lastpipe* option
set +m                      # Disabling job control
TOTAL=0
printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 |
  while read A B;do
      ((TOTAL+=A-B))
      printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[A-B] $TOTAL
    done

  9 -   4 =    5 -> TOTAL= -338
  3 -   1 =    2 -> TOTAL= -336
 77 -   2 =   75 -> TOTAL= -261
 25 -  12 =   13 -> TOTAL= -248
226 - 664 = -438 -> TOTAL= -686

echo final total: $TOTAL
-343

Это будет работать, но мне (лично) это не нравится, потому что это не стандартное и не поможет сделать script доступным для чтения. Также отключить управление заданиями представляется дорогостоящим для доступа к этому поведению.

Примечание. Управление заданием включено по умолчанию только в интерактивных сеансах. Поэтому set +m не требуется в обычных скриптах.

Итак, забытый set +m в script создавал бы разные поведения, если они запускались в консоли или запускались в script. Это не поможет сделать это легко понять или отладить...

Ответ 2

Сначала выполняется эта цепочка:

echo first second | read A B

то

echo $A-$B

Поскольку read A B выполняется в подоболочке, A и B теряются. Если вы это сделаете:

echo first second | (read A B ; echo $A-$B)

то оба read A B и echo $A-$B выполняются в одной и той же подоболочке (см. справочную страницу bash, найдите (list)

Ответ 3

гораздо более чистая работа вокруг...

read -r a b < <(echo "$first $second")
echo "$a $b"

Таким образом, чтение не выполняется в под-оболочке (которая очистит переменные, как только закончится эта под-оболочка). Вместо этого переменные, которые вы хотите использовать, отражаются в под-оболочке, которая автоматически наследует переменные из родительской оболочки.

Ответ 4

То, что вы видите, - это разделение между процессами: read происходит в подоболочке - отдельный процесс, который не может изменять переменные в основном процессе (где далее выполняются команды echo).

Конвейер (например, A | B) неявно помещает каждый компонент в под-оболочку (отдельный процесс) даже для встроенных (например, read), которые обычно выполняются в контексте оболочки (в том же процесс).

Разница в случае "трубопровода во время" - иллюзия. То же правило применяется там: цикл является второй половиной конвейера, поэтому он находится в подоболочке, но весь цикл находится в одной и той же подоболочке, поэтому разделение процессов не применяется.

Ответ 5

Если изменить оболочку на ksh, ваш самый первый пример работает i.e.

echo first second | read A B ; echo $A-$B