การเพิ่มตัวนับใน Bash loop ไม่ทำงาน


125

ฉันมีสคริปต์ง่ายๆต่อไปนี้ที่ฉันใช้งานลูปและต้องการรักษาไฟล์COUNTER. ฉันไม่สามารถเข้าใจได้ว่าเหตุใดตัวนับจึงไม่อัปเดต เป็นเพราะการสร้าง subshell หรือไม่? ฉันจะแก้ไขปัญหานี้ได้อย่างไร

#!/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

1
ที่เกี่ยวข้อง: stackoverflow.com/questions/13726764/…
Gabriel Devillers

คุณไม่จำเป็นต้องใส่ในขณะที่วนเข้าใน subshell เพียงแค่ถอดวงเล็บออกขณะวนรอบก็เพียงพอแล้ว หรืออื่น ๆ ถ้าคุณต้องใส่มันวนซ้ำใน subshell หลังจากนั้นในขณะที่ทำเสร็จแล้วให้ถ่ายโอนข้อมูลลงในไฟล์ชั่วคราวหนึ่งครั้งและเรียกคืนไฟล์นี้นอก subshell ฉันจะเตรียมคำตอบขั้นสุดท้ายให้คุณ
Znik

คำตอบ:


156

ขั้นแรกคุณไม่ได้เพิ่มเคาน์เตอร์ เปลี่ยนCOUNTER=$((COUNTER))เป็นCOUNTER=$((COUNTER + 1))หรือCOUNTER=$[COUNTER + 1]จะเพิ่มขึ้น

ประการที่สองมันยากกว่าที่จะเผยแพร่ตัวแปร subshell กลับไปยัง callee เมื่อคุณคาดเดา ตัวแปรใน subshell ไม่พร้อมใช้งานนอก subshell ตัวแปรเหล่านี้เป็นตัวแปรเฉพาะของกระบวนการย่อย

วิธีหนึ่งในการแก้ปัญหาคือการใช้ไฟล์ชั่วคราวเพื่อจัดเก็บค่ากลาง:

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 คุณมีข้อมูลอ้างอิงที่บอกว่า$[...]เลิกใช้แล้วหรือยัง? มีทางเลือกอื่นหรือไม่?
blong

9
$[...]ถูกใช้โดยbashก่อนหน้า$((...))นี้โดย POSIX เชลล์ ฉันไม่แน่ใจว่าเคยเลิกใช้งานอย่างเป็นทางการแล้ว แต่ฉันไม่พบการกล่าวถึงในbashหน้าคนและดูเหมือนว่าจะได้รับการสนับสนุนสำหรับความเข้ากันได้แบบย้อนกลับเท่านั้น
chepner

นอกจากนี้ $ (... ) เป็นที่ต้องการมากกว่า...
Lennart Rolland

7
@blong นี่คือคำถาม SO ใน $ [... ] เทียบกับ $ ((... )) ที่กล่าวถึงและอ้างอิงถึงการเลิกใช้งาน: stackoverflow.com/questions/2415724/…
Ogre Psalm33

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

TESTED BASH: Centos, SuSE, RH


1
@kroonwijk ต้องเว้นวรรคก่อนวงเล็บเหลี่ยม (เพื่อ 'คั่นคำ' พูดอย่างเป็นทางการ) Bash ไม่สามารถมองเห็นจุดสิ้นสุดของนิพจน์ก่อนหน้านี้ได้
EdwardGarson

1
คำถามเกี่ยวกับไปป์อยู่พักหนึ่งดังนั้นเมื่อสร้าง subshell คำตอบของคุณถูกต้อง แต่คุณไม่ได้ใช้ไปป์จึงไม่ตอบคำถาม
chrisweb

2
ตามความคิดเห็นของ chepner เกี่ยวกับคำตอบอื่น$[ ]ไวยากรณ์ถูกเลิกใช้แล้ว stackoverflow.com/questions/10515964/…
Mark Haferkamp

