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}
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;)
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.
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.
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.
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).
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
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:
(( ))
ohne Dollar, obwohl es eine übliche Erweiterung ist wie von POSIX selbst erwähnt .[[
. [
ist hier genug. 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 wie erwähnt funktionieren durch das Bash-Handbuch .let i=i+1
: POSIX 7 2. Shell Command Language enthält nicht das Wort let
und schlägt in bash --posix
4.3.42 fehlder Dollar bei i=$i+1
ist möglicherweise erforderlich, aber ich bin mir nicht sicher. POSIX 7 2.6.4 Arithmetische Erweiterung sagt:
Wenn die Shell-Variable x einen Wert enthält, der eine gültige Ganzzahlkonstante bildet, die optional ein führendes Plus- oder Minuszeichen enthält, geben die arithmetischen Erweiterungen "$ ((x))" und "$ ((($ x))" dasselbe zurück Wert.
aber wörtlich zu lesen bedeutet nicht, dass sich $((x+1))
erweitert, da x+1
keine Variable ist.
Eine weitere Indirektionsebene:
for i in $(eval echo {1..$END}); do
∶
Sie können verwenden
for i in $(seq $END); do echo $i; done
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
Wenn Sie unter BSD/OS X arbeiten, können Sie jot anstelle von seq verwenden:
for i in $(jot $END); do echo $i; done
Dies funktioniert gut in bash
:
END=5
i=1 ; while [[ $i -le $END ]] ; do
echo $i
((i = i + 1))
done
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:
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.{a..z}
_; Klammer Expansion wird. Die Frage bezog sich jedoch auf Bereiche von Zahlen , so dass dies ein Streitpunkt ist.{1..10}
_-Syntax mit geschweiften Klammern, sodass Programme, die beide verwenden, möglicherweise etwas schwerer zu lesen sind.$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.
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}
Dies ist ein anderer Weg:
end=5
for i in $(bash -c "echo {1..${end}}"); do echo $i; done
Ersetzen Sie {}
durch (( ))
:
tmpstart=0;
tmpend=4;
for (( i=$tmpstart; i<=$tmpend; i++ )) ; do
echo $i ;
done
Erträge:
0
1
2
3
4
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.
Ich habe hier einige der Ideen zusammengefasst und die Leistung gemessen.
seq
und {..}
sind sehr schnellfor
und while
Schleifen sind langsam$( )
ist langsamfor (( ; ; ))
Loops sind langsamer$(( ))
ist noch langsamer 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.
# 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
Wenn Sie Shell-Befehle ausführen und (wie ich) einen Fetisch für Pipelining haben, ist dieser gut:
seq 1 $END | xargs -I {} echo {}
Es gibt viele Möglichkeiten, dies zu tun, aber die, die ich bevorzuge, sind unten angegeben
seq
verwendenInhaltsangabe von
man seq
$ seq [-w] [-f format] [-s string] [-t string] [first [incr]] last
Syntax
Voller Befehlseq first incr last
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}
verwendenHier 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
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"
}
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