web-dev-qa-db-de.com

Wie kann ich einen durch Variablen in Bash definierten Zahlenbereich durchlaufen?

Wie kann ich einen Zahlenbereich in Bash durchlaufen, wenn der Bereich durch eine Variable vorgegeben wird?

Ich weiß, dass ich dies tun kann (in der Bash Dokumentation als "Sequenzausdruck" bezeichnet):

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

Welches gibt:

1
2
3
4
5

Wie kann ich jedoch einen der Bereichsendpunkte durch eine Variable ersetzen? Das geht nicht:

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

Welche Drucke:

{1..5}

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

edit: Ich bevorzuge seq gegenüber den anderen Methoden, weil ich mich tatsächlich daran erinnern kann;)

1498
Jiaaro

Die seq -Methode ist die einfachste, Bash hat jedoch eine integrierte 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.

394
ephemient

diskussion

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

ganzzahlige Arithmetik

Dies ist eine verbesserte Version der Bash-Schleife:

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

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

ephemient hat mir etwas beigebracht: Bash erlaubt for ((expr;expr;expr)) Konstrukte. Da ich noch nie die ganze Manpage für Bash gelesen habe (wie ich es mit der Korn Shell (ksh) Manpage gemacht habe und das schon lange her ist), habe ich das verpasst.

Damit,

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

dies scheint der speichereffizienteste Weg zu sein (es ist nicht erforderlich, Speicher zuzuweisen, um die Ausgabe von seq zu verbrauchen, was ein Problem sein kann, wenn END sehr groß ist), obwohl dies wahrscheinlich nicht der „schnellste“ Weg ist.

die anfängliche Frage

eschercycle stellte fest, dass die { a .. b } Bash-Notation nur funktioniert mit Literalen; Richtig, entsprechend dem Bash-Handbuch. Man kann dieses Hindernis mit einem einzigen (internen) fork() ohne ein exec() überwinden (wie es beim Aufruf von seq der Fall ist, wozu ein anderes Bild eine Fork + Exec benötigt):

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

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

175
tzot

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

Von man bash:

Die Klammererweiterung wird vor allen anderen Erweiterungen durchgeführt, und alle Sonderzeichen für andere Erweiterungen bleiben im Ergebnis erhalten. Es ist streng in Textform. Bash wendet keine syntaktische Interpretation auf den Kontext der Erweiterung oder den Text zwischen den geschweiften Klammern an.

Also, Klammererweiterung ist etwas, das früher als reine Textmakrooperation ausgeführt wurde, bevor Parametererweiterung

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

Empfehlung

Ich würde vorschlagen, bei Posix zu bleiben1 Eigenschaften. Dies bedeutet, dass for i in <list>; do verwendet wird, 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 füge keine Bash-Ismen in meine Skripte ein. Skripte benötigen möglicherweise eine schnellere, sicherere und eingebettete Shell. Möglicherweise müssen sie unter/bin/sh ausgeführt werden, und dann gibt es alle üblichen Pro-Standards-Argumente. Denken Sie daran, Shellshock, aka bashdoor?
96
DigitalRoss

Der POSIX-Weg

Wenn Sie Wert auf Portabilität 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:

Eine weitere Indirektionsebene:

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

Sie können verwenden

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

Wenn Sie ein Präfix benötigen, könnte Ihnen dieses gefallen

 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
19
hossbear

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

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

Dies funktioniert gut in bash:

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

Wenn Sie so nah wie möglich an der Syntax des Klammerausdrucks bleiben möchten, probieren Sie die Funktion range AUS BASH-TRICKS '_range.bash aus.

Beispiel: Alle folgenden Aktionen führen genau das Gleiche aus wie _echo {1..10}_:

_source range.bash
one=1
ten=10

range {$one..$ten}
range $one $ten
range {1..$ten}
range {1..10}
_

Es wird versucht, die native Bash-Syntax mit möglichst wenigen "Fallstricken" zu unterstützen: Es werden nicht nur Variablen unterstützt, sondern auch das häufig unerwünschte Verhalten von ungültigen Bereichen, die als Zeichenfolgen (z. B. _for i in {1..a}; do echo $i; done_) bereitgestellt werden.

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

  • Viele von ihnen verwenden Subshells , was Leistung beeinträchtigen und möglicherweise nicht möglich auf einigen Systemen bedeuten kann.
  • Viele von ihnen verlassen sich auf externe Programme. Sogar seq ist eine Binärdatei, die installiert werden muss, um verwendet zu werden, von der Bash geladen werden muss und das erwartete Programm enthält, damit es in diesem Fall funktioniert. Allgegenwärtig oder nicht, das ist viel mehr als nur die Bash-Sprache selbst.
  • Lösungen, die nur native Bash-Funktionen wie @ ephemient verwenden, funktionieren nicht in alphabetischen Bereichen wie _{a..z}_; Klammer Expansion wird. Die Frage bezog sich jedoch auf Bereiche von Zahlen , so dass dies ein Streitpunkt ist.
  • Die meisten von ihnen ähneln optisch nicht der _{1..10}_-Syntax mit geschweiften Klammern, sodass Programme, die beide verwenden, möglicherweise etwas schwerer zu lesen sind.
  • Die Antwort von @ bobbogo verwendet einige der bekannten Syntax, führt jedoch etwas Unerwartetes aus, wenn die Variable _$END_ kein gültiger Bereich "bookend" für die andere Seite des Bereichs ist. Wenn zum Beispiel _END=a_ ein Fehler auftritt und der wörtliche Wert _{1..a}_ zurückgegeben wird. Dies ist auch das Standardverhalten von Bash - es ist nur oft unerwartet.

