Hoe kan ik regel voor regel lezen van een variabele in bash?

Ik heb een variabele die de uitvoer van een commando op meerdere regels bevat. Wat is de meest efficiënte manier om de uitvoer regel voor regel uit de variabele te lezen?

Bijvoorbeeld:

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

Antwoord

U kunt een while-lus gebruiken met procesvervanging:

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

Een optimale manier om lees een variabele met meerdere regels is om een lege IFS variabele en printf de variabele in te stellen met een nieuwe regel erop:

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

Opmerking: volgens shellcheck sc2031 verdient het gebruik van processubstitutie de voorkeur boven een pipe om [subtiel] te vermijden het maken van een subshell.

Realiseer je ook dat door de variabele jobs te noemen dit verwarring kan veroorzaken, aangezien dit ook de naam is van een algemeen shellcommando.

Reacties

  • Als je al je witruimte wilt behouden, gebruik dan while IFS= read …. Als je wil \ interpretatie voorkomen, gebruik dan read -r
  • Ik ‘ heb de punten die fred.bear genoemd heeft hersteld, en ook echo gewijzigd in printf %s, zodat uw script zelfs zou werken met niet-tamme invoer.
  • Om te lezen van een variabele met meerdere regels, heeft een herestring de voorkeur boven piping van printf (zie l0b0 ‘ s antwoord).
  • @ata Hoewel ik ‘ dit heb gehoord ” bij voorkeur ” vaak genoeg, moet worden opgemerkt dat een herestring altijd vereist dat de /tmp -directory beschrijfbaar is, omdat deze afhankelijk is van in staat om een tijdelijk werkbestand te maken. Mocht u zich ooit op een beperkt systeem bevinden waarbij /tmp alleen-lezen is (en niet door u kan worden gewijzigd), dan zult u blij zijn met de mogelijkheid om een alternatieve oplossing te gebruiken, bijv. g. met de printf pipe.
  • In het tweede voorbeeld, als de meerregelige variabele ‘ t een achter een nieuwe regel verliest u het laatste element. Verander het in: printf "%s\n" "$var" | while IFS= read -r line

Antwoord

Om de uitvoer van een opdracht regel voor regel ( uitleg ):

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

Als u de gegevens in een variabele al:

printf %s "$foo" | … 

printf %s "$foo" is bijna identiek aan echo "$foo", maar drukt $foo letterlijk af, terwijl echo "$foo" $foo als een optie zou kunnen interpreteren naar het echo-commando als het begint met een -, en backslash-reeksen in $foo in sommige shells kunnen uitbreiden.

Merk op dat in sommige shells (ash, bash, pdksh, maar niet ksh of zsh) de rechterkant van een pijplijn in een apart proces wordt uitgevoerd, dus elke variabele die je in de lus instelt, gaat verloren. Het volgende script voor het tellen van regels drukt bijvoorbeeld 0 af in deze shells:

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

Een oplossing is om de rest van het script (of in ieder geval het deel die de waarde $n uit de lus) in een lijst met opdrachten nodig heeft:

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

Als handelen op de niet-lege regels is goed genoeg en de invoer is niet enorm, je kunt woordsplitsing gebruiken:

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

Uitleg: setting IFS naar een enkele nieuwe regel zorgt ervoor dat woordsplitsing alleen plaatsvindt bij nieuwe regels (in tegenstelling tot een witruimte-teken onder de standaardinstelling). set -f schakelt globbing uit (dwz wildcard-uitbreiding), wat anders zou gebeuren met het resultaat van een opdrachtvervanging $(jobs) of een variabele vervanging $foo. De for -lus werkt op alle delen van $(jobs), dit zijn alle niet-lege regels in de opdrachtuitvoer. Herstel ten slotte de instellingen voor globbing en IFS naar waarden die equivalent zijn aan de standaardwaarden.

