bash: zmienna traci wartość na końcu pętli odczytu podczas

36

Mam problem z jednym ze skryptów powłoki. Zapytałem kilku kolegów, ale wszyscy tylko potrząsali głowami (po pewnym zadrapaniu), więc przyszedłem tutaj po odpowiedź.

Według mojego zrozumienia następujący skrypt powłoki powinien wypisać „Count is 5” jako ostatni wiersz. Tyle że nie. Wyświetla „Liczba to 0”. Jeśli „podczas odczytu” zostanie zastąpione jakąkolwiek inną pętlą, działa dobrze. Oto skrypt:

echo „1”> input.data
echo „2” >> input.data
echo „3” >> input.data
echo „4” >> input.data
echo „5” >> input.data

CNT = 0 

dane wejściowe kota podczas czytania;
robić
  niech CNT ++;
  echo „Zliczanie do $ CNT”
gotowy 
echo „Liczba to $ CNT”

Dlaczego tak się dzieje i jak mogę temu zapobiec? Próbowałem tego w Debian Lenny i Squeeze, ten sam wynik (tj. Bash 3.2.39 i bash 4.1.5. W pełni przyznaję, że nie jestem kreatorem skryptów powłoki, więc wszelkie wskazówki będą mile widziane.

wolfgangsz
źródło

Odpowiedzi:

30

Patrz argument @ Bash FAQ wpis nr 24: „Ustawiam zmienne w pętli. Dlaczego nagle znikają po zakończeniu pętli? Lub dlaczego nie mogę potokować danych do odczytu?” (ostatnio zarchiwizowane tutaj ).

Podsumowanie: Obsługiwane jest tylko od wersji bash 4.2 i nowszych. Jeśli używasz bash, musisz używać różnych metod, takich jak zastępowanie poleceń zamiast potoku.

Ignacio Vazquez-Abrams
źródło
Dostajesz bonus, ponieważ twoja odpowiedź zapewniła mi najszerszy zakres opcji.
wolfgangsz
5
Link jest martwy. Dlatego odpowiedzi tylko na link są złe. Przynajmniej streść odpowiedź tutaj.
rudolfbyker
Boże, jeszcze raz, gdy ksh jest po prostu o wiele lepszy ... dlaczego, po prostu dlaczego wszyscy gromadzili się wokół bashu.
Florian Heigl,
@FlorianHeigl: Czy twierdzisz, że ksh to One True Shell?
Ignacio Vazquez-Abrams,
@ IgnacioVazquez-Abrams nie, ale twierdzę, że obsługa pętli while w bash jest strasznie PITA. Obsługa pętli była długotrwała, uniemożliwiając jej przywrócenie funkcjonalności do 1993 roku. Inne rzeczy to obsługa getopt, w której wbudowany moduł obsługi (również z 1993 r.) Był prosty i zdolny, czego wciąż nie można uzyskać, chyba że używa się np. Docopt. Twierdzę, że bash od ponad 20 lat kładzie się za kratką, nalegając, a ilość czasu spędzonego na TYM RZECZY lub milionach złych przypadków getopts jest poza miarą - akceptowane tylko dlatego, że większość ludzi nigdy się nie dowie.
Florian Heigl,
30

To rodzaj „powszechnego” błędu. Rury tworzą SubShells, więc while readdziała na innej powłoce niż skrypt, co sprawia, że ​​twoja CNTzmienna nigdy się nie zmienia (tylko ta wewnątrz podpowłoki rury).

echoZgrupuj ostatni z podpowłoką, whileaby to naprawić (istnieje wiele innych sposobów, aby to naprawić, to jest jeden. Odpowiedzi Iaina i Ignacio mają inne.)

CNT=0

 cat input.data | ( while read 
do
  let CNT++;
  echo "Counting to $CNT"
done 
echo "Count is $CNT" )

Długie wyjaśnienie:

  1. W CNTskrypcie deklarujesz wartość 0;
  2. Podpowłoce jest uruchamiany na |celu while read;
  3. Twoja $CNTzmienna jest eksportowana do SubShell o wartości 0;
  4. SubShell liczy się i zwiększa CNTwartość do 5;
  5. Kończy się SubShell, zmienne i wartości są niszczone (nie wracają do procesu / skryptu wywołującego).
  6. Jesteś echooryginalna CNTwartość 0.
rdzeń rdzeniowy
źródło
2
Pierwszy skrypt, który napisałem, dał mi te same problemy, uderzyłem głową przez chwilę na ścianę, zanim dowiedziałem się, że te rury spawnują dodatkowe pociski. Każda zmienna, z którą będziesz bałaganić w potoku, zniknie z zasięgu, gdy tylko potok się skończy - co oznacza, że ​​jeśli naprawdę naprawdę chcesz zrobić coś ze zmienną na zewnątrz potoku, w którym został użyty, musisz utrzymuj stan przez coś funky, takiego jak plik tymczasowy.
fotoionizowane
Doskonała odpowiedź, niestety mogę dać tylko jeden bonus akceptacyjny. Przepraszam.
wolfgangsz
10

To działa

CNT=0 

while read ;
do
  let CNT++;
  echo "Counting to $CNT"
done <input.data
echo "Count is $CNT"
user9517 obsługuje GoFundMonica
źródło
Podoba mi się to, inteligentny sposób, ponieważ wiesz, gdzie są potrzebne dane i wystarczy je odzyskać. Jeśli nie znasz wysokich umiejętności, zawsze możesz „odczytać plik” hahahha. +1 dla ciebie.
m3nda
1
Każdy, kto to czyta, pamiętaj, że rozwiązanie dostarczone przez Iain działa tylko wtedy, gdy skrypt wyraźnie wywołuje bash, mając pierwszą linię: #! / Bin / bash i że: #! / Bin / sh nie będzie działać.
Roadowl
1
Ciekawe, pierwszy przykład, jaki kiedykolwiek widziałem, gdzie bezużyteczne użycie Cat faktycznie uniemożliwiło działanie kodu . Nawiasem mówiąc, @Roadowl, jedynym bashizmem jest tutaj linia, let CNT++która powinna zamiast CNT="$((CNT+1))"tego korzystać z rozszerzania arytmetycznego zgodnego z POSIX . Reszta jest już przenośna.
Wildcard
6

Zamiast tego spróbuj przekazać dane do podpowłoki, tak jak plik przed pętlą while. Jest to podobne do rozwiązania Laina, ale zakłada, że ​​nie chcesz jakiegoś pliku przerywanego:

total=0
while read var
do
  echo "variable: $var"
  ((total+=var))
done < <(echo 45) #output from a command, script, or function
echo "total: $total"
Steve
źródło