Mám proměnnou, která obsahuje víceřádkový výstup příkazu. Jaký je nejúčinnější způsob čtení výstupního řádku po řádku z proměnné?
Například:
jobs="$(jobs)" if [ "$jobs" ]; then # read lines from $jobs fi
Odpověď
Můžete použít while smyčku s náhradou procesu:
while read -r line do echo "$line" done < <(jobs)
Optimální způsob, jak přečíst víceřádkovou proměnnou je nastavit prázdnou IFS
proměnnou a printf
proměnnou s koncovým novým řádkem:
# 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")
Poznámka: Podle shellcheck sc2031 je vhodnější použít procesní substituci než potrubí, aby se zabránilo [nenápadně] vytvoření subshellu.
Uvědomte si také, že pojmenováním proměnné jobs
to může způsobit zmatek, protože to je také název běžného příkazu prostředí.
Komentáře
Odpovědět
Zpracovat výstup příkazového řádku po řádku ( vysvětlení ):
jobs | while IFS= read -r line; do process "$line" done
Pokud máte data v proměnné již:
printf %s "$foo" | …
printf %s "$foo"
jsou téměř totožná s echo "$foo"
, ale tiskne $foo
doslovně, zatímco echo "$foo"
může interpretovat $foo
jako možnost na příkaz echo, pokud začíná -
a může v některých skořápkách rozšířit sekvence zpětného lomítka v $foo
.
Všimněte si, že v některých skořápkách (ash, bash, pdksh, ale ne ksh nebo zsh) běží pravá strana potrubí v samostatném procesu, takže dojde ke ztrátě jakékoli proměnné, kterou nastavíte ve smyčce. Například následující skript pro počítání řádků vytiskne 0 v těchto skořápkách:
n=0 printf %s "$foo" | while IFS= read -r line; do n=$(($n + 1)) done echo $n
Řešením je umístit zbytek skriptu (nebo alespoň část) který potřebuje hodnotu $n
ze smyčky) v seznamu příkazů:
n=0 printf %s "$foo" | { while IFS= read -r line; do n=$(($n + 1)) done echo $n }
Pokud působení na neprázdné řádky je dost dobré a vstup není obrovský, můžete použít rozdělení slov:
IFS=" " set -f for line in $(jobs); do # process line done set +f unset IFS
Vysvětlení: nastavení IFS
na jeden nový řádek způsobí, že k rozdělení slov dojde pouze na nových řádcích (na rozdíl od jakéhokoli znaku mezer ve výchozím nastavení). set -f
vypne globování (tj. rozšíření zástupných znaků), k čemuž by jinak došlo v důsledku nahrazení příkazu $(jobs)
nebo variabilní substituce $foo
. Smyčka for
působí na všechny části $(jobs)
, což jsou všechny neprázdné řádky ve výstupu příkazu. Nakonec obnovte nastavení globbing a IFS
na hodnoty, které jsou ekvivalentní výchozím hodnotám.
Komentáře
- Měl jsem potíže s nastavením IFS a deaktivací IFS. Myslím, že správná věc je uložit starou hodnotu IFS a nastavit IFS zpět na tuto starou hodnotu. Já ‚ nejsem expert na bash, ale podle mých zkušeností se tím dostanete zpět k původnímu chování.
- @BjornRoche: uvnitř funkce použijte
local IFS=something
. To ‚ neovlivní hodnotu globálního rozsahu. IIRC,unset IFS
vás ‚ nedostane zpět na výchozí nastavení (a určitě ‚ Pokud to nebylo ‚ t výchozí předem). - Zajímalo by mě, zda použít
set
takovým způsobem zobrazené v posledním příkladu je správné.Fragment kódu předpokládá, žeset +f
byl na začátku aktivní, a proto toto nastavení na konci obnoví. Tento předpoklad však může být nesprávný. Co když bylset -f
na začátku aktivní? - @Binarus Obnovím pouze nastavení odpovídající výchozím hodnotám. Opravdu, pokud chcete obnovit původní nastavení, musíte udělat více práce. U
set -f
uložte původní$-
. ProIFS
je ‚ otravně fiddly, pokud ‚ nemátelocal
a chcete podpořit nenastavený případ; chcete-li jej obnovit, doporučuji vynutit invariant, kterýIFS
zůstane nastaven. - Použití
local
by být skutečně tím nejlepším řešením, protoželocal -
umožňuje místní volby prostředí alocal IFS
IFS
místní.local
je bohužel platný pouze v rámci funkcí, což vyžaduje restrukturalizaci kódu. Váš návrh zavést zásadu, žeIFS
je vždy nastaven, zní také velmi rozumně a řeší největší část problému. Děkujeme!
Odpověď
Problém: pokud použijete smyčku while, bude spuštěna v subshellu a všechny proměnné budou ztracený. Řešení: použijte pro smyčku
# 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=
Komentáře
- DĚKUJI VÁM !! Všechna výše uvedená řešení pro mě selhala.
- Piping do smyčky
while read
v bash znamená, že smyčka while je v subshellu, takže proměnné nejsou ‚ t globální.while read;do ;done <<< "$var"
způsobí, že tělo smyčky nebude subshell. (Nedávný bash má možnost umístit tělocmd | while
smyčky ne do subshellu, jako to vždy měl ksh.) - Viz také tento související příspěvek .
- V podobných situacích mi připadalo překvapivě obtížné správně zacházet
IFS
. Toto řešení má také problém: Co kdyžIFS
není na začátku vůbec nastaven (tj. Není definován)? Bude definován v každém případě po tomto fragmentu kódu; to se ‚ nezdá být správné.
Odpověď
jobs="$(jobs)" while IFS= read -r do echo "$REPLY" done <<< "$jobs"
Odkazy:
Komentáře
-
-r
je také dobrý nápad; Zabraňuje\` interpretation... (it is in your links, but its probably worth mentioning, just to round out your
IFS = `(což je zásadní, aby se zabránilo ztrátě mezer) - Pouze toto řešení pro mě fungovalo. Díky brácho.
- Ne ‚ t toto řešení netrpí stejným problémem, jaký je zmíněn v komentářích k @dogbane ‚ s odpověď? Co když poslední řádek proměnné není ukončen znakem nového řádku?
- Tato odpověď poskytuje nejčistší způsob, jak přenést obsah proměnné do
while read
konstrukce.
Odpověď
V posledních verzích bash použijte mapfile
nebo readarray
k efektivnímu načtení výstupu příkazu do polí
$ readarray test < <(ls -ltrR) $ echo ${#test[@]} 6305
Zřeknutí se odpovědnosti: hrozný příklad, ale můžete přijít s lepším použitelným příkazem než sami
Komentáře
- Je to ‚ hezký způsob, ale vrh / var / tmp s dočasnými soubory v mém systému. Každopádně +1
- @eugene: to je ‚ zábavné. Na jakém systému (distro / OS) je to?
- Je to ‚ s FreeBSD 8. Jak reprodukovat: put
readarray
ve funkci a funkci několikrát zavolat. - Pěkná, @sehe. +1
Odpověď
Společné vzorce řešení tohoto problému jsou uvedeny v ostatních odpovědích.
Rád bych však přidal svůj přístup, i když si nejsem jistý, jak efektivní je. Ale je (alespoň pro mě) zcela srozumitelný, nemění původní proměnnou (všechna řešení, která používají read
musí mít danou proměnnou s koncovým novým řádkem, a proto ji přidat, což mění proměnnou), nevytváří podsloupy (což dělají všechna řešení založená na potrubí), zde se nepoužívá -strings (které mají své vlastní problémy) a nepoužívá substituci procesu (nic proti tomu, ale někdy trochu těžko pochopitelné).
Vlastně nechápu proč se používají tak zřídka.Možná nejsou přenosné, ale protože OP používá značku bash
, nezastaví mě to 🙂
#!/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"
Výstup:
root@cerberus:~/scripts# ./test4 a1 b1 c1 a2 b2 c2a3 b3 c3root@cerberus:~/scripts#
Několik poznámek:
Chování závisí na variantě ProcessText
které používáte. Ve výše uvedeném příkladu jsem použil ProcessText1
.
Pamatujte, že
-
ProcessText1
ponechá znaky nového řádku na konci řádků -
ProcessText1
zpracuje poslední řádek proměnné (který obsahuje textc3
) ačkoli tento řádek neobsahuje koncový znak nového řádku. Kvůli chybějícímu koncovému novému řádku je příkazový řádek po provedení skriptu připojen k poslednímu řádek proměnné, aniž by byl oddělen od výstupu. -
ProcessText1
vždy považuje část mezi posledním novým řádkem v proměnné a koncem proměnné za řádek , i když je prázdný; tento řádek, ať už prázdný nebo ne, samozřejmě nemá koncový znak nového řádku. To znamená, že i když je posledním znakem v proměnné nový řádek,ProcessText1
bude považovat prázdnou část (prázdný řetězec) mezi posledním novým řádkem a koncem proměnné za (zatím prázdný) řádek a předá jej zpracování řádku. Tomuto chování můžete snadno zabránit zabalením druhého voláníProcessLine
do příslušného stavu check-if-empty; myslím si však, že je logičtější nechat to tak, jak je.
ProcessText1
musí zavolat ProcessLine
na dvou místech, což by mohlo být nepříjemné, pokud byste tam chtěli umístit blok kódu, který přímo zpracovává linku, místo volání funkce, která linku zpracovává; museli byste opakovat kód, který je náchylný k chybám.
Naproti tomu ProcessText3
zpracuje linku nebo zavolá příslušnou funkci pouze na jednom místě, čímž nahradí volání funkce kódovým blokem nebere v úvahu. Přichází to za cenu dvou while
podmínek namísto jedné. Kromě rozdílů v implementaci se ProcessText3
chová úplně stejně jako ProcessText1
, kromě toho, že nezohledňuje část mezi posledním znakem nového řádku v proměnná a konec proměnné jako řádek, pokud je tato část prázdná. To znamená, že ProcessText3
nepůjde do řádkového zpracování po posledním znaku nového řádku proměnné, pokud je tento znak nového řádku posledním znakem v proměnné.
ProcessText2
funguje jako ProcessText1
, kromě toho, že řádky musí mít koncový znak nového řádku. To znamená, že část mezi posledním znakem nového řádku v proměnné a koncem proměnné je není považována za řádek a je tiše zahozena. Pokud tedy proměnná neobsahuje žádný znak nového řádku, nedojde vůbec k žádnému zpracování řádku.
Líbí se mi tento přístup více než ostatní řešení uvedená výše, ale pravděpodobně mi něco chybělo (není příliš zkušený v bash
programování a přílišný zájem o jiné skořápky).
Odpověď
Pomocí < < < můžete jednoduše číst z proměnné obsahující nový řádek -separated data:
while read -r line do echo "A line of input: $line" done <<<"$lines"
Komentáře
- Vítejte v Unixu & Linux! To v podstatě duplikuje odpověď z doby před čtyřmi lety. Neposílejte odpovědi, pokud nemáte něco nového, čím byste přispěli.
while IFS= read
…. Pokud chcete zabránit \ interpretaci, použijteread -r
echo
naprintf %s
, aby váš skript fungoval i při neskrotném vstupu./tmp
zapisovatelný, protože se spoléhá na to, že schopen vytvořit dočasný pracovní soubor. Pokud se někdy ocitnete v omezeném systému, kde/tmp
je pouze pro čtení (a vy jej nemůžete změnit), budete rádi, že můžete použít alternativní řešení, např. G. s potrubímprintf
.printf "%s\n" "$var" | while IFS= read -r line