Jak mohu číst řádek po řádku z proměnné v bash?

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

  • Pokud si chcete ponechat celý svůj prázdný prostor, použijte while IFS= read …. Pokud chcete zabránit \ interpretaci, použijte read -r
  • I ‚ jsem opravil výše zmíněné body fred.bear a změnil echo na printf %s, aby váš skript fungoval i při neskrotném vstupu.
  • Chcete-li číst z víceřádkové proměnné, je lepší použít herestring než piping z printf (viz l0b0 ‚ s odpověď).
  • @ata Ačkoli jsem to ‚ slyšel “ vhodnější “ dost často, je třeba poznamenat, že herestring vždy vyžaduje, aby byl /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ím printf.
  • Ve druhém příkladu, pokud víceřádková proměnná neobsahuje ‚ t koncový nový řádek ztratíte poslední prvek. Změňte to na: printf "%s\n" "$var" | while IFS= read -r line

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á, že set +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ž byl set -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í $-. Pro IFS je ‚ otravně fiddly, pokud ‚ nemáte local 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že local - umožňuje místní volby prostředí a local 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, že IFS 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ělo cmd | 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 text c3) 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.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *