Подтвердить что ты не робот

Как изменить глобальную переменную внутри функции в bash?

Я работаю с этим:

GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)

У меня есть script, как показано ниже:

#!/bin/bash

e=2

function test1() {
  e=4
  echo "hello"
}

test1 
echo "$e"

Что возвращает:

hello
4

Но если я назначу результат функции переменной, глобальная переменная e не будет изменена:

#!/bin/bash

e=2

function test1() {
  e=4
  echo "hello"
}

ret=$(test1)

echo "$ret"
echo "$e"

Возврат:

hello
2

Я слышал о использовании eval в этом случае, поэтому я сделал это в test1:

eval 'e=4'

Но тот же результат.

Не могли бы вы объяснить мне, почему он не изменен? Как сохранить эхо функции test1 в ret и изменить глобальную переменную тоже?

4b9b3361

Ответ 1

Когда вы используете подстановку команд (то есть конструкцию $(...)), вы создаете подоболочку. Подоболочки наследуют переменные от своих родительских оболочек, но это работает только одним способом - подоболочка не может изменять среду своей родительской оболочки. Ваша переменная e установлена в подоболочке, но не в родительской оболочке. Есть два способа передать значения из подоболочки в ее родительский объект. Сначала вы можете вывести что-то в stdout, а затем записать это с помощью подстановки команд:

myfunc() {
    echo "Hello"
}

var="$(myfunc)"

echo "$var"

дает:

Hello

Для числового значения от 0 до 255 вы можете использовать return чтобы передать число в качестве состояния выхода:

mysecondfunc() {
    echo "Hello"
    return 4
}

var="$(mysecondfunc)"
num_var=$?

echo "$var - num is $num_var"

дает:

Hello - num is 4

Ответ 2

Это требует bash 4.1, если вы используете {fd} или local -n.

Остальное должно работать в Bash 3.x Я надеюсь. Я не совсем уверен из-за printf %q - это может быть функция bash 4.

Резюме

Ваш пример может быть изменен следующим образом для архивации желаемого эффекта:

# Add following 4 lines:
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "[email protected]" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "[email protected]")"; }

e=2

# Add following line, called "Annotation"
function test1_() { passback e; }
function test1() {
  e=4
  echo "hello"
}

# Change following line to:
capture ret test1 

echo "$ret"
echo "$e"

печатает по желанию:

hello
4

Обратите внимание, что это решение:

  • Работает на e=1000 тоже.
  • Сохраняет $? если вам нужен $?

Единственные плохие побочные эффекты:

  • Для этого нужен современный bash.
  • Это разветвляется довольно часто.
  • Нужна аннотация (названная в честь вашей функции с добавленным _)
  • Он жертвует файловым дескриптором 3.
    • Вы можете изменить его на другой FD, если вам это нужно.
      • В _capture просто замените все вхождения 3 на другое (большее) число.

Надеемся, что следующее (что довольно долго, извините за это) объясняет, как добавить этот рецепт в другие сценарии.

Эта проблема

d() { let x++; date +%Y%m%d-%H%M%S; }

x=0
d1=$(d)
d2=$(d)
d3=$(d)
d4=$(d)
echo $x $d1 $d2 $d3 $d4

выходы

0 20171129-123521 20171129-123521 20171129-123521 20171129-123521

в то время как желаемый результат

4 20171129-123521 20171129-123521 20171129-123521 20171129-123521

Причина проблемы

Переменные оболочки (или, вообще говоря, среда) передаются от родительских процессов дочерним процессам, но не наоборот.

Если вы делаете захват вывода, это обычно выполняется в подоболочке, поэтому возвращать переменные сложно.

Некоторые даже говорят вам, что это невозможно исправить. Это неправильно, но это давно известная проблема, которую трудно решить.

Есть несколько способов, как решить это лучше всего, это зависит от ваших потребностей.

Вот пошаговое руководство о том, как это сделать.

Передача переменных назад в родительскую оболочку

Существует способ передать переменные в родительскую оболочку. Однако это опасный путь, потому что он использует eval. Если вы поступите неправильно, вы рискуете множеством злых дел. Но если все сделано правильно, это совершенно безопасно, при условии, что в bash нет ошибок.

_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }

d() { let x++; d=$(date +%Y%m%d-%H%M%S); _passback x d; }

