Jak mogę czytać wiersz po wierszu ze zmiennej w bash?

Mam zmienną, która zawiera wielowierszowe wyjście polecenia. Jaki jest najskuteczniejszy sposób odczytywania wyniku ze zmiennej wiersz po wierszu?

Na przykład:

jobs="$(jobs)" if [ "$jobs" ]; then # read lines from $jobs fi 

Odpowiedź

Możesz użyć pętli while z podstawianiem procesów:

while read -r line do echo "$line" done < <(jobs) 

Optymalny sposób na odczyt zmiennej wielowierszowej to ustawienie pustej IFS i printf zmiennej z końcowym znakiem nowej linii:

# Printf "%s\n" "$var" is necessary because printf "%s" "$var" on a # variable that doesn"t end with a newline then the while loop will # completely miss the last line of the variable. while IFS= read -r line do echo "$line" done < <(printf "%s\n" "$var") 

Uwaga: zgodnie z shellcheck sc2031 , użycie zastępowania procesu jest lepsze niż potoku, którego należy unikać [subtly] tworzenie podpowłoki.

Pamiętaj też, że nazwanie zmiennej jobs może spowodować zamieszanie, ponieważ jest to również nazwa zwykłego polecenia powłoki.

Komentarze

  • Jeśli chcesz zachować wszystkie spacje, użyj while IFS= read …. Jeśli chcesz chcesz zapobiec \ interpretacji, użyj read -r
  • Naprawiłem ' punkty wymienione przez fred.bear, a także zmieniłem echo na printf %s, aby twój skrypt działał nawet przy danych wejściowych innych niż tame.
  • Aby odczytać ze zmiennej wielowierszowej, preferowany jest łańcuch herestring zamiast pipingu z printf (zobacz l0b0 ' odpowiedź).
  • @ata Chociaż ' słyszałem to ” preferowane ” wystarczająco często, należy zauważyć, że ciąg tutaj zawsze wymaga, aby katalog /tmp był zapisywalny, ponieważ opiera się na byciu w stanie utworzyć tymczasowy plik roboczy. Jeśli kiedykolwiek znajdziesz się w systemie z ograniczeniami, w którym /tmp jest tylko do odczytu (i nie możesz go zmienić), będziesz zadowolony z możliwości skorzystania z alternatywnego rozwiązania, np. sol. z rurką printf.
  • W drugim przykładzie, jeśli zmienna wielowierszowa nie ' nie zawiera kończący znak nowej linii spowoduje utratę ostatniego elementu. Zmień to na: printf "%s\n" "$var" | while IFS= read -r line

Odpowiedź

Aby przetworzyć wyjście wiersza poleceń po wierszu ( wyjaśnienie ):

jobs | while IFS= read -r line; do process "$line" done 

Jeśli masz dane już w zmiennej:

printf %s "$foo" | … 

printf %s "$foo" jest prawie identyczne z echo "$foo", ale drukuje $foo dosłownie, podczas gdy echo "$foo" może zinterpretować $foo jako opcję do polecenia echo, jeśli zaczyna się od - i może rozwinąć sekwencje ukośników odwrotnych w $foo w niektórych powłokach.

Zauważ, że w niektórych powłokach (ash, bash, pdksh, ale nie ksh lub zsh), prawa strona potoku działa w oddzielnym procesie, więc każda zmienna ustawiona w pętli jest tracona. Na przykład następujący skrypt zliczający wiersze wypisuje 0 w tych powłokach:

n=0 printf %s "$foo" | while IFS= read -r line; do n=$(($n + 1)) done echo $n 

Rozwiązaniem jest umieszczenie pozostałej części skryptu (lub przynajmniej części który potrzebuje wartości $n z pętli) na liście poleceń:

n=0 printf %s "$foo" | { while IFS= read -r line; do n=$(($n + 1)) done echo $n } 

Jeśli działanie na niepustych wierszach jest wystarczająco dobre, a dane wejściowe nie są duże, możesz użyć podziału na słowa:

IFS=" " set -f for line in $(jobs); do # process line done set +f unset IFS 

Wyjaśnienie: ustawienie IFS do pojedynczej nowej linii powoduje, że dzielenie słów następuje tylko w nowej linii (w przeciwieństwie do wszelkich białych znaków przy ustawieniu domyślnym). set -f wyłącza globowanie (tj. rozwinięcie symboli wieloznacznych), które w przeciwnym razie zdarzyłoby się w wyniku podstawienia polecenia $(jobs) lub podstawienia zmiennej $foo. Pętla for działa na wszystkich elementach $(jobs), które są niepustymi wierszami w wyniku polecenia. Na koniec przywróć ustawienia globbingu i IFS do wartości, które są równoważne domyślnym.

Komentarze

  • Miałem problem z ustawieniem IFS i rozbrojeniem IFS. Myślę, że właściwą rzeczą jest zapisanie starej wartości IFS i przywrócenie IFS tej starej wartości. Nie jestem ' ekspertem od bash, ale z mojego doświadczenia wynika, że w ten sposób wracasz do pierwotnego bahaviora.
  • @BjornRoche: wewnątrz funkcji użyj local IFS=something. Nie wpłynęło to ' na wartość zakresu globalnego. IIRC, unset IFS nie ' nie przywraca ustawień domyślnych (i na pewno nie ' t działa, jeśli wcześniej nie było ' t domyślnym).
  • Zastanawiam się, czy przy okazji używam set pokazany w ostatnim przykładzie jest poprawny.Fragment kodu zakłada, że set +f był aktywny na początku i dlatego przywraca to ustawienie na końcu. Jednak to założenie może być błędne. Co jeśli set -f był aktywny na początku?
  • @Binarus Przywracam tylko ustawienia równoważne domyślnym. Rzeczywiście, jeśli chcesz przywrócić oryginalne ustawienia, musisz wykonać więcej pracy. W przypadku set -f zapisz oryginał $-. W przypadku IFS ' jest irytująco kłopotliwe, jeśli nie ' nie masz local i chcesz wesprzeć nieustawiony przypadek; jeśli chcesz go przywrócić, zalecam wymuszenie niezmiennika, który IFS pozostaje ustawiony.
  • Używanie local rzeczywiście będzie najlepszym rozwiązaniem, ponieważ local - sprawia, że opcje powłoki są lokalne, a local IFS sprawia, że IFS lokalnie. Niestety, local działa tylko w ramach funkcji, co sprawia, że konieczna jest restrukturyzacja kodu. Twoja sugestia wprowadzenia zasady, która IFS jest zawsze ustawiona, również brzmi bardzo rozsądnie i rozwiązuje największą część problemu. Dzięki!

Odpowiedź

Problem: jeśli użyjesz pętli while, będzie ona działać w podpowłoce, a wszystkie zmienne będą Stracony. Rozwiązanie: użyj pętli for

# change delimiter (IFS) to new line. IFS_BAK=$IFS IFS=$"\n" for line in $variableWithSeveralLines; do echo "$line" # return IFS back if you need to split new line by spaces: IFS=$IFS_BAK IFS_BAK= lineConvertedToArraySplittedBySpaces=( $line ) echo "{lineConvertedToArraySplittedBySpaces[0]}" # return IFS back to newline for "for" loop IFS_BAK=$IFS IFS=$"\n" done # return delimiter to previous value IFS=$IFS_BAK IFS_BAK= 

Komentarze

  • BARDZO DZIĘKUJEMY !! Wszystkie powyższe rozwiązania zawiodły.
  • przekierowanie do pętli while read w bash oznacza, że pętla while znajduje się w podpowłoce, więc zmienne nie są t globalny. while read;do ;done <<< "$var" sprawia, że treść pętli nie jest podpowłoką. (W ostatnim bash istnieje opcja umieszczenia treści pętli cmd | while nie w podpowłoce, jak zawsze w ksh).
  • Zobacz także ten powiązany post .
  • W podobnych sytuacjach zaskakująco trudno było prawidłowo traktować IFS. To rozwiązanie również ma problem: co się stanie, jeśli IFS nie jest w ogóle ustawione na początku (tj. Jest niezdefiniowane)? Będzie on definiowany w każdym przypadku po tym fragmencie kodu; to nie ' wydaje się być poprawne.

Odpowiedź

jobs="$(jobs)" while IFS= read -r do echo "$REPLY" done <<< "$jobs" 

Referencje:

Komentarze

  • -r to również dobry pomysł; Zapobiega \` interpretation... (it is in your links, but its probably worth mentioning, just to round out your IFS = `(co jest niezbędne, aby zapobiec utracie białych znaków)
  • Tylko to rozwiązanie działało dla mnie. Dzięki brachu.
  • Nie ' t to rozwiązanie ma ten sam problem, o którym jest mowa w komentarzach do @dogbane ' s odpowiedź? Co się stanie, jeśli ostatni wiersz zmiennej nie jest zakończony znakiem nowej linii?
  • Ta odpowiedź zapewnia najczystszy sposób przekazania zawartości zmiennej do while read konstrukcja.

Odpowiedź

W ostatnich wersjach basha użyj mapfile lub readarray, aby efektywnie odczytywać dane wyjściowe poleceń w tablicach

$ readarray test < <(ls -ltrR) $ echo ${#test[@]} 6305 

Zastrzeżenie: okropny przykład, ale możesz przyjść z lepszym poleceniem niż ty sam

Komentarze

  • To ' to fajny sposób, ale mioty / var / tmp z plikami tymczasowymi w moim systemie. I tak +1
  • @eugene: to ' jest zabawne. Jaki system (dystrybucja / system operacyjny) jest włączony?
  • To ' s FreeBSD 8. Jak odtworzyć: umieść readarray w funkcji i wywołaj tę funkcję kilka razy.
  • Niezłe, @sehe. +1

Odpowiedź

Typowe wzorce rozwiązania tego problemu podano w innych odpowiedziach.

Chciałbym jednak dodać swoje podejście, chociaż nie jestem pewien, na ile jest wydajne. Ale jest (przynajmniej dla mnie) całkiem zrozumiałe, nie zmienia oryginalnej zmiennej (wszystkie rozwiązania wykorzystujące read musi mieć daną zmienną z końcową nową linią i dlatego dodaje ją, co zmienia zmienną), nie tworzy podpowłok (co robią wszystkie rozwiązania oparte na potokach), nie używa tutaj -strings (które mają swoje własne problemy) i nie używa zastępowania procesów (nic przeciwko temu, ale czasami trochę trudne do zrozumienia).

Właściwie nie rozumiem, dlaczego bash” zintegrowane RE są używane tak rzadko.Być może nie są one przenośne, ale ponieważ OP użył znacznika bash, który nie „nie zatrzyma mnie 🙂

#!/bin/bash function ProcessLine() { printf "%s" "$1" } function ProcessText1() { local Text=$1 local Pattern=$"^([^\n]*\n)(.*)$" while [[ "$Text" =~ $Pattern ]]; do ProcessLine "${BASH_REMATCH[1]}" Text="${BASH_REMATCH[2]}" done ProcessLine "$Text" } function ProcessText2() { local Text=$1 local Pattern=$"^([^\n]*\n)(.*)$" while [[ "$Text" =~ $Pattern ]]; do ProcessLine "${BASH_REMATCH[1]}" Text="${BASH_REMATCH[2]}" done } function ProcessText3() { local Text=$1 local Pattern=$"^([^\n]*\n?)(.*)$" while [[ ("$Text" != "") && ("$Text" =~ $Pattern) ]]; do ProcessLine "${BASH_REMATCH[1]}" Text="${BASH_REMATCH[2]}" done } MyVar1=$"a1\nb1\nc1\n" MyVar2=$"a2\n\nb2\nc2" MyVar3=$"a3\nb3\nc3" ProcessText1 "$MyVar1" ProcessText1 "$MyVar2" ProcessText1 "$MyVar3" 

Wynik:

root@cerberus:~/scripts# ./test4 a1 b1 c1 a2 b2 c2a3 b3 c3root@cerberus:~/scripts# 

Kilka uwag:

Zachowanie zależy od wariantu ProcessText, którego używasz. W powyższym przykładzie użyłem ProcessText1.

Zwróć uwagę, że

  • ProcessText1 zachowuje znaki nowej linii na końcu linii
  • ProcessText1 przetwarza ostatni wiersz zmiennej (który zawiera tekst c3) chociaż ta linia nie zawiera końcowego znaku nowej linii. Ze względu na brak końcowego znaku nowej linii, wiersz polecenia po wykonaniu skryptu jest dołączany do ostatniej wiersz zmiennej bez oddzielania go od wyniku.
  • ProcessText1 zawsze traktuje część między ostatnim znakiem nowej linii w zmiennej a końcem zmiennej jako wiersz , nawet jeśli jest pusty; oczywiście ta linia, bez względu na to, czy jest pusta, czy nie, nie ma końcowego znaku nowej linii. Oznacza to, że nawet jeśli ostatnim znakiem w zmiennej jest nowa linia, ProcessText1 potraktuje pustą część (ciąg pusty) między ostatnią nową linią a końcem zmiennej jako (jeszcze pusta) i przekaże ją do przetwarzania linii. Możesz łatwo zapobiec temu zachowaniu, opakowując drugie wywołanie ProcessLine w odpowiedni warunek sprawdź, czy jest pusty; jednak myślę, że bardziej logiczne jest pozostawienie go bez zmian.

ProcessText1 musi wywołać ProcessLine w dwóch miejscach, co może być niewygodne, jeśli chcesz umieścić tam blok kodu, który bezpośrednio przetwarza linię, zamiast wywoływać funkcję, która przetwarza linię; musiałbyś powtórzyć kod, który jest podatny na błędy.

W przeciwieństwie do tego ProcessText3 przetwarza linię lub wywołuje odpowiednią funkcję tylko w jednym miejscu, zastępując funkcja wywoływana przez kod blokuje nie myślenia. Odbywa się to kosztem dwóch while warunków zamiast jednego. Oprócz różnic w implementacji, ProcessText3 zachowuje się dokładnie tak samo jak ProcessText1, z tą różnicą, że nie uwzględnia części między ostatnim znakiem nowej linii w zmienna i koniec zmiennej jako wiersz, jeśli ta część jest pusta. Oznacza to, że ProcessText3 nie przejdzie do przetwarzania linii po ostatnim znaku nowego wiersza zmiennej, jeśli ten znak nowego wiersza jest ostatnim znakiem w zmiennej.

ProcessText2 działa jak ProcessText1, z wyjątkiem tego, że wiersze muszą mieć na końcu znak nowej linii. Oznacza to, że część między ostatnim znakiem nowej linii w zmiennej a końcem zmiennej jest nie uważana za linię i jest po cichu odrzucana. W konsekwencji, jeśli zmienna nie zawiera żadnego znaku nowej linii, przetwarzanie linii w ogóle się nie dzieje.

Takie podejście podoba mi się bardziej niż inne rozwiązania pokazane powyżej, ale prawdopodobnie coś przeoczyłem (niezbyt doświadczony bash programowanie i brak zainteresowania innymi powłokami).

Odpowiedź

Możesz użyć < < <, aby po prostu odczytać zmienną zawierającą znak nowej linii -separated data:

while read -r line do echo "A line of input: $line" done <<<"$lines" 

Komentarze

  • Witamy w Uniksie & Linux! To zasadniczo powiela odpowiedź sprzed czterech lat. Nie publikuj odpowiedzi, chyba że masz coś nowego do przekazania.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *