Вопрос или проблема
Почему я получаю разные значения для $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; }
:
-
Перенаправьте ввод из подстановки процесса:
while read var ; do x=55 ; done < <(echo fred) echo "$x"
Вывод команды в
<(...)
заставляет его выглядеть так, будто это именованный канал. -
Опция
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
.
Решения
Чтобы изменения переменной сохранялись, вы можете использовать следующие подходы:
-
Группировка команд с использованием фигурных скобок:
cat junk | { while read var ; do x=55 ; done echo x=$x }
В этом случае все команды внутри
{ ... }
выполняются в основном процессе, и вы сможете видеть обновленное значениеx
. -
Использование подстановки процессов:
while read var ; do x=55 ; done < <(echo fred) echo "$x"
Это позволяет вам избежать создания подпроцесса для цикла
while
, и изменения переменнойx
будут видны, так как цикл выполняется в основном процессе. -
Опция
lastpipe
:
Если вам необходимо использовать конвейеры, вы можете включить опциюlastpipe
в Bash, которая заставляет последнюю команду в конвейере выполняться в основном процессе, как это делается вksh
. Это требует, чтобы управление заданиями было отключено (не в интерактивной оболочке):shopt -s lastpipe echo fred | while read var ; do x=55 ; done echo "$x"
Таким образом, проблема, с которой вы столкнулись, связанна с особенностями работы с подпроцессами в Bash и областью видимости переменных. Понимание этих концепций поможет вам избежать подобных недоразумений в будущем.