Hoe herhaal ik een reeks getallen in Bash wanneer het bereik wordt gegeven door een variabele?
Ik weet dat ik dit kan doen (‘sequence expression’ genoemd in de Bash documentatie):
for i in {1..5}; do echo $i; done
Wat geeft:
1
2
3
4
5
Toch, hoe kan ik een van de bereikeindpunten vervangen door een variabele? Dit werkt niet:
END=5
for i in {1..$END}; do echo $i; done
Welke afdrukken:
{1..5}
Antwoord 1, autoriteit 100%
for i in $(seq 1 $END); do echo $i; done
edit: ik geef de voorkeur aan seq
boven de andere methoden omdat ik het me echt kan herinneren 😉
Antwoord 2, autoriteit 27%
De seq
methode is de eenvoudigste, maar Bash heeft een ingebouwde rekenkundige evaluatie.
END=5
for ((i=1;i<=END;i++)); do
echo $i
done
# ==> outputs 1 2 3 4 5 on separate lines
De for ((expr1;expr2;expr3));
constructie werkt net als for (expr1;expr2;expr3)
in C en vergelijkbare talen, en net als andere ((expr))
gevallen, behandelt Bash ze als rekenkundig.
Antwoord 3, autoriteit 10%
discussie
Het gebruik van seq
is prima, zoals Jiaaro suggereerde. Pax Diablo stelde een Bash-loop voor om te voorkomen dat een subproces wordt aangeroepen, met als bijkomend voordeel dat het geheugenvriendelijker is als $END te groot is. Zathrus ontdekte een typische bug in de lus-implementatie en liet ook doorschemeren dat, aangezien i
een tekstvariabele is, continue conversies heen en weer worden uitgevoerd met een bijbehorende vertraging.
gehele rekenkunde
Dit is een verbeterde versie van de Bash-loop:
typeset -i i END
let END=5 i=1
while ((i<=END)); do
echo $i
…
let i++
done
Als het enige dat we willen de echo
is, dan kunnen we echo $((i++))
schrijven.
efeminent leerde me iets: Bash staat for ((expr;expr;expr))
constructies toe. Aangezien ik nog nooit de hele man-pagina voor Bash heb gelezen (zoals ik heb gedaan met de man-pagina van Korn-shell (ksh
), en dat is lang geleden), heb ik dat gemist.
Dus,
typeset -i i END # Let's be explicit
for ((i=1;i<=END;++i)); do echo $i; done
lijkt de meest geheugenefficiënte manier te zijn (het is niet nodig om geheugen toe te wijzen om de uitvoer van seq
te gebruiken, wat een probleem kan zijn als END erg groot is), hoewel waarschijnlijk niet de snelste .
de eerste vraag
eschercycle merkte op dat de {a..b} Bash-notatie alleen werkt met letterlijke waarden; true, volgens de Bash-handleiding. Men kan dit obstakel overwinnen met een enkele (interne) fork()
zonder een exec()
(zoals het geval is met het aanroepen van seq
, wat een andere afbeelding zijn vereist een fork+exec):
for i in $(eval echo "{1..$END}"); do
Zowel eval
als echo
zijn ingebouwde Bash-elementen, maar een fork()
is vereist voor de vervanging van de opdracht (de $(…)
constructie).
Antwoord 4, autoriteit 5%
Dit is waarom de oorspronkelijke uitdrukking niet werkte.
Van man bash:
Brace-uitbreiding is eerder uitgevoerd
alle andere uitbreidingen en eventuele
tekens speciaal voor andere
uitbreidingen worden bewaard in de
resultaat. Het is strikt tekstueel. bash
past geen syntactische toe
interpretatie naar de context van
de uitbreiding of de tekst tussen de
beugels.
Dus, schoorsteenuitbreiding is iets dat vroeg wordt gedaan als een puur tekstuele macrobewerking, vóór uitbreiding van parameters.
Shells zijn sterk geoptimaliseerde hybriden tussen macroprocessors en meer formele programmeertalen. Om de typische gebruiksgevallen te optimaliseren, is de taal wat complexer gemaakt en worden enkele beperkingen geaccepteerd.
Aanbeveling
Ik zou willen voorstellen om bij de Posix1-functies te blijven. Dit betekent het gebruik van for i in <list>; do
, als de lijst al bekend is, gebruik anders while
of seq
, zoals in:
#!/bin/sh
limit=4
i=1; while [ $i -le $limit ]; do
echo $i
i=$(($i + 1))
done
# Or -----------------------
for i in $(seq 1 $limit); do
echo $i
done
1. Bash is een geweldige shell en ik gebruik het interactief, maar ik stop geen bash-ismen in mijn scripts. Scripts hebben mogelijk een snellere shell nodig, een veiligere, een meer ingesloten stijl. Het kan zijn dat ze moeten draaien op alles wat is geïnstalleerd als /bin/sh, en dan zijn er nog alle gebruikelijke pro-standaardargumenten. Onthoud shellshock, oftewel bashdoor?
Antwoord 5, autoriteit 2%
Nog een indirecte laag:
for i in $(eval echo {1..$END}); do
:
Antwoord 6
U kunt gebruiken
for i in $(seq $END); do echo $i; done
Antwoord 7
Als je het voorvoegsel nodig hebt, dan vind je dit misschien leuk
for ((i=7;i<=12;i++)); do echo `printf "%2.0d\n" $i |sed "s/ /0/"`;done
dat zal opleveren
07
08
09
10
11
12
Antwoord 8
Als je BSD / OS X gebruikt, kun je jot gebruiken in plaats van seq:
for i in $(jot $END); do echo $i; done
Antwoord 9
Ik heb een paar van de ideeën hier gecombineerd en de prestaties gemeten.
TL;DR afhaalrestaurants:
seq
en{..}
zijn erg snelfor
enwhile
loops traag zijn$(…)
is traagfor (( ; ; ))
loops zijn langzamer$(( ))
is nog langzamer- Je zorgen maken over N getallen in het geheugen (seq of {..}) is dwaas (ten minste tot 1 miljoen.)
Dit zijn geen conclusies. Je zou naar de C-code achter elk van deze moeten kijken om conclusies te trekken. Dit gaat meer over hoe we de neiging hebben om elk van deze mechanismen te gebruiken voor het doorlopen van code. De meeste afzonderlijke bewerkingen zijn dicht genoeg bij dezelfde snelheid dat het in de meeste gevallen niet uitmaakt. Maar een mechanisme als for (( i=1; i<=1000000; i++ ))
is veel bewerkingen, zoals je visueel kunt zien. Het zijn ook veel meer bewerkingen per lus dan je krijgt van for i in $(seq 1 1000000)
. En dat is misschien niet voor de hand liggend voor u, daarom is het zo waardevol om tests te doen.
Demo’s
# show that seq is fast
$ time (seq 1 1000000 | wc)
1000000 1000000 6888894
real 0m0.227s
user 0m0.239s
sys 0m0.008s
# show that {..} is fast
$ time (echo {1..1000000} | wc)
1 1000000 6888896
real 0m1.778s
user 0m1.735s
sys 0m0.072s
# Show that for loops (even with a : noop) are slow
$ time (for i in {1..1000000} ; do :; done | wc)
0 0 0
real 0m3.642s
user 0m3.582s
sys 0m0.057s
# show that echo is slow
$ time (for i in {1..1000000} ; do echo $i; done | wc)
1000000 1000000 6888896
real 0m7.480s
user 0m6.803s
sys 0m2.580s
$ time (for i in $(seq 1 1000000) ; do echo $i; done | wc)
1000000 1000000 6888894
real 0m7.029s
user 0m6.335s
sys 0m2.666s
# show that C-style for loops are slower
$ time (for (( i=1; i<=1000000; i++ )) ; do echo $i; done | wc)
1000000 1000000 6888896
real 0m12.391s
user 0m11.069s
sys 0m3.437s
# show that arithmetic expansion is even slower
$ time (i=1; e=1000000; while [ $i -le $e ]; do echo $i; i=$(($i+1)); done | wc)
1000000 1000000 6888896
real 0m19.696s
user 0m18.017s
sys 0m3.806s
$ time (i=1; e=1000000; while [ $i -le $e ]; do echo $i; ((i=i+1)); done | wc)
1000000 1000000 6888896
real 0m18.629s
user 0m16.843s
sys 0m3.936s
$ time (i=1; e=1000000; while [ $i -le $e ]; do echo $((i++)); done | wc)
1000000 1000000 6888896
real 0m17.012s
user 0m15.319s
sys 0m3.906s
# even a noop is slow
$ time (i=1; e=1000000; while [ $((i++)) -le $e ]; do :; done | wc)
0 0 0
real 0m12.679s
user 0m11.658s
sys 0m1.004s
Antwoord 10
Dit werkt prima in bash
:
END=5
i=1 ; while [[ $i -le $END ]] ; do
echo $i
((i = i + 1))
done
Antwoord 11
Ik weet dat deze vraag over bash
gaat, maar – voor de goede orde – ksh93
is slimmer en implementeert het zoals verwacht:
$ ksh -c 'i=5; for x in {1..$i}; do echo "$x"; done'
1
2
3
4
5
$ ksh -c 'echo $KSH_VERSION'
Version JM 93u+ 2012-02-29
$ bash -c 'i=5; for x in {1..$i}; do echo "$x"; done'
{1..5}
Antwoord 12
Dit is een andere manier:
end=5
for i in $(bash -c "echo {1..${end}}"); do echo $i; done
Antwoord 13
Als je zo dicht mogelijk bij de syntaxis van de brace-expressie wilt blijven, probeer dan de range
functie van bash-tricks’ range.bash
.
Al het volgende doet bijvoorbeeld exact hetzelfde als echo {1..10}
:
source range.bash
one=1
ten=10
range {$one..$ten}
range $one $ten
range {1..$ten}
range {1..10}
Het probeert de native bash-syntaxis te ondersteunen met zo min mogelijk “gotchas”: niet alleen worden variabelen ondersteund, maar het vaak ongewenste gedrag van ongeldige bereiken wordt geleverd als strings (bijv. for i in {1..a}; do echo $i; done
) wordt ook voorkomen.
De andere antwoorden zullen in de meeste gevallen werken, maar ze hebben allemaal minstens één van de volgende nadelen:
- Veel van hen gebruiken subshells, die prestatie schaden en is misschien niet mogelijk op sommige systemen.
- Veel daarvan vertrouwen op externe programma’s. Zelfs
seq
is een binair bestand dat moet worden geïnstalleerd om te kunnen worden gebruikt, moet worden geladen door bash en het programma moet bevatten dat u verwacht, wil het in dit geval werken. Alomtegenwoordig of niet, dat is veel meer om op te vertrouwen dan alleen de Bash-taal zelf. - Oplossingen die alleen native Bash-functionaliteit gebruiken, zoals die van @ephemient, werken niet op alfabetische bereiken, zoals
{a..z}
; brace expansie zal. De vraag ging echter over reeksen van getallen, dus dit is een klacht. - De meeste lijken niet visueel op de syntaxis van het
{1..10}
accolade-uitgebreid bereik, dus programma’s die beide gebruiken, kunnen een klein beetje moeilijker te lezen zijn. - @bobbogo’s antwoord gebruikt een deel van de bekende syntaxis, maar doet iets onverwachts als de variabele
$END
geen geldig bereik “boekensteun” is voor de andere kant van het bereik. AlsEND=a
bijvoorbeeld, zal er geen fout optreden en wordt de woordelijke waarde{1..a}
herhaald. Dit is ook het standaardgedrag van Bash – het is gewoon vaak onverwacht.
Disclaimer: ik ben de auteur van de gekoppelde code.
Antwoord 14
Deze zijn allemaal leuk, maar seq is zogenaamd verouderd en de meeste werken alleen met numerieke bereiken.
Als je je for-lus tussen dubbele aanhalingstekens plaatst, worden de begin- en eindvariabelen verwijderd wanneer je de tekenreeks echo, en kun je de tekenreeks direct terugsturen naar BASH voor uitvoering. $i
moet worden geëscaped met \’s, zodat het NIET wordt geëvalueerd voordat het naar de subshell wordt verzonden.
RANGE_START=a
RANGE_END=z
echo -e "for i in {$RANGE_START..$RANGE_END}; do echo \\${i}; done" | bash
Deze uitgang kan ook aan een variabele worden toegewezen:
VAR=`echo -e "for i in {$RANGE_START..$RANGE_END}; do echo \\${i}; done" | bash`
De enige “overhead” die dit zou moeten genereren, zou de tweede instantie van bash moeten zijn, dus het zou geschikt moeten zijn voor intensieve operaties.
Antwoord 15
Vervang {}
door (( ))
:
tmpstart=0;
tmpend=4;
for (( i=$tmpstart; i<=$tmpend; i++ )) ; do
echo $i ;
done
Opbrengsten:
0
1
2
3
4
Antwoord 16
Als je shell-commando’s doet en je (zoals ik) een fetisj hebt voor pipelining, dan is deze goed:
seq 1 $END | xargs -I {} echo {}
Antwoord 17
Er zijn veel manieren om dit te doen, maar de manieren waarop ik de voorkeur geef, worden hieronder gegeven
Gebruik seq
Synopsis van
man seq
$ seq [-w] [-f format] [-s string] [-t string] [first [incr]] last
Syntaxis
Volledige opdracht
seq first incr last
- eerste is het startnummer in de reeks [is optioneel, standaard:1]
- incr is increment [is optioneel, standaard:1]
- laatste is het laatste getal in de reeks
Voorbeeld:
$ seq 1 2 10
1 3 5 7 9
Alleen met eerste en laatste:
$ seq 1 5
1 2 3 4 5
Alleen met laatste:
$ seq 5
1 2 3 4 5
Gebruik {first..last..incr}
Hier zijn eerste en laatste verplicht en incr is optioneel
Alleen eerste en laatste gebruiken
$ echo {1..5}
1 2 3 4 5
Incr gebruiken
$ echo {1..10..2}
1 3 5 7 9
Je kunt dit zelfs gebruiken voor karakters zoals hieronder
$ echo {a..z}
a b c d e f g h i j k l m n o p q r s t u v w x y z
Antwoord 18
als u ‘seq
‘ of ‘eval
‘ of jot
of rekenkundige uitbreidingsindelingen niet wilt gebruiken, bijv. for ((i=1;i<=END;i++))
, of andere lussen, bijv. while
, en je wilt niet alleen ‘printf
‘ en graag alleen ‘echo
‘, dan past deze eenvoudige oplossing wellicht in je budget:
a=1; b=5; d='for i in {'$a'..'$b'}; do echo -n "$i"; done;' echo "$d" | bash
PS: Mijn bash heeft sowieso geen ‘seq
‘ commando.
Getest op Mac OSX 10.6.8, Bash 3.2.48
Antwoord 19
Dit werkt in Bash en Korn, het kan ook van hogere naar lagere nummers gaan. Waarschijnlijk niet de snelste of mooiste, maar werkt goed genoeg. Verwerkt ook negatieven.
function num_range {
# Return a range of whole numbers from beginning value to ending value.
# >>> num_range start end
# start: Whole number to start with.
# end: Whole number to end with.
typeset s e v
s=${1}
e=${2}
if (( ${e} >= ${s} )); then
v=${s}
while (( ${v} <= ${e} )); do
echo ${v}
((v=v+1))
done
elif (( ${e} < ${s} )); then
v=${s}
while (( ${v} >= ${e} )); do
echo ${v}
((v=v-1))
done
fi
}
function test_num_range {
num_range 1 3 | egrep "1|2|3" | assert_lc 3
num_range 1 3 | head -1 | assert_eq 1
num_range -1 1 | head -1 | assert_eq "-1"
num_range 3 1 | egrep "1|2|3" | assert_lc 3
num_range 3 1 | head -1 | assert_eq 3
num_range 1 -1 | tail -1 | assert_eq "-1"
}