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はサブシェル内で設定されますが、親シェルでは設定されません。サブシェルからその親に値を渡す方法は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

ポイントをありがとう、しかし私は文字列配列を返さなければなりません、そして関数内で私は2つのグローバル文字列配列に要素を追加しなければなりません。
harrison4 2014年

3
変数に割り当てずに関数を実行すると、関数内のすべてのグローバル変数が更新されることがわかります。文字列配列を返す代わりに、関数の文字列配列を更新して、関数が終了した後でそれを別の変数に割り当てるのはどうですか?

@JohnDoe:関数から「文字列配列」を返すことはできません。できることは、文字列を出力することだけです。:しかし、あなたはこのような何か行うことができますsetarray() { declare -ag "$1=(a b c)"; }
RICI

34

{fd}またはを使用する場合は、bash 4.1が必要ですlocal -n

残りはbash 3.xで動作するはずです。私は完全に確かではありませんprintf %q-これはbash 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いるため、危険なパスです。不適切に行われると、多くの悪事の危険を冒します。しかし、正しく行われていれば、にバグがない限り、これは完全に安全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 ..の問題です。

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

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

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

少し後戻りして、やりたいことを簡単に表現できる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親シェルに戻されておらず、もう少しタイプする必要があるため、これはほとんど変更されていないようです。

ただし、関数にうまくラップされているため、シェルの能力を最大限に発揮できます。

再利用しやすいインターフェースについて考える

2つ目は、DRY(Do n't Repeat Yourself)になりたいということです。だから私たちは間違いなく次のようなものを入力したくない

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これは、という名前の1つの変数をシャドウイング(焼き付け)するため、この変数を返すことはできません。
  • それは協力する必要があります _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;before printfまたはを追加すると、自分でこれを確認できます_passback_xcapture a d; echo次に、出力xまたはa最初に、それぞれ。
  • はの_passback x一部であってはなりません_xcapture。これは、そのレシピを再利用することを困難にするためです。
  • また、ここにはいくつかの不要なフォーク($(cat))がありますが、この解決策なので、!DO NOT USE!私は最短ルートをとりました。

しかし、これは、変更せずにd()(そしてなしでlocal -n)実行できることを示しています!

_xcaptureですべての権利を記述できたため、必ずしも必要ではないことに注意してくださいeval

ただし、これを行うことは通常、あまり読みやすくありません。また、数年以内にスクリプトに戻ってきた場合は、それほど問題なくもう一度読みたいと思うでしょう。

レースを修正する

次に、競合状態を修正しましょう。

トリックはprintf、STDOUTが閉じるまで待機してから、出力すること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コマンドラインが完全にアセンブルされるまでコマンドは実行されないためprintfprintf実装方法とは関係なく、からのアーティファクトを確認できないことに注意してください。

したがって、最初に_passback実行され、次にprintf

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

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

より一般的にする

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

さて、もう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=$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に合格した場合は、そのことを覚えておく必要があります。4.1以降では、未使用のFDを使用する必要があることに
    注意してください。 (おそらく、ここで解決策を追加します。) これがのように別の関数に配置するのに使用している理由です。これをすべて1行に詰め込むことは可能ですが、読み取りと理解がますます難しくなります。bash{fd}

    _capture

  • おそらく、呼び出された関数のSTDERRもキャプチャしたいでしょう。または、複数のファイル記述子を変数に渡したり、変数に渡したりすることもできます。
    私にはまだ解決策はありませんが、ここでは複数のFDをキャッチする方法があるため、おそらくこの方法でも変数を返すことができます。

また、忘れないでください:

これは、外部コマンドではなく、シェル関数を呼び出す必要があります。

外部コマンドから環境変数を渡す簡単な方法はありません。(LD_PRELOAD=しかし、それは可能です!)しかし、これは完全に異なるものです。

最後の言葉

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

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

ここで提示された解決策は、完璧であるとはほど遠いものです。

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

しかし、私はそれが非常に使いやすいと思います:

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

2
you are awesome
Eliran Malka

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)

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

あなたは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回-呼び出しごとに1つ繰り返します)。他に方法はありませんね。
harrison4 2014年

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回の書き込み操作と読み取り操作の間でディスクの内容を永続化する必要がある場合もあります。


-1

エイリアスはいつでも使用できます。

alias next='printf "blah_%02d" $count;count=$((count+1))'
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.