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


48

Mam zmienną, która zawiera wieloliniowe wyjście polecenia. Jaki jest najskuteczniejszy sposób odczytywania danych wyjściowych linia po linii ze zmiennej?

Na przykład:

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

Odpowiedzi:


64

Możesz użyć pętli while z podstawieniem procesu:

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

Optymalnym sposobem odczytu zmiennej wielowierszowej jest ustawienie pustej IFSzmiennej i printfzmiennej 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 podstacji procesu jest lepsze niż potok, aby uniknąć [subtelnie] tworzenia podpowłoki.

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


3
Jeśli chcesz zachować wszystkie białe znaki, użyj while IFS= read.... Jeśli chcesz zapobiec \ interpretacji, użyjread -r
Peter.O

Naprawiłem wspomniane punkty fred.bear, a także zmieniłem echona printf %s, aby twój skrypt działał nawet przy nieposkromionym wejściu.
Gilles „SO- przestań być zły”

Aby odczytać ze zmiennej wielowierszowej, ciąg znaków jest lepszy niż przesyłanie potokowe z printf (patrz odpowiedź l0b0).
ata

1
@ata Chociaż często słyszałem o tym „preferowanym”, należy zauważyć, że ciąg znaków zawsze wymaga /tmpzapisu katalogu, ponieważ polega on na możliwości utworzenia tymczasowego pliku roboczego. Jeśli kiedykolwiek znajdziesz się w systemie z ograniczonym /tmpdostępem, który jest tylko do odczytu (i nie możesz go zmienić), będziesz zadowolony z możliwości zastosowania alternatywnego rozwiązania, np. Z printfrurką.
składnia błąd

1
W drugim przykładzie, jeśli zmienna wieloliniowa nie zawiera nowej linii końcowej, stracisz ostatni element. Zmień na:printf "%s\n" "$var" | while IFS= read -r line
David H. Bennett

26

Aby przetworzyć dane wyjściowe wiersza polecenia po wierszu ( wyjaśnienie ):

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

Jeśli masz już dane w zmiennej:

printf %s "$foo" | 

printf %s "$foo"jest prawie identyczny echo "$foo", ale drukuje $foodosłownie, echo "$foo"ale może interpretować $foojako opcję polecenia echo, jeśli zaczyna się od -, i może rozszerzać sekwencje odwrotnego ukośnika $foow 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 osobnym procesie, więc każda zmienna ustawiona w pętli zostaje utracona. Na przykład poniższy 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

Obejściem tego problemu jest umieszczenie pozostałej części skryptu (lub przynajmniej części wymagającej wartości $nz 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 liniach jest wystarczająco dobre, a dane wejściowe nie są ogromne, możesz użyć podziału słów:

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

Objaśnienie: ustawienie IFSpojedynczej nowej linii powoduje, że dzielenie słów występuje tylko w nowej linii (w przeciwieństwie do dowolnego znaku spacji w ustawieniu domyślnym). set -fwyłącza masek (tj dopasowywanie), które w przeciwnym razie zdarzyć się w wyniku substytucji poleceń $(jobs)lub zmienna substitutino $foo. forPętla działa na wszystkich kawałków $(jobs), które są wszystkie niepuste linie wyjścia polecenia. Na koniec przywróć globowanie i IFSustawienia.


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 z powrotem do tej starej wartości. Nie jestem ekspertem od bashów, ale z mojego doświadczenia wynika, że ​​wrócisz do pierwotnego zachowania.
Bjorn Roche,

1
@BjornRoche: wewnątrz funkcji użyj local IFS=something. Nie wpłynie to na wartość zasięgu globalnego. IIRC unset IFSnie przywraca ustawień domyślnych (i na pewno nie działa, jeśli wcześniej nie był domyślny).
Peter Cordes

14

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

# 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=

DZIĘKUJĘ BARDZO!! Wszystkie powyższe rozwiązania zawiodły dla mnie.
hax0r_n_code

pipowanie do while readpętli w bash oznacza, że ​​pętla while znajduje się w podpowłoce, więc zmienne nie są globalne. while read;do ;done <<< "$var"sprawia, że ​​ciało pętli nie jest podpowłoką. (Ostatnie bash ma opcję umieszczenia treści cmd | whilepętli nie w podpowłoce, jak zawsze miało to miejsce w ksh.)
Peter Cordes

Zobacz także ten powiązany post .
Wildcard

10

W najnowszych wersjach bash użyj mapfilelub, readarrayaby efektywnie odczytać dane wyjściowe poleceń do tablic

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

Oświadczenie: okropny przykład, ale możesz wymyślić lepszą komendę do użycia niż sam


To fajny sposób, ale mioty / var / tmp z plikami tymczasowymi w moim systemie. W każdym razie +1
Eugene Yarmash

@eugene: to zabawne. Jaki system (dystrybucja / system operacyjny) jest włączony?
sehe

To FreeBSD 8. Jak się rozmnażać: włącz readarrayfunkcję i wywołaj funkcję kilka razy.
Eugene Yarmash

Niezły, @sehe. +1
Teresa e Junior

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

Bibliografia:


3
-rto także 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)
Peter.O

-1

Możesz użyć <<<, aby po prostu odczytać ze zmiennej zawierającej dane oddzielone znakiem nowej linii:

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

1
Witamy w systemach Unix i Linux! Zasadniczo powiela to odpowiedź sprzed czterech lat. Nie publikuj odpowiedzi, chyba że masz coś nowego do dodania.
G-Man mówi „Przywróć Monikę”
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.