9.6. $RANDOM : générer un nombre aléatoire

$RANDOM est une fonction interne Bash (pas une constante) renvoyant un entier pseudo-aléatoire [32] dans l'intervalle 0 - 32767. Il ne devrait pas être utilisé pour générer une clé de chiffrement.

Exemple 9.26. Générer des nombres aléatoires

#!/bin/bash

# $RANDOM renvoie un entier différent à chaque appel.
# Échelle : 0 - 32767 (entier signé sur 16 bits).

NBMAX=10
index=1

echo
echo "$NBMAX nombres aléatoires :"
echo "-----------------"
while [ "$index" -le $NBMAX ]      # Génère 10 ($NBMAX) entiers aléatoires.
do
  nombre=$RANDOM
  echo $nombre
  let "index += 1"  # Incrémente l'index.
done
echo "-----------------"

#  Si vous avez besoin d'un entier aléatoire dans une certaine échelle, utilisez
#+ l'opérateur 'modulo'.
#  Il renvoie le reste d'une division.

ECHELLE=500

echo

nombre=$RANDOM
let "nombre %= $ECHELLE"
#           ^^
echo "Nombre aléatoire inférieur à $ECHELLE  ---  $nombre"

echo

#  Si vous avez besoin d'un entier aléatoire supérieur à une borne, alors
#+ faites un test pour annuler tous les nombres en dessous de cette borne.

PLANCHER=200

nombre=0   #initialise
while [ "$nombre" -le $PLANCHER ]
do
  nombre=$RANDOM
done
echo "Nombre aléatoire supérieur à $PLANCHER ---  $nombre"
echo

   # Examinons une alternative simple  à la boucle ci-dessus
   #       let "nombre = $RANDOM + $PLANCHER"
   # Ceci éliminerait la boucle while et s'exécuterait plus rapidement.
   # Mais, il resterait un problème. Lequel ?



# Combine les deux techniques pour récupérer un nombre aléatoire
# compris entre deux limites.
nombre=0   #initialise
while [ "$nombre" -le $PLANCHER ]
do
  nombre=$RANDOM
  let "nombre %= $ECHELLE"  # Ramène $nombre dans $ECHELLE.
done
echo "Nombre aléatoire compris entre $PLANCHER et $ECHELLE ---  $nombre"
echo


# Génère un choix binaire, c'est-à-dire "vrai" ou "faux".
BINAIRE=2
T=1
nombre=$RANDOM

let "nombre %= $BINAIRE"
# Notez que let "nombre >>= 14"    donne une meilleure distribution aléatoire
# (les décalages droits enlèvent tout sauf le dernier nombre binaire).
if [ "$nombre" -eq $T ]
then
  echo "VRAI"
else
  echo "FAUX"
fi  

echo


# Peut générer un lancer de dés
SPOTS=6   # Modulo 6 donne une échelle de 0 à 5.
          # Incrémenter de 1 donne l'échelle désirée, de 1 à 6.
          # Merci, Paulo Marcel Coelho Aragao, pour cette simplification.
die1=0
die2=0
# Serait-il mieux de seulement initialiser SPOTS=7 et de ne pas ajouter 1 ?
# Pourquoi ou pourquoi pas ?

# Jette chaque dé séparément, et donne ainsi une chance correcte.

    let "die1 = $RANDOM % $SPOTS +1" # Le premier.
    let "die2 = $RANDOM % $SPOTS +1" # Et le second.
    # Quelle opération arithmétique ci-dessus a la plus grande précédence
    # le modulo (%) ou l'addition (+) ?

let "throw = $die1 + $die2"
echo "Throw of the dice = $throw"
echo


exit 0

Exemple 9.27. Piocher une carte au hasard dans un tas

#!/bin/bash
# pick-card.sh

# Ceci est un exemple pour choisir au hasard des éléments d'un tableau.


# Prenez une carte, n'importe quelle carte.

Suites="Carreau
Pique
Coeur
Trefle"

Denominations="2
3
4
5
6
7
8
9
10
Valet
Dame
Roi
As"

# Notez que le contenu de la variable continue sur plusieurs lignes.

