9.3. $RANDOM : générer un entier aléatoire

Quiconque essaie de produire des nombres aléatoires par des moyens déterministes est bien évidemment en état de péché.

--John von Neumann

$RANDOM est une fonction interne de Bash (pas une constante) qui renvoie un entier pseudo-aléatoire [46] dans l'intervalle 0 - 32767. Il ne faut pas l'utiliser pour générer une clé de chiffrement.

Exemple 9.11. 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.12. 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

Exemple 9.13. Simulation « Brownian Motion »

#!/bin/bash
# brownian.sh
# Author: Mendel Cooper
# Reldate: 10/26/07
# License: GPL3

#  ----------------------------------------------------------------
#  This script models Brownian motion:
#+ the random wanderings of tiny particles in a fluid,
#+ as they are buffeted by random currents and collisions.
#+ This is colloquially known as the "Drunkard's Walk."

#  It can also be considered as a stripped-down simulation of a
#+ Galton Board, a slanted board with a pattern of pegs,
#+ down which rolls a succession of marbles, one at a time.
#+ At the bottom is a row of slots or catch basins in which
#+ the marbles come to rest at the end of their journey.
#  Think of it as a kind of bare-bones Pachinko game.
#  As you see by running the script,
#+ most of the marbles cluster around the center slot.
#+ This is consistent with the expected binomial distribution.
#  As a Galton Board simulation, the script
#+ disregards such parameters as
#+ board tilt-angle, rolling friction of the marbles,
#+ angles of impact, and elasticity of the pegs.
#  To what extent does this affect the accuracy of the simulation?
#  ----------------------------------------------------------------

PASSES=500            #  Number of particle interactions / marbles.
ROWS=10               #  Number of "collisions" (or horiz. peg rows).
RANGE=3               #  0 - 2 output range from $RANDOM.
POS=0                 #  Left/right position.
RANDOM=$$             #  Seeds the random number generator from PID
                      #+ of script.

declare -a Slots      # Array holding cumulative results of passes.
NUMSLOTS=21           # Number of slots at bottom of board.


Initialize_Slots () { # Zero out all elements of the array.
for i in $( seq $NUMSLOTS )
do
  Slots[$i]=0
done

echo                  # Blank line at beginning of run.
  }


Show_Slots () {
echo; echo
echo -n " "
for i in $( seq $NUMSLOTS )   # Pretty-print array elements.
do
  printf "%3d" ${Slots[$i]}   # Allot three spaces per result.
done

echo # Row of slots:
echo " |__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|"
echo "                                ||"
echo #  Note that if the count within any particular slot exceeds 99,
     #+ it messes up the display.
     #  Running only(!) 500 passes usually avoids this.
  }


Move () {              # Move one unit right / left, or stay put.
  Move=$RANDOM         # How random is $RANDOM? Well, let's see ...
  let "Move %= RANGE"  # Normalize into range of 0 - 2.
  case "$Move" in
    0 ) ;;                   # Do nothing, i.e., stay in place.
    1 ) ((POS--));;          # Left.
    2 ) ((POS++));;          # Right.
    * ) echo -n "Error ";;   # Anomaly! (Should never occur.)
  esac
  }


Play () {                    # Single pass (inner loop).
i=0
while [ "$i" -lt "$ROWS" ]   # One event per row.
do
  Move
  ((i++));
done

SHIFT=11                     # Why 11, and not 10?
let "POS += $SHIFT"          # Shift "zero position" to center.
(( Slots[$POS]++ ))          # DEBUG: echo $POS

# echo -n "$POS "

  }


Run () {                     # Outer loop.
p=0
while [ "$p" -lt "$PASSES" ]
do
  Play
  (( p++ ))
  POS=0                      # Reset to zero. Why?
done
  }


# --------------
# main ()
Initialize_Slots
Run
Show_Slots
# --------------

exit $?

#  Exercises:
#  ---------
#  1) Show the results in a vertical bar graph, or as an alternative,
#+    a scattergram.
#  2) Alter the script to use /dev/urandom instead of $RANDOM.
#     Will this make the results more random?
#  3) Provide some sort of "animation" or graphic output
#     for each marble played.

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.14. Un nombre au hasard entre deux valeurs

#!/bin/bash
# random-between.sh
# Random number between two specified values. 
# Script by Bill Gradwohl, with minor modifications by the document author.
# Corrections in lines 187 and 189 by Anthony Le Clezio.
# Used with permission.


