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
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 datset +f
aan het begin actief was, en herstelt daarom die instelling aan het einde. Deze aanname kan echter onjuist zijn. Wat alsset -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. VoorIFS
is het ‘ irritant lastig als je ‘ tlocal
en u wilt het niet-ingestelde hoofdlettergebruik ondersteunen; als je het toch wilt herstellen, raad ik aan om de invariant af te dwingen dieIFS
blijft staan. - Gebruik van
local
zou inderdaad de beste oplossing zijn, omdatlocal -
de shell-opties lokaal maakt, enlocal IFS
IFS
lokaal. Helaas islocal
alleen geldig binnen functies, wat herstructurering van de code noodzakelijk maakt. Uw suggestie om het beleid te introduceren datIFS
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 eencmd | 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 alsIFS
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 bevatc3
) 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, behandeltProcessText1
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 naarProcessLine
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.
while IFS= read
…. Als je wil \ interpretatie voorkomen, gebruik danread -r
echo
gewijzigd inprintf %s
, zodat uw script zelfs zou werken met niet-tamme invoer./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 deprintf
pipe.printf "%s\n" "$var" | while IFS= read -r line