suite=($Suites)                # Lire dans une variable de type tableau.
denomination=($Denominations)

num_suites=${#suite[*]}        # Compter le nombre d'éléments.
num_denominations=${#denomination[*]}

echo -n "${denomination[$((RANDOM%num_denominations))]} of "
echo ${suite[$((RANDOM%num_suites))]}


# $bozo sh pick-cards.sh
# Valet de trèfle


# Merci, "jipe", pour m'avoir indiqué cette utilisation de $RANDOM.
exit 0

Jipe nous a indiqué un autre ensemble de techniques pour générer des nombres aléatoires à l'intérieur d'un intervalle donné.

#  Génére des nombres aléatoires entre 6 et 30.
rnumber=$((RANDOM%25+6))

#  Générer des nombres aléatoires dans le même intervalle de 6 à 30,
#+ mais le nombre doit être divisible de façon exacte par 3.
rnumber=$(((RANDOM%30/3+1)*3))

# Notez que ceci ne fonctionnera pas tout le temps.
# Il échoue si $RANDOM%30 renvoie 0.

#  Frank Wang suggère l'alternative suivante :
   rnumber=$(( RANDOM%27/3*3+6 ))

Bill Gradwohl est parvenu à une formule améliorée fonctionnant avec les numéros positifs.

rnumber=$(((RANDOM%(max-min+divisiblePar))/divisiblePar*divisiblePar+min))

Ici, Bill présente une fonction versatile renvoyant un numéro au hasard entre deux valeurs spécifiques.

Exemple 9.28. Un nombre au hasard entre deux valeurs

#!/bin/bash
# random-between.sh
# Nombre aléatoire entre deux valeurs spécifiées.
# Script par Bill Gradwohl, avec des modifications mineures par l'auteur du document.
# Utilisé avec les droits.


aleatoireEntre() {
   #  Génère un numéro aléatoire positif ou négatif
   #+ entre $min et $max
   #+ et divisible par $divisiblePar.
   #  Donne une distribution "raisonnablement aléatoire" des valeurs renvoyées.
   #
   #  Bill Gradwohl - 1er octobre 2003

   syntax() {
   # Fonction imbriquée dans la fonction.
      echo
      echo    "Syntax: aleatoireEntre [min] [max] [multiple]"
      echo
      echo    "Attend au plus trois paramètres mais tous sont complètement optionnels."
      echo    "min est la valeur minimale"
      echo    "max est la valeur maximale"
      echo    "multiple spécifie que la réponse est un multiple de cette valeur."
      echo    "    c'est-à-dire qu'une réponse doit être divisible de manière entière"
      echo    "    par ce numéro."
      echo    
      echo    "Si cette valeur manque, l'aire par défaut supportée est : 0 32767 1"
      echo    "Un résultat avec succès renvoie 0. Sinon, la syntaxe de la fonction"
      echo    "est renvoyée avec un 1."
      echo    "La réponse est renvoyée dans la variable globale aleatoireEntreAnswer"
      echo    "Les valeurs négatives pour tout paramètre passé sont gérées correctement."
   }

   local min=${1:-0}
   local max=${2:-32767}
   local divisiblePar=${3:-1}
   #  Valeurs par défaut affectées, au cas où les paramètres ne sont pas passés à la
   #+ fonction.

   local x
   local spread

   # Assurez-vous que la valeur divisiblePar est positive.
   [ ${divisiblePar} -lt 0 ] && divisiblePar=$((0-divisiblePar))

   # Vérification.
   if [ $# -gt 3 -o ${divisiblePar} -eq 0 -o  ${min} -eq ${max} ]; then 
      syntax
      return 1
   fi

   # Vérifiez si min et max ne sont pas inversés.
   if [ ${min} -gt ${max} ]; then
      # Les inversez.
      x=${min}
      min=${max}
      max=${x}
   fi

   #  Si min est lui-même non divisible par $divisiblePar,
   #+ alors corrigez le min pour être à l'échelle.
   if [ $((min/divisiblePar*divisiblePar)) -ne ${min} ]; then 
      if [ ${min} -lt 0 ]; then
         min=$((min/divisiblePar*divisiblePar))
      else
         min=$((((min/divisiblePar)+1)*divisiblePar))
      fi
   fi

   #  Si max est lui-même non divisible par $divisiblePar,
   #+ alors corrigez le max pour être à l'échelle.
   if [ $((max/divisiblePar*divisiblePar)) -ne ${max} ]; then 
      if [ ${max} -lt 0 ]; then
         max=$((((max/divisiblePar)-1)*divisiblePar))
      else
         max=$((max/divisiblePar*divisiblePar))
      fi
   fi

   #  ---------------------------------------------------------------------
   #  Maintenant, pour faire le vrai travail.

   #  Notez que pour obtenir une distribution correcte pour les points finaux,
   #+ l'échelle des valeurs aléatoires doit être autorisée pour aller entre 0 et
   #+ abs(max-min)+divisiblePar, et non pas seulement abs(max-min)+1.

   #  La légère augmentation produira une distribution correcte des points finaux.

   #  Changer la formule pour utiliser abs(max-min)+1 produira toujours des réponses
   #+ correctes mais le côté aléatoire des réponses est erroné dans le fait que le
   #+ nombre de fois où les points finaux ($min et $max) sont renvoyés est
   #+ considérablement plus petit que lorsque la formule correcte est utilisée.
   #  ---------------------------------------------------------------------

   spread=$((max-min))
   #  Omair Eshkenazi indique que ce test n'est pas nécessaire
   #+ car max et min ont déjà été basculés.
   [ ${spread} -lt 0 ] && spread=$((0-spread))
   let spread+=divisiblePar
   aleatoireEntreAnswer=$(((RANDOM%spread)/divisiblePar*divisiblePar+min))   

   return 0

   #  Néanmoins, Paulo Marcel Coelho Aragao indique que
   #+ quand $max et $min ne sont pas divisibles par $divisiblePar,
   #+ la formule échoue.
   #
   #  Il suggère à la place la formule suivante :
   #    rnumber = $(((RANDOM%(max-min+1)+min)/divisiblePar*divisiblePar))

}

# Testons la fonction.
min=-14
max=20
divisiblePar=3


#  Génère un tableau des réponses attendues et vérifie pour s'assurer que nous obtenons
#+ au moins une réponse si nous bouclons assez longtemps.

declare -a reponse
minimum=${min}
maximum=${max}
   if [ $((minimum/divisiblePar*divisiblePar)) -ne ${minimum} ]; then 
      if [ ${minimum} -lt 0 ]; then
         minimum=$((minimum/divisiblePar*divisiblePar))
      else
         minimum=$((((minimum/divisiblePar)+1)*divisiblePar))
      fi
   fi


   #  Si max est lui-même non divisible par $divisiblePar,
   #+ alors corrigez le max pour être à l'échelle.

   if [ $((maximum/divisiblePar*divisiblePar)) -ne ${maximum} ]; then 
      if [ ${maximum} -lt 0 ]; then
         maximum=$((((maximum/divisiblePar)-1)*divisiblePar))
      else
         maximum=$((maximum/divisiblePar*divisiblePar))
      fi
   fi


#  Nous avons besoin de générer seulement les sous-scripts de tableaux positifs,
#+ donc nous avons besoin d'un déplacement qui nous garantie des résultats positifs.

deplacement=$((0-minimum))
for ((i=${minimum}; i<=${maximum}; i+=divisiblePar)); do
   reponse[i+deplacement]=0
done


# Maintenant, bouclons avec un gros nombre de fois pour voir ce que nous obtenons.
loopIt=1000   #  L'auteur du script suggère 100000,
              #+ mais cela prend beaucoup de temps.

for ((i=0; i<${loopIt}; ++i)); do

   #  Notez que nous spécifions min et max en ordre inverse ici pour s'assurer que les
   #+ fonctions sont correctes dans ce cas.

   aleatoireEntre ${max} ${min} ${divisiblePar}

   # Rapporte une erreur si une réponse est inattendue.
   [ ${aleatoireEntreAnswer} -lt ${min} -o ${aleatoireEntreAnswer} -gt ${max} ] \
     && echo MIN or MAX error - ${aleatoireEntreAnswer}!
   [ $((aleatoireEntreAnswer%${divisiblePar})) -ne 0 ] \
     && echo DIVISIBLE BY error - ${aleatoireEntreAnswer}!

   # Stocke la réponse statistiquement.
   reponse[aleatoireEntreAnswer+deplacement]=$((reponse[aleatoireEntreAnswer+deplacement]+1))
done



# Vérifions les résultats.

for ((i=${minimum}; i<=${maximum}; i+=divisiblePar)); do
   [ ${reponse[i+deplacement]} -eq 0 ] \
    && echo "We never got an reponse of $i." \
    || echo "${i} occurred ${reponse[i+deplacement]} times."
done


exit 0

À quel point $RANDOM est-il aléatoire ? la meilleure façon de le tester est d'écrire un script qui enregistre la suite des nombres « aléatoires » générés par $RANDOM. Faisons tourner $RANDOM plusieurs fois...

Exemple 9.29. Lancement d'un seul dé avec RANDOM

#!/bin/bash
# À quel point RANDOM est aléatoire?

RANDOM=$$      #  Réinitialise le générateur de nombres aléatoires en utilisant
               #+ le PID du script.

PIPS=6         # Un dé a 6 faces.
COMPTEURMAX=600# Augmentez ceci si vous n'avez rien de mieux à faire.
compteur=0     # Compteur.

un=0           # Doit initialiser les comptes à zéro
deux=0         # car une variable non initialisée est nulle, et ne vaut pas zéro.
trois=0
quatre=0
cinq=0
six=0

Affiche_resultat ()
{
echo
echo "un =   $un"
echo "deux =   $deux"
echo "trois = $trois"
echo "quatre =  $quatre"
echo "cinq =  $cinq"
echo "six =  $six"
echo
}

mise_a_jour_compteur()
{
case "$1" in
  0) let "un += 1";;     # Comme le dé n'a pas de "zéro", ceci correspond à 1.
  1) let "deux += 1";;   # Et ceci à 2, etc.
  2) let "trois += 1";;
  3) let "quatre += 1";;
  4) let "cinq += 1";;
  5) let "six += 1";;
