Wie iteriere ich über einen Bereich von Zahlen, die durch Variablen in Bash definiert sind?

Wie wiederhole ich über einen Bereich von Zahlen in Bash, wenn der Bereich durch eine Variable gegeben ist?

Ich weiß, dass ich das kann (in der Bash- Dokumentation “Sequenz-Ausdruck” genannt):

for i in {1..5}; do echo $i; done 

Was gibt:

1
2
3
4
5

Wie kann ich jedoch einen der Bereichsendpunkte durch eine Variable ersetzen? Dies funktioniert nicht:

 END=5 for i in {1..$END}; do echo $i; done 

Welche Drucke:

{1..5}

 for i in $(seq 1 $END); do echo $i; done 

edit: Ich bevorzuge seq über die anderen Methoden, weil ich mich daran erinnern kann;)

Die seq Methode ist die einfachste, aber Bash hat eine eingebaute arithmetische Auswertung.

 END=5 for ((i=1;i< =END;i++)); do echo $i done # ==> outputs 1 2 3 4 5 on separate lines 

Das for ((expr1;expr2;expr3)); Konstrukt funktioniert genau wie for (expr1;expr2;expr3) in C und ähnlichen Sprachen, und wie andere ((expr)) Fälle behandelt Bash sie als arithmetisch.

Diskussion

Die Verwendung von seq ist in Ordnung, wie Jiaaro vorgeschlagen hat. Pax Diablo schlug eine Bash-Schleife vor, um den Aufruf eines Subprozesses zu vermeiden, mit dem zusätzlichen Vorteil, speicherfreundlicher zu sein, wenn $ END zu groß ist. Zathrus entdeckte einen typischen Fehler in der Schleifenimplementierung und deutete auch an, dass, da i eine Textvariable ist, fortlaufende Konvertierungen von Hin- und Herzahlen mit einer damit verbundenen Verlangsamung durchgeführt werden.

Ganzzahlarithmetik

Dies ist eine verbesserte Version der Bash-Schleife:

 typeset -ii END let END=5 i=1 while ((i< =END)); do echo $i … let i++ done 

Wenn das einzige, was wir wollen, das echo , könnten wir echo $((i++)) schreiben.

Ephemier lehrte mich etwas: Bash erlaubt for ((expr;expr;expr)) Konstrukte. Da ich nie die ganze Manpage für Bash gelesen habe (wie ich es mit der Kornshell ( ksh ) Manpage gemacht habe, und das war lange her), habe ich das vermisst.

Damit,

 typeset -ii END # Let's be explicit for ((i=1;i< =END;++i)); do echo $i; done 

scheint der speicherfreundlichste Weg zu sein (es wird nicht notwendig sein, Speicher zuzuweisen, um seq Ausgabe zu verbrauchen, was ein Problem sein könnte, wenn END sehr groß ist), obwohl wahrscheinlich nicht der "schnellste" ist.

die erste Frage

eschercycle hat festgestellt, dass die Bash-Schreibweise nur mit Literalen funktioniert. wahr, entsprechend dem Bash Handbuch. Man kann dieses Hindernis mit einem einzigen (internen) fork() ohne exec() überwinden (wie es beim Aufruf von seq der Fall ist, wo ein anderes Bild eine fork + exec erfordert):

 for i in $(eval echo "{1..$END}"); do 

Sowohl eval als auch echo sind Bash-Butintins, aber fork() ist für die Befehlsersetzung (das Konstrukt $(…) erforderlich).

Hier ist der Grund, warum der ursprüngliche Ausdruck nicht funktioniert hat.

Von Mann bash :

Die Klammerexpansion wird vor allen anderen Expansionen ausgeführt, und alle Zeichen, die für andere Expansionen besonders sind, werden im Ergebnis beibehalten. Es ist streng textlich. Bash wendet keine syntaktische Interpretation auf den Kontext der Erweiterung oder den Text zwischen den geschweiften Klammern an.

Klammerexpansion ist also etwas früher als eine rein textuelle Makrooperation vor der Parametererweiterung.

Shells sind hochoptimierte Hybride zwischen Makroprozessoren und formaleren Programmiersprachen. Um die typischen Anwendungsfälle zu optimieren, wird die Sprache etwas komplexer und einige Einschränkungen werden akzeptiert.

Empfehlung

Ich würde vorschlagen, mit Posix 1 Features zu bleiben. Das bedeutet for i in ; do for i in ; do , wenn die Liste bereits bekannt ist, andernfalls while oder seq , wie 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 ist eine großartige Shell und ich benutze sie interaktiv, aber ich setze keine Bash-Ismen in meine Skripte. Skripte benötigen möglicherweise eine schnellere Shell, eine sicherere, eine eingebettetere. Sie müssen möglicherweise auf dem laufen, was als / bin / sh installiert ist, und dann gibt es alle üblichen Pro-Standard-Argumente. Erinnerst du dich an Shellshock, alias Bashdoor?

Der POSIX-Weg

Wenn Sie auf Portabilität Wert legen, verwenden Sie das Beispiel aus dem POSIX-Standard :

 i=2 end=5 while [ $i -le $end ]; do echo $i i=$(($i+1)) done 

Ausgabe:

 2 3 4 5 

