Cum pot citi rând cu rând dintr-o variabilă din bash?

Am o variabilă care conține ieșirea multilinie a unei comenzi. Care este cel mai eficient mod de a citi ieșirea linie cu linie din variabilă?

De exemplu:

jobs="$(jobs)" if [ "$jobs" ]; then # read lines from $jobs fi 

Răspuns

Puteți utiliza o buclă while cu substituirea procesului:

while read -r line do echo "$line" done < <(jobs) 

O modalitate optimă de a citiți o variabilă multilinie este să setați o variabilă IFS goală și printf variabila cu o linie nouă finală:

# 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") 

Notă: conform shellcheck sc2031 , utilizarea substiției de proces este preferabilă unei conducte pentru a evita [subtil] crearea unui subshell.

De asemenea, vă rugăm să vă dați seama că denumirea variabilei jobs poate provoca confuzie, deoarece acesta este și numele unei comenzi shell comune.

Comentarii

  • Dacă doriți să păstrați tot spațiul dvs. alb, folosiți while IFS= read …. Dacă doriți să împiedicați interpretarea, apoi utilizați read -r
  • Am ‘ am remediat punctele menționate de fred.bear, precum și am schimbat echo în printf %s, astfel încât scriptul dvs. să funcționeze chiar și cu intrări care nu sunt îmblânzite.
  • Pentru a citi dintr-o variabilă multilinie, este preferabil un herestring decât canalizarea din printf (consultați l0b0 ‘ răspunsul).
  • @ata Deși am ‘ am auzit acest ” preferabil ” suficient de des, trebuie remarcat faptul că un herestring necesită întotdeauna ca directorul /tmp să poată fi scris, deoarece se bazează pe a fi capabil să creeze un fișier de lucru temporar. În cazul în care vă veți găsi vreodată într-un sistem restricționat cu /tmp numai în citire (și care nu poate fi modificat de dvs.), vă veți bucura de posibilitatea utilizării unei soluții alternative, de exemplu. g. cu conducta printf.
  • În al doilea exemplu, dacă variabila multi-linie nu conține o ‘ urmând linia nouă, veți pierde ultimul element. Schimbați-l în: printf "%s\n" "$var" | while IFS= read -r line

Răspuns

Pentru a procesa ieșirea unei linii de comandă cu linie ( explicație ):

jobs | while IFS= read -r line; do process "$line" done 

Dacă aveți datele dintr-o variabilă deja:

printf %s "$foo" | … 

printf %s "$foo" este aproape identic cu echo "$foo", dar imprimă literalmente $foo, în timp ce echo "$foo" ar putea interpreta $foo ca opțiune la comanda echo dacă începe cu un - și poate extinde secvențele de bară inversă în $foo în unele shell-uri.

Rețineți că în unele shell-uri (cenușă, bash, pdksh, dar nu ksh sau zsh), partea dreaptă a unei conducte rulează într-un proces separat, astfel încât orice variabilă setată în buclă se pierde. De exemplu, următorul script de numărare a liniei tipărește 0 în aceste cochilii:

n=0 printf %s "$foo" | while IFS= read -r line; do n=$(($n + 1)) done echo $n 

O soluție este să puneți restul scriptului (sau cel puțin partea care are nevoie de valoarea $n din buclă) într-o listă de comenzi:

n=0 printf %s "$foo" | { while IFS= read -r line; do n=$(($n + 1)) done echo $n } 

Dacă acționarea asupra liniilor care nu sunt goale este suficient de bună și intrarea nu este uriașă, puteți utiliza divizarea cuvintelor:

IFS=" " set -f for line in $(jobs); do # process line done set +f unset IFS 

Explicație: setarea IFS la o singură linie nouă face ca împărțirea cuvintelor să apară numai la linii noi (spre deosebire de orice caracter alb în setarea implicită). set -f dezactivează globbulizarea (adică extinderea cu metacaracter), care altfel s-ar întâmpla cu rezultatul unei înlocuiri de comenzi $(jobs) sau cu o substituție variabilă $foo. Bucla for acționează asupra tuturor pieselor din $(jobs), care sunt toate liniile ne-goale din ieșirea comenzii. În cele din urmă, restaurați setările globulare și IFS la valori echivalente cu valorile implicite.

