COUNTER
誰かが(私の観点から)次のコードで変数の奇妙な動作を説明しますか?
#!/bin/bash
COUNTER=0
function increment {
((COUNTER++))
}
function report {
echo "COUNTER: $COUNTER ($1)"
}
function reset_counter {
COUNTER=0
}
function increment_if_yes {
answer=$1
if [ "$answer" == "yes" ]
then
increment
fi
}
function break_it {
echo -e "maybe\nyes\nno" | \
while read LL
do
increment_if_yes $LL
done
}
report start # counter should be 0
increment
report one
increment_if_yes yes
report two
increment_if_yes no
report "still two"
reset_counter
report reset
break_it
report "I'd expect one"
私はそれがスクリプトの終わりにCOUNTER
なりたいのですが、それは次のようになります:1
0
$ ./broken_variable.sh
COUNTER: 0 (start)
COUNTER: 1 (one)
COUNTER: 2 (two)
COUNTER: 2 (still two)
COUNTER: 0 (reset)
COUNTER: 0 (I'd expect one)
答え1
OPの現在のコードはで期待どおりに機能し、ksh
他のシェルでも機能できますが、機能しない可能性がありますbash
。
ループがbreakit()
子プロセス内で実行されるようにします。これは、最終的にwhileループのすべての関数呼び出しが子プロセス内でも実行されることを意味します。echo ... | while ...
while
子プロセス(while
この場合はループ)はコピー変数COUNTER
なので、子プロセスの変更は次のようにのみCOUNTER
適用されます。コピー変わりやすい。子プロセスが終了するとコピー紛失しましたCOUNTER
。制御権が親プロセスに返された場合(元)、COUNTER
変数は子プロセスを開始する前と同じ値を持ちます。
while
目的の動作を達成するには、ループが親プロセスで実行されていることを確認する必要があります。プロセス置換を使用する1つの方法:
while read LL
do
increment_if_yes "$LL"
done < <( echo -e "maybe\nyes\nno" )
答え2
この簡単な例が役に立ちます。
$ c=0
$ printf 'a\nb\nc\n' | while read i; do (( c++ )); echo "c is now $c"; done
c is now 1
c is now 2
c is now 3
$ echo "$c"
0
ご覧のとおり、これはスクリプトで観察された動作を再現します。その理由は、データをパイピングするため、while
すべての親変数のコピーを継承するサブシェルが開始されますが、ループが終了した場合、そのコピーを親変数に再エクスポートするわけではないためです。つまり、COUNTER
変数を増やすことなく変数のコピーを増やし、そのコピーはループの後すぐに削除されます。
修正されたバージョンのスクリプトを試してみると、実際に動作することがわかります。
#!/bin/bash
COUNTER=0
function increment {
echo "increment called"
((COUNTER++))
}
function report {
echo "COUNTER: $COUNTER ($1)"
}
function reset_counter {
COUNTER=0
}
function increment_if_yes {
answer=$1
if [ "$answer" == "yes" ]
then
increment
fi
}
function break_it {
echo "aa COUNTER at start of break_it: $COUNTER"
echo -e "maybe\nyes\nno" | \
while read LL
do
echo "bb COUNTER in loop top: $COUNTER"
increment_if_yes $LL
echo "bb COUNTER in loop bottom: $COUNTER"
done
echo "aa COUNTER at end of break_it: $COUNTER"
}
report start # counter should be 0
increment
report one
increment_if_yes yes
report two
increment_if_yes no
report "still two"
reset_counter
report reset
break_it
report "I'd expect one"
この印刷を実行します。
COUNTER: 0 (start)
increment called
COUNTER: 1 (one)
increment called
COUNTER: 2 (two)
COUNTER: 2 (still two)
COUNTER: 0 (reset)
aa COUNTER at start of break_it: 0
bb COUNTER in loop top: 0
bb COUNTER in loop bottom: 0
bb COUNTER in loop top: 0
increment called
bb COUNTER in loop bottom: 1
bb COUNTER in loop top: 1
bb COUNTER in loop bottom: 1
aa COUNTER at end of break_it: 0
COUNTER: 0 (I'd expect one)
スクリプトの残りの部分で使用できる変数ではなく、実際にその変数のコピーであることを除いて、関数で名前付き変数が増加するという値がどのようにbb COUNTER
表示されるかを確認してください。$COUNTER
break_it
最後に、bashマニュアルのコマンド実行環境セクションをよく読むこともできます。特に以下を強調します。
組み込み機能やシェル機能以外の簡単なコマンドを実行したい場合、別の実行環境で呼び出されます。これには以下が含まれます。特に明記しない限り、これらの値はシェルから継承されます。
- シェルの開かれたファイルとコマンドにリダイレクトされ、指定された修正と追加
- 現在の作業ディレクトリ
- ファイル生成モードマスク
- エクスポート用にマークされたシェル変数と関数コマンドのエクスポートされた変数が環境に渡されます(環境を参照)。
- シェルが捕捉したトラップは、シェルの親から継承された値にリセットされ、シェルが無視したトラップは無視されます。
この別の環境で呼び出されるコマンドは影響を与えません。 シェルの実行環境。
最後の文は問題の核心です。別の環境で呼び出されたコマンドは親環境に影響を与えることができないため、目的の方法で変数を増やすことはできません。
ただし、シェルがサポートしているため可能です。プロセスの交換、機能を次のように変更すると機能します。
function break_it {
while read LL
do
increment_if_yes $LL
done < <(printf 'maybe\nyes\nno\n')
}
元のスクリプトを実行し、上記のbreak_it
ように関数を変更すると、次のような結果が得られます。
$ foo.sh
COUNTER: 0 (start)
COUNTER: 1 (one)
COUNTER: 2 (two)
COUNTER: 2 (still two)
COUNTER: 0 (reset)
COUNTER: 1 (I'd expect one)