Program Tip

bash의 함수 내에서 전역 변수를 수정하는 방법은 무엇입니까?

programtip 2020. 10. 14. 20:53
반응형

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'

그러나 같은 결과.

왜 수정되지 않았는지 설명해 주시겠습니까? test1함수 의 에코를 저장 ret하고 전역 변수도 수정하려면 어떻게해야합니까?


명령 대체 (예 : $(...)구성)를 사용하면 서브 쉘이 생성됩니다. 서브 셸은 부모 셸에서 변수를 상속 받지만 한 가지 방식으로 만 작동합니다. 서브 셸은 부모 셸의 환경을 수정할 수 없습니다. 변수 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

{fd}또는 을 사용하는 경우 bash 4.1이 필요합니다 local -n.

나머지는 bash 3.x에서 작동합니다. 나는 printf %qbash 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입니다. 부적절하게 수행하면 많은 악한 일을 할 위험이 있습니다. 그러나 제대로 수행하면 .NET에 버그가없는 한 완벽하게 안전 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'모든 것을 인용하는 으로 인해 쉘 컨텍스트에서 안전하게 재사용 할 수 있습니다.

그러나 이것은 a ..

이것은보기 흉하게 보일뿐만 아니라 타이핑도 많이하기 때문에 오류가 발생하기 쉽습니다. 단 한 번의 실수로 당신은 운명을 맞았습니다.

음, 우리는 쉘 수준에 있으므로 개선 할 수 있습니다. 보고 싶은 인터페이스에 대해 생각하면 구현할 수 있습니다.

증강, 쉘이 사물을 처리하는 방법

한 걸음 물러서서 우리가하고 싶은 일을 쉽게 표현할 수있는 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부모 쉘로 다시 전달되지 않으며 조금 더 입력해야합니다.

그러나 이제 우리는 셸이 함수에 멋지게 감싸여 있기 때문에 셸의 모든 기능을 사용할 수 있습니다.

재사용하기 쉬운 인터페이스에 대해 생각하십시오.

두 번째는 DRY (반복하지 마세요)가되고 싶다는 것입니다. 따라서 우리는 다음과 같은 것을 입력하고 싶지 않습니다.

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 번 사용한 다음 변수를 추가하면 어떨까요? 전화 d가 관련된 1000 개의 위치를 ​​모두 변경하고 싶지는 않습니다 .

그래서 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.
  • 또한 여기에 unneded fork ( $(cat))가 있지만이 솔루션은 !DO NOT USE!가장 짧은 경로를 사용했습니다.

그러나 이것은 우리가 수정 d()하지 않고 (그리고없이 local -n) 할 수 있음을 보여줍니다 !

_xcapture.NET Framework에서 모든 권한을 작성할 수 있었 으므로 필요하지 않습니다 eval.

그러나 이렇게하는 것은 일반적으로 읽기가 쉽지 않습니다. 그리고 몇 년 후에 스크립트로 돌아 오면 큰 문제없이 다시 읽을 수 있기를 원할 것입니다.

레이스 수정

이제 경쟁 조건을 수정하겠습니다.

트릭은 printfSTDOUT이 닫힐 때까지 기다린 다음 출력하는 것 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 "$@")"; }

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을 캡처해야하므로 먼저 '3> & 1'을 사용하여 FD3 (물론 다른 사용자도 사용할 수 있음)에 "저장"한 다음 >&3.
  • $("${@:2}" 3<&-; _passback x >&3)애프터 완료 _passback, 서브 쉘이 STDOUT을 닫습니다.
  • 따라서 시간 얼마나 걸리는지에 관계없이 printf이전에 발생할 수 없습니다 ._passback_passback
  • 있습니다 printf우리가에서 유물을 볼 수 있도록 명령은 전체 명령 행하기 전에 실행되지는 조립 printf독립적 방법을 printf구현한다.

따라서 먼저 _passback실행 한 다음 printf.

이것은 경합을 해결하고 하나의 고정 파일 설명자 3을 희생합니다. 물론 FD3가 쉘 스크립트에서 자유롭지 않은 경우 다른 파일 설명자를 선택할 수 있습니다.

3<&-함수에 전달되는 FD3를 보호하는 에도 유의하십시오 .

더 일반적으로 만드십시오

_captured()재사용 성 관점에서 잘못된 에 속하는 부품을 포함 합니다. 이것을 해결하는 방법?

글쎄요, 한 가지 더, 추가 함수를 도입하여 필사적으로 수행하십시오. 올바른 것을 반환해야하는 추가 함수는 첨부 된 원래 함수의 이름을 따서 명명되었습니다 _.

이 함수는 실제 함수 이후에 호출되며 사물을 증가시킬 수 있습니다. 이렇게하면 주석으로 읽을 수 있으므로 매우 읽기 쉽습니다.

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

  • The solution pollutes a file descriptor (here 3) by using it internally. You need to keep that in mind if you happen to pass FDs.
    Note thatbash 4.1 and above has {fd} to use some unused FD.
    (Perhaps I will add a solution here when I come around.)
    Note that this is why I use to put it in separate functions like _capture, because stuffing this all into one line is possible, but makes it increasingly harder to read and understand

  • Perhaps you want to capture STDERR of the called function, too. Or you want to even pass in and out more than one filedescriptor from and to variables.
    I have no solution yet, however here is a way to catch more than one FD, so we can probably pass back the variables this way, too.

Also do not forget:

This must call a shell function, not an external command.

There is no easy way to pass environment variables out of external commands. (With LD_PRELOAD= it should be possible, though!) But this then is something completely different.

Last words

This is not the only possible solution. It is one example to a solution.

As always you have many ways to express things in the shell. So feel free to improve and find something better.

The solution presented here is quite far from being perfect:

  • It was nearly not testet at all, so please forgive typos.
  • There is a lot of room for improvement, see above.
  • It uses many features from modern bash, so probably is hard to port to other shells.
  • And there might be some quirks I haven't thought about.

However I think it is quite easy to use:

  • Add just 4 lines of "library".
  • Add just 1 line of "annotation" for your shell function.
  • Sacrifices just one file descriptor temporarily.
  • And each step should be easy to understand even years later.

Maybe you can use a file, write to file inside function, read from file after it. I have changed e to an array. In this example blanks are used as separator when reading back the array.

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

Output:

hi
first second third
first
second
third

What you are doing, you are executing test1

$(test1)

in a sub-shell( child shell ) and Child shells cannot modify anything in parent.

You can find it in bash manual

Please Check: Things results in a subshell here


I had a similar problem, when I wanted to automatically remove temp files I had created. The solution I came up with was not to use command substitution, but rather to pass the name of the variable, that should take the final result, into the function. E.g.

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

So, in your case that would be:

#!/bin/bash

e=2

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

test1 ret

echo "$ret"
echo "$e"

Works and has no restrictions on the "return value".


It's because command substitution is performed in a subshell, so while the subshell inherits the variables, changes to them are lost when the subshell ends.

Reference:

Command substitution, commands grouped with parentheses, and asynchronous commands are invoked in a subshell environment that is a duplicate of the shell environment


You can always use an alias:

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

참고URL : https://stackoverflow.com/questions/23564995/how-to-modify-a-global-variable-within-a-function-in-bash

반응형