Reacties

  • Ik heb problemen gehad met het instellen van IFS en het uitschakelen van IFS. Ik denk dat het juiste is om de oude waarde van IFS op te slaan en IFS terug te zetten naar die oude waarde. Ik ‘ ben geen bash-expert, maar naar mijn ervaring brengt dit je terug naar de oorspronkelijke bahavior.
  • @BjornRoche: gebruik in een functie local IFS=something. Het heeft ‘ geen invloed op de waarde voor global-scope. IIRC, unset IFS brengt ‘ u niet terug naar de standaardinstelling (en zeker niet ‘ t werkt als het niet ‘ t de standaard was).
  • Ik vraag me af of het gebruik van set in de weg staat getoond in het laatste voorbeeld is correct.Het codefragment gaat ervan uit dat set +f aan het begin actief was, en herstelt daarom die instelling aan het einde. Deze aanname kan echter onjuist zijn. Wat als set -f in het begin actief was?
  • @Binarus Ik herstel alleen instellingen die gelijk zijn aan de standaardwaarden. Inderdaad, als u de oorspronkelijke instellingen wilt herstellen, moet u meer werk verzetten. Voor set -f, sla de originele $- op. Voor IFS is het ‘ irritant lastig als je ‘ t local en u wilt het niet-ingestelde hoofdlettergebruik ondersteunen; als je het toch wilt herstellen, raad ik aan om de invariant af te dwingen die IFS blijft staan.
  • Gebruik van local zou inderdaad de beste oplossing zijn, omdat local - de shell-opties lokaal maakt, en local IFS IFS lokaal. Helaas is local alleen geldig binnen functies, wat herstructurering van de code noodzakelijk maakt. Uw suggestie om het beleid te introduceren dat IFS altijd is ingesteld, klinkt ook heel redelijk en lost het grootste deel van het probleem op. Bedankt!

Answer

Probleem: als je de while-lus gebruikt, wordt deze in een subshell uitgevoerd en worden alle variabelen verloren. Oplossing: gebruik voor lus

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

Opmerkingen

  • DANK U !! Alle bovenstaande oplossingen mislukten voor mij.
  • piping in een while read -lus in bash betekent dat de while-lus zich in een subshell bevindt, dus variabelen zijn niet ‘ t globaal. while read;do ;done <<< "$var" zorgt ervoor dat de body van de lus geen subshell is. (Recente bash heeft een optie om de body van een cmd | while -lus niet in een subshell te plaatsen, zoals ksh altijd heeft gehad.)
  • Zie ook dit gerelateerde bericht .
  • In vergelijkbare situaties vond ik het verrassend moeilijk om IFS correct te behandelen. Deze oplossing heeft ook een probleem: wat als IFS in het begin helemaal niet is ingesteld (d.w.z. ongedefinieerd is)? Het zal in elk geval na dat codefragment worden gedefinieerd; dit lijkt niet ‘ t correct te zijn.

Antwoord

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

Referenties:

Reacties

  • -r is ook een goed idee; Het voorkomt \` interpretation... (it is in your links, but its probably worth mentioning, just to round out your IFS = `(wat essentieel is om te voorkomen dat witruimte verloren gaat)
  • Alleen deze oplossing werkte voor mij. Bedankt brah.
  • Heeft ‘ niet last van hetzelfde probleem dat wordt genoemd in de commentaren op @dogbane ‘ s antwoord? Wat moet ik doen als de laatste regel van de variabele niet wordt beëindigd door een teken voor een nieuwe regel?
  • Dit antwoord biedt de duidelijkste manier om de inhoud van een variabele naar de while read construct.

Answer

Gebruik in recente bash-versies mapfile of readarray om de uitvoer van opdrachten efficiënt in arrays te lezen

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

Disclaimer: vreselijk voorbeeld, maar je kunt snel komen met een beter commando om te gebruiken dan ls jezelf

Reacties

  • Het ‘ is een leuke manier, maar nesten / var / tmp met tijdelijke bestanden op mijn systeem. Hoe dan ook een +1 geven
  • @eugene: dat is ‘ grappig. Op welk systeem (distro / OS) staat dat?
  • Het ‘ is FreeBSD 8. Hoe te reproduceren: zet readarray in een functie en roep de functie een paar keer aan.
  • Leuke, @sehe. +1

Antwoord

De algemene patronen om dit probleem op te lossen zijn gegeven in de andere antwoorden.

