9.6. $RANDOM: genera un intero casuale

$RANDOM è una funzione interna di Bash (non una costante) che restituisce un intero pseudocasuale [1] nell'intervallo 0 - 32767. Non dovrebbe essere utilizzata per generare una chiave di cifratura.

Esempio 9-25. Generare numeri casuali

#!/bin/bash

# $RANDOM restituisce un intero casuale diverso ad ogni chiamata.
# Intervallo nominale: 0 - 32767 (intero con segno di 16-bit).

NUM_MASSIMO=10
contatore=1

echo
echo "$NUM_MASSIMO numeri casuali:"
echo "-----------------"
while [ "$contatore" -le $NUM_MASSIMO ]   #  Genera 10 ($NUM_MASSIMO)
                                          #+ interi casuali.
do
  numero=$RANDOM
  echo $numero
  let "contatore += 1"  # Incrementa il contatore.
done
echo "-----------------"

#  Se è necessario un intero casuale entro un dato intervallo, si usa
#+ l'operatore 'modulo', che restituisce il resto di una divisione.

INTERVALLO=500

echo

numero=$RANDOM
let "numero %= $INTERVALLO"
#           ^^
echo "Il numero casuale è inferiore a $INTERVALLO  ---  $numero"

echo



#  Se è necessario un intero casuale non inferiore a un certo limite, 
#+ occorre impostare una verifica per eliminare tutti i numeri al di 
#+ sotto di tale limite.

LIMITE_INFERIORE=200

numero=0   # inizializzazione
while [ "$numero" -le $LIMITE_INFERIORE ]
do
  numero=$RANDOM
done
echo "Numero casuale maggiore di $LIMITE_INFERIORE ---  $numero"

echo

   #  Prendiamo in considerazione una semplice alternativa al ciclo precedente,
   #+ in particolare
   #       let "numero = $RANDOM + $LIMITE_INFERIORE"
   # Elimineremmo il ciclo while e l'esecuzione sarebbe più veloce.
   # Ma ci sarebbe un problema. Quale?



#  Combiniamo le due tecniche precedenti per ottenere un 
#+ numero compreso tra due limiti.

numero=0   # inizializzazione
while [ "$numero" -le $LIMITE_INFERIORE ]
do
  numero=$RANDOM
  let "numero %= $INTERVALLO"  # Riduce $numero entro $INTERVALLO.
done
echo "Numero casuale tra $LIMITE_INFERIORE e $INTERVALLO ---  $numero"
echo



# Genera una scelta binaria, vale a dire, il valore "vero" o "falso".
BINARIO=2
T=1
numero=$RANDOM

let "numero %= $BINARIO"
#  Da notare che  let "numero >>= 14" dà una migliore distribuzione casuale
#+ (lo scorrimento a destra elimina tutto tranne l'ultima cifra binaria).
if [ "$numero" -eq $T ]
then
  echo "VERO"
else
  echo "FALSO"
fi

echo


# Si può simulare il lancio dei dadi.
MODULO=6  # Modulo 6 per un intervallo 0 - 5.
          # Aumentandolo di 1 si ottiene il desiderato intervallo 1 - 6.
          # Grazie a Paulo Marcel Coelho Aragao per la semplificazione.
dado1=0
dado2=0
#  Sarebbe stato meglio impostare semplicemente MODULO=7 e non aggiungere 1? 
#  Perché o perché no?

#  Si lancia ciascun dado separatamente in modo da ottenere la corretta
#+ probabilità.

    let "dado1 = $RANDOM % $MODULO +1" # Lancio del primo dado.
    let "dado2 = $RANDOM % $MODULO +1" # Lancio del secondo dado.
    #  Quale, tra le precedenti operazioni aritmetiche, ha la precedenza --
    #+ modulo (%) o addizione (+)?


let "punteggio = $dado1 + $dado2"
echo "Lancio dei dadi = $punteggio"
echo


exit 0

Esempio 9-26. Scegliere una carta a caso dal mazzo

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

