Dlaczego moja zmienna jest lokalna w jednej pętli „podczas odczytu”, ale nie w innej na pozór podobnej pętli?


25

Dlaczego otrzymuję różne wartości $xz poniższych fragmentów?

#!/bin/bash

x=1
echo fred > junk ; while read var ; do x=55 ; done < junk
echo x=$x 
#    x=55 .. I'd expect this result

x=1
cat junk | while read var ; do x=55 ; done
echo x=$x 
#    x=1 .. but why?

x=1
echo fred | while read var ; do x=55 ; done
echo x=$x 
#    x=1  .. but why?

Podobny post na temat przepełnienia stosu: Zmienna zmodyfikowana w pętli while nie jest zapamiętywana .
codeforester

Odpowiedzi:


26

Właściwe wyjaśnienie zostało już podane przez jsbillings i geekozaura , ale pozwólcie, że rozwinę się nieco.

W większości powłok, w tym bash, każda strona potoku działa w podpowłoce, więc wszelkie zmiany stanu wewnętrznego powłoki (takie jak ustawianie zmiennych) pozostają ograniczone do tego segmentu potoku. Jedyną informacją, którą można uzyskać z podpowłoki, jest to, co wyprowadza (do standardowego wyjścia i innych deskryptorów plików) i jej kod wyjścia (który jest liczbą od 0 do 255). Na przykład następujący fragment drukuje 0:

a=0; a=1 | a=2; echo $a

W ksh (warianty pochodzące z kodu AT&T, a nie warianty pdksh / mksh) i zsh, ostatni element w potoku jest wykonywany w powłoce nadrzędnej. (POSIX pozwala na oba zachowania.) Tak więc powyższy fragment drukuje 2.

Przydatnym idiomem jest uwzględnienie w potoku kontynuacji pętli while (lub cokolwiek, co masz po prawej stronie potoku, ale pętla while jest tutaj powszechna):

cat junk | {
  while read var ; do x=55 ; done
  echo x=$x 
}

1
Dzięki Gilles .. To a = 0; a = 1 | a = 2 daje bardzo wyraźny obraz .. i nie tylko lokalizacji stanu wewnętrznego, ale także tego, że potok tak naprawdę nie musi niczego wysyłać przez potok (inny niż kod wyjścia (?)). to ciekawy wgląd w fajkę ... Udało mi się uruchomić mój skrypt < <(locate -ber ^\.tag$), dzięki oryginalnej, nieco niejasnej odpowiedzi i komentacjom geekozaura i glenna jackmana. Początkowo miałem dylemat, czy przyjąć odpowiedź, ale wynik netto było dość jasne, szczególnie z komentarzem uzupełniającym
jsbillings

wygląda na to, że wpakowałem do funkcji, więc przeniosłem do niej niektóre zmienne i testy i działało świetnie, dzięki!
Aquarius Power

8

Masz problem ze zmiennym zakresem. Zmienne zdefiniowane w pętli while znajdującej się po prawej stronie potoku mają swój własny kontekst zakresu lokalnego, a zmiany w zmiennej nie będą widoczne poza pętlą. Pętla while jest w zasadzie podpowłoką, która otrzymuje KOPIĘ środowiska powłoki, a wszelkie zmiany w środowisku są tracone na końcu powłoki. Zobacz to pytanie StackOverflow .

ZAKTUALIZOWANO : Zlekceważyłem wskazując na ważny fakt, że pętla while z własną podpowłoką była spowodowana tym, że była punktem końcowym potoku, zaktualizowałem to w odpowiedzi.


@jsbillings .. Okej, to wyjaśnia dwa ostatnie fragmenty, ale nie wyjaśnia pierwszego, w którym wartość $ x ustawiona w pętli jest zachowana jako 55 (poza zakresem pętli „while”)
Peter.O

5
@ fred.bear: whilePętla działa jako tylny koniec potoku, który wrzuca ją do podpowłoki.
geekozaur

2
Tutaj zaczyna się podstawienie procesu bash. Zamiast tego blah|blah|while read ...możesz miećwhile read ...; done < <(blah|blah)
glenn jackman

1
@geekosaur: dziękuję za wypełnienie szczegółów, które pominąłem w mojej odpowiedzi.
jsbillings

1
-1 Przepraszam, ale ta odpowiedź jest po prostu zła. Wyjaśnia, jak to działa w wielu językach programowania, ale nie w powłoce. @Gilles poniżej ma rację.
jpc

6

Jak wspomniano w innych odpowiedziach , części potoku działają w podpowłokach, więc wprowadzone tam modyfikacje nie są widoczne dla głównej powłoki.

Jeśli weźmiemy pod uwagę tylko Bash, oprócz cmd | { stuff; more stuff; }struktury istnieją dwa inne obejścia :

  1. Przekieruj dane wejściowe z podstawienia procesu :

    while read var ; do x=55 ; done < <(echo fred)
    echo "$x"

    Dane wyjściowe z komendy in <(...)wyglądają tak, jakby były nazwanymi potokami.

  2. lastpipeOpcja, która sprawia, że praca atakujących jak ksh i uruchamia ostatnią część rurociągu w głównym procesie powłoki. Chociaż działa to tylko wtedy, gdy kontrola zadań jest wyłączona, tj. Nie w interaktywnej powłoce:

    bash -c '
      shopt -s lastpipe
      echo fred | while read var ; do x=55 ; done; 
      echo "$x"
    '

    lub

    bash -O lastpipe -c '
      echo fred | while read var ; do x=55 ; done; 
      echo "$x"
    '

Podstawianie procesów jest oczywiście obsługiwane także w ksh i zsh. Ale ponieważ i tak uruchamiają ostatnią część potoku w głównej powłoce, użycie go jako obejścia nie jest tak naprawdę konieczne.


0
#!/bin/bash
set -x

# prepare test data.
mkdir -p ~/test_var_global
cd ~/test_var_global
echo "a"> core.1
echo "b"> core.2
echo "c"> core.3


var=0

coreFiles=$(find . -type f -name "core*")
while read -r file;
do
  # perform computations on $i
  ((var++))
done <<EOF
$coreFiles
EOF

echo $var

Result:
...
+ echo 3
3

to może działać.

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.