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


104

나는 이것으로 일하고있다 :

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를 에코하여 반환 할 수 있습니다. 아니면 원하는 모든 것을 에코하고 결과를 구문 분석합니까?

답변:


98

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

요점을 주셔서 감사합니다.하지만 문자열 배열을 반환해야하고 함수 내에서 두 개의 전역 문자열 배열에 요소를 추가해야합니다.
harrison4 2014 년

3
변수에 할당하지 않고 함수를 실행하면 그 안의 모든 전역 변수가 업데이트된다는 것을 알 수 있습니다. 문자열 배열을 반환하는 대신 함수의 문자열 배열을 업데이트 한 다음 함수가 완료된 후 다른 변수에 할당하지 않는 이유는 무엇입니까?

@JohnDoe : 함수에서 "문자열 배열"을 반환 할 수 없습니다. 할 수있는 일은 문자열을 인쇄하는 것뿐입니다. 그러나 다음과 같이 할 수 있습니다.setarray() { declare -ag "$1=(a b c)"; }
rici

34

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

  • 이 솔루션은 내부적으로 사용하여 파일 설명자 (여기서는 3)를 오염시킵니다. FD를 통과하는 경우이를 염두에 두어야합니다.
    참고 bash위 4.1은이 {fd}일부 사용되지 않는 FD를 사용 할 수 있습니다.
    (아마도 내가 돌아올 때. 여기에 솔루션을 추가 할 것입니다)
    내가 좋아하는 별도의 기능에 넣어 사용하는 이유가 있음을 참고 _capture한 줄에이 모든 때우는 것이 가능하기 때문에,하지만 점점 더 열심히 읽고 이해할 수 있습니다

  • 호출 된 함수의 STDERR도 캡처하고 싶을 것입니다. 또는 변수에서 둘 이상의 파일 설명자를 전달하고 싶을 수도 있습니다.
    아직 해결책이 없지만 여기에 둘 이상의 FD를 잡는 방법이 있으므로이 방법으로 변수를 다시 전달할 수도 있습니다.

또한 잊지 마세요 :

이것은 외부 명령이 아닌 쉘 함수를 호출해야합니다.

외부 명령에서 환경 변수를 전달하는 쉬운 방법은 없습니다. ( LD_PRELOAD=하지만 가능해야합니다!) 그러나 이것은 완전히 다른 것입니다.

마지막 말

이것이 가능한 유일한 해결책은 아닙니다. 솔루션의 한 예입니다.

항상 그렇듯이 쉘에서 사물을 표현하는 방법은 많습니다. 그러니 자유롭게 개선하고 더 나은 것을 찾으십시오.

여기에 제시된 솔루션은 완벽하지 않습니다.

  • 거의 testet이 아니니 오타를 용서 해주세요.
  • 개선의 여지가 많이 있습니다. 위를 참조하십시오.
  • 그것은 현대의 많은 기능을 사용 bash하므로 아마도 다른 쉘로 이식하기가 어려울 것입니다.
  • 그리고 내가 생각하지 못한 몇 가지 단점이있을 수 있습니다.

그러나 나는 사용하기가 매우 쉽다고 생각합니다.

  • 4 줄의 "라이브러리"만 추가하십시오.
  • 쉘 기능에 대해 단 한 줄의 "주석"을 추가하십시오.
  • 일시적으로 하나의 파일 설명 자만 희생합니다.
  • 그리고 각 단계는 몇 년 후에도 이해하기 쉬워야합니다.

2
당신은 최고입니다
Eliran 말카

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)

하위 셸 (자식 셸) 및 자식 셸에서는 parent에서 아무것도 수정할 수 없습니다 .

bash 매뉴얼 에서 찾을 수 있습니다.

확인하십시오 : 상황은 여기 에 하위 쉘이 됩니다.


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

이 문제에 대한 해결책은 복잡한 기능을 도입하고 원래 기능을 크게 수정할 필요없이 값을 임시 파일에 저장하고 필요할 때 읽고 쓰는 것입니다.

이 접근 방식은 bats 테스트 케이스에서 여러 번 호출되는 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))'
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.