Вопрос или проблема
После прочтения 24.2. Локальные переменные я думал, что объявление переменной var
с ключевым словом local
означает, что значение var
доступно только в блоке кода, ограниченном фигурными скобками функции.
Однако после выполнения следующего примера я выяснил, что к var
можно также обращаться, читать и записывать из функций, вызываемых этим блоком кода — т.е. даже несмотря на то, что var
объявлен как local
для outerFunc
, innerFunc
все еще может его читать и изменять его значение.
#!/usr/bin/env bash
function innerFunc() {
var="новое значение"
echo "innerFunc: [var:${var}]"
}
function outerFunc() {
local var="начальное значение"
echo "outerFunc: перед innerFunc: [var:${var}]"
innerFunc
echo "outerFunc: после innerFunc: [var:${var}]"
}
echo "глобально: перед outerFunc: [var:${var}]"
outerFunc
echo "глобально: после outerFunc: [var:${var}]"
Вывод:
глобально: перед outerFunc: [var:] # как и ожидалось, `var` недоступен вне `outerFunc`
outerFunc: перед innerFunc: [var:начальное значение]
innerFunc: [var:новое значение] # `innerFunc` имеет доступ к `var` ??
outerFunc: после innerFunc: [var:новое значение] # изменение `var` в `innerFunc` фиксируется в `outerFunc` ??
глобально: после outerFunc: [var:]
В: Это ошибка в моей оболочке (bash 4.3.42, Ubuntu 16.04, 64bit) или ожидаемое поведение?
ИЗМЕНЕНИЕ: Проблема решена. Как отметил @MarkPlotnick, это действительно ожидаемое поведение.
</div><div class="s-prose js-post-body" itemprop="text">
Переменные оболочки имеют динамическую область видимости. Если переменная объявлена как локальная для функции, эта область действия остается в силе до тех пор, пока функция не вернется, включая вызовы других функций!
Это контрастирует с большинством языков программирования, которые имеют лексическую область видимости. Perl имеет оба типа: my
для лексической области, local
или отсутствие объявления для динамической области.
Существуют два исключения:
-
в ksh93, если функция определена с использованием стандартного синтаксиса
function_name () { … }
, то ее локальные переменные подчиняются динамическому охвату. Но если функция определена с синтаксисом kshfunction function_name { … }
, то ее локальные переменные подчиняются лексическому/статическому охвату, так что ими нельзя пользоваться в других функциях, вызываемых этой. -
автозагружаемый плагин
zsh/private
вzsh
предоставляет ключевое слово/встроенную командуprivate
, которая может использоваться для объявления переменной со статической областью видимости.
ash
, bash
, pdksh
и производные от них, bosh
имеют только динамическую область видимости.
</div><div class="s-prose js-post-body" itemprop="text">
В function innerFunc()
var="новое значение"
не была объявлена как локальная, поэтому она доступна в видимой области (как только функция была вызвана).
Наоборот, в function outerFunc()
local var="начальное значение"
была объявлена как локальная, поэтому она недоступна в глобальной области (даже если функция была вызвана).
Поскольку innerFunc()
была вызвана как дочерняя функция outerFunc()
, var находится в локальной области видимости outerFunc()
.
man 1 bash
может помочь прояснить ситуацию
local [option] [name[=value] …]
Для каждого аргумента создается локальная переменная с именем name и
присваивается значение. Опция может быть любой из тех, что принимаются
командой declare. Когда local используется внутри функции, это приводит к тому,
что имя переменной имеет видимую область, ограниченную этой
функцией и ее дочерними функциями. …
Предполагаемое поведение, ожидаемое в описании, может быть достигнуто путем объявления local var="новое значение"
в function innerFunc()
.
Как уже отметили другие, это не ошибка в оболочке bash. Все функционирует, как и должно.
</div><div class="s-prose js-post-body" itemprop="text">
Это не ошибка, вызов внутри контекста outerFunc использует эту локальную копию $var. “local” в outerFunc означает, что глобальная переменная не изменяется. Если вы вызовете innerFunc вне outerFunc, тогда произойдет изменение глобальной $var, но не локальной $var outerFunc. Если вы добавите “local” в innerFunc, тогда $var outerFunc не изменится – по сути, их будет три:
- $global::var
- $outerFunc::var
- $innerFunc::var
это аналогично использованию формата пространства имен Perl.
</div><div class="s-prose js-post-body" itemprop="text">
Вы можете использовать функцию, чтобы принудительно задать локальную область:
sh_local() {
eval "$(set)" command eval '\"\$@\"'
}
Пример:
x() {
z='новое значение'
printf 'функция x, z = [%s]\n' "$z"
}
y() {
z='начальное значение'
printf 'функция y перед x, z = [%s]\n' "$z"
sh_local x
printf 'функция y после x, z = [%s]\n' "$z"
}
printf 'глобально перед y, z = [%s]\n' "$z"
y
printf 'глобально после y, z = [%s]\n' "$z"
Результат:
глобально перед y, z = []
функция y перед x, z = [начальное значение]
функция x, z = [новое значение]
функция y после x, z = [начальное значение]
глобально после y, z = [начальное значение]
</div><div class="s-prose js-post-body" itemprop="text">
Это ожидаемое поведение. Локальная переменная имеет динамическую область видимости: переменная находится в области действия до тех пор, пока функция, в которой она была объявлена, не вернется. Соответственно, такие переменные находятся в области видимости для всех функций, вызываемых из этой функции.
Если функция объявляет новую локальную переменную с тем же именем, что и другая (локальная или глобальная) переменная, новая локальная переменная будет находиться в отдельной области памяти и может содержать значения, отличные от любых других с тем же именем.
Не имеет значения, где в функции появляется локальное объявление.
Я немного изменил предоставленный код в качестве примера:
#!/usr/bin/env bash
function innerFunc() {
echo "innerFunc: [var:${var}]"
}
function outerFunc() {
local var="начальное значение"
echo "outerFunc: перед innerFunc: [var:${var}]"
innerFunc
echo "outerFunc: после innerFunc: [var:${var}]"
}
echo "глобально: перед outerFunc: [var:${var}]"
outerFunc
echo "глобально: после outerFunc: [var:${var}]"
innerFunc
echo "глобально: после outerFunc: [var:${var}]"
</div><div class="s-prose js-post-body" itemprop="text">
Мне стало интересно, как изменится состояние var
, если мы запустим innerFunc
в фоновом режиме (т.е. innerFunc &
) и намеренно введем задержку внутри нее, чтобы она завершилась после того, как outerFunc
уже вернулась. Также, что будет, если значение var
изменится во время задержки.
#!/bin/bash
runBg="$1"
outfi='./out.txt'
[ "$runBg" -eq 1 ] && outfi='./out_bg.txt'
{
innerFunc() {
if [ "$runBg" -eq 1 ]; then
echo "innerFunc: ПЕРЕД СНОМ: [var:${var}]"
sleep .5
echo "innerFunc: ПОСЛЕ СНА: [var:${var}]"
fi
var="INNER"
echo "innerFunc: [var:${var}]"
}
outerFunc() {
local var="OUTER"
echo "outerFunc: перед innerFunc: [var:${var}]"
if [ "$runBg" -eq 1 ]; then
innerFunc &
var="OUTER_B_innerFunc_is_in_bg"
else
innerFunc
fi
echo "outerFunc: после innerFunc: [var:${var}]"
}
echo "глобально: перед outerFunc: [var:${var}]"
outerFunc
echo "глобально: после outerFunc: [var:${var}]"
} > "$outfi" 2>&1
И вывод:
➜ tmp.LX8xU7Gk71 ./scopes.sh 0 /0.0s
➜ tmp.LX8xU7Gk71 ./scopes.sh 1 /0.0s
➜ tmp.LX8xU7Gk71 cat out.txt
глобально: перед outerFunc: [var:]
outerFunc: перед innerFunc: [var:OUTER]
innerFunc: [var:INNER]
outerFunc: после innerFunc: [var:INNER]
глобально: после outerFunc: [var:] /0.0s
➜ tmp.LX8xU7Gk71 cat out_bg.txt
глобально: перед outerFunc: [var:]
outerFunc: перед innerFunc: [var:OUTER]
outerFunc: после innerFunc: [var:OUTER_B_innerFunc_is_in_bg]
глобально: после outerFunc: [var:]
innerFunc: ПЕРЕД СНОМ: [var:OUTER]
innerFunc: ПОСЛЕ СНА: [var:OUTER]
innerFunc: [var:INNER]
Изменение не видно в innerFunc
, даже после того, как export var
ставится перед innerFunc &
.
</div>
Ответ или решение
Область видимости локальных переменных в функциях оболочки (Shell Functions)
При работе с функциями в оболочке (shell), важным аспектом является понимание области видимости переменных, особенно тех, которые объявлены как локальные. Этот вопрос часто вызывает недоумение у разработчиков, и, следовательно, рассмотрим его более подробно.
Понимание локальных переменных
В Bash при объявлении переменной с помощью ключевого слова local
, мы ожидаем, что её область видимости будет ограничена только тем блоком кода, в котором она была объявлена. Тем не менее, стоит отметить, что в Bash применяется динамическая область видимости переменных. Это означает, что локальные переменные остаются в пределах видимости до тех пор, пока функция, в которой они были объявлены, не завершит выполнение. Таким образом, все вызванные из этой функции функции имеют доступ к локальным переменным, поскольку они работают в одном контексте выполнения.
Пример кода
Рассмотрим пример, где это поведение демонстрируется:
#!/usr/bin/env bash
function innerFunc() {
var="new value"
echo "innerFunc: [var:${var}]"
}
function outerFunc() {
local var="initial value"
echo "outerFunc: before innerFunc: [var:${var}]"
innerFunc
echo "outerFunc: after innerFunc: [var:${var}]"
}
echo "global: before outerFunc: [var:${var}]"
outerFunc
echo "global: after outerFunc: [var:${var}]"
Ожидаемый вывод
Когда мы запускаем этот скрипт, мы можем получить следующий вывод:
global: before outerFunc: [var:]
outerFunc: before innerFunc: [var:initial value]
innerFunc: [var:new value]
outerFunc: after innerFunc: [var:new value]
global: after outerFunc: [var:]
Объяснение вывода
- Переменная
var
, объявленная как локальная вouterFunc
, служит для хранения значения "initial value" во время выполнения. - Вызов
innerFunc
создает новую переменнуюvar
, которая доступна в контекстеinnerFunc
. Это значение "new value" изменяет состояние переменнойvar
вouterFunc
благодаря динамической области видимости. - После завершения работы
innerFunc
, переменнаяvar
вouterFunc
будет иметь значение "new value".
Важные аспекты динамической области видимости
- Доступ к локальным переменным: Как правило, локальные переменные могут быть доступны любым вложенным вызовом функций, что подтверждает динамическую область видимости.
- Изменение значений: Если переменная в одной из вложенных функций изменяется, это изменение будет видно в родительской функции, поскольку они работают в одном и том же контексте выполнения.
- Отсутствие локальности в вложенных функциях: Чтобы избежать изменения значения локальной переменной, необходимо объявить её как
local
в функции, где она используется.
Исключения и альтернативы
Бывают случаи, когда вы хотите ограничить доступ к переменной на более низком уровне. В некоторых оболочках, например в ksh93
, можно использовать разные синтаксисы для достижения ожидаемой локализации переменной. В оболочке zsh
можно применить плагин, который позволяет объявлять переменную с статической областью видимости.
Заключение
Таким образом, использование команды local
в оболочке Bash действительно определяет локальную область видимости переменной, но в контексте динамической области видимости. Это поведение не является ошибкой, а является частью механизма работы с переменными в оболочке, который важно понимать при разработке сложных скриптов. Чтобы избежать нежелательных изменений, всегда указывайте local
для всех переменных, даже если они используются в вложенных функциях.