Hoe herhaal ik een reeks getallen gedefinieerd door variabelen in Bash?

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:

  1. seq en {..} zijn erg snel
  2. for en while loops traag zijn
  3. $(…) is traag
  4. for (( ; ; )) loops zijn langzamer
  5. $(( )) is nog langzamer
  6. 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. Als END=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"
}

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Other episodes