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
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. PentruIFS
, este ‘ deranjant dacă nu aveți ‘ nu avețilocal
și doriți să susțineți cazul necorespunzător; dacă doriți să o restaurați, vă recomand să aplicați invariantul careIFS
rămâne setat. - Utilizarea
local
într-adevăr să fie cea mai bună soluție, deoarecelocal -
face ca opțiunile de shell să fie locale șilocal IFS
face caIFS
local. Din păcate,local
este valabil numai în cadrul funcțiilor, ceea ce face necesară restructurarea codului. Sugestia dvs. de a introduce politica careIFS
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 buclecmd | 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 textulc3
) 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ătreProcessLine
î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.
while IFS= read
…. Dacă doriți să împiedicați interpretarea, apoi utilizațiread -r
echo
înprintf %s
, astfel încât scriptul dvs. să funcționeze chiar și cu intrări care nu sunt îmblânzite./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 conductaprintf
.printf "%s\n" "$var" | while IFS= read -r line