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

105

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

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

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

#!/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и изменить глобальную переменную?

harrison4
источник
Вам нужно ответить привет? Вы можете просто повторить $ e, чтобы он вернулся. Или повторить все, что хотите, а затем проанализировать результат?

Ответы:

98

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

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
Джош Джолли
источник
Спасибо за точку, но я должен вернуть массив строк, а внутри функции мне нужно добавить элементы в два глобальных массива строк.
harrison4
3
Вы понимаете, что если вы просто запустите функцию, не назначая ее переменной, все глобальные переменные в ней обновятся. Вместо того чтобы возвращать массив строк, почему бы просто не обновить массив строк в функции, а затем назначить его другой переменной после завершения функции?
@JohnDoe: Вы не можете вернуть «массив строк» ​​из функции. Все, что вы можете сделать, это напечатать строку. Однако вы можете сделать что-то вроде этого:setarray() { declare -ag "$1=(a b c)"; }
rici
34

Для этого нужен 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 "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }

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="$("$@")"
}

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

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Здесь не только излишним, это чревато ошибками всегда repeate в правильном контексте. Что, если вы используете его 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который не работает в обычных bash3.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 "$@")"; }

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 a d; echoзатем выдает xили aсначала соответственно.
  • Не _passback xдолжно быть частью _xcapture, потому что это затрудняет повторное использование этого рецепта.
  • Также у нас есть ненужная вилка (the $(cat)), но, поскольку это решение, !DO NOT USE!я выбрал кратчайший путь.

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

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

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

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

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

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

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

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

Следующий путь может выглядеть так (обратите внимание, что он делает 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 "$@")"; }

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 "$@")"; }

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 "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }

# 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() { set -- "$@" "$?"; 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)")"; }

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

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

Также не забывайте:

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

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

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

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

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

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

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

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

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

Может быть, вы можете использовать файл, писать в файл внутри функции, а затем читать из файла. Я перешел 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
Ашкан
источник
13

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

$(test1)

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

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

Пожалуйста, проверьте: Вещи приведены в подоболочке здесь

PradyJord
источник
7

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

#! /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"

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

Эльмар Зандер
источник
1

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

Ссылка :

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

Какой-то чувак-программист
источник
@JohnDoe Я не уверен, что это возможно. Возможно, вам придется переосмыслить дизайн сценария.
Какой-то чувак-программист
О, но мне нужно использовать глобальный массив внутри функции, в противном случае мне пришлось бы повторить много кода (повторить код функции -30 строк- 15 раз -один за вызов-). Нет другого выхода, не правда ли?
harrison4
1

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

Этот подход очень помог мне, когда мне пришлось имитировать функцию bash, вызываемую несколько раз в тестовом примере летучих мышей.

Например, у вас может быть:

# Usage read_value path_to_tmp_file
function read_value {
  cat "${1}"
}

# Usage: set_value path_to_tmp_file the_value
function set_value {
  echo "${2}" > "${1}"
}
#----

# Original code:

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


# Create the temp file
# Note that tmp_file is available in test1 as well
tmp_file=$(mktemp)

# Your logic
e=2
# Store the value
set_value "${tmp_file}" "${e}"

# Run test1
test1

# Read the value modified by test1
e=$(read_value "${tmp_file}")
echo "$e"

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

Фабио
источник
-1

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

alias next='printf "blah_%02d" $count;count=$((count+1))'
Дино Дини
источник