Số lượt truy cập trong vòng lặp Bash không hoạt động


125

Tôi có đoạn script đơn giản sau đây khi tôi đang chạy một vòng lặp và muốn duy trì một COUNTER. Tôi không thể tìm ra lý do tại sao quầy không cập nhật. Có phải là do subshell mà được tạo ra? Làm thế nào tôi có thể sửa lỗi này?

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


Bạn không cần đặt vòng lặp while vào subshell. Chỉ cần loại bỏ dấu ngoặc quanh vòng lặp while, thế là đủ. Hoặc nếu không, bạn phải đặt vòng lặp vào subshell, sau đó trong khi thực hiện xong, kết xuất bộ đếm vào tệp tạm thời một lần và khôi phục tệp này bên ngoài subshell. Tôi sẽ chuẩn bị thủ tục cuối cùng để bạn trả lời.
Znik

Câu trả lời:


156

Đầu tiên, bạn không tăng quầy. Thay đổi COUNTER=$((COUNTER))thành COUNTER=$((COUNTER + 1))hoặc COUNTER=$[COUNTER + 1]sẽ tăng nó.

Thứ hai, sẽ khó khăn hơn trong việc truyền bá các biến phụ cho callee khi bạn phỏng đoán. Các biến trong một lớp con không có sẵn bên ngoài lớp con. Đây là các biến cục bộ cho quá trình con.

Một cách để giải quyết nó là sử dụng tệp tạm thời để lưu trữ giá trị trung gian:

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
$ [...] không dùng nữa.
chepner

1
@chepner Bạn có tài liệu tham khảo nào nói $[...]là không dùng nữa không? Có một giải pháp thay thế?
blong

9
$[...]đã được sử dụng bashtrước đây $((...))được sử dụng bởi vỏ POSIX. Tôi không chắc chắn rằng nó đã từng bị phản đối chính thức, nhưng tôi không thể thấy đề cập đến nó trong bashtrang man và nó dường như chỉ được hỗ trợ cho khả năng tương thích ngược.
chepner

Ngoài ra, $ (...) được ưa thích hơn...
Lennart Rolland

7
@blong Đây là một câu hỏi SO về $ [...] so với $ ((...)) thảo luận và tham khảo về sự phản đối: stackoverflow.com/questions/2415724/
Lỗi

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

BASH KIỂM TRA: Centos, SuSE, rh


1
@kroonwijk cần có một khoảng trắng trước dấu ngoặc vuông (để 'phân định các từ', nói chính thức). Bash không thể nhìn thấy kết thúc của biểu thức trước.
EdwardGarson

1
các câu hỏi diễn ra trong một khoảng thời gian với một đường ống, do đó, nơi một mạng con được tạo ra, câu trả lời của bạn là đúng nhưng bạn không sử dụng một đường ống để nó không trả lời câu hỏi
chrisweb

2
Mỗi bình luận của chepner về một câu trả lời khác, $[ ]cú pháp không được chấp nhận. stackoverflow.com/questions/10515964/
Mark Haferkamp

điều này không giải quyết được câu hỏi chính, vòng lặp chính được đặt dưới lớp con
Znik

42
COUNTER=$((COUNTER+1)) 

là một cấu trúc khá vụng về trong lập trình hiện đại.

(( COUNTER++ ))

trông "hiện đại" hơn. Bạn cũng có thể dùng

let COUNTER++

nếu bạn nghĩ rằng cải thiện khả năng đọc. Đôi khi, Bash đưa ra quá nhiều cách để thực hiện - triết lý Perl tôi cho rằng - khi có lẽ Python "chỉ có một cách đúng để làm điều đó" có thể phù hợp hơn. Đó là một tuyên bố gây tranh cãi nếu có một! Dù sao, tôi muốn đề xuất mục tiêu (trong trường hợp này) không chỉ là tăng một biến mà là (quy tắc chung) để viết mã mà người khác có thể hiểu và hỗ trợ. Sự phù hợp đi một chặng đường dài để đạt được điều đó.

HTH


Điều này không giải quyết được câu hỏi ban đầu, đó là làm thế nào để có được giá trị được cập nhật trong bộ đếm SAU kết thúc vòng lặp (quy trình phụ)
Luis Vazquez

16

Thử sử dụng

COUNTER=$((COUNTER+1))