# Esempio di scelta a caso di elementi di un array.


# Sceglie una carta, una qualsiasi.

Semi="Fiori
Quadri
Cuori
Picche"

Denominazioni="2
3
4
5
6
7
8
9
10
Fante
Donna
Re
Asso"

# Notate le variabili elencate su più righe.


seme=($Semi)                # Inizializza l'array.
denominazione=($Denominazioni)

num_semi=${#seme[*]}        # Conta gli elementi dell'array.
num_denominazioni=${#denominazione[*]}

echo -n "${denominazione[$((RANDOM%num_denominazioni))]} di "
echo ${seme[$((RANDOM%num_semi))]}


# $bozo sh pick-cards.sh
# Fante di Fiori


# Grazie, "jipe," per aver puntualizzato quest'uso di $RANDOM.
exit 0

Jipe ha evidenziato una serie di tecniche per generare numeri casuali in un intervallo dato.

#  Generare un numero casuale compreso tra 6 e 30.
   numeroc=$((RANDOM%25+6))	

#  Generare un numero casuale, sempre nell'intervallo 6 - 30,
#+ ma che deve essere divisibile per 3.
   numeroc=$(((RANDOM%30/3+1)*3))

#  È da notare che questo non sempre funziona.
#  Fallisce quando $RANDOM%30 restituisce 0.

#  Frank Wang suggerisce la seguente alternativa:
   numeroc=$(( RANDOM%27/3*3+6 ))

Bill Gradwohl ha elaborato una formula più perfezionata che funziona con i numeri positivi.

numeroc=$(((RANDOM%(max-min+divisibilePer))/divisibilePer*divisibilePer+min))

Qui Bill presenta una versatile funzione che restituisce un numero casuale compreso tra due valori specificati.

Esempio 9-27. Numero casuale in un intervallo dato

#!/bin/bash
# random-between.sh
#  Numero casuale compreso tra due valori specificati.
#  Script di Bill Gradwohl, con modifiche di secondaria importanza fatte
#+ dall'autore del libro.
#  Utilizzato con il permesso dell'autore.


interCasuale() {
   #  Genera un numero casuale positivo o negativo
   #+ compreso tra $min e $max
   #+ e divisibile per $divisibilePer.
   #  Restituisce una distribuzione di valori "ragionevolmente casuale".
   #
   #  Bill Gradwohl - 1 Ott, 2003

   sintassi() {
   # Funzione all'interno di una funzione.
      echo
      echo    "Sintassi: interCasuale [min] [max] [multiplo]"
      echo
      echo    "Si aspetta che vengano passati fino a 3 parametri,"
      echo    "tutti però opzionali."
      echo    "min è il valore minimo"
      echo    "max è il valore massimo"
      echo    "multiplo specifica che il numero generato deve essere un"
      echo    "multiplo di questo valore."
      echo    "    cioè divisibile esattamente per questo numero."
      echo    
      echo    "Se si omette qualche valore, vengono usati"
      echo    "quelli preimpostati: 0 32767 1"
      echo    "L'esecuzione senza errori restituisce 0, altrimenti viene"
      echo    "richiamata la funzione sintassi e restituito 1."
      echo    "Il numero generato viene restituito nella variabile globale"
      echo    "interCasualeNum"
      echo    "Valori negativi passati come parametri vengono gestiti"
      echo    "anch'essi correttamente."
   }

   local min=${1:-0}
   local max=${2:-32767}
   local divisibilePer=${3:-1}
   #  Assegnazione dei valori preimpostati, nel caso di mancato passaggio
   #+ dei parametri alla funzione.

   local x
   local intervallo

   # Verifica che il valore di divisibilePer sia positivo.
   [ ${divisibilePer} -lt 0 ] && divisibilePer=$((0-divisibilePer))

   # Controllo di sicurezza.
   if [ $# -gt 3 -o ${divisibilePer} -eq 0 -o  ${min} -eq ${max} ]; then
      sintassi
      return 1
   fi

   # Verifica se min e max sono scambiati.
   if [ ${min} -gt ${max} ]; then
      # Li scambia.
      x=${min}
      min=${max}
      max=${x}
   fi

   #  Se min non è esattamente divisibile per $divisibilePer,
   #+ viene ricalcolato.
   if [ $((min/divisibilePer*divisibilePer)) -ne ${min} ]; then
      if [ ${min} -lt 0 ]; then
         min=$((min/divisibilePer*divisibilePer))
      else
         min=$((((min/divisibilePer)+1)*divisibilePer))
      fi
   fi

   #  Se max non è esattamente divisibile per $divisibilePer,
   #+ viene ricalcolato.
   if [ $((max/divisibilePer*divisibilePer)) -ne ${max} ]; then
      if [ ${max} -lt 0 ]; then
         max=$((((max/divisibilePer)-1)*divisibilePer))
      else
         max=$((max/divisibilePer*divisibilePer))
      fi
   fi

   #  -----------------------------------------------------------------------
   #  Ora il lavoro vero.

   #  E' da notare che per ottenere una corretta distribuzione dei valori
   #+ estremi, si deve agire su un intervallo che va da 0 a
   #+ abs(max-min)+divisibilePer, non semplicemente abs(max-min)+1.

   #  Il leggero incremento produrrà la giusta distribuzione per i
   #+ valori limite.

   #  Se si cambia la formula e si usa abs(max-min)+1 si otterranno ancora
   #+ dei risultati corretti, ma la loro casualità sarà falsata
   #+ dal fatto che il numero di volte in cui verranno restituiti gli estremi
   #+ ($min e $max) sarà considerevolmente inferiore a quella ottenuta
   #+ usando la formula corretta.
   #  -----------------------------------------------------------------------

   intervallo=$((max-min))
   #  Omair Eshkenazi sottolinea che la verifica seguente non è necessaria,
   #+ perché max e min sono già stati scambiati.
   [ ${intervallo} -lt 0 ] && intervallo=$((0-intervallo))
   let intervallo+=divisibilePer
   interCasualeNum=$(((RANDOM%intervallo)/divisibilePer*divisibilePer+min))

   return 0
   
   #  Tuttavia, Paulo Marcel Coelho Aragao sottolinea che
   #+ quando $max e $min non sono divisibili per $divisibilePer,
   #+ la formula sbaglia.
   #
   #  Suggerisce invece la seguente:
   #    numeroc = $(((RANDOM%(max-min+1)+min)/divisibilePer*divisibilePer))
}

# Verifichiamo la funzione.

min=-14
max=20
divisibilePer=3


#  Genera un array e controlla che si sia ottenuto almeno uno dei risultati
#+ possibili, se si effettua un numero sufficiente di tentativi.

declare -a risultati
minimo=${min}
massimo=${max}
   if [ $((minimo/divisibilePer*divisibilePer)) -ne ${minimo} ]; then
      if [ ${minimo} -lt 0 ]; then
         minimo=$((minimo/divisibilePer*divisibilePer))
      else
         minimo=$((((minimo/divisibilePer)+1)*divisibilePer))
      fi
   fi


   #  Se max non è esattamente divisibile per $divisibilePer,
   #+ viene ricalcolato.

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


#  Poiché gli indici degli array possono avere solo valori positivi,
#+ è necessario uno spiazzamento che garantisca il raggiungimento
#+ di questo risultato.

spiazzamento=$((0-minimo))
for ((i=${minimo}; i<=${massimo}; i+=divisibilePer)); do
   risultati[i+spiazzamento]=0
done


# Ora si esegue per un elevato numero di volte, per vedere cosa si ottiene.
nr_volte=1000  #  L'autore dello script suggeriva 100000,
               #+ ma sarebbe occorso veramente molto tempo.

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

   #  Notate che qui min e max sono specificate in ordine inverso
   #+ per vedere, in questo caso, il corretto comportamento della funzione.

   interCasuale ${max} ${min} ${divisibilePer}

   # Riporta un errore se si verifica un risultato inatteso.
   [ ${interCasualeNum} -lt ${min} -o ${interCasualeNum} -gt ${max} ] \
&& echo errore MIN o MAX  - ${interCasualeNum}!
   [ $((interCasualeNum%${divisibilePer})) -ne 0 ] \
&& echo DIVISIBILE PER errore - ${interCasualeNum}!

   # Registra i risultati statisticamente.
   risultati[interCasualeNum+spiazzamento]=\
$((risultati[interCasualeNum+spiazzamento]+1))
done



# Controllo dei risultati

for ((i=${minimo}; i<=${massimo}; i+=divisibilePer)); do
   [ ${risultati[i+spiazzamento]} -eq 0 ] && echo "Nessun risultato per $i." \
|| echo "${i} generato ${risultati[i+spiazzamento]} volte."
done


exit 0

Ma, quant'è casuale $RANDOM? Il modo migliore per verificarlo è scrivere uno script che mostri la distribuzione dei numeri "casuali" generati da $RANDOM. Si lancia alcune volte un dado con $RANDOM. . .

Esempio 9-28. Lanciare un dado con RANDOM

#!/bin/bash
# Quant'è casuale RANDOM?

RANDOM=$$        #  Cambia il seme del generatore di numeri 
                 #+ casuali usando l'ID di processo dello script.

FACCE=6          # Un dado ha 6 facce.
NUMMAX_LANCI=600 # Aumentatelo se non avete nient'altro di meglio da fare.
lanci=0          # Contatore dei lanci.

tot_uno=0        #  I contatori devono essere inizializzati a 0 perché
tot_due=0        #+ una variabile non inizializzata ha valore nullo, non zero.
tot_tre=0
tot_quattro=0
tot_cinque=0
tot_sei=0

visualizza_risultati ()
{
echo
echo "totale degli uno = $tot_uno"
echo "totale dei due = $tot_due"
echo "totale dei tre = $tot_tre"
echo "totale dei quattro = $tot_quattro"
echo "totale dei cinque = $tot_cinque"
echo "totale dei sei = $tot_sei"
echo
}

aggiorna_contatori()
{
case "$1" in
  0) let "tot_uno += 1";;   #  Poiché un dado non ha lo "zero",
                            #+ lo facciamo corrispondere a 1.
  1) let "tot_due += 1";;   #  1 a 2, ecc.
  2) let "tot_tre += 1";;
  3) let "tot_quattro += 1";;
  4) let "tot_cinque += 1";;
  5) let "tot_sei += 1";;
