bash: переменная теряет значение в конце цикла чтения

36

У меня проблема в одном из моих скриптов оболочки. Спросил несколько коллег, но они все только качают головами (после некоторого расчесывания), поэтому я пришел сюда за ответом.

Согласно моему пониманию следующий скрипт оболочки должен вывести «Count is 5» в качестве последней строки. За исключением того, что это не так. Это печатает "Количество равно 0". Если заменить «while read» на любой другой вид цикла, он работает просто отлично. Вот сценарий:

echo "1"> input.data
echo "2" >> input.data
echo "3" >> input.data
echo "4" >> input.data
echo "5" >> input.data

CNT = 0 

cat input.data | пока читаешь;
делать
  пусть CNT ++;
  echo "Подсчет в $ CNT"
сделанный 
echo "Count is $ CNT"

Почему это происходит и как я могу предотвратить это? Я пробовал это в Debian Lenny и Squeeze, тот же результат (то есть bash 3.2.39 и bash 4.1.5. Я полностью признаю, что не являюсь мастером сценариев оболочки, поэтому любые указатели приветствуются.

wolfgangsz
источник

Ответы:

30

См. Аргумент @ Bash FAQ # 24: «Я устанавливаю переменные в цикле. Почему они внезапно исчезают после завершения цикла? Или, почему я не могу передать данные для чтения?» (последнее архивировано здесь ).

Резюме: это поддерживается только в bash 4.2 и выше. Вы должны использовать различные способы, такие как подстановка команд вместо канала, если вы используете bash.

Игнасио Васкес-Абрамс
источник
Вы получаете бонус, так как ваш ответ предоставил мне самый широкий спектр возможностей.
wolfgangsz
5
Ссылка мертва. Вот почему ответы только на ссылки плохие. По крайней мере, суммируйте ответ здесь.
rudolfbyker
Боже, еще один раз, когда ksh просто намного лучше ... почему, просто почему все собрались вокруг bash.
Флориан Хейгл
@FlorianHeigl: Вы утверждаете, что ksh - это One True Shell?
Игнасио Васкес-Абрамс
@ IgnacioVazquez-Abrams нет, но я утверждаю, что обработка цикла while в bash - это ужасно PITA. Обработка цикла была долгосрочным препятствием на пути к функциональности 1993 года. Другая вещь - это обработка getopt, когда встроенный обработчик (также 1993) был простым и способным, чего вы все еще не можете получить, не используя ie docopt. Я утверждаю, что bash настойчиво стоит за кривой более 20 лет, и количество времени, затрачиваемого на ЭТО ВЕЩЬ ЗДЕСЬ или миллионы неправильных использований getopts, не поддается измерению - принято только потому, что большинство людей никогда не узнают.
Флориан Хейгл
30

Это своего рода «распространенная» ошибка. Каналы создают SubShells, поэтому они while readработают в оболочке, отличной от вашего скрипта, что делает вашу CNTпеременную никогда не изменяющейся (только ту, которая находится внутри подоболочки трубы).

Сгруппируйте последний echoс подоболочкой, whileчтобы исправить это (есть много других способов исправить это, это один. Ответы Iain и Ignacio имеют другие.)

CNT=0

 cat input.data | ( while read 
do
  let CNT++;
  echo "Counting to $CNT"
done 
echo "Count is $CNT" )

Длинное объяснение:

  1. Вы объявляете CNTв своем скрипте значение 0;
  2. SubShell запускается на |to while read;
  3. Ваша $CNTпеременная экспортируется в SubShell со значением 0;
  4. SubShell считает и увеличивает CNTзначение до 5;
  5. SubShell заканчивается, переменные и значения уничтожаются (они не возвращаются в вызывающий процесс / скрипт).
  6. Вы echoваше первоначальное CNTзначение 0.
CoreDump
источник
2
Первый сценарий оболочки, который я когда-либо писал, вызывал у меня те же проблемы, некоторое время бился головой об стену, прежде чем обнаружил, что эти трубы порождают дополнительные снаряды. Любая переменная, с которой вы связываетесь в канале, выйдет из области видимости, как только закончится конвейер. Это означает, что если вы действительно, действительно хотите что-то сделать с переменной вне канала, в котором она использовалась, вам придется держать состояние через что-то напуганное, как временный файл.
фотоионизировано
Отличный ответ, к сожалению, я могу дать только один приемочный бонус. Сожалею.
wolfgangsz
10

Это работает

CNT=0 

while read ;
do
  let CNT++;
  echo "Counting to $CNT"
done <input.data
echo "Count is $CNT"
user9517 поддерживает GoFundMonica
источник
Мне нравится это, умный способ, потому что вы знаете, где находятся необходимые данные, и вам нужно только вернуть их. Если вы не знаете высококвалифицированных решений, вы всегда можете «прочитать файл», хахахаха. +1 для вас.
m3nda
1
Любой, кто читает это, должен знать, что решение, предоставленное Iain, работает только тогда, когда ваш скрипт явно вызывает bash, имея первую строку: #! / Bin / bash и что: #! / Bin / sh не будет работать.
Roadowl
1
Интересно, первый пример, который я когда-либо видел, где Бесполезное Использование Кошки фактически препятствовало работе кода . Между прочим, @Roadowl, единственная ошибка здесь - это строка, let CNT++которая должна вместо этого CNT="$((CNT+1))"использовать POSIX-совместимое арифметическое расширение . Остальное уже портативно.
Wildcard
6

Попробуйте вместо этого передать данные в под-оболочку, как будто это файл перед циклом while. Это похоже на решение Lain, но предполагает, что вам не нужен прерывистый файл:

total=0
while read var
do
  echo "variable: $var"
  ((total+=var))
done < <(echo 45) #output from a command, script, or function
echo "total: $total"
Стив
источник