Comment itérer sur une plage de nombres dans Bash lorsque la plage est donnée par une variable?
Je sais que je peux le faire (appelé "expression de séquence" dans le Bash documentation ):
for i in {1..5}; do echo $i; done
Qui donne:
1
2
3
4
5
Cependant, comment puis-je remplacer l'un des extrémités de la plage par une variable? Cela ne marche pas:
END=5
for i in {1..$END}; do echo $i; done
Quelles impressions:
{1..5}
for i in $(seq 1 $END); do echo $i; done
edit: je préfère seq
aux autres méthodes car je peux vraiment m'en souvenir;)
La méthode seq
est la plus simple, mais Bash a une évaluation arithmétique intégrée.
END=5
for ((i=1;i<=END;i++)); do
echo $i
done
# ==> outputs 1 2 3 4 5 on separate lines
La construction for ((expr1;expr2;expr3));
fonctionne comme for (expr1;expr2;expr3)
en C et dans les langages similaires, et comme dans les autres cas ((expr))
, Bash les traite comme des méthodes arithmétiques.
Utiliser seq
est bon, comme suggéré par Jiaaro. Pax Diablo a suggéré une boucle Bash pour éviter d'appeler un sous-processus, avec l'avantage supplémentaire d'être plus convivial pour la mémoire si $ END est trop volumineux. Zathrus a repéré un bogue typique dans l'implémentation de la boucle, et a également laissé entendre que, puisque i
est une variable de texte, les conversions en continu sont effectuées avec des ralentissements associés.
Ceci est une version améliorée de la boucle Bash:
typeset -i i END
let END=5 i=1
while ((i<=END)); do
echo $i
…
let i++
done
Si la seule chose que nous voulons est la echo
, alors nous pourrions écrire echo $((i++))
.
éphémère m'a appris quelque chose: Bash permet à for ((expr;expr;expr))
de construire. Comme je n’ai jamais lu toute la page de manuel de Bash (comme je l’ai fait avec la page de manuel de Korn Shell (ksh
), et cela remonte à longtemps), j’ai loupé ça.
Alors,
typeset -i i END # Let's be explicit
for ((i=1;i<=END;++i)); do echo $i; done
semble être le moyen le plus efficace en termes de mémoire (il ne sera pas nécessaire d’allouer de la mémoire pour utiliser la sortie de seq
, ce qui pourrait poser un problème si END est très volumineux), bien que ce ne soit probablement pas le plus rapide.
eschercycle a noté que la notation { a .. b } ne fonctionne que avec des littéraux; vrai, selon le manuel Bash. On peut surmonter cet obstacle avec un seul (interne) fork()
sans exec()
(comme c'est le cas avec l'appel de seq
, qui est une autre image et requiert un fork + un exec):
for i in $(eval echo "{1..$END}"); do
eval
et echo
sont des commandes intégrées à Bash, mais une fork()
est requise pour la substitution de commande (la construction $(…)
.).
Voici pourquoi l'expression originale n'a pas fonctionné.
De homme bash:
Le développement d'accolade est effectué avant toute autre extension, et les caractères spéciaux par rapport à d'autres extensions sont conservés dans le résultat. C'est strictement textuel. Bash n'applique aucune interprétation syntaxique au contexte de l'extension ni au texte entre les accolades.
Donc, expansion d'accolade est une opération effectuée tôt sous forme de macro purement textuelle, avant expansion de paramètre.
Les shells sont des hybrides hautement optimisés entre les macro-processeurs et les langages de programmation plus formels. Afin d’optimiser les cas d’utilisation typiques, le langage est rendu un peu plus complexe et certaines limitations sont acceptées.
Recommandation
Je suggère de rester avec Posix1 traits. Cela signifie que vous utiliserez for i in <list>; do
si la liste est déjà connue. Sinon, utilisez while
ou seq
, comme dans:
#!/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
La méthode POSIX
Si vous vous souciez de la portabilité, utilisez le exemple du standard POSIX :
i=2
end=5
while [ $i -le $end ]; do
echo $i
i=$(($i+1))
done
Sortie:
2
3
4
5
Choses qui sont pas POSIX:
(( ))
sans dollar, bien que ce soit une extension commune comme mentionné par POSIX lui-même .[[
. [
est suffisant ici. Voir aussi: Quelle est la différence entre les crochets simples et doubles dans Bash?for ((;;))
seq
(GNU Coreutils){start..end}
, et cela ne peut pas fonctionner avec les variables mentionnées par le manuel de Bash .let i=i+1
: POSIX 7 2. Langage de commande Shell ne contient pas le mot let
et échoue le bash --posix
4.3.42.le dollar à i=$i+1
peut être requis, mais je ne suis pas sûr. POSIX 7 2.6.4 Expansion arithmétique dit:
Si la variable Shell x contient une valeur qui forme une constante entière valide, incluant éventuellement un signe plus ou moins, les développements arithmétiques "$ ((x))" et "$ (($ x))" renvoient le même résultat. valeur.
mais le lire littéralement n'implique pas que $((x+1))
se développe puisque x+1
n'est pas une variable.
Une autre couche d'indirection:
for i in $(eval echo {1..$END}); do
∶
Vous pouvez utiliser
for i in $(seq $END); do echo $i; done
Si vous en avez besoin, préfixez-le.
for ((i=7;i<=12;i++)); do echo `printf "%2.0d\n" $i |sed "s/ /0/"`;done
cela donnera
07
08
09
10
11
12
Si vous utilisez BSD/OS X, vous pouvez utiliser jot au lieu de seq:
for i in $(jot $END); do echo $i; done
Cela fonctionne très bien dans bash
:
END=5
i=1 ; while [[ $i -le $END ]] ; do
echo $i
((i = i + 1))
done
Si vous voulez rester aussi proche que possible de la syntaxe de l'expression d'accolade, essayez la fonction range
DE BASH-ASTUCES '_range.bash
.
Par exemple, tout ce qui suit fera exactement la même chose que _echo {1..10}
_:
_source range.bash
one=1
ten=10
range {$one..$ten}
range $one $ten
range {1..$ten}
range {1..10}
_
Il essaie de prendre en charge la syntaxe native bash avec le moins possible de "pièges": non seulement les variables sont prises en charge, mais le comportement souvent indésirable des plages non valides fournies en tant que chaînes (par exemple, _for i in {1..a}; do echo $i; done
_) est également évité.
Les autres réponses fonctionneront dans la plupart des cas, mais elles présentent toutes au moins l'un des inconvénients suivants:
seq
est un binaire qui doit être installé pour être utilisé, doit être chargé par bash et doit contenir le programme que vous attendez pour qu'il fonctionne dans ce cas. Omniprésent ou pas, c'est beaucoup plus sur lequel compter que le langage Bash lui-même.{a..z}
_; expansion brace sera. La question portait sur les plages de nombres , alors c'est un casse-tête.{1..10}
_, élargie, donc les programmes qui utilisent les deux peuvent être un peu plus difficiles à lire.$END
_ n'est pas une plage valide "bookend" pour l'autre côté de la plage. Si _END=a
_, par exemple, aucune erreur ne se produira et la valeur in extenso _{1..a}
_ sera répercutée. C'est également le comportement par défaut de Bash - il est juste souvent inattendu.Disclaimer: Je suis l'auteur du code lié.
Je sais que cette question concerne bash
, mais - juste pour mémoire - ksh93
est plus intelligent et l'implémente comme prévu:
$ 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}
C'est une autre façon:
end=5
for i in $(bash -c "echo {1..${end}}"); do echo $i; done
Remplacez {}
par (( ))
:
tmpstart=0;
tmpend=4;
for (( i=$tmpstart; i<=$tmpend; i++ )) ; do
echo $i ;
done
Rendements:
0
1
2
3
4
Celles-ci sont toutes agréables, mais seq est censé être obsolète et la plupart ne fonctionnent qu'avec des plages numériques.
Si vous placez votre boucle for entre guillemets, les variables de début et de fin seront déréférencées lors de l'écho de la chaîne. Vous pourrez alors renvoyer la chaîne à BASH pour qu'elle soit exécutée. $i
doit être échappé avec\'s pour qu'il ne soit PAS évalué avant d'être envoyé au sous-shell.
RANGE_START=a
RANGE_END=z
echo -e "for i in {$RANGE_START..$RANGE_END}; do echo \\${i}; done" | bash
Cette sortie peut également être affectée à une variable:
VAR=`echo -e "for i in {$RANGE_START..$RANGE_END}; do echo \\${i}; done" | bash`
La seule "surcharge" que cela devrait générer devrait être la deuxième instance de bash et devrait donc convenir à des opérations intensives.
J'ai combiné quelques idées ici et mesuré la performance.
seq
et {..}
sont vraiment rapidesfor
et while
les boucles sont lentes$( )
est lentfor (( ; ; ))
les boucles sont plus lentes$(( ))
est encore plus lent Ce ne sont pas conclusions. Il faudrait examiner le code C derrière chacun de ces éléments pour tirer des conclusions. Cela concerne davantage la façon dont nous avons tendance à utiliser chacun de ces mécanismes pour boucler sur du code. La plupart des opérations simples sont suffisamment proches pour atteindre la même vitesse que dans la plupart des cas. Mais un mécanisme comme for (( i=1; i<=1000000; i++ ))
comporte de nombreuses opérations visibles visuellement. Il y a aussi beaucoup plus d'opérations par boucle que ce que vous obtenez de for i in $(seq 1 1000000)
. Et cela peut ne pas être évident pour vous, c'est pourquoi il est utile de faire de tels tests.
# 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
Si vous faites des commandes Shell et que vous (comme moi) avez un fétiche pour le traitement en pipeline, celui-ci est bon:
seq 1 $END | xargs -I {} echo {}
Il existe de nombreuses façons de le faire, mais celles que je préfère sont indiquées ci-dessous.
seq
Synopsis de
man seq
$ seq [-w] [-f format] [-s string] [-t string] [first [incr]] last
Syntaxe
Commande complèteseq first incr last
Exemple:
$ seq 1 2 10
1 3 5 7 9
Seulement avec le premier et le dernier:
$ seq 1 5
1 2 3 4 5
Seulement avec le dernier:
$ seq 5
1 2 3 4 5
{first..last..incr}
Ici le premier et le dernier sont obligatoires et incr est optionnel
Utiliser seulement le premier et le dernier
$ echo {1..5}
1 2 3 4 5
Utiliser incr
$ echo {1..10..2}
1 3 5 7 9
Vous pouvez l'utiliser même pour les personnages comme ci-dessous
$ 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
Cela fonctionne dans Bash et Korn, peut également aller de plus en plus bas. Probablement pas le plus rapide ni le plus joli, mais fonctionne assez bien. Gère les négatifs aussi.
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"
}
si vous ne voulez pas utiliser 'seq
' ou 'eval
' ou jot
ou un format d'expansion arithmétique, par exemple. for ((i=1;i<=END;i++))
, ou d'autres boucles, par exemple. while
, et vous ne voulez pas "printf
" et content de "echo
" seulement, cette solution de contournement simple pourrait convenir à votre budget:
a=1; b=5; d='for i in {'$a'..'$b'}; do echo -n "$i"; done;' echo "$d" | bash
PS: Mon bash n'a de toute façon pas la commande 'seq
'.
testé sur Mac OSX 10.6.8, Bash 3.2.48