esac
}

echo


while [ "$lanci" -lt "$NUMMAX_LANCI" ]
do
  let "dado1 = RANDOM % $FACCE"
  aggiorna_contatori $dado1
  let "lanci += 1"
done  

visualizza_risultati

exit 0 

#  I punteggi dovrebbero essere distribuiti abbastanza equamente, nell'ipotesi
#+ che RANDOM sia veramente casuale. 
#  Con $NUMMAX_LANCI impostata a 600, la frequenza di ognuno dei sei numeri 
#+ dovrebbe aggirarsi attorno a 100, più o meno 20 circa.
# 
#  Ricordate che RANDOM è un generatore pseudocasuale, e neanche 
#+ particolarmente valido. 

#  La casualità è un argomento esteso e complesso.
#  Sequenze "casuali" sufficientemente lunghe possono mostrare
#+ un andamento caotico e "non-casuale".

# Esercizio (facile):
# ------------------
# Riscrivete lo script per simulare il lancio di una moneta 1000 volte.
# Le possibilità sono "TESTA" o "CROCE". 

Come si è visto nell'ultimo esempio, è meglio ricalcolare il seme del generatore RANDOM ogni volta che viene invocato. Utilizzando lo stesso seme, RANDOM ripete le stesse serie di numeri. [2] (Rispecchiando il comportamento della funzione random() del C.)

