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

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

Согласно моему пониманию следующий скрипт оболочки должен вывести "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. Я полностью признаю, что не являюсь мастером сценариев оболочки, поэтому любые указатели приветствуются.

4 ответа

Решение

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

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

Это своего рода "распространенная" ошибка. Трубы создают 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 запускается на | в while read;
  3. Ваш $CNT переменная экспортируется в SubShell со значением 0;
  4. SubShell считает и увеличивает CNT значение до 5;
  5. SubShell заканчивается, переменные и значения уничтожаются (они не возвращаются в вызывающий процесс / скрипт).
  6. Вы echo ваш оригинал CNT значение 0.

Это работает

CNT=0 

while read ;
do
  let CNT++;
  echo "Counting to $CNT"
done <input.data
echo "Count is $CNT"

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

total=0
while read var
do
  echo "variable: $var"
  ((total+=var))
done < <(echo 45) #output from a command, script, or function
echo "total: $total"

Другое решение - просто добавить shopt -s lastpipe перед циклом while.

Я говорю, что если проблема возникает из-за того, что время находится в последнем сегменте конвейера, а в Bash все команды в конвейере выполняются в подоболочке в отдельном процессе, тогда, используя lastpipe выполнит последнюю команду в конвейере на переднем плане.

Например:

CNT=0
shopt -s lastpipe
cat input.data | while read ;
...

И почти все осталось прежним.

Я нашел способ использовать файл stderr для хранения значения var i.

# reading lines of content from 2 files concatenated
# inside loop: write value of var i to stderr (before iteration)
# outside: read var i from stderr, has last iterative value
f=/tmp/file1
g=/tmp/file2

i=1
cat $f $g | \
while read -r s;
do
  echo $s > /dev/null;  # some work
  echo $i > 2
  let i++
done;
read -r i < 2
echo $i
Другие вопросы по тегам