最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

zsh - RANDOM is not random when used inside a function in bash - Stack Overflow

programmeradmin3浏览0评论

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

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

Share edited Mar 17 at 10:10 Vincenzo asked Mar 14 at 14:49 VincenzoVincenzo 6,5008 gold badges56 silver badges125 bronze badges 23
  • 2 RANDOM=$$$(date +%s) - why are you using 3 dollars here? – Arkadiusz Drabczyk Commented Mar 14 at 14:50
  • 3 RANDOM is a reserved variable! Don't assign them!! – F. Hauri - Give Up GitHub Commented Mar 14 at 14:53
  • 1 See xkcd/221 – F. Hauri - Give Up GitHub Commented Mar 14 at 14:53
  • 3 You're getting the same values in every loop because you use the constant RANDOM. Do not set it. Your script may need some more work if you want all generated strings to have the same length. – Arkadiusz Drabczyk Commented Mar 14 at 14:53
  • 3 RANDOM=$$$(date +%s) sets the seed; your sample script is likely running through the 5 loops so fast that all 5 RANDOM=$$$(date +%s) are performed within the same 1 second thus generating the same exact seed which in turn means you're seeing the same results; try adding a 1 second delay in the loop to get a different seed each time – markp-fuso Commented Mar 14 at 14:58
 |  Show 18 more comments

3 Answers 3

Reset to default 5

You have two problems:

  • Because you call $(generate_plate), each call to the function is inside a subshell, so it can't update the RNG state in the parent shell. This is why you had the problem before you started trying to seed the RNG at all.
  • Because in your code that does call RANDOM=$(date +s) you're doing so inside a tight loop, whenever two calls are less than a second apart they set the same seed; this is why trying to explicitly seed the RNG did not help. (Using $$ doesn't mutate the seed because $$ is the PID of the parent shell, not the subshell -- you'd need to use $BASHPID to have a distinct value per subprocess, but since only one subprocess is running at a time an OS could theoretically reuse them, so that's not a very strong guarantee)

Any easy solution is to use a RNG run on the outside to initialize the seed on the inside:

generate_plate() {
    if [[ $1 ]]; then RANDOM=$1; fi
    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 "$$""$(date +%s)""$RANDOM")
  echo "Plate $i: $targa_random"
done

See this running with correct output at https://ideone/lWwE52

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:

  • remove the RANDOM=$$$(date +%s) line or ...
  • add a sleep 1 to the 5-pass for loop

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

Random field from bash array

How I would do this:

  1. I hate to repeat my code!
  2. Your math syntax is wrong: $(($RANDOM % ${#first_letter[@]} + 1 )) you must enclose ${#first_letter[@]} + 1 into parenthesis. Btw + 1 is useless as ${#array[@]} show number of elements and elements indexes start at 0 and end at N - 1.
rndFromArry() { 
    local -n _array=$1
    printf -v "$2" "${_array[ RANDOM % ${#_array[@]} ]}"
}
generatePlate() { 
    [[ $1 == -r ]] && local RANDOM=$2 && shift 2
    local firstLetters=({A..F}) otherLetters=({A..Z}) numbers=({0..9})
    local pointer value result=''
    for pointer in {first,other}Letters numbers{,,} otherLetters{,}; do
        rndFromArry $pointer value
        result+="$value"
    done
    printf -v "$1" '%s' "$result"
}

Note: In order to avoid forks, ( var=$(...) ), I prefer to localize all variables used in my functions and populate variables submitted as arguments instead of echoing results.

Note: syntax: {first,other}Letters numbers{,,} otherLetters{,} will render your list of array is same order you did:

printf ' - %s\n' {first,other}Letters numbers{,,} otherLetters{,}
 - firstLetters
 - otherLetters
 - numbers
 - numbers
 - numbers
 - otherLetters
 - otherLetters

Then:

for i in {1..5};do
    generatePlate targaRandom 
    echo $targaRandom
done

Could output something like:

DI477OF
FS846HE
EL442BB
EC049YA
FT098BH

But:

for i in {1..5};do
    generatePlate -r 12345 targaRandom 
    echo $targaRandom
done

should output exactly this:

DV555VV
DV555VV
DV555VV
DV555VV
DV555VV

Even more reusable functions:

Here is a more generic function to get field from array:

randomFromArray() { 
    if [[ $1 == -v ]]; then
        local -n _res=$2
        shift 2
    else
        local _res
    fi
    local -i _rand
    _rand=" ( $# > 32767 ? RANDOM << 15 | RANDOM : RANDOM ) % $# + 1"
    printf -v _res '%s' ${!_rand}
    [[ ${_res@A} == _res=* ]] && echo $_res
}

Note: To be more generic, I've added a second $RANDOM, shifted left by 15 bits in case number of fields is greater than 32767 (max $RANDOM value)

Then

randomFromArray {0..9}
5  # Or any between 0 and 9

randomFromArray {A..F}
B  # Or any between A and F

Force $RANDOM value for testing purpose

Note: if you want to force random value in order to test your function, you could simply:

RANDOM=12345 randomFromArray {0..9}
5  # Should be 5!

RANDOM=12345 randomFromArray {A..F}
D  # Should be D!

Populate variable instead of doing forks

Then as I previously said I prefer to assign values instead of forking new shell to a function:

randomFromArray -v myValue {0..9}

Will have same effect than

myValue=$(randomFromArray {0..9})

But without having to run a subshell to execute my function. (So this will be more system friendly and a lot quicker).

Then your specific script become:

We will use nameref variable in generatePlate in order to run this new randomFromArray function:

generatePlate() { 
    local firstLetters=({A..F}) otherLetters=({A..Z}) numbers=({0..9})
    local pointer array value result=''
    for pointer in {first,other}Letters numbers{,,} otherLetters{,}; do
        local -n array=$pointer
        randomFromArray -v value "${array[@]}"
        result+="$value"
    done
    if [[ $1 ]]; then printf -v "$1" '%s' "$result"
    else echo "$result"
    fi
}

Then

generatePlate
EL227ME   # Or any other *plate*
RANDOM=12345 generatePlate
DV555VV   # Strictly 'DV555VV'!
generatePlate myPlate
echo $myPlate
EV387VR

for i in {1..5}; do
    generatePlate targa_random
    echo "Plate $i: $targa_random"
done
Plate 1: EI931XC
Plate 2: BS329SB
Plate 3: BP535IZ
Plate 4: BH479DW
Plate 5: BC596KA
发布评论

评论列表(0)

  1. 暂无评论