« Ubuntu 8.04 sem som ?! | Main | Três falhas corrigidas no Firefox 3. »
Como gerar números (pseudo)aletórios em um shell script?
By admin | Julho 25, 2008
Apesar de, na maioria dos casos, os shell scripts serem utilizados para manipular arquivos, agrupar logicamente utilitários comuns do sistema operacional e editar textos (de configuração de aplicativos, por exemplo), é inerente à vida de um administrador de sistemas passar por novos desafios e surpresas.
Dia desses, estava precisando implementar uma nova funcionalidade em um shell script e, seguindo a lógica que havia estabelecido, seria necessária a geração de alguns números aleatórios (ou pseudo-aleatórios, na verdade, já que falar de aleatoriedade em computação não é um assunto tão trivial). Após alguma investigação e depois de conhecer soluções mais "ortodoxas", que comento a seguir, encontrei uma maneira rápida e simples. O mais interessante é que essa alternativa não está amplamente comentada em livros e referências sobre o assunto. Daí o motivo de estar compartilhando essa dica por aqui.
Trata-se de uma variável especial, $RANDOM, existente na bash, que, uma vez acessada, gera como saída um número aleatório. Veja só isso:
$ echo $RANDOM
28214
Simples, não? Toda vez que seu conteúdo é acessado, a variável $RANDOM retorna um número aleatório entre 0 e 32767. Bem, entretanto, para dar maior confiabilidade ao processo, como em qualquer função para geração de números dessa natureza, é interessante utilizar uma semente (seed) para melhorar a qualidade do resultado. Para fazer isso, basta inserir algum valor na variável $RANDOM, antes de utilizá-la:
$ RANDOM=100
$ echo $RANDOM
12662
$ echo $RANDOM
23392
$ echo $RANDOM
22561
Por outro lado, colocar um número como semente não é uma boa idéia: mesma semente, mesmo resultado! Em outras palavras, se a semente é a mesma, a seqüência também será a mesma. Observe:
$ RANDOM=100
$ echo $RANDOM
12662
$ echo $RANDOM
23392
$ echo $RANDOM
22561
$ RANDOM=100
$ echo $RANDOM
12662
$ echo $RANDOM
23392
$ echo $RANDOM
22561
Diante disso, é preciso colocar, como semente, uma informação que varie a cada vez que a variável RANDOM for ser utilizada em um shell script. Dentre as várias alternativas existentes, selecionei duas que julgo ser de fácil entendimento. A primeira, consiste na utilização do relógio do sistema operacional. Como? Basta utilizar o comando date para retornar o horário no formato de segundos decorridos desde 1 de janeiro de 1970 e atribuir esse valor como sendo a semente da variável $RANDOM.
Comando date retornando o número de segundos decorridos desde 1 de janeiro de 1970:
$ date +%s
1216902541
Utilizando o resultado do comando date como semente para a variável $RANDOM:
$ RANDOM=$(date +%s)
$ echo $RANDOM
3050
$ echo $RANDOM
4684
$ echo $RANDOM
14178
Observe que ao inserir a semente por meio do comando date, os resultados apresentados em uma segunda execução são totalmente diferentes daqueles exibidos na execução anterior:
$ RANDOM=$(date +%s)
$ echo $RANDOM
24431
$ echo $RANDOM
28489
$ echo $RANDOM
15561
Com isso, pode-se utilizar uma semente que capaz de gerar números diferentes a cada execução dos seus shell scripts que necessitem de números aleatórios.
A segunda alternativa, também de fácil entendimento, consiste na utilização da variável especial "$$", que contém o número do processo (PID) atribuído a uma determinada execução do seu shell script. Como para cada execução de um shell script, um novo PID será gerado, esse valor também pode ser uma forma fácil de incrementar a qualidade do resultado oferecido pela variável $RANDOM:
Script random.sh:
#!/bin/bash
RANDOM=$$
echo "Valor gerado é: $RANDOM"
Execução do script random.sh:
$ ./random.sh
Valor gerado é: 1872
$ ./random.sh
Valor gerado é: 917
IMPORTANTE: Observe que essa segunda alternativa não irá funcionar caso a variável $RANDOM seja utilizada várias vezes em um mesmo terminal de comandos:
$ RANDOM=$$
$ echo $RANDOM
19872
$ echo $RANDOM
719
$ RANDOM=$$
$ echo $RANDOM
19872
$ echo $RANDOM
719
A explicação para esse resultado é muito simples. Ao atribuir o conteúdo de "$$" à variável $RANDOM em um mesmo terminal, está se utilizando o mesmo valor de semente, uma vez que, nesse caso, "$$" contém o PID do terminal, que irá permanecer o mesmo durante toda sua execução. Por outro lado, quando essa alternativa é utilizada dentro de um shell script, cada uma de suas execuções irá conter um valor diferente em $$ já que o valor do PID será distinto daquele gerado em outras execuções. Nesse caso, a variável $RANDOM gerará valores diferentes mesmo que o script seja executado diversas vezes no mesmo terminal de comandos.
Gerando números em uma determinada faixa de valores
No meu caso, precisa gerar números dentro de uma faixa pré-determinada de valores. Para isso, é possível combinar o resultado retornado pela variável $RANDOM com a operação matemática de resto.
Por exemplo, para gerar números entre 0 e 19, basta executar o seguinte:
$ echo "$(($RANDOM % 20))"
19
$ echo "$(($RANDOM % 20))"
17
$ echo "$(($RANDOM % 20))"
12
$ echo "$(($RANDOM % 20))"
0
Para que a seqüência não inclua o "0" e vá até o 20, some um ao resultado:
$ echo "$(($RANDOM % 20 + 1))"
Outros métodos
Existem outros métodos para gerar números aleatórios para serem utilizados em shell scripts e valores para as sementes. Alguns mais criativos que os outros. Em especial, um dos métodos consiste na combinação de alguns utilitários convencionais em sistemas Unix/Linux:
$ echo "Número: $(fortune | cksum | cut -f1 -d" ")"
Número: 3328996908
Se o número ficou grande demais para as suas necessidades, basta utilizar, novamente, a operaçãode módulo. Por exemplo, para gerar números entre 1 e 50, aqui está a solução:
$ echo "Número (entre 1 e 50): $(($(fortune | cksum | cut -f1 -d" ")%50 + 1))"
Número (entre 1 e 50): 38
Para entender o que está acontecendo: o primeiro comando, clássico em ambientes Unix, gera uma frase aleatória qualquer. Por meio do "pipe" ("|"), a frase gerada é repassada ao comando cksum que calcula o checksum de qualquer conteúdo e conta o número de bytes, apresentando os resultados nessa mesma ordem, separados por um espaço. Como o valor do checksum é mais adequado para ser utilizado como número aleatório, o comando cut processa o resultado gerado pelo cksum utilizando como delimitador um espaço (’-d" "’) e retornando o primeiro campo (’-f 1′). Isso fará com que apenas o valor do checksum seja exibido como resultado final.
Observe a execução dos três comandos:
$ fortune
You will remember, Watson, how the dreadful business of the
Abernetty family was first brought to my notice by the depth which the
parsley had sunk into the butter upon a hot day.
— Sherlock Holmes
$ fortune | cksum
1528275332 68
$ fortune | cksum | cut -f 1 -d" "
1528275332
Quanto mais aleatório e não-determinístico for o texto repassado ao comando cksum, melhor será a qualidade do resultado final. Portanto, podemos combinar a saída de vários comandos para gerar o texto a ser submetido ao checksum:
$ (fortune ; w ; ps ) | cksum | cut -f 1 -d" "
708058322
Ou ainda:
$ (fortune ; w ; ps; date +%s) | cksum | cut -f 1 -d" "
275255825
Uma segunda alternativa, que também consiste da combinação de alguns utilitários de sistema, utiliza o comando dd para extrair informações do dispositivo /dev/urandom:
$ dd if=/dev/urandom count=1 2> /dev/null | cksum | cut -f1 -d" "
112261124
Esse exemplo é apresentado em [1].
Links:
[1] http://linuxgazette.net/issue55/tag/4.html
[2] http://www.livefirelabs.com/unix_tip_trick_shell_script/oct_2003/10202003.htm
Topics: Dicas e truques |