esac
}

echo


while [ "$compteur" -lt "$COMPTEURMAX" ]
do
  let "die1 = RANDOM % $PIPS"
  mise_a_jour_compteur $die1
  let "compteur += 1"
done  

Affiche_resultat

exit 0

#  Les scores devraient être distribués de façon égale en supposant que RANDOM
#+ soit correctement aléatoire.
#  Avec $COMPTEURMAX à 600, tout devrait tourner autour de 100, plus ou moins
#+ 20.
#
# Gardez en tête que RANDOM est un générateur pseudo-aléatoire,
# et pas un particulièrement bon.

#  Le hasard est un sujet profond et complexe.
#  Des séquences "au hasard" suffisamment longues pourraient exhiber un
#+ comportement cahotique et un autre comportement non aléatoire.

# Exercice (facile):
# -----------------
# Réécrire ce script pour lancer une pièce 1000 fois.
# Les choix sont "PILE" ou "FACE".

Comme nous avons vu sur le dernier exemple, il est préférable de réinitialiser le générateur RANDOM à chaque fois qu'il est invoqué. Utiliser le même germe pour RANDOM ne fera que répéter la même série de nombres [33] (ceci reflète le comportement de la fonction C random()).

Exemple 9.30. Réinitialiser RANDOM

#!/bin/bash
# seeding-random.sh: Utiliser la variable RANDOM.

