web-dev-qa-db-fra.com

Comment itérer sur une plage de nombres définie par des variables dans Bash?

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}

1338
eschercycle
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;)

1498
Jiaaro

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.

394
ephemient

discussion

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.

arithmétique entière

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.

la question initiale

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 $(…).).

175
tzot

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

1. Bash est un super Shell et je l’utilise de manière interactive, mais je n’inscris pas bash-isms dans mes scripts. Les scripts peuvent nécessiter un shell plus rapide, plus sécurisé, plus intégré. Ils auront peut-être besoin de s’exécuter sur tout ce qui est installé en tant que/bin/sh, puis il y aura tous les arguments usuels des normes. Rappelez-vous Shellshock, aka bashdoor?
96
DigitalRoss

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:

Une autre couche d'indirection:

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

Vous pouvez utiliser

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

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

Si vous utilisez BSD/OS X, vous pouvez utiliser jot au lieu de seq:

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

Cela fonctionne très bien dans bash:

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

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:

  • Beaucoup d'entre eux utilisent sous-couches , ce qui peut performances négatives et peut être impossible sur certains systèmes.
  • Beaucoup d'entre eux s'appuient sur des programmes externes. Même 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.
  • Les solutions qui utilisent uniquement la fonctionnalité native de Bash, telle que celle de @ ephemient, ne fonctionneront pas sur les plages alphabétiques, telles que _{a..z}_; expansion brace sera. La question portait sur les plages de nombres , alors c'est un casse-tête.
  • La plupart d'entre eux ne ressemblent visuellement pas à la syntaxe de plage _{1..10}_, élargie, donc les programmes qui utilisent les deux peuvent être un peu plus difficiles à lire.
  • La réponse de @ bobbogo utilise une partie de la syntaxe familière, mais fait quelque chose d'inattendu si la variable _$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é.

7
Zac B

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}
7
Adrian Frühwirth

C'est une autre façon:

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

Remplacez {} par (( )):

tmpstart=0;
tmpend=4;

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

Rendements:

0
1
2
3
4
6
BashTheKeyboard

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.

6
SuperBob

J'ai combiné quelques idées ici et mesuré la performance.

TL; DR À emporter:

  1. seq et {..} sont vraiment rapides
  2. for et while les boucles sont lentes
  3. $( ) est lent
  4. for (( ; ; )) les boucles sont plus lentes
  5. $(( )) est encore plus lent
  6. S'inquiéter des N nombres en mémoire (seq ou {..}) est ridicule (au moins jusqu'à 1 million.)

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.

Démos

# 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

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

5
Alex Spangher

Il existe de nombreuses façons de le faire, mais celles que je préfère sont indiquées ci-dessous.

Utiliser seq

Synopsis de man seq

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

Syntaxe

Commande complète
seq first incr last

  • le premier est le numéro de départ dans la séquence [est facultatif, par défaut: 1]
  • incr is increment [est facultatif, par défaut: 1]
  • last est le dernier numéro de la séquence

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

Utiliser {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
1
theBuzzyCoder

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"
}
0
Ethan Post

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

0
Zimba