x=0
eval 'd'
d1=$d
eval 'd'
d2=$d
eval 'd'
d3=$d
eval 'd'
d4=$d
echo $x $d1 $d2 $d3 $d4

печать

4 20171129-124945 20171129-124945 20171129-124945 20171129-124945

Обратите внимание, что это работает и для опасных вещей:

danger() { danger="$*"; passback danger; }
eval 'danger '; /bin/echo *''
echo "$danger"

печать

; /bin/echo *

Это связано с printf '%q', который цитирует все так, что вы можете безопасно использовать его в контексте оболочки.

Но это боль в...

Это не только выглядит некрасиво, но и много печатает, поэтому оно подвержено ошибкам. Всего одна ошибка, и вы обречены, верно?

Ну, мы на уровне оболочки, так что вы можете улучшить его. Просто подумайте об интерфейсе, который вы хотите увидеть, и тогда вы сможете его реализовать.

Дополнение, как оболочка обрабатывает вещи

Отпустите шаг назад и подумайте о каком-то API, который позволяет нам легко выразить, что мы хотим сделать.

Ну, что мы хотим сделать с функцией d()?

Мы хотим захватить вывод в переменную. Хорошо, тогда давайте реализуем API именно для этого:

# This needs a modern bash 4.3 (see "help declare" if "-n" is present,
# we get rid of it below anyway).
: capture VARIABLE command args..
capture()
{
local -n output="$1"
shift
output="$("[email protected]")"
}

Теперь вместо того, чтобы писать

d1=$(d)

мы можем написать

capture d1 d

Что ж, похоже, мы не сильно изменились, так как, опять же, переменные не передаются из d в родительскую оболочку, и нам нужно набрать немного больше.

Однако теперь мы можем использовать всю мощь оболочки, так как она прекрасно встроена в функцию.

Подумайте о простом в использовании интерфейсе

Во-вторых, мы хотим быть СУХИМ (не повторять себя). Поэтому мы окончательно не хотим набирать что-то вроде

x=0
capture1 x d1 d
capture1 x d2 d
capture1 x d3 d
capture1 x d4 d
echo $x $d1 $d2 $d3 $d4

Здесь x не только избыточен, он подвержен ошибкам и всегда повторяется в правильном контексте. Что если вы используете его 1000 раз в сценарии, а затем добавляете переменную? Вы определенно не хотите изменять все 1000 мест, где задействован вызов d.

Так что оставьте x, чтобы мы могли написать:

_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }

d() { let x++; output=$(date +%Y%m%d-%H%M%S); _passback output x; }

xcapture() { local -n output="$1"; eval "$("${@:2}")"; }

x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4

выходы

4 20171129-132414 20171129-132414 20171129-132414 20171129-132414

Это уже выглядит очень хорошо. (Но все еще есть local -n который не работает в oder common bash 3.x)

Избегайте изменения d()

Последнее решение имеет некоторые большие недостатки:

  • d() должен быть изменен
  • Для передачи вывода необходимо использовать некоторые внутренние детали xcapture.
    • Обратите внимание, что это скрывает (сжигает) одну переменную с именем output, поэтому мы никогда не сможем передать ее обратно.
  • Нужно сотрудничать с _passback

Можем ли мы избавиться от этого тоже?

Конечно, мы можем! Мы находимся в оболочке, поэтому есть все, что нам нужно, чтобы это сделать.

Если вы посмотрите немного ближе к вызову eval вы увидите, что у нас есть 100% контроль в этом месте. "Внутри" eval мы находимся в недолговечности, поэтому мы можем делать все, что хотим, не опасаясь сделать что-то плохое для родительской оболочки.

Да, хорошо, так что давайте добавим еще одну оболочку, теперь прямо в eval:

_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
# !DO NOT USE!
_xcapture() { "${@:2}" > >(printf "%q=%q;" "$1" "$(cat)"); _passback x; }  # !DO NOT USE!
# !DO NOT USE!
xcapture() { eval "$(_xcapture "[email protected]")"; }

d() { let x++; date +%Y%m%d-%H%M%S; }

x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4

печать

4 20171129-132414 20171129-132414 20171129-132414 20171129-132414                                                    

