bashzsh

RANDOM is not random when used inside a function in bash


I am pretty new to bash scripting and I'm facing a behavior I can't explain. I need to create random plate numbers, however the plate generated from the function I wrote always creates the same plate number as calling it in a loop shown.

generate_plate() {

    RANDOM=$$$(date +%s)
    first_letter=(A B C D E F )
    other_letters=(A B C D E F G H J K L M N P R S T V W X Y Z)
    numbers=( {0..9} )

    first_random_letter=${first_letter[$(($RANDOM % ${#first_letter[@]} + 1 ))]}
    second_random_letter=${other_letters[$(($RANDOM % ${#other_letters[@]} + 1 ))]}
    first_number=${numbers[$(($RANDOM % ${#numbers[@]} + 1 ))]}
    second_number=${numbers[$(($RANDOM % ${#numbers[@]} + 1 ))]}
    third_number=${numbers[$(($RANDOM % ${#numbers[@]} + 1 ))]}
    third_random_letter=${other_letters[$(($RANDOM % ${#other_letters[@]} + 1 ))]}
    fourth_random_letter=${other_letters[$(($RANDOM % ${#other_letters[@]} + 1 ))]}
    plate="$first_random_letter$second_random_letter$first_number$second_number$third_number$third_random_letter$fourth_random_letter"

    echo "$plate"

}

for i in {1..5}; do
  targa_random=$(generate_plate)
  echo "Plate $i: $targa_random"
done

If instead i call each line of the method in a loop then it will create all different plate numbers.

while [ 1 ]
do
first_letter=(A B C D E F )
other_letters=(A B C D E F G H J K L M N P R S T V W X Y Z)
numbers=( {0..9} )
RANDOM=$$$(date +%s)

    first_random_letter=${first_letter[$(($RANDOM % ${#first_letter[@]} + 1 ))]}
    second_random_letter=${other_letters[$(($RANDOM % ${#other_letters[@]} + 1 ))]}
    first_number=${numbers[$(($RANDOM % ${#numbers[@]} + 1 ))]}
    second_number=${numbers[$(($RANDOM % ${#numbers[@]} + 1 ))]}
    third_number=${numbers[$(($RANDOM % ${#numbers[@]} + 1 ))]}
    third_random_letter=${other_letters[$(($RANDOM % ${#other_letters[@]} + 1 ))]}
    fourth_random_letter=${other_letters[$(($RANDOM % ${#other_letters[@]} + 1 ))]}
    plate="$first_random_letter$second_random_letter$first_number$second_number$third_number$third_random_letter$fourth_random_letter"
    echo $plate
    sleep 1 
done

Using seed for RANDOM won't make a difference. Why is it so? what's different between the two approaches? Can you spot what I'm doing wrong? The +1 in the array index is because I'm using the zsh shell which has the array index starting at 1 as I found out here Select a random item from an array in Bash

Many thanks


Solution

  • I'm running bash 5.1.16 and was able to get OP's function + 5-pass for loop to work with either of these modifications:

    However, OP has stated these modifications do not work for their bash 3.2.57 environment (see Charles Duffy's answer for possible explanations) so the question becomes how can we get this function to work in OP's environment?

    One quick-n-dirty option would be to use the same variable name in the function and the parent process. This would require 2 changes ... 1) remove the RANDOM=$$$(date +%s) and echo "$plate" lines from the function and 2) modify the for loop as follows:

    for i in {1..5}; do
        generate_plate
        echo "Plate $i: $plate"
    done
    

    One downside to this approach is the parent process needs to know the function populates the variable named plate.

    With some code changes we could tell the function what variable to populate. With bash 4.3+ this would be relatively easy with a nameref but this is not an option in OP's bash 3.2.57 environment so that leaves us with a second option of indirect variable references.

    One idea for implementing an indirect variable reference:

    generate_plate() {
    
        first_letter=(A B C D E F )
        other_letters=(A B C D E F G H J K L M N P R S T V W X Y Z)
        numbers=( {0..9} )
    
        first_random_letter=${first_letter[$(($RANDOM % ${#first_letter[@]} + 1 ))]}
        second_random_letter=${other_letters[$(($RANDOM % ${#other_letters[@]} + 1 ))]}
        first_number=${numbers[$(($RANDOM % ${#numbers[@]} + 1 ))]}
        second_number=${numbers[$(($RANDOM % ${#numbers[@]} + 1 ))]}
        third_number=${numbers[$(($RANDOM % ${#numbers[@]} + 1 ))]}
        third_random_letter=${other_letters[$(($RANDOM % ${#other_letters[@]} + 1 ))]}
        fourth_random_letter=${other_letters[$(($RANDOM % ${#other_letters[@]} + 1 ))]}
        plate="$first_random_letter$second_random_letter$first_number$second_number$third_number$third_random_letter$fourth_random_letter"
    
        if [[ -n $1 ]]                  # if a variable name was provided then ...
        then
            read -r $1 <<< "$plate"     # set the variable via indirect reference else ...
        else
            echo "$plate"               # print to stdout
        fi
    }
    

    Taking for a test drive ...

    OP's for loop updated:

    for i in {1..5}; do
        generate_plate targa_random       # pass the name of the variable to be populated
        echo "Plate $i: $targa_random"
    done
    
    Plate 1: EW364YB
    Plate 2: CJ85EK
    Plate 3: P289HZ
    Plate 4: EP31CL
    Plate 5: CG18BM
    

    Allowing the function to print to stdout:

    $ generate_plate
    DY462TG
    
    $ generate_plate
    FZ418ZC
    
    $ generate_plate
    DZ812WH
    
    $ generate_plate
    FG34HC