Haftungsausschluss: Ich bin der Autor des verlinkten Codes.

7
Zac B

Ich weiß, dass es bei dieser Frage um bash geht, aber - nur zur Veranschaulichung - ksh93 ist schlauer und setzt sie wie erwartet um:

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

Dies ist ein anderer Weg:

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

Ersetzen Sie {} durch (( )):

tmpstart=0;
tmpend=4;

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

Erträge:

0
1
2
3
4
6
BashTheKeyboard

Dies sind alles Nizza, aber seq ist angeblich veraltet und die meisten arbeiten nur mit numerischen Bereichen.

Wenn Sie Ihre for-Schleife in doppelte Anführungszeichen setzen, werden die Start- und Endvariablen dereferenziert, wenn Sie die Zeichenfolge als Echo ausgeben, und Sie können die Zeichenfolge zur Ausführung direkt an BASH zurücksenden. $i muss mit\'s maskiert werden, 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 zugeordnet werden:

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

Der einzige "Overhead", den dies verursachen sollte, sollte die zweite Bash-Instanz sein, damit es für intensive Operationen geeignet ist.

6
SuperBob

Ich habe hier einige der Ideen zusammengefasst und die Leistung gemessen.

TL; DR Imbissbuden:

  1. seq und {..} sind sehr schnell
  2. for und while Schleifen sind langsam
  3. $( ) ist langsam
  4. for (( ; ; )) Loops sind langsamer
  5. $(( )) ist noch langsamer
  6. Sich Gedanken über N Zahlen im Speicher (seq oder {..}) zu machen, ist albern (mindestens bis zu 1 Million).

Dies sind nicht Schlussfolgerungen. Sie müssten sich den C-Code hinter jedem dieser Elemente ansehen, um Schlussfolgerungen zu ziehen. Hier geht es eher darum, wie wir diese Mechanismen normalerweise zum Durchlaufen von Code verwenden. Die meisten Einzeloperationen sind so schnell, dass sie in den meisten Fällen keine Rolle spielen. Aber ein Mechanismus wie for (( i=1; i<=1000000; i++ )) ist viele Operationen, wie Sie visuell sehen können. Es gibt auch viel mehr Operationen pro Schleife als Sie von for i in $(seq 1 1000000) erhalten. Und das ist Ihnen vielleicht nicht klar, weshalb es wertvoll ist, solche Tests durchzuführen.

Demos

# 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
6
Bruno Bronosky

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

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

5
Alex Spangher

Es gibt viele Möglichkeiten, dies zu tun, aber die, die ich bevorzuge, sind unten angegeben

seq verwenden

Inhaltsangabe von man seq

$ seq [-w] [-f format] [-s string] [-t string] [first [incr]] last

Syntax

Voller Befehl
seq first incr last

  • das erste ist die Startnummer in der Sequenz [ist optional, standardmäßig: 1].
  • incr ist increment [ist optional, standardmäßig: 1]
  • last ist die letzte Nummer in der Sequenz

Beispiel:

$ seq 1 2 10
1 3 5 7 9

Nur mit erstem und letztem:

$ seq 1 5
1 2 3 4 5

Nur mit letzter:

$ seq 5
1 2 3 4 5

{first..last..incr} verwenden

Hier sind first und last obligatorisch und incr ist optional

Mit nur ersten und letzten

$ echo {1..5}
1 2 3 4 5

Verwenden von Inkr

$ echo {1..10..2}
1 3 5 7 9

Sie können dies auch für Zeichen wie unten verwenden

$ 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
1
theBuzzyCoder

Dies funktioniert in Bash und Korn, kann auch von höheren zu niedrigeren Zahlen gehen. Wahrscheinlich nicht am schnellsten oder am schönsten, funktioniert aber gut genug. Behandelt 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 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"
}
0
Ethan Post

wenn Sie nicht 'seq' oder 'eval' oder jot oder ein arithmetisches Erweiterungsformat verwenden möchten, z. for ((i=1;i<=END;i++)) oder andere Schleifen, z. while, und Sie möchten nicht "printf" und möchten nur "echo", dann passt diese einfache Problemumgehung möglicherweise zu Ihrem Budget:

a=1; b=5; d='for i in {'$a'..'$b'}; do echo -n "$i"; done;' echo "$d" | bash

PS: Meine Bash hat sowieso nicht den Befehl 'seq'.

Getestet unter Mac OSX 10.6.8, Bash 3.2.48

0
Zimba