Почему моя переменная локальна в одном цикле 'while read', а не в другом, на первый взгляд, похожем цикле?

25

Почему я получаю разные значения $xиз приведенных ниже фрагментов?

#!/bin/bash

x=1
echo fred > junk ; while read var ; do x=55 ; done < junk
echo x=$x 
#    x=55 .. I'd expect this result

x=1
cat junk | while read var ; do x=55 ; done
echo x=$x 
#    x=1 .. but why?

x=1
echo fred | while read var ; do x=55 ; done
echo x=$x 
#    x=1  .. but why?
Peter.O
источник
Аналогичная запись о переполнении стека: переменная, измененная внутри цикла while, не запоминается .
codeforester

Ответы:

26

Правильные объяснения уже были даны jsbillings и geekosaur , но позвольте мне немного остановиться на этом.

В большинстве оболочек, включая bash, каждая сторона конвейера работает в подоболочке, поэтому любое изменение внутреннего состояния оболочки (например, установка переменных) остается ограниченным этим сегментом конвейера. Единственная информация, которую вы можете получить из подоболочки - это то, что она выводит (для стандартного вывода и других файловых дескрипторов) и ее код выхода (который находится в диапазоне от 0 до 255). Например, следующий фрагмент печатает 0:

a=0; a=1 | a=2; echo $a

В ksh (варианты, полученные из кода AT & T, а не в вариантах pdksh / mksh) и zsh, последний элемент конвейера выполняется в родительской оболочке. (POSIX допускает оба поведения.) Таким образом, фрагмент выше печатает 2.

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

cat junk | {
  while read var ; do x=55 ; done
  echo x=$x 
}
Жиль "ТАК - перестань быть злым"
источник
1
Спасибо, Жиль. Что = 0; а = 1 | a = 2 дает очень четкую картину ... и не только о локализации внутреннего состояния, но и о том, что конвейеру на самом деле не нужно ничего отправлять через канал (кроме кода выхода (?)). Само по себе это это интересное представление о трубе ... Мне удалось запустить мой сценарий < <(locate -ber ^\.tag$), благодаря оригинальному немного неясному ответу и комментам geekosaur и glenn jackman .. Сначала я был в дилемме о принятии ответа, но нетто-результата было довольно ясно, особенно с последующим комментарием
jsbillings
Такое ощущение, что я подключился к функции, поэтому я переместил некоторые переменные и тесты внутрь нее, и это отлично работает, спасибо!
Водолей Сила
8

Вы столкнулись с проблемой переменной области. Переменные, определенные в цикле while, который находится с правой стороны канала, имеют свой собственный локальный контекст области видимости, и изменения в переменной не будут видны за пределами цикла. Цикл while по сути является подоболочкой, которая получает копию среды оболочки, и любые изменения в среде теряются в конце оболочки. Смотрите этот вопрос StackOverflow .

ОБНОВЛЕНО : я не упомянул о важном факте, что цикл while со своим собственным подоболочком был обусловлен тем, что он является конечной точкой канала, я обновил это в ответе.

jsbillings
источник
@jsbillings .. Хорошо, это объясняет два последних фрагмента, но это не объясняет первый, где значение $ x, установленное в цикле, определяется как 55 (за пределами цикла while)
Peter.O
5
@ fred.bear: он запускает whileцикл как хвостовую часть конвейера, который бросает его в подоболочку.
geekosaur
2
Это где замена процесса bash вступает в игру. Вместо blah|blah|while read ..., вы можете иметьwhile read ...; done < <(blah|blah)
Гленн Джекман
1
@geekosaur: спасибо за заполнение деталей, которые я не упомянул в своем ответе.
Jsbillings
1
-1 Извините, но этот ответ неверен. Это объясняет, как это работает во многих языках программирования, но не в оболочке. @ Жиль, ниже, все правильно.
jpc
6

Как упоминалось в других ответах , части конвейера работают в подоболочках, поэтому сделанные в них модификации не видны основной оболочке.

Если мы рассмотрим только Bash, в дополнение к cmd | { stuff; more stuff; }структуре есть два других обходных пути :

  1. Перенаправить ввод от подстановки процесса :

    while read var ; do x=55 ; done < <(echo fred)
    echo "$x"

    Вывод команды in <(...)выглядит так, как если бы это был именованный канал.

  2. lastpipeВариант, который делает Bash работу как KSH, и запускает последнюю часть трубопровода в основном процессе оболочки. Хотя это работает, только если управление заданиями отключено, т.е. не в интерактивной оболочке:

    bash -c '
      shopt -s lastpipe
      echo fred | while read var ; do x=55 ; done; 
      echo "$x"
    '

    или

    bash -O lastpipe -c '
      echo fred | while read var ; do x=55 ; done; 
      echo "$x"
    '

Подстановка процессов также поддерживается в ksh и zsh. Но поскольку в любом случае они запускают последнюю часть конвейера в основной оболочке, использовать ее в качестве обходного пути не обязательно.

ilkkachu
источник
0
#!/bin/bash
set -x

# prepare test data.
mkdir -p ~/test_var_global
cd ~/test_var_global
echo "a"> core.1
echo "b"> core.2
echo "c"> core.3


var=0

coreFiles=$(find . -type f -name "core*")
while read -r file;
do
  # perform computations on $i
  ((var++))
done <<EOF
$coreFiles
EOF

echo $var

Result:
...
+ echo 3
3

это может работать.

GPS
источник