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 IFSvá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
settakovým způsobem zobrazené v posledním příkladu je správné.Fragment kódu předpokládá, žeset +fbyl 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 -fna 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 -fuložte původní$-. ProIFSje ‚ otravně fiddly, pokud ‚ nemátelocala chcete podpořit nenastavený případ; chcete-li jej obnovit, doporučuji vynutit invariant, kterýIFSzůstane nastaven. - Použití
localby být skutečně tím nejlepším řešením, protoželocal -umožňuje místní volby prostředí alocal IFSIFSmístní.localje bohužel platný pouze v rámci funkcí, což vyžaduje restrukturalizaci kódu. Váš návrh zavést zásadu, žeIFSje 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 readv 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 | whilesmyč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žIFSnení 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
-
-rje také dobrý nápad; Zabraňuje\` interpretation... (it is in your links, but its probably worth mentioning, just to round out yourIFS = `(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 readkonstrukce.
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
readarrayve 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
-
ProcessText1ponechá znaky nového řádku na konci řádků -
ProcessText1zpracuje 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. -
ProcessText1vž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,ProcessText1bude 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íProcessLinedo 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 -rechonaprintf %s, aby váš skript fungoval i při neskrotném vstupu./tmpzapisovatelný, 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/tmpje 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