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
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:
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.
echona printf %s, aby twój skrypt działał nawet przy nieposkromionym wejściu.
/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ą.
printf "%s\n" "$var" | while IFS= read -r line
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.
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).
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=
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.)
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
readarrayfunkcję i wywołaj funkcję kilka razy.
jobs="$(jobs)"
while IFS= read
do
echo $REPLY
done <<< "$jobs"
Bibliografia:
-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)
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"
while IFS= read.... Jeśli chcesz zapobiec \ interpretacji, użyjread -r