Тем не менее, это, опять же, имеет несколько существенных недостатков:

  • !DO NOT USE! маркеры есть, потому что в этом есть очень плохое состояние гонки, которое вы не можете легко увидеть:
    • >(printf..) является фоновым заданием. Таким образом, он все еще может выполняться во время работы _passback x.
    • Вы можете увидеть это сами, если добавите sleep 1; перед printf или _passback. _xcapture ad; echo Затем _xcapture ad; echo выводит x или a first соответственно.
  • _passback x не должен быть частью _xcapture, потому что это затрудняет повторное использование этого рецепта.
  • Также у нас есть несколько ненужных форков ($(cat)), но так как это решение !DO NOT USE! Я выбрал кратчайший путь.

Тем не менее, это показывает, что мы можем сделать это без модификации d() (и без local -n)!

Обратите внимание, что нам необязательно нужен _xcapture, так как мы могли бы написать все правильно в eval.

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

Исправить гонку

Теперь давайте исправим состояние гонки.

Трюк может заключаться в том, чтобы подождать, пока printf закроет его STDOUT, а затем вывести x.

Есть много способов архивировать это:

  • Вы не можете использовать трубы оболочки, потому что трубы работают в разных процессах.
  • Можно использовать временные файлы,
  • или что-то вроде файла блокировки или FIFO. Это позволяет ждать блокировки или FIFO,
  • или другие каналы, чтобы вывести информацию, а затем собрать вывод в некоторой правильной последовательности.

Следующий путь может выглядеть следующим образом (обратите внимание, что он выполняет printf последним, потому что здесь это работает лучше):

_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }

_xcapture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; _passback x >&3)"; } 3>&1; }

xcapture() { eval "$(_xcapture "[email protected]")"; }

d() { let x++; date +%Y%m%d-%H%M%S; }

x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4

выходы

4 20171129-144845 20171129-144845 20171129-144845 20171129-144845

Почему это правильно?

  • _passback x напрямую общается с STDOUT.
  • Однако, поскольку STDOUT необходимо зафиксировать во внутренней команде, мы сначала "сохраняем" его в FD3 (вы, конечно, можете использовать другие) с помощью "3> & 1", а затем повторно используете его с >&3.
  • $("${@:2}" 3<&-; _passback x >&3) завершается после _passback, когда подоболочка закрывает STDOUT.
  • Таким образом, printf не может произойти до _passback, независимо от того, сколько времени занимает _passback.
  • Обратите внимание, что команда printf не выполняется до полной сборки командной строки, поэтому мы не можем видеть артефакты из printf, независимо от того, как реализован printf.

Следовательно, сначала выполняется _passback, затем printf.

Это решает проблему, жертвуя одним фиксированным файловым дескриптором 3. Конечно, вы можете выбрать другой файловый дескриптор в том случае, если FD3 не свободен в вашем шеллскрипте.

Также обратите внимание на 3<&- который защищает FD3 для передачи в функцию.

Сделайте это более общим

_capture содержит части, которые относятся к d(), что плохо с точки зрения повторного использования. Как это решить?

Хорошо, сделайте это в отдельности, введя еще одну вещь, дополнительную функцию, которая должна возвращать правильные вещи, которая названа в честь оригинальной функции с присоединенным _.

Эта функция вызывается после реальной функции и может дополнять вещи. Таким образом, это может быть прочитано как некоторая аннотация, так что это очень читабельно:

_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
_capture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; "$2_" >&3)"; } 3>&1; }
capture() { eval "$(_capture "[email protected]")"; }

d_() { _passback x; }
d() { let x++; date +%Y%m%d-%H%M%S; }

x=0
capture d1 d
capture d2 d
capture d3 d
capture d4 d
echo $x $d1 $d2 $d3 $d4

все еще печатает

4 20171129-151954 20171129-151954 20171129-151954 20171129-151954

Разрешить доступ к коду возврата

Осталось только по битам:

v=$(fn) устанавливает $? к чему вернулся fn. Так что вы, вероятно, тоже этого хотите. Это требует большей настройки, хотя:

# This is all the interface you need.
# Remember, that this burns FD=3!
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "[email protected]" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "[email protected]")"; }

# Here is your function, annotated with which sideffects it has.
fails_() { passback x y; }
fails() { x=$1; y=69; echo FAIL; return 23; }

# And now the code which uses it all
x=0
y=0
capture wtf fails 42
echo $? $x $y $wtf

печать

23 42 69 FAIL

Есть еще много возможностей для улучшения

  • _passback() может быть ликвидирован с помощью passback passback() { set -- "[email protected]" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; } passback() { set -- "[email protected]" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
  • _capture() можно удалить с помощью capture() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; } capture() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; }

  • Решение загрязняет файловый дескриптор (здесь 3), используя его для внутреннего использования. Вы должны иметь это в виду, если вам случится пройти FDs.
    Обратите внимание, что в bash 4.1 и выше есть {fd} для использования неиспользованного FD.
    (Возможно, я добавлю решение здесь, когда приду.)
    Обратите внимание, что именно поэтому я использую его в отдельных функциях, таких как _capture, потому что это возможно в одной строке, но все труднее читать и понимать

  • Возможно, вы хотите захватить STDERR вызываемой функции тоже. Или вы хотите даже передать и вывести более одного файлового дескриптора из и в переменные.
    У меня пока нет решения, однако здесь есть способ перехватить более одного FD, поэтому мы, вероятно, можем также передать переменные таким же образом.

И не забудь

Это должно вызывать функцию оболочки, а не внешнюю команду.

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

Последние слова

Это не единственно возможное решение. Это один из примеров решения.

Как всегда у вас есть много способов выразить вещи в оболочке. Так что не стесняйтесь улучшать и находить что-то лучшее.

Представленное здесь решение довольно далеко от идеального:

  • Это был почти не тест, так что, пожалуйста, прости опечатки.
  • Существует много возможностей для улучшения, см. Выше.
  • Он использует много функций из современного bash, поэтому, вероятно, его сложно портировать на другие оболочки.
  • И могут быть некоторые причуды, о которых я не думал.

Однако я думаю, что это довольно легко использовать:

  • Добавьте всего 4 строчки "библиотеки".
  • Добавьте только 1 строку "аннотации" для вашей функции оболочки.
  • Временно жертвует только одним файловым дескриптором.
  • И каждый шаг должен быть понятен даже спустя годы.

Ответ 3

Может быть, вы можете использовать файл, записывать в файл внутри функции, читать из файла после него. Я изменил e на массив. В этом примере пробелы используются в качестве разделителя при чтении массива.

#!/bin/bash

declare -a e
e[0]="first"
e[1]="secondddd"

function test1 () {
 e[2]="third"
 e[1]="second"
 echo "${e[@]}" > /tmp/tempout
 echo hi
}

ret=$(test1)

echo "$ret"

read -r -a e < /tmp/tempout
echo "${e[@]}"
echo "${e[0]}"
echo "${e[1]}"
echo "${e[2]}"

Вывод:

hi
first second third
first
second
third

Ответ 4

Что вы делаете, вы выполняете test1

$(test1)

в под-оболочке (дочерней оболочке) и дочерних оболочках ничего нельзя изменить в родительской.

Вы можете найти это в руководстве по bash

Пожалуйста Проверьте: Things результаты в субоболочке здесь

Ответ 5

У меня была аналогичная проблема, когда я хотел автоматически удалить временные файлы, которые я создал. Решение, с которым я столкнулся, заключалось не в использовании подстановки команд, а в том, чтобы передать имя переменной, которая должна принять окончательный результат, в функцию. Например.

#! /bin/bash

remove_later=""
new_tmp_file() {
    file=$(mktemp)
    remove_later="$remove_later $file"
    eval $1=$file
}
remove_tmp_files() {
    rm $remove_later
}
trap remove_tmp_files EXIT

new_tmp_file tmpfile1
new_tmp_file tmpfile2

Итак, в вашем случае это будет:

#!/bin/bash

e=2

function test1() {
  e=4
  eval $1="hello"
}

test1 ret

echo "$ret"
echo "$e"

Работает и не имеет ограничений на "возвращаемое значение".

Ответ 6

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

Ссылка:

Подстановка команд, команды, сгруппированные с круглыми скобками, и асинхронные команды вызывается в среде подсетей, которая является дубликатом среды оболочки

Ответ 7

Вы всегда можете использовать псевдоним:

alias next='printf "blah_%02d" $count;count=$((count+1))'