Ik zou echter graag mijn benadering willen toevoegen, hoewel ik niet zeker weet hoe efficiënt het is. Maar het is (althans voor mij) heel begrijpelijk, verandert niets aan de oorspronkelijke variabele (alle oplossingen die read moet de variabele in kwestie hebben met een volg nieuwe regel en daarom deze toevoegen, wat de variabele verandert), maakt geen subshells aan (wat alle pipe-gebaseerde oplossingen doen), gebruikt hier niet -strings (die hun eigen problemen hebben), en gebruikt geen procesvervanging (niets ertegen, maar soms een beetje moeilijk te begrijpen).

Eigenlijk begrijp ik niet waarom bash” s geïntegreerde REs worden zo zelden gebruikt.Misschien zijn ze niet draagbaar, maar aangezien het OP de bash -tag heeft gebruikt, zal dat “me niet stoppen 🙂

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

Uitvoer:

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

Een paar opmerkingen:

Het gedrag hangt af van welke variant van ProcessText die u gebruikt. In het bovenstaande voorbeeld heb ik ProcessText1 gebruikt.

Merk op dat

  • ProcessText1 houdt tekens voor nieuwe regels aan het einde van regels
  • ProcessText1 verwerkt de laatste regel van de variabele (die de tekst bevat c3) hoewel die regel geen nieuw-regel-teken bevat. Vanwege de ontbrekende nieuwe regel op het einde, wordt de opdrachtprompt na het uitvoeren van het script aan de laatste toegevoegd regel van de variabele zonder te worden gescheiden van de uitvoer.
  • ProcessText1 beschouwt altijd het gedeelte tussen de laatste nieuwe regel in de variabele en het einde van de variabele als een regel , zelfs als het leeg is; die regel, of deze nu leeg is of niet, heeft natuurlijk geen nieuw teken achter de regel. Dat wil zeggen, zelfs als het laatste teken in de variabele een nieuwe regel is, behandelt ProcessText1 het lege gedeelte (null-string) tussen die laatste nieuwe regel en het einde van de variabele als een (nog lege) regel en zal deze doorgeven aan regelverwerking. U kunt dit gedrag gemakkelijk voorkomen door de tweede aanroep naar ProcessLine in een geschikte “check-if-empty” -conditie te plaatsen; ik denk echter dat het logischer is om het te laten zoals het is.

ProcessText1 moet ProcessLine op twee plaatsen, wat ongemakkelijk kan zijn als je daar een blok code wilt plaatsen die de lijn direct verwerkt, in plaats van een functie aan te roepen die de lijn verwerkt; je zou de code moeten herhalen die foutgevoelig is.

In tegenstelling hiermee verwerkt ProcessText3 de regel of roept de betreffende functie slechts op één plaats aan, waardoor vervanging de functieaanroep door een codeblok een no-brainer. Dit gaat ten koste van twee while voorwaarden in plaats van één. Afgezien van de implementatieverschillen, gedraagt ProcessText3 zich precies hetzelfde als ProcessText1, behalve dat het geen rekening houdt met het deel tussen het laatste newline-teken in de variabele en het einde van de variabele als regel als dat deel leeg is. Dat wil zeggen, ProcessText3 zal niet in de regelverwerking gaan na het laatste newline-teken van de variabele als dat newline-teken het laatste teken in de variabele is.

ProcessText2 werkt als ProcessText1, behalve dat regels een nieuw teken moeten hebben achteraan. Dat wil zeggen, het gedeelte tussen het laatste newline-teken in de variabele en het einde van de variabele wordt niet als een regel beschouwd en wordt stilletjes weggegooid. Als de variabele dus geen newline-teken bevat, vindt er helemaal geen regelverwerking plaats.

Ik vind die benadering leuker dan de andere hierboven getoonde oplossingen, maar waarschijnlijk heb ik iets gemist (ik heb niet veel ervaring met bash programmeren, en niet erg geïnteresseerd zijn in andere shells).

Antwoord

U kunt < < < gebruiken om eenvoudig te lezen uit de variabele met de nieuwe regel -gescheiden gegevens:

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

Reacties

  • Welkom bij Unix & Linux! Dit is in wezen een duplicaat van een antwoord van vier jaar geleden. Plaats alstublieft geen antwoord tenzij u iets nieuws heeft om bij te dragen.

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *