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
Sparsh Gupta
źródło
1
Związane z: stackoverflow.com/questions/13726764/…
Gabriel Devillers
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
bos
źródło
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

Jay Stan
źródło
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

Bill Parker
źródło
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))
dbf
źródło
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
}
glenn jackman
źródło
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
12
count=0   
base=1
(( count += base ))
pkm
źródło
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
Wstrzymano do odwołania.
źródło
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
8

minimalistyczny

counter=0
((counter++))
echo $counter
geekzspot
źródło
Prosty :-). Dzięki @geekzspot
Hussain K
nie działa np. w pytaniu, bo jest podpowłoka
Znik
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/

CE Montijo
źródło
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
zwitterion
źródło
1
prosty przykład, ale nie nadający się do kwestionowania.
Znik
0

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

yjshen
źródło
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

Augustus Hill
źródło
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
Znik
źródło