Comentarii

  • Am avut probleme cu setarea IFS și dezactivarea IFS. Cred că ceea ce trebuie făcut este să stochezi vechea valoare a IFS și să o readuci pe IFS la acea valoare veche. Nu ‘ nu sunt un expert bash, dar, din experiența mea, acest lucru vă readuce la comportamentul original.
  • @BjornRoche: în interiorul unei funcții, utilizați local IFS=something. ‘ nu va afecta valoarea globală. IIRC, unset IFS nu vă readuce la valoarea implicită (și cu siguranță nu ‘ nu funcționează dacă nu a fost ‘ t implicit în prealabil).
  • Mă întreb dacă folosesc set prezentat în ultimul exemplu este corect.Fragmentul de cod presupune că set +f a fost activ la început și, prin urmare, restabilește acea setare la sfârșit. Cu toate acestea, această presupunere ar putea fi greșită. Ce se întâmplă dacă set -f era activ la început?
  • @Binarus Restabilesc doar setările echivalente cu valorile implicite. Într-adevăr, dacă doriți să restaurați setările originale, trebuie să faceți mai multă muncă. Pentru set -f, salvați $- original. Pentru IFS, este ‘ deranjant dacă nu aveți ‘ nu aveți local și doriți să susțineți cazul necorespunzător; dacă doriți să o restaurați, vă recomand să aplicați invariantul care IFS rămâne setat.
  • Utilizarea local într-adevăr să fie cea mai bună soluție, deoarece local - face ca opțiunile de shell să fie locale și local IFS face ca IFS local. Din păcate, local este valabil numai în cadrul funcțiilor, ceea ce face necesară restructurarea codului. Sugestia dvs. de a introduce politica care IFS este întotdeauna stabilită sună foarte rezonabil și rezolvă cea mai mare parte a problemei. Mulțumesc!

Răspuns

Problemă: dacă utilizați bucla while va rula în subshell și toate variabilele vor fi pierdut. Soluție: utilizați pentru buclă

# 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= 

Comentarii

  • MULȚUMESC MULTE !! Toate soluțiile de mai sus nu au reușit pentru mine.
  • canalizarea într-o buclă while read în bash înseamnă că bucla while este într-un subshell, deci variabilele nu sunt ‘ t global. while read;do ;done <<< "$var" face ca corpul buclei să nu fie un sub-shell. (Bash-ul recent are opțiunea de a pune corpul unei bucle cmd | while nu într-o sub-coajă, așa cum a avut întotdeauna ksh.)
  • De asemenea, consultați această postare conexă .
  • În situații similare, mi s-a părut surprinzător de dificil să tratez corect IFS. Această soluție are și o problemă: ce se întâmplă dacă IFS nu este deloc setat la început (adică este nedefinit)? Acesta va fi definit în fiecare caz după acel fragment de cod; acest lucru nu pare ‘ să fie corect.

Răspuns

jobs="$(jobs)" while IFS= read -r do echo "$REPLY" done <<< "$jobs" 

Referințe:

Comentarii

  • -r este și o idee bună; Împiedică \` interpretation... (it is in your links, but its probably worth mentioning, just to round out your IFS = `(care este esențial pentru a preveni pierderea spațiului alb)
  • Numai această soluție a funcționat pentru mine. Mulțumesc brah.
  • Nu ‘ această soluție suferă de aceeași problemă menționată în comentariile către @dogbane ‘ s? Ce se întâmplă dacă ultima linie a variabilei nu este terminată de un caracter de linie nouă?
  • Acest răspuns oferă cel mai curat mod de a alimenta conținutul unei variabile către while read construct.

Răspuns

În versiunile bash recente, utilizați mapfile sau readarray pentru a citi în mod eficient ieșirea comenzii în tablouri