สิ่งนี้ไม่สามารถแก้ไขคำถามหลักลูปหลักวางอยู่ใต้ subshell
Znik

42
COUNTER=$((COUNTER+1)) 

เป็นโครงสร้างที่ค่อนข้างเงอะงะในการเขียนโปรแกรมสมัยใหม่

(( COUNTER++ ))

ดู "ทันสมัย" มากขึ้น คุณยังสามารถใช้

let COUNTER++

หากคุณคิดว่าช่วยเพิ่มความสามารถในการอ่าน บางครั้ง Bash ให้วิธีการทำสิ่งต่างๆมากเกินไป - ปรัชญา Perl ที่ฉันคิดไว้ - เมื่อบางที Python "มีทางเดียวเท่านั้นที่จะทำได้" อาจจะเหมาะสมกว่า นั่นเป็นคำพูดที่ถกเถียงกันได้ถ้าเคยมี! อย่างไรก็ตามฉันขอแนะนำจุดมุ่งหมาย (ในกรณีนี้) ไม่ใช่แค่การเพิ่มตัวแปร แต่ (กฎทั่วไป) เพื่อเขียนโค้ดที่คนอื่นสามารถเข้าใจและสนับสนุนได้ ความสอดคล้องไปได้ไกลในการบรรลุเป้าหมายนั้น

HTH


นี่ไม่ได้ตอบคำถามเดิมซึ่งเป็นวิธีรับค่า updatedd ในตัวนับหลังจากสิ้นสุดลูป (กระบวนการย่อย)
Luis Vazquez

16

ลองนำไปใช้

COUNTER=$((COUNTER+1))

แทน

COUNTER=$((COUNTER))

8
หรือเพียงแค่let "COUNTER++"
nullpotent

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 / sh แทน / bin / bash?
สูงสุด

12

ฉันคิดว่าการโทร awk เดียวนี้เทียบเท่ากับgrep|grep|awk|awkไปป์ไลน์ของคุณ: โปรดทดสอบ คำสั่ง awk สุดท้ายของคุณดูเหมือนจะไม่เปลี่ยนแปลงอะไรเลย

ปัญหาเกี่ยวกับ COUNTER คือ while loop กำลังทำงานใน subshell ดังนั้นการเปลี่ยนแปลงใด ๆ ในตัวแปรจะหายไปเมื่อออกจาก subshell คุณต้องเข้าถึงค่าของ COUNTER ใน subshell เดียวกันนั้น หรือรับคำแนะนำของ @ DennisWilliamson ใช้การทดแทนกระบวนการและหลีกเลี่ยง subshell โดยสิ้นเชิง

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

1
@SparshGupta awk ก่อนหน้านี้ไม่พิมพ์อะไรเลยหลังจาก "end = 1"
Glenn Jackman

สิ่งนี้ช่วยปรับปรุงสคริปต์คำถามได้ดีมาก แต่ไม่สามารถแก้ไขปัญหาได้ด้วยการเพิ่มตัวนับภายใน subshell
Znik


11

แทนที่จะใช้ไฟล์ชั่วคราวคุณสามารถหลีกเลี่ยงการสร้าง subshell รอบ ๆ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

นี่เป็นวิธีแก้ปัญหาที่เหมาะกับฉันและไม่ต้องใช้ไฟล์ภายนอกเพื่อจัดเก็บค่าของตัวแปรซึ่งสำหรับคนเดินเท้ามากในความคิดของฉัน
Luis Vazquez

8

ที่เรียบง่าย

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

ง่ายๆ :-) ขอบคุณ @geekzspot
Hussain K

ไม่ได้ผลเช่นในคำถามเนื่องจากมี subshell
Znik

3

นี่คือสิ่งที่คุณต้องทำ:

$((COUNTER++))

นี่คือข้อความที่ตัดตอนมาจากLearning the bash Shell , 3rd Edition, pp. 147, 148:

bashนิพจน์ทางคณิตศาสตร์เทียบเท่ากับคู่ของมันในภาษา Java และ C [9] ลำดับความสำคัญและความสัมพันธ์จะเหมือนกับใน C ตารางที่ 6-2 แสดงตัวดำเนินการทางคณิตศาสตร์ที่ได้รับการสนับสนุน แม้ว่าอักขระเหล่านี้บางตัวจะเป็นอักขระพิเศษ (หรือมี) แต่ก็ไม่จำเป็นต้องแบ็กสแลช - Escape เพราะอยู่ในไวยากรณ์ $ ((... ))

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

ตัวดำเนินการ ++ และ - มีประโยชน์เมื่อคุณต้องการเพิ่มหรือลดค่าทีละค่า [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

สิ่งที่สำคัญเกี่ยวกับแบบฟอร์มนี้คือคุณสามารถใช้การเพิ่มในขั้นตอนเดียว i=1; while true; do echo $((i++)); sleep .1; done
Bruno Bronosky


การใช้ "echo $ ((i ++))" ใน bash ฉันมักจะได้รับ "/opt/xyz/init.sh: บรรทัดที่ 29: i: command not found" ฉันทำอะไรผิด?
mmo

สิ่งนี้ไม่ได้ตอบคำถามเกี่ยวกับการรับค่าตัวนับนอกลูป
Luis Vazquez

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

ไม่ว่าจะเพิ่มขึ้นด้วยค่า + 1 หรือตามค่า ++ ก็ตาม หลังจากจบ subshell ค่าตัวนับจะหายไปและเปลี่ยนกลับเป็นค่าเริ่มต้น 0 ที่ตั้งไว้เมื่อเริ่มต้นบนสคริปต์นี้
Znik

0

มีสองเงื่อนไขที่ทำให้นิพจน์((var++))ล้มเหลวสำหรับฉัน:

  1. ถ้าฉันตั้งค่า bash เป็นโหมดเข้มงวด ( set -euo pipefail) และถ้าฉันเริ่มการเพิ่มที่ศูนย์ (0)

  2. การเริ่มต้นที่หนึ่ง (1) เป็นสิ่งที่ดี แต่ศูนย์ทำให้ส่วนเพิ่มคืนค่าเป็น "1" เมื่อประเมิน "++" ซึ่งเป็นความล้มเหลวของโค้ดส่งคืนที่ไม่ใช่ศูนย์ในโหมดเข้มงวด

ฉันสามารถใช้((var+=1))หรือvar=$((var+1))เพื่อหลีกเลี่ยงพฤติกรรมนี้ได้


0

ซอร์สสคริปต์มีปัญหากับ subshell ตัวอย่างแรกคุณอาจไม่ต้องการ subshell แต่เราไม่รู้ว่ามีอะไรซ่อนอยู่ภายใต้ "Some more action" คำตอบยอดนิยมมีบั๊กซ่อนอยู่ซึ่งจะเพิ่ม I / O และจะไม่ทำงานกับ subshell เพราะจะคืนค่า couter ภายในลูป

อย่าใส่เครื่องหมาย '\' เสริมเพราะจะแจ้งให้ล่าม bash ทราบเกี่ยวกับความต่อเนื่องของบรรทัด ฉันหวังว่ามันจะช่วยคุณหรือใครก็ได้ แต่ในความคิดของฉันสคริปต์นี้ควรถูกแปลงเป็นสคริปต์ AWK อย่างสมบูรณ์หรือเขียนใหม่เป็น python โดยใช้ regexp หรือ perl แต่ความนิยม perl ในช่วงหลายปีที่ผ่านมาลดลง ดีกว่าทำด้วย python

เวอร์ชันที่แก้ไขโดยไม่มี subshell:

#!/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

เวอร์ชันที่มี subshell หากจำเป็นจริงๆ

#!/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.