Dinge, die nicht POSIX sind:

  • (( )) ohne Dollar, obwohl es eine von POSIX selbst erwähnte Erweiterung ist.
  • [[ . [ ist genug hier. Siehe auch: Was ist der Unterschied zwischen einfachen und doppelten eckigen Klammern in Bash?
  • for ((;;))
  • seq (GNU Coreutils)
  • {start..end} , und das kann nicht mit Variablen funktionieren, wie im Bash-Handbuch erwähnt .
  • let i=i+1 : POSIX 7 2. Shell Command Language enthält nicht das Wort let , und es scheitert an bash --posix 4.3.42
  • der Dollar bei i=$i+1 könnte erforderlich sein, aber ich bin mir nicht sicher. POSIX 7 2.6.4 Arithmetische Expansion sagt:

    Wenn die Shellvariable x einen Wert enthält, der eine gültige Ganzzahlkonstante bildet und optional ein vorangestelltes Plus- oder Minuszeichen enthält, dann müssen die arithmetischen Erweiterungen “$ ((x))” und “$ (($ x))” dasselbe Ergebnis liefern Wert.

    Aber wenn man es wörtlich liest, bedeutet das nicht, dass $((x+1)) expandiert, da x+1 keine Variable ist.

Eine andere Ebene der Indirektion:

 for i in $(eval echo {1..$END}); do ∶ 

Sie können verwenden

 for i in $(seq $END); do echo $i; done 

Wenn Sie BSD / OS X verwenden, können Sie jot anstelle von seq verwenden:

 for i in $(jot $END); do echo $i; done 

Das funktioniert gut in bash :

 END=5 i=1 ; while [[ $i -le $END ]] ; do echo $i ((i = i + 1)) done 

Wenn Sie dieses Präfix benötigen, können Sie dies vielleicht tun

  for ((i=7;i< =12;i++)); do echo `printf "%2.0d\n" $i |sed "s/ /0/"`;done 

das wird nachgeben

 07 08 09 10 11 12 

Ich weiß, dass es sich bei dieser Frage um bash , aber – nur für das Protokoll – ist ksh93 schlauer und implementiert es wie erwartet:

 $ 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} 

Dies ist ein anderer Weg:

 end=5 for i in $(bash -c "echo {1..${end}}"); do echo $i; done 

Diese sind alle nett, aber Seq ist angeblich veraltet und die meisten arbeiten nur mit numerischen Bereichen.

Wenn Sie Ihre for-Schleife in doppelte Anführungszeichen einschließen, werden die Start- und Endvariablen dereferenziert, wenn Sie die Zeichenfolge wiederholen, und Sie können die Zeichenfolge zur Ausführung an BASH zurückschicken. $i muss mit Escapezeichen versehen sein, damit es NICHT ausgewertet wird, bevor es an die Subshell gesendet wird.

 RANGE_START=a RANGE_END=z echo -e "for i in {$RANGE_START..$RANGE_END}; do echo \\${i}; done" | bash 

Dieser Ausgang kann auch einer Variablen zugewiesen werden:

 VAR=`echo -e "for i in {$RANGE_START..$RANGE_END}; do echo \\${i}; done" | bash` 

Der einzige “Overhead”, der erzeugt werden sollte, sollte die zweite Instanz von bash sein, so dass sie für intensive Operationen geeignet sein sollte.

Ersetzen Sie {} durch (( )) :

 tmpstart=0; tmpend=4; for (( i=$tmpstart; i< =$tmpend; i++ )) ; do echo $i ; done 

Erträge:

 0 1 2 3 4 

Wenn Sie Shell-Befehle ausführen und Sie (wie ich) einen Fetisch für Pipelining haben, ist dieser hier gut:

seq 1 $END | xargs -I {} echo {}

Wenn Sie so nahe wie möglich an der geschweiften Ausdruckssyntax bleiben möchten, probieren Sie die range aus bash-Tricks ‘ range.bash .

Zum Beispiel werden alle der folgenden genau dasselbe wie echo {1..10} :

 source range.bash one=1 ten=10 range {$one..$ten} range $one $ten range {1..$ten} range {1..10} 

Es versucht, die native Bash-Syntax mit möglichst wenigen “gotchas” zu unterstützen: Es werden nicht nur Variablen unterstützt, sondern das oft unerwünschte Verhalten von ungültigen Bereichen als Strings (zB for i in {1..a}; do echo $i; done ) wird ebenfalls verhindert.

Die anderen Antworten werden in den meisten Fällen funktionieren, aber alle haben mindestens einen der folgenden Nachteile:

  • Viele von ihnen verwenden Subshells , die die performance beeinträchtigen können und auf einigen Systemen möglicherweise nicht möglich sind.
  • Viele von ihnen sind auf externe Programme angewiesen. Auch seq ist eine Binärdatei, die zur Verwendung installiert werden muss, von bash geladen werden muss und das erwartete Programm enthalten muss, damit es in diesem Fall funktioniert. Ubiquitous oder nicht, das ist viel mehr als nur die Bash Sprache selbst zu verlassen.
  • Lösungen, die nur native Bash-functionalität verwenden, wie z. B. @ ephemient, funktionieren nicht in alphabetischen Bereichen wie {a..z} ; Klammererweiterung wird. Die Frage war jedoch Zahlenbereiche , also ist das eine Frage.
  • Die meisten von ihnen sind visuell nicht der {1..10} erweiterten Bereichssyntax ähnlich, so dass Programme, die beide verwenden, ein kleines bisschen schwerer zu lesen sind.
  • @ bobbogos Antwort verwendet etwas von der bekannten Syntax, tut aber etwas Unerwartetes, wenn die Variable $END für die andere Seite des Bereichs kein gültiger Bereich “Buchstütze” ist. Wenn beispielsweise END=a , tritt kein Fehler auf und der wortgetreue Wert {1..a} wird wiederholt. Dies ist auch das Standardverhalten von Bash – es ist nur oft unerwartet.

Haftungsausschluss: Ich bin der Autor des verlinkten Codes.

Das funktioniert in Bash und Korn, kann auch von höheren zu niedrigeren Nummern gehen. Wahrscheinlich nicht am schnellsten oder am schönsten, aber funktioniert gut genug. Griffe auch Negative.

 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 sev 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" }