web-dev-qa-db-ja.com

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に保存し、グローバル変数も変更するにはどうすればよいですか?

68
harrison4

コマンド置換(つまり$(...)コンストラクト)を使用すると、サブシェルが作成されます。サブシェルは親シェルから変数を継承しますが、これは一方向にしか機能しません。サブシェルは親シェルの環境を変更できません。変数eはサブシェル内で設定されますが、親シェルでは設定されません。サブシェルからその親に値を渡すには2つの方法があります。最初に、何かを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
74
Josh Jolly

概要

サンプルを次のように変更して、目的の効果をアーカイブできます。

# 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'によるものです。# This needs a modern bash (see "help declare" if "-n" is present) : capture VARIABLE command args.. capture() { local -n output="$1" shift output="$("$@")" } は、すべてを引用しており、シェルコンテキストで安全に再利用できます。

しかし、これはa。の痛みです。

これは見苦しいだけでなく、入力することも多いため、エラーが発生しやすくなります。たった1つの間違いで、あなたは運命にありますよね?

さて、私たちはシェルレベルにいるので、改善することができます。見たいインターフェイスを考えて、それを実装することができます。

拡張、シェルが物事を処理する方法

少し戻って、やりたいことを簡単に表現できるAPIについて考えてみましょう。

さて、d()関数で何をしたいのでしょうか?

出力を変数にキャプチャする必要があります。それでは、まさにこれのためのAPIを実装しましょう。

d1=$(d)

今、書く代わりに

capture d1 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

さて、変数はdから親シェルに戻されないため、あまり変更していないように見えます。もう少し入力する必要があります。

ただし、シェルは機能でうまくラップされているため、シェルの全機能を使用できます。

再利用しやすいインターフェイスを考えてください

もう1つは、DRY(自分自身を繰り返さないでください)になりたいということです。したがって、次のように入力することは絶対にしたくない

_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

ここのxは冗長であるだけでなく、常に正しいコンテキストで繰り返されるエラーです。スクリプトで1000回使用してから変数を追加するとどうなりますか? dへの呼び出しが関係する1000の場所すべてを絶対に変更する必要はありません。

したがって、xを残して、次のように記述できます。

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

出力

_passback

これはすでに非常によく見えます。

d()の変更を避ける

最後のソリューションにはいくつかの大きな欠陥があります。

  • d()を変更する必要があります
  • 出力を渡すには、xcaptureの内部詳細を使用する必要があります。
    • これはoutputという名前の1つの変数をシャドウ(焼き付け)するため、この変数を戻すことはできません。
  • _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 と協力する必要があります

これも取り除くことができますか?

もちろん、我々はできます!私たちはシェルにいるので、これを行うために必要なものはすべてあります。

evalの呼び出しに少し近づいたら、この場所で100%制御できることがわかります。 evalの「内側」にサブシェルがあるため、親のシェルに悪いことをすることを恐れずに、必要なすべてを実行できます。

ええ、ニース、別のラッパーを追加してみましょう。今すぐevalの内部に:

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

プリント

!DO NOT USE!

ただし、これには、いくつかの大きな欠点があります。

  • _passback xマーカーがあります。これには非常に悪い競合状態があり、簡単に見ることができないためです:
    • >(printf ..)はバックグラウンドジョブです。したがって、sleep 1;の実行中に実行される可能性があります。
    • printfまたは_passbackの前に_xcapture a d; echoを追加すると、これを自分で確認できます。 _passback xは、それぞれxまたはaを最初に出力します。
  • _xcaptureは、!DO NOT USE!の一部であってはなりません。これにより、そのレシピを再利用することが難しくなります。
  • また、ここにはいくつかのunnededフォーク($(cat))がありますが、このソリューションは_xcaptureなので、最短のルートを取りました。

ただし、これは、d()を変更せずに実行できることを示しています。

evalにすべての権利を書くことができたので、必ずしも_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 を必要としないことに注意してください。

ただし、これを行うことは通常、読みにくいです。また、数年後にスクリプトに戻ってきた場合、おそらく問題なく再び読むことができます。