randomBetween() {
   #  Generates a positive or negative random number
   #+ between $min and $max
   #+ and divisible by $divisibleBy.
   #  Gives a "reasonably random" distribution of return values.
   #
   #  Bill Gradwohl - Oct 1, 2003

   syntax() {
   # Function embedded within function.
      echo
      echo    "Syntax: randomBetween [min] [max] [multiple]"
      echo
      echo -n "Expects up to 3 passed parameters, "
      echo    "but all are completely optional."
      echo    "min is the minimum value"
      echo    "max is the maximum value"
      echo -n "multiple specifies that the answer must be "
      echo     "a multiple of this value."
      echo    "    i.e. answer must be evenly divisible by this number."
      echo    
      echo    "If any value is missing, defaults area supplied as: 0 32767 1"
      echo -n "Successful completion returns 0, "
      echo     "unsuccessful completion returns"
      echo    "function syntax and 1."
      echo -n "The answer is returned in the global variable "
      echo    "randomBetweenAnswer"
      echo -n "Negative values for any passed parameter are "
      echo    "handled correctly."
   }

   local min=${1:-0}
   local max=${2:-32767}
   local divisibleBy=${3:-1}
   # Default values assigned, in case parameters not passed to function.

   local x
   local spread

   # Let's make sure the divisibleBy value is positive.
   [ ${divisibleBy} -lt 0 ] && divisibleBy=$((0-divisibleBy))

   # Sanity check.
   if [ $# -gt 3 -o ${divisibleBy} -eq 0 -o  ${min} -eq ${max} ]; then 
      syntax
      return 1
   fi

   # See if the min and max are reversed.
   if [ ${min} -gt ${max} ]; then
      # Swap them.
      x=${min}
      min=${max}
      max=${x}
   fi

   #  If min is itself not evenly divisible by $divisibleBy,
   #+ then fix the min to be within range.
   if [ $((min/divisibleBy*divisibleBy)) -ne ${min} ]; then 
      if [ ${min} -lt 0 ]; then
         min=$((min/divisibleBy*divisibleBy))
      else
         min=$((((min/divisibleBy)+1)*divisibleBy))
      fi
   fi

   #  If max is itself not evenly divisible by $divisibleBy,
   #+ then fix the max to be within range.
   if [ $((max/divisibleBy*divisibleBy)) -ne ${max} ]; then 
      if [ ${max} -lt 0 ]; then
         max=$((((max/divisibleBy)-1)*divisibleBy))
      else
         max=$((max/divisibleBy*divisibleBy))
      fi
   fi

   #  ---------------------------------------------------------------------
   #  Now, to do the real work.

   #  Note that to get a proper distribution for the end points,
   #+ the range of random values has to be allowed to go between
   #+ 0 and abs(max-min)+divisibleBy, not just abs(max-min)+1.

   #  The slight increase will produce the proper distribution for the
   #+ end points.

   #  Changing the formula to use abs(max-min)+1 will still produce
   #+ correct answers, but the randomness of those answers is faulty in
   #+ that the number of times the end points ($min and $max) are returned
   #+ is considerably lower than when the correct formula is used.
   #  ---------------------------------------------------------------------

   spread=$((max-min))
   #  Omair Eshkenazi points out that this test is unnecessary,
   #+ since max and min have already been switched around.
   [ ${spread} -lt 0 ] && spread=$((0-spread))
   let spread+=divisibleBy
   randomBetweenAnswer=$(((RANDOM%spread)/divisibleBy*divisibleBy+min))   

   return 0

   #  However, Paulo Marcel Coelho Aragao points out that
   #+ when $max and $min are not divisible by $divisibleBy,
   #+ the formula fails.
   #
   #  He suggests instead the following formula:
   #    rnumber = $(((RANDOM%(max-min+1)+min)/divisibleBy*divisibleBy))

}

# Let's test the function.
min=-14
max=20
divisibleBy=3


#  Generate an array of expected answers and check to make sure we get
#+ at least one of each answer if we loop long enough.

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


   #  If max is itself not evenly divisible by $divisibleBy,
   #+ then fix the max to be within range.

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


#  We need to generate only positive array subscripts,
#+ so we need a displacement that that will guarantee
#+ positive results.

disp=$((0-minimum))
for ((i=${minimum}; i<=${maximum}; i+=divisibleBy)); do
   answer[i+disp]=0
done


# Now loop a large number of times to see what we get.
loopIt=1000   #  The script author suggests 100000,
              #+ but that takes a good long while.

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

   #  Note that we are specifying min and max in reversed order here to
   #+ make the function correct for this case.

   randomBetween ${max} ${min} ${divisibleBy}

   # Report an error if an answer is unexpected.
   [ ${randomBetweenAnswer} -lt ${min} -o ${randomBetweenAnswer} -gt ${max} ] \
   && echo MIN or MAX error - ${randomBetweenAnswer}!
   [ $((randomBetweenAnswer%${divisibleBy})) -ne 0 ] \
   && echo DIVISIBLE BY error - ${randomBetweenAnswer}!

   # Store the answer away statistically.
   answer[randomBetweenAnswer+disp]=$((answer[randomBetweenAnswer+disp]+1))
done



# Let's check the results

for ((i=${minimum}; i<=${maximum}; i+=divisibleBy)); do
   [ ${answer[i+disp]} -eq 0 ] \
   && echo "We never got an answer of $i." \
   || echo "${i} occurred ${answer[i+disp]} 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.15. 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 [47] (ceci reflète le comportement de la fonction C random()).

Exemple 9.16. 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 16.14, « Générer des nombres aléatoires de dix chiffres » et dans Exemple A.36, « Tri d'insertion ») ou même d'envoyer via un tube dans md5sum (voir l'Exemple 36.14, « Un jeu de « courses de chevaux » »).

Il existe encore 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.17. 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.



[46] Le véritable « hasard », si tant est qu'il puisse exister, peut seulement être trouvé dans certains phénomènes naturels qu'on ne comprend qu'en partie, tels que la destruction radioactive. Les ordinateurs ne font que simuler le hasard, et les séquences générées par ordinateur de nombres « aléatoires » sont de ce fait appelées pseudo-aléatoires.

[47] La graine d'une série de nombres pseudo-aléatoires générés par un ordinateur peut être considérée comme une étiquette d'identification. Par exemple, vous pouvez désigner les séries pseudo-aléatoires de graine 23 comme les séries #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.