Przyrost licznika w pętli Bash nie działa


125

Mam następujący prosty skrypt, w którym uruchamiam pętlę i chcę zachować plik COUNTER. Nie mogę zrozumieć, dlaczego licznik się nie aktualizuje. Czy jest to spowodowane tworzeniem podpowłoki? Jak mogę to potencjalnie naprawić?

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


Nie musisz umieszczać pętli while w podpowłoce. Po prostu zdejmij wsporniki wokół pętli while, wystarczy. Lub jeśli musisz umieścić pętlę w podpowłoce, po chwili zrób to, zrzuć licznik raz do pliku tymczasowego i przywróć ten plik poza podpowłoką. W odpowiedzi przygotuję dla Ciebie ostateczną procedurę.
Znik

Odpowiedzi:


156

Po pierwsze, nie zwiększasz licznika. Zmiana COUNTER=$((COUNTER))na COUNTER=$((COUNTER + 1))lub COUNTER=$[COUNTER + 1]zwiększy to.

Po drugie, jak przypuszczasz, trudniej jest propagować zmienne podpowłoki do wywoływanego. Zmienne w podpowłoce nie są dostępne poza podpowłoką. Są to zmienne lokalne dla procesu potomnego.

Jednym ze sposobów rozwiązania tego problemu jest użycie pliku tymczasowego do przechowywania wartości pośredniej:

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
$ [...] jest przestarzałe.
chepner

1
@chepner Czy masz odwołanie, które mówi, że $[...]jest przestarzałe? Czy istnieje alternatywne rozwiązanie?
blong

9
$[...]był używany bashwcześniej $((...))przez powłokę POSIX. Nie jestem pewien, czy kiedykolwiek był formalnie przestarzały, ale nie mogę znaleźć o nim wzmianki na bashstronie podręcznika i wydaje się, że jest obsługiwany tylko w celu zapewnienia zgodności wstecznej.
chepner

Ponadto, $ (...) jest preferowany nad...
Lennart Rolland,

7
@blong Oto pytanie SO dotyczące $ [...] kontra $ ((...)), które omawia i odwołuje się do wycofania: stackoverflow.com/questions/2415724/…
Ogre Psalm33

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

TESTOWANY BASH: Centos, SuSE, RH


1
@kroonwijk musi być spacja przed nawiasem kwadratowym (aby formalnie „rozgraniczać słowa”). Bash inaczej nie widzi końca poprzedniego wyrażenia.
Edward Garson

1
pytania dotyczyły trochę czasu z fajką, więc gdy tworzona jest podpowłoka, twoja odpowiedź jest poprawna, ale nie używasz fajki, więc nie odpowiada na pytanie
chrisweb

2
Zgodnie z komentarzem Chepnera do innej odpowiedzi $[ ]składnia jest przestarzała. stackoverflow.com/questions/10515964/…
Mark Haferkamp

to nie rozwiązuje głównego pytania, główna pętla znajduje się pod podpowłoką
Znik

42
COUNTER=$((COUNTER+1)) 

jest dość niezdarną konstrukcją w nowoczesnym programowaniu.

(( COUNTER++ ))

wygląda bardziej „nowocześnie”. Możesz także użyć

let COUNTER++

jeśli uważasz, że poprawia to czytelność. Czasami Bash daje zbyt wiele sposobów robienia rzeczy - myślę, że filozofia Perla - kiedy być może Python „jest tylko jeden właściwy sposób, aby to zrobić” może być bardziej odpowiedni. To dyskusyjne stwierdzenie, jeśli kiedykolwiek istniało! W każdym razie sugerowałbym, że celem (w tym przypadku) jest nie tylko inkrementacja zmiennej, ale (zasada ogólna) również napisanie kodu, który ktoś inny może zrozumieć i wesprzeć. Zgodność pozwala to osiągnąć.

HTH


Nie dotyczy to pierwotnego pytania, jak uzyskać zaktualizowaną wartość w liczniku PO zakończeniu pętli (podprocesu)
Luis Vazquez

16

Spróbuj użyć

COUNTER=$((COUNTER+1))

zamiast

COUNTER=$((COUNTER))

8
lub po prostulet "COUNTER++"
nullpotent

2
Przepraszam, to była literówka. Właściwie to ((COUNTER + 1))
Sparsh Gupta

8
@AaronDigulla: (( COUNTER++ ))(bez znaku dolara)
wstrzymano do odwołania.

2
Nie jestem pewien, dlaczego, ale widzę, że mój skrypt wielokrotnie zawodzi podczas używania, (( COUNTER++ ))ale kiedy się na COUNTER=$((COUNTER + 1))niego przełączyłem , zadziałał. GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)
Steven Lu

Może twoja linia hash bang działa bash jako / bin / sh zamiast / bin / bash?
Max

12

Myślę, że to pojedyncze wywołanie awk jest równoważne z twoim grep|grep|awk|awkpotokiem: przetestuj go. Wydaje się, że Twoje ostatnie polecenie awk nic nie zmienia.

Problem z COUNTER polega na tym, że pętla while działa w podpowłoce, więc wszelkie zmiany w zmiennej znikają po zakończeniu działania podpowłoki. Musisz uzyskać dostęp do wartości COUNTER w tej samej podpowłoce. Lub skorzystaj z porady @ DennisWilliamson, użyj podstawienia procesu i całkowicie unikaj podpowłoki.

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
Dzięki, ostatni awk zasadniczo usunie wszystko po end = 1 i umieści nowy koniec = 1 na końcu (aby następnym razem można było usunąć wszystko, co zostanie po nim dodane).
Sparsh Gupta

1
@SparshGupta, poprzedni awk nie wypisuje niczego po „end = 1”.
glenn jackman

To bardzo dobrze poprawia skrypt pytań, ale nie rozwiązuje problemu ze zwiększaniem licznika wewnątrz podpowłoki
Znik


11

Zamiast używać pliku tymczasowego, można uniknąć tworzenia podpowłoki wokół whilepętli, używając podstawiania procesów.

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

Nawiasem mówiąc, powinieneś być w stanie przekształcić to wszystko grep, grep, awk, awk, awkw jeden awk.

Począwszy od Bash 4.2, istnieje lastpipeopcja, która

uruchamia ostatnie polecenie potoku w bieżącym kontekście powłoki. Opcja lastpipe nie działa, jeśli włączona jest kontrola zadań.

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

podstawianie procesu jest świetne, jeśli chcesz zwiększyć licznik wewnątrz pętli i użyć go na zewnątrz po zakończeniu, problem z podstawieniami procesów polega na tym, że nie znalazłem sposobu na uzyskanie również kodu stanu wykonywanego polecenia, co jest możliwe przy użyciu potoku używając $ {PIPESTATUS [*]}
chrisweb

@chrisweb: Dodałem informacje o lastpipe. Swoją drogą, prawdopodobnie powinieneś użyć "${PIPESTATUS[@]}"(at zamiast gwiazdki).
Wstrzymano do odwołania.

errata. w bashu (nie w perlu, jak pisałem wcześniej przez pomyłkę) kodem wyjścia jest tablica, wtedy możesz sprawdzić oddzielnie wszystkie kody wyjścia w łańcuchu potokowym. przed pierwszym testowaniem musisz skopiować tę tabelę, w przeciwnym razie po pierwszym poleceniu utracisz wszystkie wartości.
Znik

Jest to rozwiązanie, które zadziałało dla mnie i bez użycia zewnętrznego pliku do przechowywania wartości zmiennej, co moim zdaniem jest zbyt piesze.
Luis Vazquez


3

To wszystko, co musisz zrobić:

$((COUNTER++))