NBMAX=25       # Combien de nombres à générer.

nombres_aleatoires ()
{
compteur=0
while [ "$compteur" -lt "$NBMAX" ]
do
  nombre=$RANDOM
  echo -n "$nombre "
  let "compteur += 1"
done  
}

echo; echo

RANDOM=1          #  Initialiser RANDOM met en place le générateur de nombres
                  #+ aléatoires.
nombres_aleatoires

echo; echo

RANDOM=1          # Même élément pour RANDOM...
nombres_aleatoires    # ...reproduit la même série de nombres.
                  #
                  #  Quand est-il utile de dupliquer une série de nombres
                  #+ "aléatoires" ?

echo; echo

RANDOM=2          # Nouvel essai, mais avec un 'germe' différent...
nombres_aleatoires    # donne une autre série...

echo; echo

# RANDOM=$$  initialise RANDOM à partir du PID du script.
#  Il est aussi possible d'initialiser RANDOM à partir des commandes 'time' et
#+ 'date'.

# Un peu plus d'amusement...
SEED=$(head -1 /dev/urandom | od -N 1 | awk '{ print $2 }')
#  Sortie pseudo-aléatoire récupérée de /dev/urandom (fichier périphérique
#+ pseudo-aléatoire),
#+ puis convertit la ligne en nombres (octal) affichables avec "od".
#+ Finalement "awk" récupère un seul nombre pour SEED.
RANDOM=$SEED
nombres_aleatoires

