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

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

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'

но то же самое результат.

не могли бы вы объяснить мне, почему он не изменяется? Как я мог спасти Эхо

7 ответов


при использовании подстановки команд (т. е. $(...) construct), вы создаете подсеть. Дочерние ячейки наследуют переменные от своих родительских оболочек, но это работает только одним способом - дочерняя ячейка не может изменять среду своей родительской оболочки. Переменная 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

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

$(test1)

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

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

пожалуйста, проверьте: вещи приводят к subshell здесь


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

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

#! /bin/bash

remove_later=""
new_tmp_file() {
    file=$(mktemp)
    remove_later="$remove_later $file"
    eval =$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 ="hello"
}

test1 ret

echo "$ret"
echo "$e"

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


резюме

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

# Add following 4 lines:
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "" "${!1}"; shift; done; return ; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "_" >&3)"; ret=$?; printf "%q=%q;" "" "$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 тоже.
  • хранит $? если вам нужно $?

единственная плохая sideffects являются:

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

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

проблема

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}"; 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', который цитирует все такое, что вы можете повторно использовать его в контексте оболочки безопасно.

но это боль в собой..

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

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

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

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

хорошо, что мы хотим сделать с это.

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

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

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

xcapture() { local -n output=""; 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

это уже выглядит очень хорошо.

не менять d()

последнее решение имеет некоторые недостатки:

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

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

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

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

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

_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "" "${!1}"; shift; done; }
# !DO NOT USE!
_xcapture() { "${@:2}" > >(printf "%q=%q;" "" "$(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, потому что это затрудняет повторное использование рецепта.
  • также у нас есть некоторые unneded вилка здесь ($(cat)), но как это решение !DO NOT USE! я взял кратчайший путь.

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

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

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

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

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

трюк может быть подождать, пока printf закрыл это STDOUT, а затем output x.

есть много способов для этого:

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

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

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

_xcapture() { { printf "%q=%q;" "" "$("${@: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, когда subshell закрывает STDOUT.
  • так printf не может произойти до _passback, независимо от того, как долго _passback берет.
  • отметим, что printf команда не выполняется до завершения командная строка собрана, поэтому мы не можем видеть артефакты из printf, независимо как printf это выполненный.

Отсюда первый _passback выполняется, то printf.

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

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

сделать его более общие

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

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

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

_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "" "${!1}"; shift; done; }
_capture() { { printf "%q=%q;" "" "$("${@:2}" 3<&-; "_" >&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}"; shift; done; return ; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "_" >&3)"; ret=$?; printf "%q=%q;" "" "$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=42; y=69; echo FAIL; return 23; }

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

печать

23 42 69 FAIL

есть еще много возможностей для совершенствования

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

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

не забывайте:

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

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

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

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

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

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

  • это был почти не тестет вообще, поэтому, пожалуйста, простите опечатки.
  • есть много возможностей для улучшения, см. выше.
  • он использует множество функций от modern bash, поэтому, вероятно, трудно портировать на другие оболочки.
  • и могут быть некоторые причуды, о которых я не думал.
я думаю, что это довольно легко использовать:
  • добавить только 4 строки "библиотека".
  • добавьте только 1 строку "аннотации" для вашей функции оболочки.
  • временно жертвует только одним файловым дескриптором.
  • и каждый шаг должен быть легко понять, даже спустя годы.

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

ссылка:

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


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

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