Oto fragment książki Learning the bash Shell , wydanie trzecie, str. 147, 148:

Wyrażenia arytmetyczne basha są równoważne z ich odpowiednikami w językach Java i C. [9] Pierwszeństwo i łączność są takie same jak w C. Tabela 6-2 przedstawia obsługiwane operatory arytmetyczne. Chociaż niektóre z nich są znakami specjalnymi (lub zawierają), nie ma potrzeby stosowania ich zmiany znaczenia za pomocą odwrotnego ukośnika, ponieważ znajdują się one w składni $ ((...)).

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

Operatory ++ i - są przydatne, gdy chcesz zwiększyć lub zmniejszyć wartość o jeden. [11] Działają tak samo jak w Javie i C, np. Value ++ zwiększa wartość o 1. Nazywa się to post-inkrementacją ; istnieje również preinkrementuj : ++ wartość . Różnica staje się oczywista na przykładzie:

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

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


To jest wersja tego, której potrzebowałem, ponieważ używałem go pod warunkiem ifstwierdzenia: if [[ $((needsComma++)) -gt 0 ]]; then printf ',\n'; fi dobrze czy źle, to jedyna wersja, która działała niezawodnie.
LS

Ważne w tym formularzu jest to, że możesz użyć przyrostu w jednym kroku. i=1; while true; do echo $((i++)); sleep .1; done
Bruno Bronosky

1
@LS: if (( needsComma++ > 0 )); thenlubif (( needsComma++ )); then
Wstrzymane do odwołania.

Używając "echo $ ((i ++))" w bash zawsze otrzymuję "/opt/xyz/init.sh: line 29: i: command not found" Co robię źle?
mmo

Nie dotyczy to kwestii wyprowadzania wartości licznika poza pętlę.
Luis Vazquez

1

To jest prosty przykład

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

1
prosty przykład, ale nie nadający się do kwestionowania.
Znik

0

Wygląda na to, że nie zaktualizowałeś counterskryptu, użyjcounter++


Przepraszamy za literówkę, w rzeczywistości używam ((COUNTER + 1)) w skrypcie, który nie działa
Sparsh Gupta

nie ma znaczenia, że ​​zostanie zwiększona o wartość + 1, czy o wartość ++. Po zakończeniu podpowłoki wartość licznika jest tracona i powraca do początkowej wartości 0 ustawionej na początku tego skryptu.
Znik

0

Były dwa warunki, które spowodowały, że wyrażenie ((var++))zawiodło:

  1. Jeśli ustawię bash na tryb ścisły ( set -euo pipefail) i jeśli zacznę mój przyrost od zera (0).

  2. Rozpoczynanie od jednego (1) jest w porządku, ale zero powoduje, że przyrost zwraca „1” podczas oceny „++”, co jest niezerowym błędem kodu powrotu w trybie ścisłym.

Mogę użyć ((var+=1)) lub var=$((var+1))uciec od tego zachowania


0

Skrypt źródłowy ma problem z podpowłoką. Pierwszy przykład, prawdopodobnie nie potrzebujesz podpowłoki. Ale nie wiemy, co kryje się pod hasłem „Jeszcze więcej akcji”. Najpopularniejsza odpowiedź ma ukryty błąd, który zwiększy I / O i nie będzie działał z podpowłoką, ponieważ przywraca wewnętrzną pętlę coutera.

Nie dodawaj znaku '\', poinformuje on interpretera basha o kontynuacji linii. Mam nadzieję, że pomoże to Tobie lub komukolwiek. Ale moim zdaniem ten skrypt powinien zostać w pełni przekonwertowany do skryptu AWK lub przepisany do Pythona przy użyciu regexp lub perl, ale popularność perla przez lata spada. Lepiej zrób to za pomocą Pythona.

Poprawiona wersja bez podpowłoki:

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

Wersja z podpowłoką, jeśli jest naprawdę potrzebna

#!/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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.