$ readarray test < <(ls -ltrR) $ echo ${#test[@]} 6305 

Declinare de responsabilitate: exemplu oribil, dar poți veni mult cu o comandă mai bună de utilizat decât ls yourself

Comentarii

  • Este ‘ un mod frumos, dar litters / var / tmp cu fișiere temporare pe sistemul meu. +1 oricum
  • @eugene: este muzant ‘. Pe ce sistem (distro / OS) este activat?
  • Este ‘ FreeBSD 8. Cum se reproduce: pune readarray într-o funcție și apelați funcția de câteva ori.
  • Una frumoasă, @sehe. +1

Răspuns

Modelele comune pentru rezolvarea acestei probleme au fost date în celelalte răspunsuri.

Cu toate acestea, aș dori să adaug abordarea mea, deși nu sunt sigur cât de eficientă este. Dar este (cel puțin pentru mine) destul de ușor de înțeles, nu modifică variabila originală (toate soluțiile care utilizează read trebuie să aibă variabila în cauză cu o linie nouă finală și, prin urmare, să o adauge, care modifică variabila), nu creează sub-coajă (ceea ce fac toate soluțiile bazate pe conducte), nu o folosește aici -strings (care au propriile lor probleme) și nu utilizează substituirea proceselor (nimic împotriva ei, dar cam greu de înțeles uneori).

De fapt, nu înțeleg de ce bash” RE-urile integrate sunt utilizate atât de rar.Poate că nu sunt portabile, dar din moment ce OP a folosit eticheta bash, nu s-a reușit să mă oprească 🙂

#!/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" 

Ieșire:

root@cerberus:~/scripts# ./test4 a1 b1 c1 a2 b2 c2a3 b3 c3root@cerberus:~/scripts# 

Câteva note:

Comportamentul depinde de ce variantă a ProcessText pe care îl utilizați. În exemplul de mai sus, am folosit ProcessText1.

Rețineți că

  • ProcessText1 păstrează caractere de linie nouă la sfârșitul liniilor
  • ProcessText1 procesează ultima linie a variabilei (care conține textul c3) deși acea linie nu conține un caracter de linie nouă. Din cauza lipsei liniei noi, linia de comandă după executarea scriptului este atașată la ultima linia variabilei fără a fi separată de ieșire.
  • ProcessText1 consideră întotdeauna partea dintre ultima linie nouă din variabilă și sfârșitul variabilei ca o linie , chiar dacă este gol; desigur, acea linie, indiferent dacă este goală sau nu, nu are un caracter de linie nouă. Adică, chiar dacă ultimul caracter din variabilă este o linie nouă, ProcessText1 va trata partea goală (șir nul) dintre ultima linie nouă și sfârșitul variabilei ca fiind (încă gol) linie și o va trece la procesarea liniei. Puteți preveni cu ușurință acest comportament prin împachetarea celui de-al doilea apel către ProcessLine într-o condiție adecvată de verificare-dacă-este-gol; cu toate acestea, cred că este mai logic să o lăsați așa cum este.

ProcessText1 trebuie să apelați ProcessLine în două locuri, ceea ce ar putea fi inconfortabil dacă doriți să plasați acolo un bloc de cod care procesează direct linia, în loc să apelați o funcție care procesează linia; ar trebui să repetați codul care este predispus la erori.

În schimb, ProcessText3 procesează linia sau apelează funcția respectivă doar la un singur loc, făcând înlocuirea funcția apelată de un cod blochează un „brainer”. Aceasta costă două while condiții în loc de una. În afară de diferențele de implementare, ProcessText3 se comportă exact la fel ca ProcessText1, cu excepția faptului că nu ia în considerare partea dintre ultimul caracter newline din variabila și sfârșitul variabilei ca linie dacă acea parte este goală. Adică, ProcessText3 nu va intra în procesarea liniei după ultimul caracter al liniei noi ale variabilei dacă acel caracter al liniei noi este ultimul caracter din variabilă.

ProcessText2 funcționează ca ProcessText1, cu excepția faptului că liniile trebuie să aibă un caracter de linie nouă. Adică, partea dintre ultimul caracter de linie nouă din variabilă și sfârșitul variabilei este nu considerată a fi o linie și este aruncată în tăcere. În consecință, dacă variabila nu conține niciun caracter de linie nouă, nu se întâmplă deloc prelucrarea liniei.

Îmi place această abordare mai mult decât celelalte soluții prezentate mai sus, dar probabil că am pierdut ceva bash programare și neinteresând foarte mult de alte shell-uri).

Răspuns

Puteți utiliza < < < pentru a citi pur și simplu din variabila care conține linia nouă -date separate:

while read -r line do echo "A line of input: $line" done <<<"$lines" 

Comentarii

  • Bine ați venit la Unix & Linux! Aceasta duplică în esență un răspuns de acum patru ani. Vă rugăm să nu postați un răspuns decât dacă aveți ceva nou de contribuit.

Lasă un răspuns

Adresa ta de email nu va fi publicată. Câmpurile obligatorii sunt marcate cu *