Область видимости локальных переменных в функциях оболочки

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

После прочтения 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 или отсутствие объявления для динамической области.

Существуют два исключения:

  1. в ksh93, если функция определена с использованием стандартного синтаксиса function_name () { … }, то ее локальные переменные подчиняются динамическому охвату. Но если функция определена с синтаксисом ksh function function_name { … }, то ее локальные переменные подчиняются лексическому/статическому охвату, так что ими нельзя пользоваться в других функциях, вызываемых этой.

  2. автозагружаемый плагин 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".

Важные аспекты динамической области видимости

  1. Доступ к локальным переменным: Как правило, локальные переменные могут быть доступны любым вложенным вызовом функций, что подтверждает динамическую область видимости.
  2. Изменение значений: Если переменная в одной из вложенных функций изменяется, это изменение будет видно в родительской функции, поскольку они работают в одном и том же контексте выполнения.
  3. Отсутствие локальности в вложенных функциях: Чтобы избежать изменения значения локальной переменной, необходимо объявить её как local в функции, где она используется.

Исключения и альтернативы

Бывают случаи, когда вы хотите ограничить доступ к переменной на более низком уровне. В некоторых оболочках, например в ksh93, можно использовать разные синтаксисы для достижения ожидаемой локализации переменной. В оболочке zsh можно применить плагин, который позволяет объявлять переменную с статической областью видимости.

Заключение

Таким образом, использование команды local в оболочке Bash действительно определяет локальную область видимости переменной, но в контексте динамической области видимости. Это поведение не является ошибкой, а является частью механизма работы с переменными в оболочке, который важно понимать при разработке сложных скриптов. Чтобы избежать нежелательных изменений, всегда указывайте local для всех переменных, даже если они используются в вложенных функциях.

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

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