thay vì

COUNTER=$((COUNTER))

8
hoặc chỉlet "COUNTER++"
nullpotent

2
Xin lỗi, đó là một Typo. Nó thực sự ((QUỐC GIA + 1))
Sparsh Gupta

8
@AaronDigulla: (( COUNTER++ ))(không có ký hiệu đô la)
Tạm dừng cho đến khi có thông báo mới.

2
Tôi không chắc tại sao nhưng tôi thấy một đoạn script của tôi liên tục bị lỗi khi sử dụng (( COUNTER++ ))nhưng khi tôi chuyển sang COUNTER=$((COUNTER + 1))nó thì nó hoạt động. GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)
Steven Lu

Có lẽ dòng băm bang của bạn chạy bash là / bin / sh thay vì / bin / bash?
Tối đa

12

Tôi nghĩ rằng cuộc gọi awk duy nhất này tương đương với grep|grep|awk|awkđường ống của bạn : vui lòng kiểm tra nó. Lệnh awk cuối cùng của bạn dường như không thay đổi gì cả.

Vấn đề với COUNTER là vòng lặp while đang chạy trong một lớp con, do đó, mọi thay đổi đối với biến sẽ biến mất khi thoát khỏi lớp con. Bạn cần truy cập giá trị của COUNTER trong cùng một mạng con. Hoặc nghe lời khuyên của @ DennisWilliamson, sử dụng thay thế quy trình và tránh hoàn toàn phần phụ.

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
Cảm ơn, awk cuối cùng về cơ bản sẽ xóa mọi thứ sau khi kết thúc = 1 và đặt một kết thúc mới = 1 vào cuối (để lần sau chúng ta có thể xóa mọi thứ được nối sau nó).
Sparsh Gupta

1
@SparshGupta, awk trước không in bất cứ thứ gì sau "end = 1".
glenn jackman

Điều này giúp cải thiện rất tốt cho tập lệnh câu hỏi, nhưng không giải quyết được vấn đề với việc tăng bộ đếm bên trong subshell
Znik


11

Thay vì sử dụng một tệp tạm thời, bạn có thể tránh tạo một lớp con xung quanh whilevòng lặp bằng cách sử dụng thay thế quy trình.

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

Nhân tiện, bạn sẽ có thể biến đổi tất cả grep, grep, awk, awk, awkthành một awk.

Bắt đầu với Bash 4.2, có một lastpipetùy chọn là

chạy lệnh cuối cùng của một đường ống dẫn trong bối cảnh shell hiện tại. Tùy chọn Lastpipe không có hiệu lực nếu bật chức năng kiểm soát công việc.

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

thay thế quy trình là tuyệt vời nếu bạn muốn tăng bộ đếm bên trong vòng lặp và sử dụng nó bên ngoài khi hoàn thành, vấn đề với thay thế quy trình là tôi không tìm được cách nào để lấy mã trạng thái của lệnh đã thực hiện, điều này có thể xảy ra khi sử dụng đường ống bằng cách sử dụng $ {PIPESTATUS [*]}
chrisweb

@chrisweb: Tôi đã thêm thông tin về lastpipe. Nhân tiện, có lẽ bạn nên sử dụng "${PIPESTATUS[@]}"(thay vì dấu hoa thị).
Tạm dừng cho đến khi có thông báo mới.

errata. trong bash (không phải bằng perl như tôi đã viết trước đây do nhầm lẫn) mã thoát là một bảng, sau đó bạn có thể kiểm tra tách tất cả các mã thoát trong chuỗi ống. trước khi kiểm tra trước tiên, bước của bạn phải được sao chép bảng này, nếu không, sau lệnh đầu tiên, bạn sẽ mất tất cả các giá trị.
Znik

Đây là giải pháp hiệu quả với tôi và không sử dụng tệp bên ngoài để lưu trữ giá trị của biến đó theo ý kiến ​​của nhiều người đi bộ.
Luis Vazquez

8

tối giản

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

Đơn giản :-). Cảm ơn @geekzspot
Hussain K

không hoạt động ví dụ trong câu hỏi, bởi vì có subshell
Znik

3

Đây là tất cả những gì bạn cần làm:

$((COUNTER++))

Đây là một đoạn trích từ Học bash Shell , Ấn bản thứ 3, trang 147, 148:

các biểu thức số học bash tương đương với các đối tác của chúng trong các ngôn ngữ Java và C. [9] Ưu tiên và kết hợp giống như trong C. Bảng 6-2 cho thấy các toán tử số học được hỗ trợ. Mặc dù một số trong số này là (hoặc chứa) các ký tự đặc biệt, không cần phải gạch chéo lại để thoát chúng, bởi vì chúng nằm trong cú pháp $ ((...)).

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

Các toán tử ++ và - rất hữu ích khi bạn muốn tăng hoặc giảm một giá trị theo một. [11] Chúng hoạt động giống như trong Java và C, ví dụ: value ++ tăng giá trị lên 1. Đây được gọi là tăng sau ; cũng có một giá trị gia tăng trước : ++ . Sự khác biệt trở nên rõ ràng với một ví dụ:

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

Xem http://www.safaribooksonline.com/a/learning-the-bash/7572399/


Đây là phiên bản này tôi cần, bởi vì tôi đã sử dụng nó trong điều kiện của một iftuyên bố: if [[ $((needsComma++)) -gt 0 ]]; then printf ',\n'; fi Đúng hay sai, đây là phiên bản duy nhất hoạt động đáng tin cậy.
LS

Điều quan trọng về hình thức này là bạn có thể sử dụng gia số trong một bước duy nhất. i=1; while true; do echo $((i++)); sleep .1; done
Bruno Bronosky

1
@LS: if (( needsComma++ > 0 )); thenhoặcif (( needsComma++ )); then
Tạm dừng cho đến khi có thông báo mới.

Sử dụng "echo $ ((i ++))" trong bash tôi luôn nhận được "/opt/xyz/init.sh: dòng 29: i: lệnh không tìm thấy" Tôi đang làm gì sai?
mmo

Điều này không giải quyết câu hỏi về việc nhận giá trị truy cập bên ngoài vòng lặp.
Luis Vazquez

1

Đây là một ví dụ đơn giản

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

1
ví dụ đơn giản, nhưng không thể tán thành câu hỏi.
Znik

0

Có vẻ như bạn không cập nhật counterkịch bản, sử dụngcounter++


Xin lỗi về lỗi đánh máy, tôi thực sự đang sử dụng ((COUNTER + 1)) trong tập lệnh không hoạt động
Sparsh Gupta

không có vấn đề gì khi nó được tăng thêm bởi giá trị + 1 hoặc theo giá trị ++. Sau khi kết thúc subshell, giá trị bộ đếm bị mất và trở về giá trị 0 ban đầu được đặt khi bắt đầu trên tập lệnh này.
Znik

0

Có hai điều kiện khiến biểu thức ((var++))thất bại đối với tôi:

  1. Nếu tôi đặt bash thành chế độ nghiêm ngặt ( set -euo pipefail) và nếu tôi bắt đầu gia tăng của mình ở mức 0 (0).

  2. Bắt đầu từ một (1) là tốt nhưng bằng không làm cho số gia trả về "1" khi đánh giá "++" là lỗi mã trả về khác không trong chế độ nghiêm ngặt.

Tôi có thể sử dụng ((var+=1))hoặc var=$((var+1))để thoát khỏi hành vi này


0

Kịch bản nguồn có một số vấn đề với subshell. Ví dụ đầu tiên, bạn có thể không cần subshell. Nhưng chúng tôi không biết những gì được ẩn trong "Một số hành động nữa". Câu trả lời phổ biến nhất có lỗi ẩn, nó sẽ tăng I / O và sẽ không hoạt động với subshell, vì nó khôi phục couter bên trong vòng lặp.

Không fortot thêm dấu '\', nó sẽ thông báo cho trình thông dịch bash về việc tiếp tục dòng. Tôi hy vọng nó sẽ giúp bạn hoặc bất cứ ai. Nhưng theo tôi, tập lệnh này nên được chuyển đổi hoàn toàn thành tập lệnh AWK, hoặc nếu không thì viết lại thành python bằng cách sử dụng regrec, hoặc perl, nhưng mức độ phổ biến của perl trong nhiều năm bị suy giảm. Tốt hơn làm điều đó với python.

Phiên bản sửa lỗi không có 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

Phiên bản có subshell nếu thực sự cần thiết

#!/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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.