グローバル変数が異常に動作するのはwhileループですか、それともパイプラインですか?

グローバル変数が異常に動作するのはwhileループですか、それともパイプラインですか?

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なりたいのですが、それは次のようになります:10

$ ./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表示されるかを確認してください。$COUNTERbreak_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)

関連情報