PostHeaderIcon Como gerar números (pseudo)aletórios em um shell script?

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

 

2 Responses to “Como gerar números (pseudo)aletórios em um shell script?”

Leave a Reply