Bashループのカウンターの増分が機能しない


125

ループを実行していて、を維持したい次の簡単なスクリプトがありCOUNTERます。カウンターが更新されない理由を理解できません。サブシェルが作成されるためですか?どうすればこれを修正できますか?

#!/bin/bash

WFY_PATH=/var/log/nginx
WFY_FILE=error.log
COUNTER=0
grep 'GET /log_' $WFY_PATH/$WFY_FILE | grep 'upstream timed out' | awk -F ', ' '{print $2,$4,$0}' | awk '{print "http://domain.com"$5"&ip="$2"&date="$7"&time="$8"&end=1"}' | awk -F '&end=1' '{print $1"&end=1"}' |
(
while read WFY_URL
do
    echo $WFY_URL #Some more action
    COUNTER=$((COUNTER+1))
done
)

echo $COUNTER # output = 0


whileループをサブシェルに入れる必要はありません。whileループの周りのブラケットを削除するだけで十分です。または、ループをサブシェルに入れる必要がある場合は、しばらくしてからカウンターを一時ファイルに一度ダンプし、このファイルをサブシェルの外で復元します。回答の最終手順をご用意いたします。
Znik

回答:


156

まず、カウンターを増やしていません。変更COUNTER=$((COUNTER))COUNTER=$((COUNTER + 1))またはCOUNTER=$[COUNTER + 1]それを増加します。

第2に、想定どおり、サブシェル変数を呼び出し先に逆伝播するのはより困難です。サブシェル内の変数は、サブシェルの外部では使用できません。これらは、子プロセスにローカルな変数です。

これを解決する1つの方法は、中間値を格納するために一時ファイルを使用することです。

TEMPFILE=/tmp/$$.tmp
echo 0 > $TEMPFILE

# Loop goes here
  # Fetch the value and increase it
  COUNTER=$[$(cat $TEMPFILE) + 1]

  # Store the new value
  echo $COUNTER > $TEMPFILE

# Loop done, script done, delete the file
unlink $TEMPFILE

30
$ [...]は非推奨です。
-chepner

1
@chepner $[...]廃止予定であるという参照がありますか?代替ソリューションはありますか?
2014年

9
$[...]で使用されたbash前の$((...))POSIXシェルで採択されました。正式に非推奨になったかどうかはわかりませんが、bashmanページには記載されておらず、下位互換性のためにのみサポートされているようです。
chepner 2014年

また、$(...)が優先されます...
Lennart Rolland 14

7
:ここでは@blongする$ [...]対$((...))のSOの質問が説明し、参照廃止ということですstackoverflow.com/questions/2415724/...
オーガPsalm33

87
COUNTER=1
while [ Your != "done" ]
do
     echo " $COUNTER "
     COUNTER=$[$COUNTER +1]
done

テスト済みBASH:Centos、SuSE、RH


1
@kroonwijk角括弧の前にスペースが必要です(正式に言えば「単語を区切る」ため)。そうでなければ、bashは前の式の終わりを見ることができません。
EdwardGarson 2018年

1
質問はしばらくパイプを使用していたため、サブシェルが作成された場合、あなたの答えは正しいですが、パイプを使用しないため、質問に答えていません
chrisweb

2
chepnerの別の回答に対するコメントに従って、$[ ]構文は非推奨になりました。stackoverflow.com/questions/10515964/...
マーク・Haferkamp

これはメインの質問を解決しません。メインループはサブシェルの下に配置されます
Znik

42
COUNTER=$((COUNTER+1)) 

現代のプログラミングでは、非常に扱いにくい構成です。

(( COUNTER++ ))

より「モダン」に見えます。あなたも使うことができます

let COUNTER++

あなたがそれが読みやすさを改善すると思うなら。時々、Bashは物事を実行する方法が多すぎます-私が思うPerlの哲学-多分、Pythonが「それを行うにはたった1つの正しい方法しかない」がより適切かもしれないとき。それがあったとしても、それは議論の余地のある声明です!とにかく、(この場合)目的は変数をインクリメントすることだけではなく、(一般的なルール)他の誰かが理解してサポートできるコードを書くことでもあると思います。適合性はそれを達成するための長い道のりです。

HTH


これは、(サブプロセス)ループの終了後にカウンターで更新された値を取得する方法である元の質問に対処していません
Luis Vazquez

16

使ってみる

COUNTER=$((COUNTER+1))

の代わりに

COUNTER=$((COUNTER))

8
または単にlet "COUNTER++"
12

