私はこれで働いています:
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
はサブシェル内で設定されますが、親シェルでは設定されません。サブシェルからその親に値を渡すには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
サンプルを次のように変更して、目的の効果をアーカイブできます。
# 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
が必要です。_
が追加されています)_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="$("$@")"
}
は、すべてを引用しており、シェルコンテキストで安全に再利用できます。
これは見苦しいだけでなく、入力することも多いため、エラーが発生しやすくなります。たった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!
の一部であってはなりません。これにより、そのレシピを再利用することが難しくなります。$(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
を出力することです。
これをアーカイブするには多くの方法があります。
最後のパスをたどると、次のようになります(ここでうまく機能するため、printf
が最後に実行されることに注意してください)。
4 20171129-144845 20171129-144845 20171129-144845 20171129-144845
出力
_passback x
なぜこれが正しいのですか?
>&3
は、STDOUTと直接通信します。_passback
で再利用します。$("${@: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
の多くの機能を使用するため、他のシェルに移植するのはおそらく難しいでしょう。ただし、非常に使いやすいと思います。
ファイルを使用したり、関数内のファイルに書き込んだり、その後のファイルから読み取ったりできます。 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 $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"
動作し、「戻り値」に制限はありません。
これは、コマンドの置換がサブシェルで実行されるため、サブシェルが変数を継承している間、サブシェルが終了すると変数への変更が失われるためです。
参照 :
コマンド置換、括弧でグループ化されたコマンド、および非同期コマンドは、シェル環境の複製であるサブシェル環境で呼び出されます