bash - script - if shell



Comment puis-je itérer sur une plage de nombres définis par des variables dans Bash? (12)

discussion

L'utilisation de seq est très bien, comme Jiaaro l'a suggéré. Pax Diablo a suggéré une boucle Bash pour éviter d'appeler un sous-processus, avec l'avantage supplémentaire d'être plus compatible avec la mémoire si $ END est trop grand. 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 continues des nombres de va-et-vient sont effectuées avec un ralentissement associé.

arithmétique entière

C'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 l' echo , alors nous pourrions écrire echo $((i++)) .

ephemient m'a appris quelque chose: Bash permet for ((expr;expr;expr)) constructions. Comme je n'ai jamais lu toute la page de manuel de Bash (comme je l'ai fait avec la page de manuel Korn shell ( ksh ), c'était il y a longtemps), j'ai raté ç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 sur le plan de la mémoire (il ne sera pas nécessaire d'allouer de la mémoire pour consommer la sortie de seq , ce qui pourrait être un problème si END est très grand), mais probablement pas le "plus rapide".

la question initiale

eschercycle a noté que la notation { a .. b } Bash ne fonctionne qu'avec des littéraux; vrai, selon le manuel de Bash. On peut surmonter cet obstacle avec une seule fork() (interne) fork() sans exec() (comme c'est le cas avec l'appel à seq , qui étant une autre image nécessite un fork + exec):

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

eval et echo sont tous deux des builtins Bash, mais un fork() est requis pour la substitution de commande (la construction $(…) ).

https://src-bin.com

Comment puis-je 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 la documentation Bash):

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

Qui donne:

1
2
3
4
5

Cependant, comment puis-je remplacer l'un ou l'autre des points de terminaison de la plage par une variable? Cela ne fonctionne pas:

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

Qui imprime:

{1..5}


Answer #1

C'est une autre façon:

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

Answer #2

Cela fonctionne dans Bash et Korn, peut également aller de plus haut nombre plus bas. Probablement pas le plus rapide ou le plus joli mais fonctionne assez bien. Traite 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"
}

Answer #3

Je sais que cette question concerne bash , mais - juste pour l'enregistrement - 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}

Answer #4

Remplacez {} par (( )) :

tmpstart=0;
tmpend=4;

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

Rendements:

0
1
2
3
4

Answer #5

Si vous en avez besoin préfixe que vous pourriez aimer ce

 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

Answer #6

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

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

Answer #7

Si vous voulez rester aussi proche que possible de la syntaxe d'expression de l'accolade, essayez la fonction range de range.bash de bash-tricks .

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 bash native avec le moins de "gotchas" possible: non seulement les variables sont supportées, mais le comportement souvent indésirable des plages invalides est fourni sous forme de 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 ont toutes au moins l'un des inconvénients suivants:

  • Beaucoup d'entre eux utilisent des subshells - subshells , ce qui peut nuire aux performances et peut ne pas être possible 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ésents ou non, c'est beaucoup plus sur que le langage Bash lui-même.
  • Les solutions qui utilisent uniquement la fonctionnalité Bash native, comme @ ephemient, ne fonctionneront pas sur les plages alphabétiques, comme {a..z} ; accolade expansion sera. La question portait sur les fourchettes de chiffres , donc, c'est un argument.
  • La plupart d'entre eux ne sont pas visuellement similaires à la syntaxe {1..10} plage de l'accolade-expansion, de sorte que 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, une erreur ne se produira pas et la valeur verbatim {1..a} sera {1..a} en écho. C'est aussi le comportement par défaut de Bash, c'est souvent inattendu.

Disclaimer: Je suis l'auteur du code lié.


Answer #8

Une autre couche d'indirection:

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

Answer #9

Voici pourquoi l'expression originale n'a pas fonctionné.

De l' homme bash :

L'expansion de l'accolade est effectuée avant toute autre expansion, et tous les caractères spéciaux pour d'autres expansions sont conservés dans le résultat. C'est strictement textuel. Bash n'applique aucune interprétation syntaxique au contexte de l'expansion ou du texte entre les accolades.

Donc, l' expansion de l'accolade est quelque chose qui est fait très tôt comme une opération de macro purement textuelle, avant l' expansion du paramètre.

Les shells sont des hybrides hautement optimisés entre les macro-processeurs et des langages de programmation plus formels. Afin d'optimiser les cas d'utilisation typiques, le langage est rendu plus complexe et certaines limitations sont acceptées.

Recommandation

Je suggère de coller avec les fonctionnalités de Posix 1 . Cela signifie utiliser for i in <list>; do 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 ne mets pas de bash-ismes dans mes scripts. Les scripts peuvent nécessiter un shell plus rapide, plus sécurisé, plus intégré. Ils peuvent avoir besoin de fonctionner sur tout ce qui est installé en tant que / bin / sh, et il y a ensuite tous les arguments standards habituels. Rappelez-vous shellshock, alias bashdoor?


Answer #10

La façon POSIX

Si vous vous souciez de la portabilité, utilisez l' exemple de la norme POSIX :

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

Sortie:

2
3
4
5

Choses qui ne sont pas POSIX:

  • (( )) sans dollar, bien qu'il s'agisse d'une extension commune mentionnée par POSIX lui-même .
  • [[ . [ est assez 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 fonctionne pas avec les variables mentionnées dans le manuel de Bash .
  • let i=i+1 : POSIX 7 2. Shell Command Language ne contient pas le mot let , et il échoue sur bash --posix 4.3.42
  • le dollar à i=$i+1 pourrait être requis, mais je ne suis pas sûr. POSIX 7 2.6.4 L'expansion arithmétique dit:

    Si la variable shell x contient une valeur qui forme une constante entière valide, incluant facultativement un signe plus ou moins, alors les extensions arithmétiques "$ ((x))" et "$ (($ x))" retourneront la même valeur.

    mais en le lisant littéralement cela n'implique pas que $((x+1)) développe puisque x+1 n'est pas une variable.


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

edit: Je préfère seq sur les autres méthodes car je peux m'en souvenir;)





shell