2
申し訳ありませんが、それはタイプミスでした。その実際((COUNTER + 1))
Sparsh Gupta

8
@AaronDigulla:((( COUNTER++ ))ドル記号なし)
通知があるまで一時停止。

2
理由はわかりませんが、私のスクリプトが使用中に何度も失敗する(( COUNTER++ ))のに、切り替えたときに機能するCOUNTER=$((COUNTER + 1))ことがわかりました。GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)
Steven Lu

多分あなたのハッシュバング行はbashを/ bin / bashの代わりに/ bin / shとして実行していますか?
最大

12

この1つのawk呼び出しはgrep|grep|awk|awkパイプラインと同等だと思います。テストしてください。最後のawkコマンドは何も変更していないようです。

COUNTERの問題は、whileループがサブシェルで実行されているため、サブシェルが終了すると、変数への変更がすべて消えることです。同じサブシェルでCOUNTERの値にアクセスする必要があります。または、@ DennisWilliamsonのアドバイスに従って、プロセス置換を使用し、サブシェルを完全に回避します。

awk '
  /GET \/log_/ && /upstream timed out/ {
    split($0, a, ", ")
    split(a[2] FS a[4] FS $0, b)
    print "http://example.com" b[5] "&ip=" b[2] "&date=" b[7] "&time=" b[8] "&end=1"
  }
' | {
    while read WFY_URL
    do
        echo $WFY_URL #Some more action
        (( COUNTER++ ))
    done
    echo $COUNTER
}

1
おかげで、最後のawkは基本的にend = 1の後のすべてを削除し、新しいend = 1を最後に配置します(そのため、次に追加されたすべてを削除できるようになります)。
Sparsh Gupta、2012

1
@SparshGupta、以前のawkは「end = 1」の後に何も出力しません。
グレン・ジャックマン

これは質問スクリプトを非常に改善しますが、サブシェル内のカウンターの増加に関する問題は解決しません
Znik


11

一時ファイルを使用する代わりに、whileプロセス置換を使用して、ループの周りにサブシェルを作成することを回避できます。

while ...
do
   ...
done < <(grep ...)

ところで、これらすべてをgrep, grep, awk, awk, awk単一のに変換できるはずawkです。

Bash 4.2以降、次のlastpipeオプションがあります。

現在のシェルコンテキストでパイプラインの最後のコマンドを実行します。ジョブ制御が有効になっている場合、lastpipeオプションは効果がありません。

bash -c 'echo foo | while read -r s; do c=3; done; echo "$c"'

bash -c 'shopt -s lastpipe; echo foo | while read -r s; do c=3; done; echo "$c"'
3

ループ内でカウンターをインクリメントし、実行後にそれを使用したい場合、プロセス置換は素晴らしいです。プロセス置換の問題は、実行されたコマンドのステータスコードも取得できないことです。これは、パイプを使用しているときに可能です。 $ {PIPESTATUS [*]}を使用して
chrisweb

@chrisweb:に関する情報を追加しましたlastpipe。ちなみに、おそらく"${PIPESTATUS[@]}"(アスタリスクの代わりに)を使用する必要があります。
追って通知があるまで一時停止。

エラッタ。bash(誤って以前に書いたためperlではありません)では、終了コードはテーブルなので、パイプチェーン内のすべての終了コードを個別にチェックできます。最初にテストする前に、ステップをこのテーブルにコピーする必要があります。そうしないと、最初のコマンドの後にすべての値が失われます。
Znik

これは私にとって有効な解決策であり、外部ファイルを使用せずに変数の値を格納することはありません。
ルイスバスケス

8

ミニマリスト

counter=0
((counter++))
echo $counter

単純なもの:-)。ありがとう@geekzspot
フセインK

サブシェルがあるため、問題の例では機能しません
Znik

3

これがあなたがする必要があるすべてです:

$((COUNTER++))

Learning the bash Shell、3rd Edition、pp。147、148からの抜粋です。

bashの算術式は、JavaおよびC言語の対応するものと同等です。[9] 優先順位と結合性はCと同じです。表6-2に、サポートされている算術演算子を示します。これらの一部は特殊文字(またはそれらを含む)ですが、$((...))構文内にあるため、バックスラッシュでエスケープする必要はありません。

..........................

++および-演算子は、値を1つずつ増減する場合に便利です。[11] これらはJavaやCと同じように機能します。たとえば、 ++は値を1ずつインクリメントします。これはpost-incrementと呼ばれます。あり、プリインクリメント ++:。違いは例で明らかになります:

$ i=0
$ echo $i
0
$ echo $((i++))
0
$ echo $i
1
$ echo $((++i))
2
$ echo $i
2

http://www.safaribooksonline.com/a/learning-the-bash/7572399/を参照してください


これは、私がifステートメントの条件で使用していたため、これが必要なバージョンです。 if [[ $((needsComma++)) -gt 0 ]]; then printf ',\n'; fi 正しいか間違っているか、これは確実に機能した唯一のバージョンです。
LS

このフォームで重要なのは、1つのステップで増分を使用できることです。i=1; while true; do echo $((i++)); sleep .1; done
Bruno Bronosky、

1
@LS:if (( needsComma++ > 0 )); thenまたはif (( needsComma++ )); then
通知があるまで一時停止。

bashで "echo $((i ++))"を使用すると、常に "/opt/xyz/init.sh:line 29:i:command not found"が表示されます。
mmo

これは、ループの外でカウンター値を取得することに関する質問には対応していません。
ルイスバスケス

1

これは簡単な例です

COUNTER=1
for i in {1..5}
do   
   echo $COUNTER;
   //echo "Welcome $i times"
   ((COUNTER++));    
done

1
簡単な例ですが、質問には適用できません。
Znik

0

counterスクリプトを更新していないようです。使用してくださいcounter++


タイプミスの謝罪、私は実際に((COUNTER + 1))をスクリプトで使用しています
Sparsh Gupta

value + 1またはvalue ++によってインクリメントされても関係ありません。サブシェルが終了すると、カウンター値が失われ、このスクリプトの開始時に設定された初期値0に戻ります。
Znik

0

((var++))が失敗する原因となった2つの条件がありました。

  1. bashを厳格モードset -euo pipefail)に設定し増分をゼロ(0)から開始した場合。

  2. 1から始めても問題ありませんが、厳密モードでゼロ以外のリターンコードエラーである "++"を評価するときに、ゼロはインクリメントに "1"を返します。

この動作を使用する((var+=1))var=$((var+1))、回避することができます


0

ソーススクリプトのサブシェルに問題があります。最初の例では、おそらくサブシェルは必要ありません。しかし、「Some more action」に何が隠されているのかはわかりません。最も人気のある答えはバグを隠しており、I / Oが増加し、ループ内のコンピューターが復元されるため、サブシェルでは動作しません。

'\'記号を追加するのを忘れないでください。行の継続についてbashインタープリターに通知されます。私はそれがあなたや誰かを助けることを願っています。しかし、私の意見では、このスクリプトは完全にAWKスクリプトに変換するか、正規表現またはperlを使用してpythonに書き直す必要がありますが、perlの人気は何年にもわたって低下しています。Pythonでそれを行う方が良いです。

サブシェルなしの修正バージョン:

#!/bin/bash
WFY_PATH=/var/log/nginx
WFY_FILE=error.log
COUNTER=0
grep 'GET /log_' $WFY_PATH/$WFY_FILE | grep 'upstream timed out' |\
awk -F ', ' '{print $2,$4,$0}' |\
awk '{print "http://example.com"$5"&ip="$2"&date="$7"&time="$8"&end=1"}' |\
awk -F '&end=1' '{print $1"&end=1"}' |\
#(  #unneeded bracket
while read WFY_URL
do
    echo $WFY_URL #Some more action
    COUNTER=$((COUNTER+1))
done
# ) unneeded bracket

echo $COUNTER # output = 0

本当に必要な場合はサブシェル付きのバージョン

#!/bin/bash

TEMPFILE=/tmp/$$.tmp  #I've got it from the most popular answer
WFY_PATH=/var/log/nginx
WFY_FILE=error.log
COUNTER=0
grep 'GET /log_' $WFY_PATH/$WFY_FILE | grep 'upstream timed out' |\
awk -F ', ' '{print $2,$4,$0}' |\
awk '{print "http://example.com"$5"&ip="$2"&date="$7"&time="$8"&end=1"}' |\
awk -F '&end=1' '{print $1"&end=1"}' |\
(
while read WFY_URL
do
    echo $WFY_URL #Some more action
    COUNTER=$((COUNTER+1))
done
echo $COUNTER > $TEMPFILE  #store counter only once, do it after loop, you will save I/O
)

COUNTER=$(cat $TEMPFILE)  #restore counter
unlink $TEMPFILE
echo $COUNTER # output = 0
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.