echo; echo

exit 0

[Note]

Note

Le pseudo fichier périphérique /dev/urandom apporte une méthode pour générer des nombres pseudo-aléatoires bien plus « aléatoires » que la variable $RANDOM. dd if=/dev/urandom of=fichier_cible bs=1 count=XX crée un fichier de nombres pseudo-aléatoires bien distribués. Néanmoins, assigner ces nombres à une variable dans un script nécessite un petit travail supplémentaire, tel qu'un filtrage par l'intermédiaire de od (comme dans l'exemple ci-dessus, dans l'Exemple 15.13, « Générer des nombres aléatoires de dix chiffres » et dans Exemple A.37, « Tri d'insertion ») ou tel que l'utilisation de dd (voir l'Exemple 15.56, « Effacer les fichiers de façon sûre ») ou même d'envoyer via un tube dans md5sum (voir l'Exemple 33.14, « Un jeu de « courses de chevaux » »).

Il existe aussi d'autres moyens pour générer des nombres pseudo aléatoires dans un script. Awk propose une façon agréable de le faire.

Exemple 9.31. Nombres pseudo-aléatoires, en utilisant awk

#!/bin/bash
# random2.sh: Renvoie un nombre pseudo-aléatoire compris entre 0 et 1.
# Utilise la fonction rand() d'awk.

SCRIPTAWK=' { srand(); print rand() } '
# Commande(s) / paramètres passés à awk
# Notez que srand() réinitialise le générateur de nombre aléatoire de awk.

echo -n "Nombre aléatoire entre 0 et 1 = "

echo | awk "$SCRIPTAWK"
# Que se passe-t-il si vous oubliez le 'echo' ?

exit 0


# Exercices :
# ----------

# 1) En utilisant une construction boucle, affichez 10 nombres aléatoires
#    différents.
#      (Astuce : vous devez réinitialiser la fonction "srand()" avec une donnée
#      différente à chaque tour de la boucle. Qu'arrive-t'il si vous échouez à le
#      faire ?)

# 2) En utilisant un multiplicateur entier comme facteur d'échelle, générez des
#    nombres aléatoires compris entre 10 et 100.

# 3) De même que l'exercice #2, ci-dessus, mais en générant des nombres
#    aléatoires entiers cette fois.

La commande date tend elle-même à générer des séquences d'entiers pseudo-aléatoires.



[32] Un vrai « hasard », si tant est qu'il puisse exister, peut seulement être trouvé dans certains phénomènes naturels compris partiellement tels que la destruction radioactive. Les ordinateurs peuvent seulement simuler le hasard et les séquences générées par ordinateur de nombres « aléatoires » sont du coup appelés pseudo-aléatoires.

[33] La graine d'une série de nombres pseudo-aléatoires générés par un ordinateur peut être considérée comme un label d'identification. Par exemple, pensez à la série pseudo-aléatoire avec une graine de 23 comme la série #23.

Une propriété d'une série de nombres pseudo-aléatoires est la longueur du cycle avant qu'il ne commence à se répéter. Un bon générateur pseudo-aléatoire produira des séries avec de très longs cycles.