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

Вопрос или проблема

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

#!/bin/bash

x=1
echo fred > junk ; while read var ; do x=55 ; done < junk
echo x=$x 
#    x=55 .. Я ожидал такой результат

x=1
cat junk | while read var ; do x=55 ; done
echo x=$x 
#    x=1 .. но почему?

x=1
echo fred | while read var ; do x=55 ; done
echo x=$x 
#    x=1  .. но почему?

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

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

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

В ksh (варианты, производные от кода AT&T, а не pdksh/mksh) и zsh последний элемент в конвейере выполняется в родительской оболочке. (POSIX допускает оба поведения.) Поэтому приведенный выше фрагмент выводит 2. Вы можете получить это поведение в современном bash, установив опцию lastpipe с помощью shopt -s lastpipe (в интерактивной оболочке также нужно отключить управление заданиями с помощью set +m).

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

cat junk | {
  while read var ; do x=55 ; done
  echo x=$x 
}

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

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

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

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

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

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

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

  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. Но поскольку они все равно выполняют последнюю часть конвейера в основной оболочке, использование ее в качестве обходного пути не является действительно необходимым.

Ответ или решение

Проблема, с которой вы столкнулись, связана с областью видимости переменных в Bash, особенно в контексте использования конвейеров (pipeline).

В Bash, когда вы создаете конвейер, каждая часть (или команда) конвейера выполняется в отдельном подпроцессе (subshell). Это означает, что все изменения переменных внутри этого подпроцесса не будут видны в основном процессе оболочки. Давайте разберем каждый из ваших примеров.

Пример 1

x=1
echo fred > junk ; while read var ; do x=55 ; done < junk
echo x=$x 

В этом случае while read var не находится в конвейере, он выполняется в основном процессе, поэтому изменение переменной x внутри цикла while сохраняется, и вы получаете x=55.

Пример 2

x=1
cat junk | while read var ; do x=55 ; done
echo x=$x 

Здесь cat junk выводит содержимое файла в конвейер, и while read var выполняется в подпроцессе. Изменение переменной x внутри этого неподключенного цикла не отражается на переменной x в основном процессе, поэтому вы получаете x=1.

Пример 3

x=1
echo fred | while read var ; do x=55 ; done
echo x=$x 

Суть та же — while read var выполняется в подпроцессе из-за конвейера, и изменения переменной x не сохраняются в основном процессе. Поэтому вы снова получите x=1.

Решения

Чтобы изменения переменной сохранялись, вы можете использовать следующие подходы:

  1. Группировка команд с использованием фигурных скобок:

    cat junk | {
     while read var ; do x=55 ; done
     echo x=$x
    }

    В этом случае все команды внутри { ... } выполняются в основном процессе, и вы сможете видеть обновленное значение x.

  2. Использование подстановки процессов:

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

    Это позволяет вам избежать создания подпроцесса для цикла while, и изменения переменной x будут видны, так как цикл выполняется в основном процессе.

  3. Опция lastpipe:
    Если вам необходимо использовать конвейеры, вы можете включить опцию lastpipe в Bash, которая заставляет последнюю команду в конвейере выполняться в основном процессе, как это делается в ksh. Это требует, чтобы управление заданиями было отключено (не в интерактивной оболочке):

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

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

Оцените материал
Добавить комментарий

Капча загружается...