レースを修正

それでは、競合状態を修正しましょう。

トリックは、printfがSTDOUTを閉じるまで待機してから、xを出力することです。

これをアーカイブするには多くの方法があります。

  • パイプは異なるプロセスで実行されるため、シェルパイプは使用できません。
  • 一時ファイルを使用できますが、
  • またはロックファイルまたはfifoのようなもの。これにより、ロックまたはfifoを待つことができます。
  • または情報を出力するために異なるチャネルを使用し、正しい順序で出力を組み立てます。

最後のパスをたどると、次のようになります(ここでうまく機能するため、printfが最後に実行されることに注意してください)。

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

出力

_passback x

なぜこれが正しいのですか?

  • >&3は、STDOUTと直接通信します。
  • ただし、STDOUTは内部コマンドでキャプチャする必要があるため、最初に「3>&1」でFD3に「保存」し(もちろん、他のものも使用できます)、_passbackで再利用します。
  • サブシェルがSTDOUTを閉じると、$("${@:2}" 3<&-; _passback x >&3)_passbackの後に終了します。
  • したがって、printfは、_passbackの時間に関係なく、_passbackの前に発生することはできません。
  • printfコマンドは、完全なコマンドラインがアセンブルされるまで実行されないため、printfのアーティファクトは、printfの実装方法とは無関係に見ることができません。

したがって、最初に3<&-が実行され、次にprintfが実行されます。

これにより、競合が解決され、1つの固定ファイル記述子3が犠牲になります。もちろん、FD3がシェルスクリプトでフリーでない場合は、別のファイル記述子を選択できます。

関数に渡されるFD3を保護する_captureにも注意してください。

より一般的にする

_には、d()に属するパーツが含まれていますが、これは再利用性の観点からは悪いです。これを解決するには?

さて、もう1つ、適切なものを返す必要のある追加の関数を導入することにより、別の方法でそれを行います。

この関数は実関数の後に呼び出され、物事を補強できます。これにより、これは何らかの注釈として読み取ることができるため、非常に読みやすくなります。

_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=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=を使用することも可能です!)しかし、これはまったく異なるものです。

最後の言葉

これが唯一の可能な解決策ではありません。これは解決策の一例です。

いつものように、シェルで物事を表現する多くの方法があります。自由に改善して、より良いものを見つけてください。

ここで紹介するソリューションは、完璧とはほど遠いものです。

  • まったくテストされていなかったので、タイプミスはご容赦ください。
  • 改善の余地はたくさんあります。上記を参照してください。
  • 現代のbashの多くの機能を使用するため、他のシェルに移植するのはおそらく難しいでしょう。
  • そして、私が考えていないいくつかの癖があるかもしれません。

ただし、非常に使いやすいと思います。

  • 「ライブラリ」を4行だけ追加します。
  • シェル関数に「注釈」を1行だけ追加します。
  • 一時的に1つのファイル記述子のみを犠牲にします。
  • そして、各ステップは数年後でも簡単に理解できるはずです。
16
Tino

ファイルを使用したり、関数内のファイルに書き込んだり、その後のファイルから読み取ったりできます。 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
Ashkan

あなたがしていること、あなたはtest1を実行しています

$(test1)

サブシェル(子シェル)および子シェルでは、親の何も変更できません。

Bashmanualで見つけることができます

チェックしてください:サブシェルになりますhere

11
PradyJord

作成した一時ファイルを自動的に削除したいときに、同様の問題が発生しました。私が思いついた解決策は、コマンド置換を使用するのではなく、変数の名前を渡すことでした。変数の名前は関数に最終結果を取得する必要があります。例えば。

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

動作し、「戻り値」に制限はありません。

4
Elmar Zander

これは、コマンドの置換がサブシェルで実行されるため、サブシェルが変数を継承している間、サブシェルが終了すると変数への変更が失われるためです。

参照

コマンド置換、括弧でグループ化されたコマンド、および非同期コマンドは、シェル環境の複製であるサブシェル環境で呼び出されます