Esempio 9-29. Cambiare il seme di RANDOM

#!/bin/bash
# seeding-random.sh: Cambiare il seme della variabile RANDOM.

MAX_NUMERI=25     # Quantità di numeri che devono essere generati.

numeri_casuali ()
{
contatore=0
while [ "$contatore" -lt "$MAX_NUMERI" ]
do
  numero=$RANDOM
  echo -n "$numero "
  let "contatore += 1"
done
}

echo; echo

RANDOM=1          # Impostazione del seme di RANDOM.
numeri_casuali

echo; echo

RANDOM=1          # Stesso seme...
numeri_casuali    # ...riproduce esattamente la serie precedente.
                  #
                  # Ma, quant'è utile duplicare una serie di numeri "casuali"?

echo; echo

RANDOM=2          # Altro tentativo, ma con seme diverso...
numeri_casuali    # viene generata una serie differente.

echo; echo

# RANDOM=$$  imposta il seme di RANDOM all'id di processo dello script.
# È anche possibile usare come seme di RANDOM i comandi 'time' o 'date'.

# Ancora più elegante...
SEME=$(head -1 /dev/urandom | od -N 1 | awk '{ print $2 }')
#  Output pseudocasuale prelevato da /dev/urandom (file di 
#+ dispositivo di sistema pseudo-casuale), quindi convertito 
#+ con "od" in una riga di numeri (ottali) visualizzabili,
#+ infine "awk" ne recupera solamente uno per SEME.

