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 IFS
zmiennej 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 podstacji procesu jest lepsze niż potok, aby uniknąć [subtelnie] tworzenia podpowłoki.
Pamiętaj również, że nazwanie zmiennej jobs
może powodować zamieszanie, ponieważ jest to również nazwa zwykłego polecenia powłoki.
echo
na printf %s
, aby twój skrypt działał nawet przy nieposkromionym wejściu.
/tmp
zapisu katalogu, ponieważ polega on na możliwości utworzenia tymczasowego pliku roboczego. Jeśli kiedykolwiek znajdziesz się w systemie z ograniczonym /tmp
dostę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 printf
rurką.
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 $foo
dosłownie, echo "$foo"
ale może interpretować $foo
jako opcję polecenia echo, jeśli zaczyna się od -
, i może rozszerzać sekwencje odwrotnego ukośnika $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 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 $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 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 IFS
pojedynczej 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 -f
wyłącza masek (tj dopasowywanie), które w przeciwnym razie zdarzyć się w wyniku substytucji poleceń $(jobs)
lub zmienna substitutino $foo
. for
Pętla działa na wszystkich kawałków $(jobs)
, które są wszystkie niepuste linie wyjścia polecenia. Na koniec przywróć globowanie i IFS
ustawienia.
local IFS=something
. Nie wpłynie to na wartość zasięgu globalnego. IIRC unset IFS
nie 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 read
pę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 | while
pętli nie w podpowłoce, jak zawsze miało to miejsce w ksh.)
W najnowszych wersjach bash użyj mapfile
lub, readarray
aby 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
readarray
funkcję i wywołaj funkcję kilka razy.
jobs="$(jobs)"
while IFS= read
do
echo $REPLY
done <<< "$jobs"
Bibliografia:
-r
to 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