RANDOM=$SEME
numeri_casuali

echo; echo

exit 0

Nota

Il file di dispositivo /dev/urandom fornisce un metodo per generare numeri pseudocasuali molto più "casuali" che non la variabile $RANDOM. dd if=/dev/urandom of=nomefile bs=1 count=XX crea un file di numeri casuali ben distribuiti . Tuttavia, per assegnarli ad una variabile in uno script è necessario un espediente, come filtrarli attraverso od (come nell'esempio precedente e Esempio 15-13) o utilizzare dd (vedi Esempio 15-55) o anche collegandoli con una pipe a md5sum (vedi Esempio 33-14).

Esistono altri modi per generare numeri pseudocasuali in uno script. Awk ne fornisce uno molto comodo.

Esempio 9-30. Numeri pseudocasuali utilizzando awk

#!/bin/bash
# random2.sh: Restituisce un numero pseudo-casuale nell'intervallo 0 - 1.
# Uso della funzione rand() di awk.

SCRIPTAWK=' { srand(); print rand() } '
#            Comando(i) / parametri passati ad awk
# Notate che srand() ricalcola il seme del generatore di numeri di awk.


echo -n "Numeri casuali tra 0 e 1 = "

echo | awk "$SCRIPTAWK"
# Cosa succede se si omette 'echo'?

exit 0


# Esercizi:
# ---------

# 1) Usando un ciclo, visualizzare 10 differenti numeri casuali.
#    (Suggerimento: bisogna ricalcolare un diverso seme per la funzione 
#+   "srand()" ad ogni passo del ciclo. Cosa succede se non viene fatto?)

# 2) Usando come fattore di scala un multiplo intero, generare numeri
#+   casuali nell'intervallo tra 10 e 100.

# 3) Come il precedente esercizio nr.2, ma senza intervallo.

Anche il comando date si presta a generare sequenze di interi pseudocasuali.

Note

[1]

La reale "casualità," per quanto esista veramente, la si può trovare solo in alcuni fenomeni naturali ancora non completamente compresi come il decadimento radioattivo. I computer possono solamente simularla, di conseguenza ci si riferisce alle sequenze "casuali" da essi generate col termine di numeri pseudocasuali.

[2]

Il seme di una serie di numeri pseudocasuali generata dal computer può essere considerata un'etichetta identificativa. Per esempio, si pensi a una serie pseudocasuale con seme 23 come serie #23.

Una proprietà della serie di numeri pseudocasuali è rappresentata dall'ampiezza del ciclo prima che la serie incominci a ripetersi. Un buon generatore di numeri pseudocasuali produce serie con cicli molto grandi.